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:
6. Code:
class BetterPlayerPage extends StatefulWidget {
const BetterPlayerPage({super.key, required this.title});
final String title;
@override
State<BetterPlayerPage> createState() => _BetterPlayerPageState();
}
class _BetterPlayerPageState extends State<BetterPlayerPage> {
late BetterPlayerController _videoController;
@override
void initState() {
super.initState();
_videoController = BetterPlayerController(
BetterPlayerConfiguration(
autoDispose: true,
controlsConfiguration: BetterPlayerControlsConfiguration(
controlsHideTime: const Duration(seconds: 1),
playerTheme: BetterPlayerTheme.custom,
customControlsBuilder:
(videoController, onPlayerVisibilityChanged) =>
CustomPlayerControl(controller: videoController),
),
aspectRatio: 16 / 9,
looping: true,
autoPlay: true),
betterPlayerDataSource: BetterPlayerDataSource(
BetterPlayerDataSourceType.network,
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'),
);
}
@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),
),
),
);
}
}
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
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!