Extend the basic video player with picture-in-picture mode, playback speed selection (0.5x-2x), quality switching (360p/720p/1080p), and subtitles from SRT files. Create a Custom Widget using the better_player package which bundles all these features with a polished control overlay. Configure BetterPlayerController with BetterPlayerConfiguration and BetterPlayerDataSource that includes resolution maps for quality switching and subtitle sources for closed captions.
Building an Advanced Video Player in FlutterFlow
The basic video player (#7) covers simple playback. For streaming apps, e-learning platforms, and media-rich apps, you need playback speed, quality options, subtitles, and PiP. The better_player package wraps all these features into one widget.
Prerequisites
- FlutterFlow Pro plan or higher (Custom Code required)
- Video files available in multiple resolutions (360p, 720p, 1080p URLs)
- SRT subtitle files hosted at accessible URLs (optional)
- Understanding of the basic video player Custom Widget pattern
Step-by-step guide
Add the better_player dependency to your project
Add the better_player dependency to your project
Go to Custom Code → Pubspec Dependencies and add better_player with version ^0.0.84. Click Save. better_player wraps the standard video_player with a comprehensive control overlay, quality switching, subtitles, PiP, and caching — all configured via its controller objects. It is the most feature-complete video package for Flutter.
Expected result: The better_player package appears in Pubspec Dependencies without errors.
Create the Custom Widget with BetterPlayerController
Create the Custom Widget with BetterPlayerController
Create a Custom Widget called AdvancedPlayer. Add parameters: videoUrl (String, required), video720Url (String, optional), video1080Url (String, optional), subtitleUrl (String, optional). In initState, create BetterPlayerConfiguration with autoPlay, looping, and controlsConfiguration. Create BetterPlayerDataSource with the videoUrl. If quality URLs are provided, add them to the resolutions map. Build returns a BetterPlayer widget with the controller.
1import 'package:better_player/better_player.dart';23class AdvancedPlayer extends StatefulWidget {4 const AdvancedPlayer({5 super.key, this.width, this.height,6 required this.videoUrl,7 this.video720Url, this.video1080Url, this.subtitleUrl,8 });9 final double? width;10 final double? height;11 final String videoUrl;12 final String? video720Url;13 final String? video1080Url;14 final String? subtitleUrl;1516 @override17 State<AdvancedPlayer> createState() => _AdvancedPlayerState();18}1920class _AdvancedPlayerState extends State<AdvancedPlayer> {21 late BetterPlayerController _controller;2223 @override24 void initState() {25 super.initState();26 final config = BetterPlayerConfiguration(27 autoPlay: false,28 looping: false,29 fit: BoxFit.contain,30 controlsConfiguration: BetterPlayerControlsConfiguration(31 enablePlaybackSpeed: true,32 enableSubtitles: widget.subtitleUrl != null,33 enableQualities: true,34 ),35 );36 final resolutions = <String, String>{37 '360p': widget.videoUrl,38 if (widget.video720Url != null) '720p': widget.video720Url!,39 if (widget.video1080Url != null) '1080p': widget.video1080Url!,40 };41 final dataSource = BetterPlayerDataSource(42 BetterPlayerDataSourceType.network,43 widget.videoUrl,44 resolutions: resolutions,45 subtitles: widget.subtitleUrl != null46 ? [BetterPlayerSubtitlesSource(47 type: BetterPlayerSubtitlesSourceType.network,48 urls: [widget.subtitleUrl!],49 )]50 : null,51 );52 _controller = BetterPlayerController(config, betterPlayerDataSource: dataSource);53 }5455 @override56 void dispose() {57 _controller.dispose();58 super.dispose();59 }6061 @override62 Widget build(BuildContext context) {63 return SizedBox(64 width: widget.width ?? double.infinity,65 height: widget.height ?? 250,66 child: BetterPlayer(controller: _controller),67 );68 }69}Expected result: The Custom Widget compiles with quality switching, speed controls, and subtitle support.
Configure quality switching with multiple resolution URLs
Configure quality switching with multiple resolution URLs
When adding videos to your Firestore collection, store multiple resolution URLs: videoUrl360 (small, fast), videoUrl720 (medium), videoUrl1080 (high quality). Pass all three to the AdvancedPlayer widget. better_player shows a quality selector in the controls overlay where users can switch between 360p, 720p, and 1080p on the fly. The player buffers the new quality and continues from the same position.
Expected result: Users see a quality selector button in the video controls and can switch resolutions during playback.
Add subtitle support from SRT files
Add subtitle support from SRT files
Host your SRT subtitle file at an accessible URL (e.g., Firebase Storage). Pass the URL to the subtitleUrl parameter. better_player parses the SRT format and overlays subtitles on the video. Users can toggle subtitles on/off via the CC button in the controls. For multiple languages, provide an array of BetterPlayerSubtitlesSource with different language labels.
Expected result: Subtitles display over the video and users can toggle them on or off.
Enable picture-in-picture mode for background viewing
Enable picture-in-picture mode for background viewing
Add controller.enablePictureInPicture(controller.betterPlayerGlobalKey!) call when the user taps a PiP button or navigates away. On Android, PiP requires the Activity to have android:supportsPictureInPicture='true' in AndroidManifest.xml (configure after code export). iOS supports PiP natively for video. PiP lets users watch video in a small floating window while using other parts of the app.
Expected result: Video continues playing in a small floating window when the user navigates to another page.
Complete working example
1import 'package:better_player/better_player.dart';23class AdvancedPlayer extends StatefulWidget {4 const AdvancedPlayer({5 super.key,6 this.width,7 this.height,8 required this.videoUrl,9 this.video720Url,10 this.video1080Url,11 this.subtitleUrl,12 });1314 final double? width;15 final double? height;16 final String videoUrl;17 final String? video720Url;18 final String? video1080Url;19 final String? subtitleUrl;2021 @override22 State<AdvancedPlayer> createState() => _AdvancedPlayerState();23}2425class _AdvancedPlayerState extends State<AdvancedPlayer> {26 late BetterPlayerController _controller;2728 @override29 void initState() {30 super.initState();31 final config = BetterPlayerConfiguration(32 autoPlay: false,33 looping: false,34 fit: BoxFit.contain,35 controlsConfiguration: BetterPlayerControlsConfiguration(36 enablePlaybackSpeed: true,37 enableSubtitles: widget.subtitleUrl != null,38 enableQualities: true,39 enablePip: true,40 playbackSpeedList: const [0.5, 0.75, 1.0, 1.25, 1.5, 2.0],41 ),42 );4344 final resolutions = <String, String>{45 '360p': widget.videoUrl,46 };47 if (widget.video720Url != null) resolutions['720p'] = widget.video720Url!;48 if (widget.video1080Url != null) resolutions['1080p'] = widget.video1080Url!;4950 final dataSource = BetterPlayerDataSource(51 BetterPlayerDataSourceType.network,52 widget.videoUrl,53 resolutions: resolutions,54 subtitles: widget.subtitleUrl != null55 ? [56 BetterPlayerSubtitlesSource(57 type: BetterPlayerSubtitlesSourceType.network,58 urls: [widget.subtitleUrl!],59 name: 'English',60 ),61 ]62 : null,63 );6465 _controller = BetterPlayerController(66 config,67 betterPlayerDataSource: dataSource,68 );69 }7071 @override72 void dispose() {73 _controller.dispose();74 super.dispose();75 }7677 @override78 Widget build(BuildContext context) {79 return SizedBox(80 width: widget.width ?? double.infinity,81 height: widget.height ?? 250,82 child: BetterPlayer(controller: _controller),83 );84 }85}Common mistakes when building a Custom Video Player with Advanced Features in FlutterFlow
Why it's a problem: Using the basic video_player package for features that better_player includes
How to avoid: Use better_player when you need any advanced features. Only use video_player for the simplest possible playback.
Why it's a problem: Providing only one video resolution without quality options
How to avoid: Encode videos in multiple resolutions (360p, 720p, 1080p) and provide all URLs in the resolutions map for user-selectable quality.
Why it's a problem: Not disposing the BetterPlayerController on page exit
How to avoid: Override dispose() and call _controller.dispose() to stop playback and free resources.
Best practices
- Use better_player for advanced features — it wraps video_player with quality, speed, subtitles, and PiP
- Provide multiple resolution URLs for user-selectable quality switching
- Add SRT subtitle files for accessibility and multi-language support
- Dispose the controller in dispose() to prevent background playback and memory leaks
- Enable playback speed options (0.5x to 2x) for e-learning and review use cases
- Configure PiP mode for background viewing (requires Android manifest changes after export)
- Pass video URLs as Component Parameters for reusable player widgets
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a FlutterFlow Custom Widget for an advanced video player using better_player. Include quality switching with 360p/720p/1080p resolution map, playback speed options, SRT subtitle support, and picture-in-picture configuration.
Add a full-width video player container on my page at 250px height. I will add the Custom Widget with advanced controls inside it.
Frequently asked questions
Does better_player support HLS and DASH streaming?
Yes. better_player supports HLS (.m3u8) and DASH (.mpd) adaptive streaming formats. Set the data source URL to the manifest file and it handles adaptive bitrate automatically.
Can I add multiple subtitle languages?
Yes. Add multiple BetterPlayerSubtitlesSource entries to the subtitles array, each with a different URL and language name. Users select from a language picker in the controls.
Does PiP work on iOS?
iOS supports PiP natively for AVPlayer-based video. better_player integrates with this. The user must enable PiP in iOS Settings and the app must declare background audio capability.
How do I cache videos for offline playback?
better_player supports caching with BetterPlayerCacheConfiguration. Set maxCacheSize and maxCacheFileSize in the data source configuration.
Can I track video watch progress?
Yes. Listen to BetterPlayerController events for position changes. Save the current position to Firestore periodically. On reload, seek to the saved position for resume functionality.
Can RapidDev help build a video streaming platform?
Yes. RapidDev can implement video players with DRM, adaptive streaming, offline caching, progress tracking, and analytics.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation