Implementing a Custom Player Theme in Better Player for Flutter

Introduction

In Flutter app development, incorporating videos into your application is a common requirement. To enhance the user experience and maintain visual consistency, it is essential to customize the appearance of video players. In this article, we will explore how to create a custom theme for the Better Player package in Flutter, enabling you to integrate and personalize video playback within your app seamlessly.

1. Getting Started with a Better Player

Before diving into customization, let’s first understand the Better Player package. Better Player is a feature-rich video player plugin for Flutter that offers various capabilities such as adaptive streaming, subtitles, and more. To begin, add the package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  better_player: ^x.x.x  # Replace with the latest version

Import the package into your Flutter project:

import 'package:better_player/better_player.dart';

2. Defining Custom Controls

Creating custom controls involves designing and implementing a user interface for media playback controls in applications. By customizing the controls, developers can tailor the appearance, behavior, and functionality to suit their specific needs. This process typically includes defining buttons, progress bars, time displays, volume controls, and other elements essential for media playback. Custom control builders empower developers to craft unique and immersive experiences for users, enhancing usability and visual appeal within media-intensive applications.

class CustomPlayerControl extends StatelessWidget {
  const CustomPlayerControl( {required this.controller, super.key});

  final BetterPlayerController controller;

  @override
  Widget build(BuildContext context) {
     // Add custom controls here
  }
}

This CustomPlayerControl comes to the top of the video player. Here we can add Play, Pause, Seek, Full Screen, Mute, etc. controls.

3. Define a Better Player Controller

Define a better player controller with the BetterPlayerConfiguration class. For a better player configuration, you should add a player theme and a custom controls builder.

BetterPlayerController(
   BetterPlayerConfiguration(
     // Other configurations 
     playerTheme: BetterPlayerTheme.custom,
     customControlsBuilder: (videoController, onPlayerVisibilityChanged) =>
       CustomPlayerControl(controller: videoController),
     ),
   ),
   betterPlayerDataSource: BetterPlayerDataSource(
     BetterPlayerDataSourceType.network,
     'VIDEO_URL'),
);

In the player theme property, you should give BetterPlayerTheme.custom, and in the customControlsBuilder function, return the custom control widget.

4. Define a Better Player Video Widget

Define the Video player widget in the build method.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      title: Text(widget.title),
    ),
    body: Center(
      child: AspectRatio(
        aspectRatio: 16 / 9,
        child: BetterPlayer(controller: _videoController),
      ),
    ),
  );
}

5. Result

Better Played Custom Theme

6. Code

custom_player_control.widget.dart

import 'package:better_player/better_player.dart';
import 'package:bp_custom_theme/video_scrubber.widget.dart';
import 'package:flutter/material.dart';

class CustomPlayerControl extends StatelessWidget {
  const CustomPlayerControl({required this.controller, super.key});

  final BetterPlayerController controller;

  void _onTap() {
    controller.setControlsVisibility(true);
    if (controller.isPlaying()!) {
      controller.pause();
    } else {
      controller.play();
    }
  }

  void _controlVisibility() {
    controller.setControlsVisibility(true);
    Future.delayed(const Duration(seconds: 3))
        .then((value) => controller.setControlsVisibility(false));
  }

  String _formatDuration(Duration? duration) {
    if (duration != null) {
      String minutes = duration.inMinutes.toString().padLeft(2, '0');
      String seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
      return '$minutes:$seconds';
    } else {
      return '00:00';
    }
  }

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: _controlVisibility,
      child: StreamBuilder(
        initialData: false,
        stream: controller.controlsVisibilityStream,
        builder: (context, snapshot) {
          return Stack(
            children: [
              Visibility(
                visible: snapshot.data!,
                child: Positioned(
                  child: Center(
                    child: FloatingActionButton(
                      onPressed: _onTap,
                      backgroundColor: Colors.black.withOpacity(0.7),
                      child: controller.isPlaying()!
                          ? const Icon(
                              Icons.pause,
                              color: Colors.white,
                              size: 40,
                            )
                          : const Icon(
                              Icons.play_arrow_rounded,
                              color: Colors.white,
                              size: 50,
                            ),
                    ),
                  ),
                ),
              ),
              Positioned(
                left: 10,
                right: 10,
                bottom: 8,
                child: ValueListenableBuilder(
                  valueListenable: controller.videoPlayerController!,
                  builder: (context, value, child) {
                    return Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          crossAxisAlignment: CrossAxisAlignment.center,
                          children: [
                            Container(
                              height: 36,
                              width: 100,
                              alignment: Alignment.center,
                              decoration: BoxDecoration(
                                borderRadius: BorderRadius.circular(50),
                                shape: BoxShape.rectangle,
                                color: Colors.black.withOpacity(0.5),
                              ),
                              child: Text(
                                '${_formatDuration(value.position)}/${_formatDuration(value.duration)}',
                                style: const TextStyle(color: Colors.white),
                              ),
                            ),
                            IconButton(
                              onPressed: () async {
                                controller.toggleFullScreen();
                              },
                              icon: const Icon(
                                Icons.crop_free_rounded,
                                size: 22,
                                color: Colors.white,
                              ),
                            )
                          ],
                        ),
                        VideoScrubber(
                          controller: controller,
                          playerValue: value,
                        )
                      ],
                    );
                  },
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

video_scrubber.widget.dart

import 'package:better_player/better_player.dart';
import 'package:flutter/material.dart';

class VideoScrubber extends StatefulWidget {
  const VideoScrubber(
      {required this.playerValue, required this.controller, super.key});
  final VideoPlayerValue playerValue;
  final BetterPlayerController controller;

  @override
  VideoScrubberState createState() => VideoScrubberState();
}

class VideoScrubberState extends State<VideoScrubber> {
  double _value = 0.0;

  @override
  void initState() {
    super.initState();
  }

  @override
  void didUpdateWidget(covariant VideoScrubber oldWidget) {
    super.didUpdateWidget(oldWidget);
    int position = oldWidget.playerValue.position.inSeconds;
    int duration = oldWidget.playerValue.duration?.inSeconds ?? 0;
    setState(() {
      _value = position / duration;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return SliderTheme(
      data: SliderTheme.of(context).copyWith(
          thumbShape: CustomThumbShape(), // Custom thumb shape
          overlayShape: SliderComponentShape.noOverlay),
      child: Slider(
        value: _value,
        inactiveColor: Colors.grey,
        min: 0.0,
        max: 1.0,
        onChanged: (newValue) {
          setState(() {
            _value = newValue;
          });
          final newProgress = Duration(
              milliseconds: (_value *
                      widget.controller.videoPlayerController!.value.duration!
                          .inMilliseconds)
                  .toInt());
          widget.controller.seekTo(newProgress);
        },
      ),
    );
  }
}

class CustomThumbShape extends SliderComponentShape {
  final double thumbRadius = 6.0;

  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) {
    return Size.fromRadius(thumbRadius);
  }

  @override
  void paint(
    PaintingContext context,
    Offset center, {
    required Animation<double> activationAnimation,
    required Animation<double> enableAnimation,
    required bool isDiscrete,
    required TextPainter labelPainter,
    required RenderBox parentBox,
    required SliderThemeData sliderTheme,
    required TextDirection textDirection,
    required double value,
    required double textScaleFactor,
    required Size sizeWithOverflow,
  }) {
    final canvas = context.canvas;
    final fillPaint = Paint()
      ..color = sliderTheme.thumbColor!
      ..style = PaintingStyle.fill;

    canvas.drawCircle(center, thumbRadius, fillPaint);
  }
}

GitHub Repository Link: https://github.com/JenishMS/bp_custom_theme

Feel free to reach out if you have some queries or encounter any problems with this code. Also, let me know how you like this article, as feedback.

7. Conclusion

The Better Player package offers a powerful and customizable video player solution for Flutter applications. By creating a custom theme, you can tailor the appearance of the player controls, progress bar, captions, and more to match your app’s design language. This level of customization elevates the user experience and ensures visual consistency throughout your application. So go ahead, unleash your creativity, and enhance your Flutter app’s video playback with the Better Player package’s custom theme capabilities.

Thank you, feedback is appreciated!