Implementing a Custom Theme for Better Player in Flutter
Tags: flutter, appdevelopment, fluttervideoplayer, videoplayer
Published: 2024-01-19 10:57:37

## 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:
![Implementing a Custom Player Theme in Better Player for Flutter | Jenish MS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e886h1yoyyrvzxw3sh23.png)

## 6. Code:
**custom_player_control.page**

```
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.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](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!