Implementing Custom Video and Audio Players in Flutter

Flutter provides excellent support for multimedia content, allowing developers to create sophisticated video and audio player applications. While the video_player and audioplayers packages offer a solid foundation, implementing custom video and audio players allows for more control over the UI, features, and overall user experience. This article explores how to implement custom video and audio players in Flutter, complete with detailed examples and best practices.

Why Create Custom Video and Audio Players?

Creating a custom player in Flutter offers several advantages:

  • Custom UI: Design the player with a unique look and feel that matches your app’s branding.
  • Specific Features: Implement only the necessary features (e.g., custom controls, playlists, or advanced playback options).
  • Performance Optimization: Tailor the player for specific video or audio formats and optimize for performance.
  • Integration: Seamlessly integrate the player with other app features and services.

Setting Up Your Flutter Project

Before diving into code, ensure you have Flutter installed and set up. Create a new Flutter project or use an existing one. Additionally, you will need the video_player (for video) and audioplayers (for audio) packages as a foundation.

Step 1: Add Dependencies

Add the required dependencies to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  video_player: ^2.8.2
  audioplayers: ^5.2.1

Run flutter pub get to install these packages.

Creating a Custom Video Player

Implementing a custom video player involves integrating the video_player package and building a custom UI.

Step 1: Initialize the Video Controller

Use VideoPlayerController to load and control the video source:

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

class CustomVideoPlayer extends StatefulWidget {
  final String videoUrl;

  CustomVideoPlayer({required this.videoUrl});

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

class _CustomVideoPlayerState extends State<CustomVideoPlayer> {
  late VideoPlayerController _controller;
  late Future _initializeVideoPlayerFuture;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
    _initializeVideoPlayerFuture = _controller.initialize();
  }

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

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _initializeVideoPlayerFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          return AspectRatio(
            aspectRatio: _controller.value.aspectRatio,
            child: VideoPlayer(_controller),
          );
        } else {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }
}

Step 2: Implement Custom Controls

Add custom controls for play/pause, seek, and volume:

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

class CustomVideoPlayerControls extends StatefulWidget {
  final VideoPlayerController controller;

  CustomVideoPlayerControls({required this.controller});

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

class _CustomVideoPlayerControlsState extends State<CustomVideoPlayerControls> {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black54,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          IconButton(
            icon: Icon(widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow),
            onPressed: () {
              setState(() {
                if (widget.controller.value.isPlaying) {
                  widget.controller.pause();
                } else {
                  widget.controller.play();
                }
              });
            },
          ),
          IconButton(
            icon: Icon(Icons.volume_up),
            onPressed: () {
              // Implement volume control
            },
          ),
        ],
      ),
    );
  }
}

Step 3: Combine Video and Controls

Integrate the video player and custom controls into a single widget:

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

class CompleteCustomVideoPlayer extends StatefulWidget {
  final String videoUrl;

  CompleteCustomVideoPlayer({required this.videoUrl});

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

class _CompleteCustomVideoPlayerState extends State<CompleteCustomVideoPlayer> {
  late VideoPlayerController _controller;
  late Future _initializeVideoPlayerFuture;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
    _initializeVideoPlayerFuture = _controller.initialize();
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        FutureBuilder(
          future: _initializeVideoPlayerFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              return AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller),
              );
            } else {
              return Center(
                child: CircularProgressIndicator(),
              );
            }
          },
        ),
        CustomVideoPlayerControls(controller: _controller),
      ],
    );
  }
}

Step 4: Implement Video Seekbar

Add a seekbar to control video playback position.

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

class CustomVideoPlayerSeekbar extends StatefulWidget {
  final VideoPlayerController controller;

  CustomVideoPlayerSeekbar({required this.controller});

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

class _CustomVideoPlayerSeekbarState extends State<CustomVideoPlayerSeekbar> {
  @override
  Widget build(BuildContext context) {
    return Slider(
      value: widget.controller.value.position.inSeconds.toDouble(),
      min: 0,
      max: widget.controller.value.duration.inSeconds.toDouble(),
      onChanged: (value) {
        setState(() {
          widget.controller.seekTo(Duration(seconds: value.toInt()));
        });
      },
    );
  }
}

Now integrate seekbar in CompleteCustomVideoPlayer


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

class CompleteCustomVideoPlayer extends StatefulWidget {
  final String videoUrl;

  CompleteCustomVideoPlayer({required this.videoUrl});

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

class _CompleteCustomVideoPlayerState extends State<CompleteCustomVideoPlayer> {
  late VideoPlayerController _controller;
  late Future _initializeVideoPlayerFuture;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
    _initializeVideoPlayerFuture = _controller.initialize();
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        FutureBuilder(
          future: _initializeVideoPlayerFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              return AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller),
              );
            } else {
              return Center(
                child: CircularProgressIndicator(),
              );
            }
          },
        ),
        CustomVideoPlayerSeekbar(controller: _controller),
        CustomVideoPlayerControls(controller: _controller),
      ],
    );
  }
}

Creating a Custom Audio Player

Implementing a custom audio player involves similar steps but uses the audioplayers package.

Step 1: Initialize the Audio Player

Create an instance of AudioPlayer and load the audio source:

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

class CustomAudioPlayer extends StatefulWidget {
  final String audioUrl;

  CustomAudioPlayer({required this.audioUrl});

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

class _CustomAudioPlayerState extends State<CustomAudioPlayer> {
  late AudioPlayer _audioPlayer;
  bool _isPlaying = false;

  @override
  void initState() {
    super.initState();
    _audioPlayer = AudioPlayer();
    _audioPlayer.setSourceUrl(widget.audioUrl);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Custom Audio Player'),
      ),
      body: Center(
        child: IconButton(
          icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
          onPressed: () async {
            if (_isPlaying) {
              await _audioPlayer.pause();
              setState(() {
                _isPlaying = false;
              });
            } else {
              await _audioPlayer.play(UrlSource(widget.audioUrl));
              setState(() {
                _isPlaying = true;
              });
            }
          },
        ),
      ),
    );
  }
}

Step 2: Implement Custom Audio Controls

Add controls for play/pause, seek, and volume:

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

class CustomAudioControls extends StatefulWidget {
  final AudioPlayer audioPlayer;

  CustomAudioControls({required this.audioPlayer});

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

class _CustomAudioControlsState extends State<CustomAudioControls> {
  bool _isPlaying = false;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black54,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          IconButton(
            icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white),
            onPressed: () async {
              if (_isPlaying) {
                await widget.audioPlayer.pause();
                setState(() {
                  _isPlaying = false;
                });
              } else {
                await widget.audioPlayer.play(UrlSource("URL"));
                setState(() {
                  _isPlaying = true;
                });
              }
            },
          ),
          IconButton(
            icon: Icon(Icons.volume_up, color: Colors.white),
            onPressed: () {
              // Implement volume control
            },
          ),
        ],
      ),
    );
  }
}

Advanced Features and Customization

To enhance your custom player, consider the following features:

  • Gestures: Implement swipe gestures for seeking.
  • Subtitles: Add support for subtitle files (.srt, .vtt).
  • Caching: Cache video or audio for offline playback.
  • Equalizer: Add an audio equalizer for customizing sound.
  • Playlist: Implement a playlist feature for continuous playback.

Best Practices for Custom Players

  • Memory Management: Always dispose of controllers to free up resources.
  • Error Handling: Handle loading errors and playback issues gracefully.
  • Performance Optimization: Optimize video and audio encoding for efficient playback.
  • Accessibility: Ensure the player is accessible to users with disabilities.

Conclusion

Creating custom video and audio players in Flutter offers a powerful way to tailor the user experience to your specific requirements. By leveraging the video_player and audioplayers packages and combining them with custom UI and features, you can create a media player that truly stands out. Whether it’s for a dedicated media app or integrating media playback into a broader application, understanding the principles and practices outlined in this article will empower you to build outstanding Flutter media players.