Implementing Video Player in Mobile Applications
Out-of-the-box video player — AVPlayerViewController on iOS or VideoView with MediaPlayer on Android — works, but in production almost always need custom UI: subtitles, quality selection, custom controls. This is where nuances begin.
Custom Controls Over AVPlayer
On iOS use AVPlayer + AVPlayerLayer instead of AVPlayerViewController. Add layer in viewDidLayoutSubviews:
playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = videoContainerView.bounds
playerLayer.videoGravity = .resizeAspect
videoContainerView.layer.addSublayer(playerLayer)
Progress: player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: .main). Slider updates every half second, not overloading UI.
On slider drag: player.pause() at start, player.seek(to:), player.play() at end. Otherwise video stutters on fast seeking.
Android. StyledPlayerView from media3-ui — customizable via XML attributes and layout replacement. For fully custom UI — PlayerView with use_controller="false" and own buttons above. ExoPlayer + SimpleOnPlaybackStateChangedListener.
Subtitles
iOS. AVMediaCharacteristic.legible — native subtitles from HLS/MP4. For external SRT/VTT files: convert SRT → WebVTT, create AVURLAsset with config, add track via AVMutableComposition. Or — draw subtitles with own UILabel above player, parse SRT with NSRegularExpression.
Android/ExoPlayer. SubtitleConfiguration in MediaItem.Builder(). Supported formats: WebVTT, SRT, TTML, SSA/ASS. External file: MediaItem.SubtitleConfiguration.Builder(uri).setMimeType(MimeTypes.TEXT_VTT).build().
Quality Selection
For HLS streams, built-in ABR (adaptive bitrate) switches quality automatically. Manual selection — AVPlayerItem.preferredPeakBitRate on iOS. ExoPlayer: DefaultTrackSelector with parametersBuilder.setMaxVideoBitrate(bitrate).
For progressive MP4 with multiple URLs (360p, 720p, 1080p): when quality changes, save player.currentTime / player.currentPosition, replace source, seek(to: savedPosition) after player.ready.
Full-screen Mode
iOS. On landscape transition — playerLayer.frame = UIScreen.main.bounds, hide navigation bar. Back — restore. Support only landscape for fullscreen: supportedInterfaceOrientations return .landscape only for player VC.
Android. WindowInsetsControllerCompat(window).hide(WindowInsetsCompat.Type.systemBars()) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE.
Buffering and Loading Indicator
User should see video is buffering, not stuck. On iOS subscribe to KVO property AVPlayerItem.isPlaybackBufferEmpty — on true show UIActivityIndicatorView, on isPlaybackLikelyToKeepUp == true — hide.
On Android Player.Listener.onPlaybackStateChanged with STATE_BUFFERING — state when player waits for data. STATE_READY — enough data for playback.
Important nuance: don't show spinner on initial load before playback starts — user hasn't pressed play yet. Spinner needed only when playback was ongoing and suddenly stopped for buffering.
Flutter: video_player and Chewie
video_player (pub.dev) — basic player without UI. chewie builds standard controls with fullscreen and subtitle support above it. For custom UI — take video_player and draw own above VideoPlayer widget. Subtitles via VideoPlayerController with closedCaptionFile.
Timeline
Player with custom controls, subtitles and fullscreen mode — 2–3 days. Add quality selection for HLS and position saving on close — another 1 day.







