Implementing a Custom Theme for Better Player in Flutter

jenishms

Jenish MS

Posted on January 19, 2024

Implementing a Custom Theme for Better Player in 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
Enter fullscreen mode Exit fullscreen mode

Import the package into your Flutter project:

import 'package:better_player/better_player.dart';
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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'),
);
Enter fullscreen mode Exit fullscreen mode

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),
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

5. Result:

Implementing a Custom Player Theme in Better Player for Flutter | Jenish MS

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,
                        )
                      ],
                    );
                  },
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
jenishms
Jenish MS

Posted on January 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related