Embedding and controlling videos in a Flutter app is a core part of building interactive and modern mobile experiences. Many apps rely on video playback to explain features, showcase products, or deliver learning content for uninterrupted integration.

When video elements work reliably, the interface feels more natural, and users stay engaged with the flow of the app. This tutorial focuses on how Flutter handles video playback and why understanding these fundamentals helps developers build stable and consistent media-driven features.

Prerequisites

To follow along, have these ready on your computer:

  • Flutter is installed and set up, with an editor like Android Studio or VS Code.
  • A basic grasp of Dart code and how Flutter screens work.
  • A video file on a server, linked by an HTTPS address.
  • The video_player package in your project, at least version 2.6.1.

Starting Your Flutter Video Project

Begin by thinking about your video source and make sure it"s secure and reachable over HTTPS. Videos hosted on unsecured or slow servers will eventually lead to a poor playback experience.

Embedding a Video in Your App

Embedding a video in your Flutter app begins with placing the player where it fits naturally in the screen layout, helping the viewer stay connected to the flow of the content. This step allows the app to control and display the video reliably once it is properly embedded in the widget tree.

Refer to this document for embedding a video: How to Embed Videos in Flutter Apps?

Playing the Video

Use a StatefulWidget with VideoPlayerController. Here's robust control logic implementing play/pause toggle, 10-second rewind, and forward with safe clamping to video duration boundaries:

code
void _seekRelative(Duration offset) {
final position = _controller.value.position;
final duration = _controller.value.duration;
Duration target = position + offset;
if (target < Duration.zero) target = Duration.zero;
if (target > duration) target = duration;
_controller.seekTo(target);
}

Use this for rewind and forward buttons instead of naive arithmetic to avoid errors.

Controlling the Video Playback

The video_player package supports volume control internally but does not override device volume. To add mute functionality, toggle volume between 0.0 and 1.0:

code
bool isMuted = false;

IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
onPressed: () {
setState(() {
isMuted = !isMuted;
_controller.setVolume(isMuted ? 0.0 : 1.0);
});
},
);

Now, rewind or fast-forward by 10 seconds. The play button changes to pause when playing. Test it: Play the video, skip ahead, and see the time jump. For volume, add a slider. Import another package if needed, but for now, keep it simple. You can mute by setting the volume to zero:

code
_controller.setVolume(0.0);

Add a mute button in the Row. Tap it to silence the video, tap again to bring sound back.

Enhanced User Experience Notes

Buffering: Listen for player buffering states to show visual indicators. You can use _controller.value.isBuffering for this.

Error Handling: In addition to .catchError, check _controller.value.hasError during build and show appropriate UI.

Screen Rotation: Test on device orientation changes to ensure the video resizes gracefully with AspectRatio and MediaQuery.

Fullscreen: For an immersive experience, consider implementing fullscreen mode with SystemChrome.setPreferredOrientations and adjusting UI accordingly.

Performance: Dispose of the controller properly in dispose() to prevent memory leaks.

Device Compatibility: Test on real devices because different platforms have quirks in video playback and volume handling.

Example: Video playback column with controls

code
Column(
children: [
Expanded(
child: _controller.value.hasError
? Center(child: Text('Video failed to load'))
: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: Center(child: CircularProgressIndicator()),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.replay_10),
onPressed: () => _seekRelative(Duration(seconds: -10)),
),
IconButton(
icon: Icon(_controller.value.isPlaying ? Icons.pause : Icons.play_arrow),
onPressed: () {
setState(() {
_controller.value.isPlaying ? _controller.pause() : _controller.play();
});
},
),
IconButton(
icon: Icon(Icons.forward_10),
onPressed: () => _seekRelative(Duration(seconds: 10)),
),
IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up),
onPressed: () {
setState(() {
isMuted = !isMuted;
_controller.setVolume(isMuted ? 0.0 : 1.0);
});
},
),
],
),
],
)

Handling Video Issues

Videos might not load every time. Add error checks. In initState, after initialization:

code
..initialize().then((_) {
setState(() {});
}).catchError((error) {
print('Video error: $error');
});

In the build, show a message if there's an error:

code
child: _controller.value.hasError
? Text('Video failed to load')
: _controller.value.isInitialized
? AspectRatio(...)
: CircularProgressIndicator(),

If the video buffers, it pauses automatically. For uninterrupted play, please ensure that your internet connection is fast. Test on a phone to see real behavior.