Skip to content
Blog

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 follow AVURLAsset.audiovisualTypes for 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.