# How to Build a Custom Video Player with Advanced Features in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 25-30 min
- Compatibility: FlutterFlow Pro+ (Custom Code required)
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

### 2. 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.

```
import 'package:better_player/better_player.dart';

class AdvancedPlayer extends StatefulWidget {
  const AdvancedPlayer({
    super.key, this.width, this.height,
    required this.videoUrl,
    this.video720Url, this.video1080Url, this.subtitleUrl,
  });
  final double? width;
  final double? height;
  final String videoUrl;
  final String? video720Url;
  final String? video1080Url;
  final String? subtitleUrl;

  @override
  State<AdvancedPlayer> createState() => _AdvancedPlayerState();
}

class _AdvancedPlayerState extends State<AdvancedPlayer> {
  late BetterPlayerController _controller;

  @override
  void initState() {
    super.initState();
    final config = BetterPlayerConfiguration(
      autoPlay: false,
      looping: false,
      fit: BoxFit.contain,
      controlsConfiguration: BetterPlayerControlsConfiguration(
        enablePlaybackSpeed: true,
        enableSubtitles: widget.subtitleUrl != null,
        enableQualities: true,
      ),
    );
    final resolutions = <String, String>{
      '360p': widget.videoUrl,
      if (widget.video720Url != null) '720p': widget.video720Url!,
      if (widget.video1080Url != null) '1080p': widget.video1080Url!,
    };
    final dataSource = BetterPlayerDataSource(
      BetterPlayerDataSourceType.network,
      widget.videoUrl,
      resolutions: resolutions,
      subtitles: widget.subtitleUrl != null
          ? [BetterPlayerSubtitlesSource(
              type: BetterPlayerSubtitlesSourceType.network,
              urls: [widget.subtitleUrl!],
            )]
          : null,
    );
    _controller = BetterPlayerController(config, betterPlayerDataSource: dataSource);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width ?? double.infinity,
      height: widget.height ?? 250,
      child: BetterPlayer(controller: _controller),
    );
  }
}
```

**Expected result:** The Custom Widget compiles with quality switching, speed controls, and subtitle support.

### 3. 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.

### 4. 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.

### 5. 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 code example

File: `advanced_player.dart`

```dart
import 'package:better_player/better_player.dart';

class AdvancedPlayer extends StatefulWidget {
  const AdvancedPlayer({
    super.key,
    this.width,
    this.height,
    required this.videoUrl,
    this.video720Url,
    this.video1080Url,
    this.subtitleUrl,
  });

  final double? width;
  final double? height;
  final String videoUrl;
  final String? video720Url;
  final String? video1080Url;
  final String? subtitleUrl;

  @override
  State<AdvancedPlayer> createState() => _AdvancedPlayerState();
}

class _AdvancedPlayerState extends State<AdvancedPlayer> {
  late BetterPlayerController _controller;

  @override
  void initState() {
    super.initState();
    final config = BetterPlayerConfiguration(
      autoPlay: false,
      looping: false,
      fit: BoxFit.contain,
      controlsConfiguration: BetterPlayerControlsConfiguration(
        enablePlaybackSpeed: true,
        enableSubtitles: widget.subtitleUrl != null,
        enableQualities: true,
        enablePip: true,
        playbackSpeedList: const [0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
      ),
    );

    final resolutions = <String, String>{
      '360p': widget.videoUrl,
    };
    if (widget.video720Url != null) resolutions['720p'] = widget.video720Url!;
    if (widget.video1080Url != null) resolutions['1080p'] = widget.video1080Url!;

    final dataSource = BetterPlayerDataSource(
      BetterPlayerDataSourceType.network,
      widget.videoUrl,
      resolutions: resolutions,
      subtitles: widget.subtitleUrl != null
          ? [
              BetterPlayerSubtitlesSource(
                type: BetterPlayerSubtitlesSourceType.network,
                urls: [widget.subtitleUrl!],
                name: 'English',
              ),
            ]
          : null,
    );

    _controller = BetterPlayerController(
      config,
      betterPlayerDataSource: dataSource,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width ?? double.infinity,
      height: widget.height ?? 250,
      child: BetterPlayer(controller: _controller),
    );
  }
}
```

## Common mistakes

- **Using the basic video_player package for features that better_player includes** — video_player is too low-level — you must build quality switching, speed controls, subtitles, and PiP from scratch. This is weeks of work that better_player provides out of the box. Fix: Use better_player when you need any advanced features. Only use video_player for the simplest possible playback.
- **Providing only one video resolution without quality options** — Users on slow connections get buffering on high-quality video, while users on fast connections get unnecessarily low quality. No one is satisfied. Fix: Encode videos in multiple resolutions (360p, 720p, 1080p) and provide all URLs in the resolutions map for user-selectable quality.
- **Not disposing the BetterPlayerController on page exit** — The video continues playing audio in the background and consuming memory. Multiple undisposed controllers eventually crash the app. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-build-a-custom-video-player-with-advanced-features-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-build-a-custom-video-player-with-advanced-features-in-flutterflow
