# Streamline Native Player Controls

The native playback overlay was reduced from a large bottom control panel into a compact, centered control pill that leaves more of the video visible while preserving playback, seeking, jump, captions, and close actions.

## Summary

Dreamio's native player controls now occupy much less vertical and horizontal space. The bottom controls use tighter padding, smaller circular buttons, smaller time labels, and a slimmer scrubber thumb so the screen feels more like a native iOS video player.

## Changes Made

- Reworked the controls from a wide full-width panel into a centered compact overlay capped at 430 points.
- Combined elapsed time, scrubber, and remaining time into one horizontal row.
- Reduced button sizes while keeping circular targets for close, jump, play/pause, and captions.
- Reduced control padding and spacing to lower the overlay height.
- Added custom scrubber thumb images for a slimmer native-feeling slider.

## Context

The previous player overlay was visually heavy and could feel like it took over the bottom half of the playback surface. The requested direction was to make it much more streamlined and closer to a native player experience.

## Important Implementation Details

The behavior lives in `Dreamio/NativePlayerViewController.swift`. This change only adjusts the UIKit control layout and visual treatment. It does not change VLC playback, stream resolution, subtitle selection behavior, timers, or dismiss behavior.

The compact overlay keeps the scrubber usable by giving it a minimum width while allowing the container to shrink to its content and stay within the safe area.

## Relevant Diff Snippets

The rendered diff below was generated with `@pierre/diffs/ssr`.

Dreamio/NativePlayerViewController.swift
-36+54

    22 unmodified lines232425262728291 unmodified line3132333435363714 unmodified lines525354555657582 unmodified lines616263646566677 unmodified lines757677787980103 unmodified lines1841851861871881892 unmodified lines19219319419519619719819920020120220320420520620720820921021121221314 unmodified lines228229230231232233234235236237238239240241242243244245246247248249250251252253254255129 unmodified lines38538638738838939039139222 unmodified lines        button.setImage(UIImage(systemName: "xmark"), for: .normal)        button.tintColor = .white        button.backgroundColor = UIColor.black.withAlphaComponent(0.45)        button.layer.cornerRadius = 22        button.accessibilityLabel = "Close"        return button    }()1 unmodified line    private let controlsContainer: UIVisualEffectView = {        let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark))        view.translatesAutoresizingMaskIntoConstraints = false        view.layer.cornerRadius = 12        view.clipsToBounds = true        return view    }()14 unmodified lines        let label = UILabel()        label.translatesAutoresizingMaskIntoConstraints = false        label.textColor = .white        label.font = .monospacedDigitSystemFont(ofSize: 13, weight: .medium)        label.text = "0:00"        return label    }()2 unmodified lines        let label = UILabel()        label.translatesAutoresizingMaskIntoConstraints = false        label.textColor = .white        label.font = .monospacedDigitSystemFont(ofSize: 13, weight: .medium)        label.textAlignment = .right        label.text = "-0:00"        return label7 unmodified lines        slider.minimumTrackTintColor = UIColor(red: 0.64, green: 0.48, blue: 1.0, alpha: 1)        slider.maximumTrackTintColor = UIColor.white.withAlphaComponent(0.3)        slider.thumbTintColor = .white        return slider    }()
    103 unmodified lines        jumpBackButton.addTarget(self, action: #selector(jumpBack), for: .touchUpInside)        jumpForwardButton.addTarget(self, action: #selector(jumpForward), for: .touchUpInside)        captionsButton.addTarget(self, action: #selector(showCaptions), for: .touchUpInside)        scrubber.addTarget(self, action: #selector(scrubbingStarted), for: .touchDown)        scrubber.addTarget(self, action: #selector(scrubberChanged), for: .valueChanged)        scrubber.addTarget(self, action: #selector(scrubbingEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])2 unmodified lines        tap.cancelsTouchesInView = false        tapSurfaceView.addGestureRecognizer(tap)
            let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])        controlRow.translatesAutoresizingMaskIntoConstraints = false        controlRow.axis = .horizontal        controlRow.alignment = .center        controlRow.distribution = .equalCentering        controlRow.spacing = 18
            let timeRow = UIStackView(arrangedSubviews: [elapsedLabel, remainingLabel])        timeRow.translatesAutoresizingMaskIntoConstraints = false        timeRow.axis = .horizontal        timeRow.distribution = .fillEqually
            let stack = UIStackView(arrangedSubviews: [scrubber, timeRow, controlRow])        stack.translatesAutoresizingMaskIntoConstraints = false        stack.axis = .vertical        stack.spacing = 8        controlsContainer.contentView.addSubview(stack)
            NSLayoutConstraint.activate([14 unmodified lines            failureLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),            failureLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                closeButton.widthAnchor.constraint(equalToConstant: 44),            closeButton.heightAnchor.constraint(equalToConstant: 44),            closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),            closeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
                controlsContainer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 18),            controlsContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -18),            controlsContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -18),
                stack.leadingAnchor.constraint(equalTo: controlsContainer.contentView.leadingAnchor, constant: 16),            stack.trailingAnchor.constraint(equalTo: controlsContainer.contentView.trailingAnchor, constant: -16),            stack.topAnchor.constraint(equalTo: controlsContainer.contentView.topAnchor, constant: 14),            stack.bottomAnchor.constraint(equalTo: controlsContainer.contentView.bottomAnchor, constant: -14),
                jumpBackButton.widthAnchor.constraint(equalToConstant: 44),            jumpBackButton.heightAnchor.constraint(equalToConstant: 44),            playPauseButton.widthAnchor.constraint(equalToConstant: 54),            playPauseButton.heightAnchor.constraint(equalToConstant: 54),            jumpForwardButton.widthAnchor.constraint(equalToConstant: 44),            jumpForwardButton.heightAnchor.constraint(equalToConstant: 44),            captionsButton.widthAnchor.constraint(equalToConstant: 44),            captionsButton.heightAnchor.constraint(equalToConstant: 44)        ])    }
    129 unmodified lines        button.setImage(UIImage(systemName: systemName), for: .normal)        button.tintColor = .white        button.backgroundColor = UIColor.black.withAlphaComponent(0.35)        button.layer.cornerRadius = 22        button.accessibilityLabel = label        return button    }}22 unmodified lines232425262728291 unmodified line3132333435363714 unmodified lines525354555657582 unmodified lines616263646566677 unmodified lines7576777879808182103 unmodified lines1861871881891901911922 unmodified lines19519619719819920020120220320420520620720820921021121221321421521621714 unmodified lines232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264129 unmodified lines39439539639739839940040140240340440540640740840941022 unmodified lines        button.setImage(UIImage(systemName: "xmark"), for: .normal)        button.tintColor = .white        button.backgroundColor = UIColor.black.withAlphaComponent(0.45)        button.layer.cornerRadius = 18        button.accessibilityLabel = "Close"        return button    }()1 unmodified line    private let controlsContainer: UIVisualEffectView = {        let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark))        view.translatesAutoresizingMaskIntoConstraints = false        view.layer.cornerRadius = 16        view.clipsToBounds = true        return view    }()14 unmodified lines        let label = UILabel()        label.translatesAutoresizingMaskIntoConstraints = false        label.textColor = .white        label.font = .monospacedDigitSystemFont(ofSize: 11, weight: .semibold)        label.text = "0:00"        return label    }()2 unmodified lines        let label = UILabel()        label.translatesAutoresizingMaskIntoConstraints = false        label.textColor = .white        label.font = .monospacedDigitSystemFont(ofSize: 11, weight: .semibold)        label.textAlignment = .right        label.text = "-0:00"        return label7 unmodified lines        slider.minimumTrackTintColor = UIColor(red: 0.64, green: 0.48, blue: 1.0, alpha: 1)        slider.maximumTrackTintColor = UIColor.white.withAlphaComponent(0.3)        slider.thumbTintColor = .white        slider.setThumbImage(NativePlayerViewController.scrubberThumbImage(diameter: 12), for: .normal)        slider.setThumbImage(NativePlayerViewController.scrubberThumbImage(diameter: 16), for: .highlighted)        return slider    }()
    103 unmodified lines        jumpBackButton.addTarget(self, action: #selector(jumpBack), for: .touchUpInside)        jumpForwardButton.addTarget(self, action: #selector(jumpForward), for: .touchUpInside)        captionsButton.addTarget(self, action: #selector(showCaptions), for: .touchUpInside)        playPauseButton.layer.cornerRadius = 21        scrubber.addTarget(self, action: #selector(scrubbingStarted), for: .touchDown)        scrubber.addTarget(self, action: #selector(scrubberChanged), for: .valueChanged)        scrubber.addTarget(self, action: #selector(scrubbingEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])2 unmodified lines        tap.cancelsTouchesInView = false        tapSurfaceView.addGestureRecognizer(tap)
            let timeAndScrubRow = UIStackView(arrangedSubviews: [elapsedLabel, scrubber, remainingLabel])        timeAndScrubRow.translatesAutoresizingMaskIntoConstraints = false        timeAndScrubRow.axis = .horizontal        timeAndScrubRow.alignment = .center        timeAndScrubRow.spacing = 8
            let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])        controlRow.translatesAutoresizingMaskIntoConstraints = false        controlRow.axis = .horizontal        controlRow.alignment = .center        controlRow.distribution = .equalSpacing        controlRow.spacing = 14
            let stack = UIStackView(arrangedSubviews: [timeAndScrubRow, controlRow])        stack.translatesAutoresizingMaskIntoConstraints = false        stack.axis = .vertical        stack.spacing = 6        controlsContainer.contentView.addSubview(stack)
            NSLayoutConstraint.activate([14 unmodified lines            failureLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),            failureLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                closeButton.widthAnchor.constraint(equalToConstant: 36),            closeButton.heightAnchor.constraint(equalToConstant: 36),            closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),            closeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
                controlsContainer.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),            controlsContainer.widthAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.widthAnchor, constant: -24),            controlsContainer.widthAnchor.constraint(lessThanOrEqualToConstant: 430),            controlsContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
                stack.leadingAnchor.constraint(equalTo: controlsContainer.contentView.leadingAnchor, constant: 12),            stack.trailingAnchor.constraint(equalTo: controlsContainer.contentView.trailingAnchor, constant: -12),            stack.topAnchor.constraint(equalTo: controlsContainer.contentView.topAnchor, constant: 8),            stack.bottomAnchor.constraint(equalTo: controlsContainer.contentView.bottomAnchor, constant: -10),
                elapsedLabel.widthAnchor.constraint(equalToConstant: 42),            remainingLabel.widthAnchor.constraint(equalToConstant: 42),            scrubber.widthAnchor.constraint(greaterThanOrEqualToConstant: 160),
                jumpBackButton.widthAnchor.constraint(equalToConstant: 36),            jumpBackButton.heightAnchor.constraint(equalToConstant: 36),            playPauseButton.widthAnchor.constraint(equalToConstant: 42),            playPauseButton.heightAnchor.constraint(equalToConstant: 42),            jumpForwardButton.widthAnchor.constraint(equalToConstant: 36),            jumpForwardButton.heightAnchor.constraint(equalToConstant: 36),            captionsButton.widthAnchor.constraint(equalToConstant: 36),            captionsButton.heightAnchor.constraint(equalToConstant: 36)        ])    }
    129 unmodified lines        button.setImage(UIImage(systemName: systemName), for: .normal)        button.tintColor = .white        button.backgroundColor = UIColor.black.withAlphaComponent(0.35)        button.layer.cornerRadius = 18        button.accessibilityLabel = label        return button    }
        private static func scrubberThumbImage(diameter: CGFloat) -> UIImage {        let format = UIGraphicsImageRendererFormat()        format.scale = UIScreen.main.scale        return UIGraphicsImageRenderer(size: CGSize(width: diameter, height: diameter), format: format).image { context in            UIColor.white.setFill()            context.cgContext.fillEllipse(in: CGRect(origin: .zero, size: CGSize(width: diameter, height: diameter)))        }    }}

## Expected Impact for End-Users

Users should see more video and less chrome when controls are visible. Playback controls remain familiar, but the overlay is quieter and less intrusive, especially on smaller phones or landscape playback.

## Validation

Passed:

- `xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -sdk iphonesimulator -configuration Debug build`

The build completed successfully. Xcode still reports the existing MobileVLCKit script-phase warning about missing outputs; this was not introduced by this player UI change.

## Issues, Limitations, and Mitigations

- No simulator playback walkthrough was performed in this pass, so the exact visual feel should still be checked on device or simulator with real playback.
- The controls are intentionally smaller. Accessibility labels remain in place, but future work could add larger pointer or VoiceOver-specific affordances if needed.

## Follow-up Work

No required follow-up Beads issues were created. A useful next polish pass would be a simulator/device visual check during active playback, especially in portrait and landscape.