Video Playback in Flutter with the video_player Package
video_player 2.14.1 plays inline video on Android, iOS, macOS and web — here is how to build a controller, drive playback, and handle its platform quirks.
Published on • November 5, 2026
AI Assistant

Dropping a video into a Flutter app sounds trivial until you discover the official plugin hands you a bare rendering surface with no controls, no error UI, and a different native player behind every platform. video_player is that official plugin, currently at version 2.14.1 and published by the verified flutter.dev publisher as a Flutter Favorite.
It is a federated plugin, so the code you import is only the front end: playback itself is implemented by video_player_android (^2.11.0), video_player_avfoundation (^2.11.0) and video_player_web (^2.1.0), which arrive automatically as dependencies.
One API, four platform backends
The published readme states the support matrix plainly: Android SDK 24+, iOS 13.0+, macOS 10.15+ and web. Which player sits underneath decides what formats actually play:
- iOS and macOS use Apple’s
AVPlayer, so supported formats followAVURLAsset.audiovisualTypesfor that OS version. - Android uses
ExoPlayer, whose supported-formats list covers HLS, DASH, SmoothStreaming and progressive MP4. - Web depends on the user’s browser and version — check video_player_web for specifics.
HTTP playback needs platform permission first: NSAppTransportSecurity entries in ios/Runner/Info.plist for plain http URLs, android.permission.INTERNET in the Android manifest for network videos, and the com.apple.security.network.client entitlement on macOS.
Creating and initializing a controller
VideoPlayerController has five named constructors: .networkUrl(Uri) (the Uri-based form, alongside the older .network(String)), .file, .asset and .contentUri. Every one of them requires an explicit await initialize() — there is no autoInitialize flag on the video_player controller itself, so initialization and its failures are yours to handle:
import 'package:video_player/video_player.dart';
class _VideoScreenState extends State<VideoScreen> {
late final VideoPlayerController _controller;
Object? _initError;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.networkUrl(
Uri.parse('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'),
httpHeaders: const {'Referer': 'https://blog.redlinesoft.net'},
);
_initialize();
}
Future<void> _initialize() async {
try {
await _controller.initialize();
setState(() {}); // first frame is ready to draw
} catch (e) {
setState(() => _initError = e); // bad URL, codec, or network failure
}
}
@override
void dispose() {
_controller.dispose(); // releases the native player
super.dispose();
}
}
The httpHeaders argument applies to network sources only, and formatHint (an Android-only override for format detection) is passed to .networkUrl the same way. Always dispose the controller — after dispose() all further calls are ignored.
Driving playback and reading state
Playback methods are all async and operate on the native player: play(), pause(), seekTo(Duration), setLooping(bool), setVolume(double) (0.0–1.0), setPlaybackSpeed(double) (for example 2.0 for double speed, subject to platform limits), and setPreventsDisplaySleepDuringVideoPlayback(bool).
State lives in a VideoPlayerValue exposed as controller.value — a ChangeNotifier you can subscribe to:
_controller.addListener(() {
final value = _controller.value;
if (value.hasError) {
showSnack('Playback failed: ${value.errorDescription}');
} else if (value.isCompleted) {
onVideoEnded(); // reached the last frame
} else if (value.isBuffering) {
showBufferingSpinner();
}
progress.value = value.position / value.duration; // for a seek bar
});
Useful fields include isInitialized, isPlaying, isLooping, isBuffering, isCompleted, position, duration, aspectRatio, volume, playbackSpeed and the buffered ranges.
Rendering: the built-in widget is only a surface
The plugin ships exactly one widget, VideoPlayer(controller), which draws the decoded frames. You still have to size it and supply every button yourself:
child: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: const Center(child: CircularProgressIndicator()),
For production UI, chewie at version 1.17.4 wraps video_player in MaterialControls, CupertinoControls and MaterialDesktopControls. Its ChewieController adds the conveniences video_player omits, including autoInitialize: true, autoPlay, looping, startAt, subtitles and an errorBuilder. Chewie is explicitly a UI layer only — per its readme, any PlatformException thrown during playback belongs to video_player.
Platform caveats worth planning for
On web there is no dart:io, so VideoPlayerController.file throws UnimplementedError; VideoPlayerOptions.mixWithOthers is silently ignored in browsers. HLS behaves like a native feature on mobile — AVPlayer and ExoPlayer both handle it — while on web it only works where the browser itself supports it. Finally, controllers accept a viewType argument: VideoViewType.textureView by default, or VideoViewType.platformView when a platform view is required, with performance and correctness trade-offs that vary by platform.
Takeaway: Treat video_player as a reliable playback engine rather than a player UI: initialize with error handling, drive it through controller.value, and pair it with chewie (or your own controls) while remembering that web, HLS and file sources each have their own constraints.