Video Player

Flutter Video Player: How to Add Video Playback to Your Flutter App

16 min read
Flutter Video Player
Reading Time: 11 minutes

Flutter doesn’t ship with a video widget. There’s no Video() in the framework next to Image() and Text().

What you get instead is a plugin. The official Flutter video player is a package called video_player, maintained by the Flutter team, and it hands the actual decoding off to each platform’s native player.

That design is why so many Reddit threads ask whether Flutter can play video at all. It can. But the plugin is deliberately bare, and the community packages built on top of it (or around it) have gone through a lot of churn.

Most tutorials still describe that churn as it looked in 2023. As of September 2026, video_player 2.14 lets you pick HLS quality levels yourself, and better_player is actively maintained again. Below is working code for MP4 and HLS playback, plus fixes for the problems developers hit most.

What Is a Flutter Video Player?

A Flutter video player is a plugin that renders video frames inside a Flutter widget tree by wrapping a native media engine on each platform and exposing it through a Dart controller.

The official one is the video_player package. It’s published by flutter.dev, carries the Flutter Favorite badge, and pulls in about 3.3 million downloads a month.

Flutter draws its own UI, but it doesn’t decode H.264 or parse an m3u8 playlist. That work goes to the operating system’s player, and the plugin pipes the decoded frames back into Flutter as a texture.

Platform Native engine under video_player Minimum version
Android ExoPlayer (Media3) SDK 24 (Android 7.0)
iOS AVPlayer iOS 13.0
macOS AVPlayer macOS 10.15
Web HTML <video> element Any modern browser
Windows / Linux Not supported by the official plugin –

That table explains most Flutter video player behavior. Format support, DRM, HLS quirks, and codec limits all come from the engine underneath, not from Flutter.

If a video won’t play on iOS, it’s usually because AVPlayer can’t handle it. If it plays on Android and not in Chrome, the browser is the problem.

How video_player works

Every video_player setup has two pieces:

  • VideoPlayerController: owns the native player. You create it with a source, call initialize(), then control playback with play(), pause(), seekTo(), setVolume(), setLooping(), and setPlaybackSpeed().
  • VideoPlayer widget: displays the controller’s output. It has no controls, no progress bar, and no play button.

The controller exposes a VideoPlayerValue with the current position, duration, buffered ranges, aspect ratio, and error state. It’s a ValueNotifier, so you can rebuild UI whenever playback state changes.

You can create a controller from four sources:

  • VideoPlayerController.asset() for bundled files
  • VideoPlayerController.networkUrl() for HTTP(S) URLs, including HLS and DASH
  • VideoPlayerController.file() for files on the device (not available on web)
  • VideoPlayerController.contentUri() for Android content URIs

Flutter Video Player Packages Compared

The official plugin gives you playback. It doesn’t give you a UI, DRM, caching, or desktop support. That’s where the other packages come in.

Here’s how the main options compare today:

Package Latest version Engine Built-in controls HLS / DASH DRM Platforms
video_player 2.14.0 ExoPlayer, AVPlayer, HTML video No Yes (native) No Android, iOS, macOS, web
chewie 1.17.2 video_player Material, Cupertino, desktop Via video_player No Android, iOS, web
media_kit 1.2.6 libmpv Yes Yes No Android, iOS, macOS, Windows, Linux, web
better_player 1.14.0 ExoPlayer, AVPlayer, Shaka Yes Yes Widevine, FairPlay, ClearKey Android, iOS, web

Versions were checked against pub.dev on September 25, 2026.

video_player: the official plugin

Start here unless you have a specific reason not to. It’s the most downloaded Flutter video player package by a wide margin, it’s maintained by Google, and it tracks new Flutter releases closely. Version 2.14.0 requires Flutter 3.44 or later.

Recent releases filled in gaps that used to push developers to other packages:

  • 2.10.0 added optional platform views on Android and iOS
  • 2.11.0 added audio track selection with getAudioTracks() and selectAudioTrack()
  • 2.12.0 added backBufferDurationMs for tuning how much played video stays in memory
  • 2.14.0 added video quality selection for HLS and DASH streams

The catch: no controls. You build the play button, scrubber, and full screen toggle yourself.

chewie: controls on top of video_player

Chewie is a UI layer. It takes a VideoPlayerController and wraps it in Material or Cupertino controls, with full screen, playback speed, subtitles, and chapters.

Because it sits on video_player, you inherit that plugin’s format support and bugs. Chewie’s own README says so plainly: PlatformExceptions from playback belong to video_player, not chewie.

That’s the right tradeoff for most apps. You get a finished UI in about 10 lines of code and keep the official engine underneath.

media_kit: one engine everywhere

media_kit replaces the native players with libmpv, the engine behind the mpv desktop player. Every platform runs the same decoder, so behavior is consistent across Android, iOS, macOS, Windows, Linux, and web.

That’s its main draw. It’s the usual recommendation on Flutter forums for a Flutter video player on Windows or Linux, and teams building TikTok-style feeds like that one Player instance can open a new source without being disposed.

The costs: a bigger app bundle (libmpv ships with your app), no DRM, and a split package setup. You add media_kit, media_kit_video, and media_kit_libs_video separately. The media_kit repository is still active, with commits as recent as August 2026, though the last pub.dev release was December 2025.

better_player: back from the dead

Between mid-2022 and mid-2026, better_player shipped exactly one release (0.0.84, in June 2024). Most guides told you to avoid it, and forks like better_player_plus kept it alive.

That changed in August 2026. The original author shipped 0.1.0 on August 8 and 1.0.0 on August 26, with more than 20 releases since, reaching 1.14.0 on September 24. It’s now independent of video_player.

It’s the only mainstream Flutter video player with built-in DRM (Widevine, FairPlay, ClearKey), plus HLS and DASH track selection, caching, picture-in-picture, playlists, and RTSP on Android.

The honest caveat: the 1.x line is a month old and changing fast. It requires Flutter 3.47. Pin your version and read the changelog before upgrading.

Other packages

A few more show up in searches:

  • fvp swaps the video_player backend for libmdk and adds Windows and Linux support without changing your Dart code
  • video_player_win adds Windows support through Media Foundation
  • flick_video_player and pod_player haven’t had a release since mid-2024

For a YouTube link, you need a YouTube-specific package like youtube_player_iframe. YouTube doesn’t expose direct video URLs, so no general Flutter video player can play one.

How to Build a Flutter Video Player with video_player

Here’s a working player that loads a network video, shows a progress bar, and toggles play and pause. It follows the same pattern as the Flutter play and pause cookbook, with error handling added.

1. Add the package

flutter pub add video_player

2. Add platform permissions

For network video on Android, add the internet permission to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET"/>

On iOS, HTTPS URLs work with no changes. You only need NSAppTransportSecurity entries in ios/Runner/Info.plist if you load plain http URLs.

On macOS, add the com.apple.security.network.client entitlement.

3. Create and initialize the controller

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

class VideoScreen extends StatefulWidget {
  const VideoScreen({super.key, required this.url});

  final String url;

  @override
  State<VideoScreen> createState() => _VideoScreenState();
}

class _VideoScreenState extends State<VideoScreen> {
  late final VideoPlayerController _controller;
  late final Future<void> _initialize;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.url));
    _initialize = _controller.initialize();
    _controller.setLooping(true);
  }

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

Two rules matter here.

Create the controller in initState(), never in build(). Every rebuild would spin up a new native player.

Always call dispose(). A leaked controller keeps a hardware decoder and GPU texture alive, and phones have a limited number of both.

4. Display the video with the right aspect ratio

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<void>(
        future: _initialize,
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return const Center(child: CircularProgressIndicator());
          }
          if (_controller.value.hasError) {
            return Center(child: Text(_controller.value.errorDescription ?? 'Playback error'));
          }
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller),
              ),
              VideoProgressIndicator(_controller, allowScrubbing: true),
            ],
          );
        },
      ),
      floatingActionButton: ValueListenableBuilder<VideoPlayerValue>(
        valueListenable: _controller,
        builder: (context, value, _) => FloatingActionButton(
          onPressed: () => value.isPlaying ? _controller.pause() : _controller.play(),
          child: Icon(value.isPlaying ? Icons.pause : Icons.play_arrow),
        ),
      ),
    );
  }
}

Wrapping VideoPlayer in AspectRatio is what prevents stretched or squashed video. The widget fills whatever space it’s given, so without the wrapper a 16:9 video will distort to match its parent.

ValueListenableBuilder rebuilds only the button when playback state changes, not the whole screen. The controller updates its position every 100 ms, so rebuilding the full tree on every tick adds up.

How to Add Controls and Full Screen with Chewie

If you don’t want to build controls by hand, chewie adds them on top of the controller you already have.

flutter pub add chewie
import 'package:chewie/chewie.dart';

late final ChewieController _chewieController;

Future<void> _setUp() async {
  await _controller.initialize();
  _chewieController = ChewieController(
    videoPlayerController: _controller,
    autoPlay: true,
    looping: false,
    allowFullScreen: true,
    playbackSpeeds: const [0.5, 1.0, 1.5, 2.0],
  );
  setState(() {});
}

// In build():
Chewie(controller: _chewieController);

// In dispose():
_chewieController.dispose();
_controller.dispose();

Chewie picks Material controls on Android and Cupertino controls on iOS by default. It handles the full screen route, orientation, and system UI for you, which is the part most developers get wrong when they build it themselves.

Dispose both controllers. Chewie doesn’t dispose the VideoPlayerController it wraps.

How to Play HLS and m3u8 Streams in Flutter

For anything longer than a short clip, you’ll want HLS streaming instead of a single MP4. HLS splits video into short segments at several quality levels, and the player switches levels as network conditions change.

video_player plays HLS natively on Android and iOS. Pass the .m3u8 URL to networkUrl():

_controller = VideoPlayerController.networkUrl(
  Uri.parse('https://example.com/live/stream/master.m3u8'),
  formatHint: VideoFormat.hls,
);

The formatHint helps ExoPlayer when the URL doesn’t end in .m3u8, which is common with signed or tokenized URLs.

On the web, HLS support depends on the browser. Safari plays it natively. Other browsers may need a JavaScript player like hls.js, and at that point many teams render an HTML player through a platform view instead.

Adaptive quality selection in video_player 2.14

By default, the native engine handles adaptive bitrate streaming on its own. It picks a rendition based on bandwidth and screen size.

Version 2.14.0 added manual control, so you can build a quality menu:

Future<void> pickQuality(int targetHeight) async {
  if (!_controller.isVideoTrackSupportAvailable()) return;

  final tracks = await _controller.getVideoTracks();
  if (tracks.isEmpty) return;

  final match = tracks.firstWhere(
    (t) => t.height == targetHeight,
    orElse: () => tracks.first,
  );
  await _controller.selectVideoTrack(match);
}

// Hand control back to automatic switching:
await _controller.selectVideoTrack(null);

Each VideoTrack carries a label, bitrate, width, height, frame rate, and codec. Under the hood, Android uses an ExoPlayer track selection override and iOS sets preferredPeakBitRate on the player item.

Know the limits before you ship it:

  • iOS 13 and 14 return an empty list, because the underlying AVFoundation API needs iOS 15
  • Web throws UnimplementedError, so always check isVideoTrackSupportAvailable() first
  • Single-file MP4s may return one track or none

Live streams

The same code plays live HLS. The main differences show up in the UI: duration can be zero or keep growing, and a seek bar makes less sense.

Latency is set by the stream, not the player. Standard HLS runs 6 to 30 seconds behind real time. If you need faster, look at low-latency HLS, which gets closer to 2 to 5 seconds when both server and player support it.

Protected content

If your content needs DRM, video_player can’t help. Your options are better_player, which supports Widevine and FairPlay, or a commercial player SDK. Either way, you’ll need a multi-DRM setup on the server side, because Android and iOS use different DRM systems.

Flutter Video Player Performance: Feeds and Multiple Players

A single player on a detail screen rarely causes trouble. Problems start when you put video in a scrolling list.

Each VideoPlayerController holds a native player, a hardware decoder, and a GPU texture. Android devices support a limited number of hardware decoders at once, often somewhere between 5 and 16 depending on the chip. Exceed that and playback fails, sometimes silently.

For a feed or a multiple video player layout, follow these patterns:

  • Keep a small pool of controllers. Three or four is usually enough: the current video, one or two ahead, one behind.
  • Dispose controllers that leave the pool. Don’t keep one per list item.
  • Mute instead of pausing neighbors. A paused player often has to rebuffer when it resumes. A muted one stays warm.
  • Preload the next video by initializing its controller before it scrolls into view.
  • Use visibility_detector or a PageView callback to decide which video plays.

video_player ties one controller to one source, so pooling means creating and disposing controllers as the user scrolls. media_kit lets you call open() on an existing Player with a new URL, which is why feed-heavy apps often pick it.

Encoding matters as much as player code.

Short clips under 30 seconds start faster as plain MP4. Longer content is better as HLS with several renditions, so the player can drop to a lower bitrate instead of stalling. For more on this, see how to avoid buffering.

Common Flutter Video Player Problems and Fixes

These are the issues that show up over and over in GitHub and Stack Overflow.

Problem Likely cause Fix
Black screen Widget built before initialize() finished, or size is zero Show the VideoPlayer only after isInitialized is true, and wrap it in AspectRatio
Works in debug, not in release (Android) Missing INTERNET permission in the main manifest Add it to src/main/AndroidManifest.xml, not only src/debug
Nothing plays on iOS Plain http URL blocked by App Transport Security Use HTTPS, or add an ATS exception in Info.plist
UnimplementedError on web VideoPlayerController.file() isn’t supported on web Use networkUrl() or asset()
Video stretched No aspect ratio wrapper, or anamorphic content Wrap in AspectRatio; update video_player_android to 2.12.2+ for anamorphic fixes
Plays on Android, not iOS Codec AVPlayer doesn’t support (VP9, WebM) Encode to H.264 or HEVC in an MP4 or HLS container
Constant buffering Single high-bitrate file on a slow network Serve HLS with multiple renditions and a CDN
Video plays in background App lifecycle not handled Pause in didChangeAppLifecycleState, or set allowBackgroundPlayback deliberately

When in doubt, read _controller.value.errorDescription. The native engine usually tells you exactly what failed.

Where Your Flutter App’s Video Comes From

Everything so far covers the playback side. Every package above, from video_player to better_player, expects you to hand it a URL.

That URL has to come from somewhere. For a production app, that means ingest, transcoding into several renditions, packaging as HLS, storage, and delivery through a CDN for video streaming. The player can only switch quality levels that your backend actually produced.

This is the part most Flutter tutorials skip. It’s also where most of the engineering time goes when you build a video streaming app from scratch.

Using LiveAPI as the backend

LiveAPI handles the server side and gives your Flutter app an HLS URL to play.

For on-demand content, the LiveAPI video API accepts uploads of any size, encodes them instantly into adaptive bitrate renditions, and returns an HLS playback URL. Videos are playable within seconds of upload.

For live content, the live streaming API accepts RTMP or SRT from any encoder, including OBS or a mobile app, and outputs HLS in up to 4K. Delivery runs across Akamai, Cloudflare, and Fastly. Streams can be recorded automatically as live-to-VOD files.

On the Flutter side, the code doesn’t change:

final playbackUrl = await myBackend.getPlaybackUrl(videoId); // HLS URL from LiveAPI
_controller = VideoPlayerController.networkUrl(
  Uri.parse(playbackUrl),
  formatHint: VideoFormat.hls,
);
await _controller.initialize();

Because the stream already carries multiple renditions, getVideoTracks() returns real options for your quality menu, and ExoPlayer and AVPlayer can adapt on their own when the network drops.

Which Flutter Video Player Package Should You Use?

Match the package to your requirements:

  • Pick video_player if you need Android, iOS, and web playback, you’re fine building your own controls, and you want the package least likely to break on a Flutter upgrade.
  • Add chewie if you want finished Material or Cupertino controls and full screen without writing them.
  • Pick media_kit if you ship on Windows or Linux, need identical behavior on every platform, or you’re building a scrolling video feed.
  • Pick better_player if you need DRM, offline caching, or picture-in-picture, and you can live with a young, fast-moving 1.x release line.
  • Look at a commercial SDK if you need analytics, ads, or casting with vendor support.

Most apps end up with video_player plus chewie. It’s also the setup you’ll find the most answers for when something breaks.

If you’re comparing across frameworks, the same tradeoffs show up in React video players and in React Native video: a thin official layer over native engines, with community packages adding UI and features.

Flutter Video Player FAQ

What is the best video player for Flutter?

For most apps, the official video_player package paired with chewie for controls. It’s maintained by the Flutter team and supports Android, iOS, macOS, and web. Pick media_kit for desktop or feed apps, and better_player if you need DRM.

Can Flutter play video?

Yes, through plugins. The framework has no built-in video widget, but the official video_player plugin renders video inside the widget tree using ExoPlayer on Android, AVPlayer on iOS and macOS, and the HTML video element on web.

How do I play a video from a URL in Flutter?

Create a VideoPlayerController.networkUrl(Uri.parse(url)), call initialize(), and show a VideoPlayer widget inside an AspectRatio once isInitialized is true. On Android, add the internet permission to your manifest.

Does the Flutter video player support HLS and m3u8?

Yes. video_player plays HLS natively on Android and iOS: pass the .m3u8 URL to networkUrl(), and set formatHint: VideoFormat.hls if the URL doesn’t end in .m3u8. On web, support depends on the browser.

How do I make a Flutter video player full screen?

The quickest way is chewie, which handles the full screen route, orientation lock, and system UI. With plain video_player, push a new route containing the VideoPlayer, set landscape orientation with SystemChrome.setPreferredOrientations, and hide system overlays.

Why is my Flutter video player showing a black screen?

Usually the VideoPlayer widget is built before the controller finished initializing, or it has no size. Wait for initialize() to complete, check value.isInitialized, and wrap the widget in AspectRatio. Also check value.errorDescription for codec or network errors.

Can I play YouTube videos with video_player?

No. YouTube doesn’t provide direct video file URLs, so general players can’t load them. Use a package built for YouTube, such as youtube_player_iframe, which embeds YouTube’s own player.

Does video_player work on Windows and Linux?

Not officially. The endorsed implementations cover Android, iOS, macOS, and web. For desktop, use media_kit, or add fvp or video_player_win as a backend so your existing video_player code runs there.

Is better_player still maintained?

Yes, again. After years of near-silence, the original author released 1.0.0 in August 2026 and has shipped frequent updates since. The 1.x API differs from 0.0.84, so check the migration guide before upgrading an older project.

Build Your Flutter Video Player on a Solid Backend

A Flutter video player comes down to two choices: the package that handles playback and the backend that produces what it plays.

For playback, start with video_player and add chewie when you need controls. Reach for media_kit or better_player only when you hit a real limit: desktop, feeds, or DRM.

For the backend, you need encoding, adaptive renditions, and CDN delivery. Your player’s quality menu and buffering behavior both depend on it.

LiveAPI gives your Flutter app instant encoding, adaptive HLS output, live streaming from RTMP or SRT, and multi-CDN delivery, with pay-as-you-grow pricing. Get started with LiveAPI and have a playable HLS URL in your app today.

Join 200,000+ satisfied streamers

Still on the fence? Take a sneak peek and see what you can do with Castr.

No Castr Branding

No Castr Branding

We do not include our branding on your videos.

No Commitment

No Commitment

No contracts. Cancel or change your plans anytime.

24/7 Support

24/7 Support

Highly skilled in-house engineers ready to help.

  • Check Free 7-day trial
  • CheckCancel anytime
  • CheckNo credit card required

Related Articles