{"id":1344,"date":"2026-09-25T11:57:39","date_gmt":"2026-09-25T04:57:39","guid":{"rendered":"https:\/\/liveapi.com\/blog\/flutter-video-player\/"},"modified":"2026-09-28T11:06:35","modified_gmt":"2026-09-28T04:06:35","slug":"flutter-video-player","status":"publish","type":"post","link":"https:\/\/liveapi.com\/blog\/flutter-video-player\/","title":{"rendered":"Flutter Video Player: How to Add Video Playback to Your Flutter App"},"content":{"rendered":"<span class=\"rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">Reading Time: <\/span> <span class=\"rt-time\">11<\/span> <span class=\"rt-label rt-postfix\">minutes<\/span><\/span><p>Flutter doesn&#8217;t ship with a video widget. There&#8217;s no <code>Video()<\/code> in the framework next to <code>Image()<\/code> and <code>Text()<\/code>.<\/p>\n<p>What you get instead is a plugin. The official Flutter video player is a package called <code>video_player<\/code>, maintained by the Flutter team, and it hands the actual decoding off to each platform&#8217;s native player.<\/p>\n<p>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.<\/p>\n<p>Most tutorials still describe that churn as it looked in 2023. As of September 2026, <code>video_player<\/code> 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.<\/p>\n<h2>What Is a Flutter Video Player?<\/h2>\n<p>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.<\/p>\n<p>The official one is <a href=\"https:\/\/pub.dev\/packages\/video_player\" target=\"_blank\" rel=\"nofollow noopener\">the video_player package<\/a>. It&#8217;s published by flutter.dev, carries the Flutter Favorite badge, and pulls in about 3.3 million downloads a month.<\/p>\n<p>Flutter draws its own UI, but it doesn&#8217;t decode H.264 or parse an <a href=\"https:\/\/liveapi.com\/blog\/what-is-m3u8\/\" target=\"_blank\" rel=\"noopener\">m3u8 playlist<\/a>. That work goes to the operating system&#8217;s player, and the plugin pipes the decoded frames back into Flutter as a texture.<\/p>\n<table>\n<thead>\n<tr>\n<th>Platform<\/th>\n<th>Native engine under <code>video_player<\/code><\/th>\n<th>Minimum version<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Android<\/strong><\/td>\n<td><a href=\"https:\/\/liveapi.com\/blog\/exoplayer\/\" target=\"_blank\" rel=\"noopener\">ExoPlayer (Media3)<\/a><\/td>\n<td>SDK 24 (Android 7.0)<\/td>\n<\/tr>\n<tr>\n<td><strong>iOS<\/strong><\/td>\n<td><a href=\"https:\/\/liveapi.com\/blog\/avplayer\/\" target=\"_blank\" rel=\"noopener\">AVPlayer<\/a><\/td>\n<td>iOS 13.0<\/td>\n<\/tr>\n<tr>\n<td><strong>macOS<\/strong><\/td>\n<td>AVPlayer<\/td>\n<td>macOS 10.15<\/td>\n<\/tr>\n<tr>\n<td><strong>Web<\/strong><\/td>\n<td>HTML <code>&lt;video&gt;<\/code> element<\/td>\n<td>Any modern browser<\/td>\n<\/tr>\n<tr>\n<td><strong>Windows \/ Linux<\/strong><\/td>\n<td>Not supported by the official plugin<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>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.<\/p>\n<p>If a video won&#8217;t play on iOS, it&#8217;s usually because AVPlayer can&#8217;t handle it. If it plays on Android and not in Chrome, the browser is the problem.<\/p>\n<h3>How video_player works<\/h3>\n<p>Every <code>video_player<\/code> setup has two pieces:<\/p>\n<ul>\n<li><strong><code>VideoPlayerController<\/code><\/strong>: owns the native player. You create it with a source, call <code>initialize()<\/code>, then control playback with <code>play()<\/code>, <code>pause()<\/code>, <code>seekTo()<\/code>, <code>setVolume()<\/code>, <code>setLooping()<\/code>, and <code>setPlaybackSpeed()<\/code>.<\/li>\n<li><strong><code>VideoPlayer<\/code> widget<\/strong>: displays the controller&#8217;s output. It has no controls, no progress bar, and no play button.<\/li>\n<\/ul>\n<p>The controller exposes a <code>VideoPlayerValue<\/code> with the current position, duration, buffered ranges, aspect ratio, and error state. It&#8217;s a <code>ValueNotifier<\/code>, so you can rebuild UI whenever playback state changes.<\/p>\n<p>You can create a controller from four sources:<\/p>\n<ul>\n<li><code>VideoPlayerController.asset()<\/code> for bundled files<\/li>\n<li><code>VideoPlayerController.networkUrl()<\/code> for HTTP(S) URLs, including HLS and DASH<\/li>\n<li><code>VideoPlayerController.file()<\/code> for files on the device (not available on web)<\/li>\n<li><code>VideoPlayerController.contentUri()<\/code> for Android content URIs<\/li>\n<\/ul>\n<h2>Flutter Video Player Packages Compared<\/h2>\n<p>The official plugin gives you playback. It doesn&#8217;t give you a UI, DRM, caching, or desktop support. That&#8217;s where the other packages come in.<\/p>\n<p>Here&#8217;s how the main options compare today:<\/p>\n<table>\n<thead>\n<tr>\n<th>Package<\/th>\n<th>Latest version<\/th>\n<th>Engine<\/th>\n<th>Built-in controls<\/th>\n<th>HLS \/ DASH<\/th>\n<th>DRM<\/th>\n<th>Platforms<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>video_player<\/strong><\/td>\n<td>2.14.0<\/td>\n<td>ExoPlayer, AVPlayer, HTML video<\/td>\n<td>No<\/td>\n<td>Yes (native)<\/td>\n<td>No<\/td>\n<td>Android, iOS, macOS, web<\/td>\n<\/tr>\n<tr>\n<td><strong>chewie<\/strong><\/td>\n<td>1.17.2<\/td>\n<td>video_player<\/td>\n<td>Material, Cupertino, desktop<\/td>\n<td>Via video_player<\/td>\n<td>No<\/td>\n<td>Android, iOS, web<\/td>\n<\/tr>\n<tr>\n<td><strong>media_kit<\/strong><\/td>\n<td>1.2.6<\/td>\n<td>libmpv<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<td>No<\/td>\n<td>Android, iOS, macOS, Windows, Linux, web<\/td>\n<\/tr>\n<tr>\n<td><strong>better_player<\/strong><\/td>\n<td>1.14.0<\/td>\n<td>ExoPlayer, AVPlayer, Shaka<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<td>Widevine, FairPlay, ClearKey<\/td>\n<td>Android, iOS, web<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Versions were checked against pub.dev on September 25, 2026.<\/p>\n<h3>video_player: the official plugin<\/h3>\n<p>Start here unless you have a specific reason not to. It&#8217;s the most downloaded Flutter video player package by a wide margin, it&#8217;s maintained by Google, and it tracks new Flutter releases closely. Version 2.14.0 requires Flutter 3.44 or later.<\/p>\n<p>Recent releases filled in gaps that used to push developers to other packages:<\/p>\n<ul>\n<li><strong>2.10.0<\/strong> added optional platform views on Android and iOS<\/li>\n<li><strong>2.11.0<\/strong> added audio track selection with <code>getAudioTracks()<\/code> and <code>selectAudioTrack()<\/code><\/li>\n<li><strong>2.12.0<\/strong> added <code>backBufferDurationMs<\/code> for tuning how much played video stays in memory<\/li>\n<li><strong>2.14.0<\/strong> added video quality selection for HLS and DASH streams<\/li>\n<\/ul>\n<p>The catch: no controls. You build the play button, scrubber, and full screen toggle yourself.<\/p>\n<h3>chewie: controls on top of video_player<\/h3>\n<p>Chewie is a UI layer. It takes a <code>VideoPlayerController<\/code> and wraps it in Material or Cupertino controls, with full screen, playback speed, subtitles, and chapters.<\/p>\n<p>Because it sits on <code>video_player<\/code>, you inherit that plugin&#8217;s format support and bugs. Chewie&#8217;s own README says so plainly: <code>PlatformException<\/code>s from playback belong to <code>video_player<\/code>, not chewie.<\/p>\n<p>That&#8217;s the right tradeoff for most apps. You get a finished UI in about 10 lines of code and keep the official engine underneath.<\/p>\n<h3>media_kit: one engine everywhere<\/h3>\n<p>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.<\/p>\n<p>That&#8217;s its main draw. It&#8217;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 <code>Player<\/code> instance can open a new source without being disposed.<\/p>\n<p>The costs: a bigger app bundle (libmpv ships with your app), no DRM, and a split package setup. You add <code>media_kit<\/code>, <code>media_kit_video<\/code>, and <code>media_kit_libs_video<\/code> separately. The <a href=\"https:\/\/github.com\/media-kit\/media-kit\" target=\"_blank\" rel=\"nofollow noopener\">media_kit repository<\/a> is still active, with commits as recent as August 2026, though the last pub.dev release was December 2025.<\/p>\n<h3>better_player: back from the dead<\/h3>\n<p>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 <code>better_player_plus<\/code> kept it alive.<\/p>\n<p>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&#8217;s now independent of <code>video_player<\/code>.<\/p>\n<p>It&#8217;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.<\/p>\n<p>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.<\/p>\n<h3>Other packages<\/h3>\n<p>A few more show up in searches:<\/p>\n<ul>\n<li><strong>fvp<\/strong> swaps the <code>video_player<\/code> backend for libmdk and adds Windows and Linux support without changing your Dart code<\/li>\n<li><strong>video_player_win<\/strong> adds Windows support through Media Foundation<\/li>\n<li><strong>flick_video_player<\/strong> and <strong>pod_player<\/strong> haven&#8217;t had a release since mid-2024<\/li>\n<\/ul>\n<p>For a YouTube link, you need a YouTube-specific package like <code>youtube_player_iframe<\/code>. YouTube doesn&#8217;t expose direct video URLs, so no general Flutter video player can play one.<\/p>\n<h2>How to Build a Flutter Video Player with video_player<\/h2>\n<p>Here&#8217;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 <a href=\"https:\/\/docs.flutter.dev\/cookbook\/plugins\/play-video\" target=\"_blank\" rel=\"nofollow noopener\">Flutter play and pause cookbook<\/a>, with error handling added.<\/p>\n<h3>1. Add the package<\/h3>\n<pre><code class=\"language-bash\">flutter pub add video_player\r\n<\/code><\/pre>\n<h3>2. Add platform permissions<\/h3>\n<p>For network video on Android, add the internet permission to <code>android\/app\/src\/main\/AndroidManifest.xml<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;uses-permission android:name=\"android.permission.INTERNET\"\/&gt;\r\n<\/code><\/pre>\n<p>On iOS, HTTPS URLs work with no changes. You only need <code>NSAppTransportSecurity<\/code> entries in <code>ios\/Runner\/Info.plist<\/code> if you load plain <code>http<\/code> URLs.<\/p>\n<p>On macOS, add the <code>com.apple.security.network.client<\/code> entitlement.<\/p>\n<h3>3. Create and initialize the controller<\/h3>\n<pre><code class=\"language-dart\">import 'package:flutter\/material.dart';\r\nimport 'package:video_player\/video_player.dart';\r\n\r\nclass VideoScreen extends StatefulWidget {\r\n  const VideoScreen({super.key, required this.url});\r\n\r\n  final String url;\r\n\r\n  @override\r\n  State&lt;VideoScreen&gt; createState() =&gt; _VideoScreenState();\r\n}\r\n\r\nclass _VideoScreenState extends State&lt;VideoScreen&gt; {\r\n  late final VideoPlayerController _controller;\r\n  late final Future&lt;void&gt; _initialize;\r\n\r\n  @override\r\n  void initState() {\r\n    super.initState();\r\n    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.url));\r\n    _initialize = _controller.initialize();\r\n    _controller.setLooping(true);\r\n  }\r\n\r\n  @override\r\n  void dispose() {\r\n    _controller.dispose();\r\n    super.dispose();\r\n  }\r\n<\/code><\/pre>\n<p>Two rules matter here.<\/p>\n<p>Create the controller in <code>initState()<\/code>, never in <code>build()<\/code>. Every rebuild would spin up a new native player.<\/p>\n<p>Always call <code>dispose()<\/code>. A leaked controller keeps a hardware decoder and GPU texture alive, and phones have a limited number of both.<\/p>\n<h3>4. Display the video with the right aspect ratio<\/h3>\n<pre><code class=\"language-dart\">  @override\r\n  Widget build(BuildContext context) {\r\n    return Scaffold(\r\n      body: FutureBuilder&lt;void&gt;(\r\n        future: _initialize,\r\n        builder: (context, snapshot) {\r\n          if (snapshot.connectionState != ConnectionState.done) {\r\n            return const Center(child: CircularProgressIndicator());\r\n          }\r\n          if (_controller.value.hasError) {\r\n            return Center(child: Text(_controller.value.errorDescription ?? 'Playback error'));\r\n          }\r\n          return Column(\r\n            mainAxisAlignment: MainAxisAlignment.center,\r\n            children: [\r\n              AspectRatio(\r\n                aspectRatio: _controller.value.aspectRatio,\r\n                child: VideoPlayer(_controller),\r\n              ),\r\n              VideoProgressIndicator(_controller, allowScrubbing: true),\r\n            ],\r\n          );\r\n        },\r\n      ),\r\n      floatingActionButton: ValueListenableBuilder&lt;VideoPlayerValue&gt;(\r\n        valueListenable: _controller,\r\n        builder: (context, value, _) =&gt; FloatingActionButton(\r\n          onPressed: () =&gt; value.isPlaying ? _controller.pause() : _controller.play(),\r\n          child: Icon(value.isPlaying ? Icons.pause : Icons.play_arrow),\r\n        ),\r\n      ),\r\n    );\r\n  }\r\n}\r\n<\/code><\/pre>\n<p>Wrapping <code>VideoPlayer<\/code> in <code>AspectRatio<\/code> is what prevents stretched or squashed video. The widget fills whatever space it&#8217;s given, so without the wrapper a 16:9 video will distort to match its parent.<\/p>\n<p><code>ValueListenableBuilder<\/code> 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.<\/p>\n<h2>How to Add Controls and Full Screen with Chewie<\/h2>\n<p>If you don&#8217;t want to build controls by hand, chewie adds them on top of the controller you already have.<\/p>\n<pre><code class=\"language-bash\">flutter pub add chewie\r\n<\/code><\/pre>\n<pre><code class=\"language-dart\">import 'package:chewie\/chewie.dart';\r\n\r\nlate final ChewieController _chewieController;\r\n\r\nFuture&lt;void&gt; _setUp() async {\r\n  await _controller.initialize();\r\n  _chewieController = ChewieController(\r\n    videoPlayerController: _controller,\r\n    autoPlay: true,\r\n    looping: false,\r\n    allowFullScreen: true,\r\n    playbackSpeeds: const [0.5, 1.0, 1.5, 2.0],\r\n  );\r\n  setState(() {});\r\n}\r\n\r\n\/\/ In build():\r\nChewie(controller: _chewieController);\r\n\r\n\/\/ In dispose():\r\n_chewieController.dispose();\r\n_controller.dispose();\r\n<\/code><\/pre>\n<p>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.<\/p>\n<p>Dispose both controllers. Chewie doesn&#8217;t dispose the <code>VideoPlayerController<\/code> it wraps.<\/p>\n<h2>How to Play HLS and m3u8 Streams in Flutter<\/h2>\n<p>For anything longer than a short clip, you&#8217;ll want <a href=\"https:\/\/liveapi.com\/blog\/what-is-hls-streaming\/\" target=\"_blank\" rel=\"noopener\">HLS streaming<\/a> instead of a single MP4. HLS splits video into short segments at several quality levels, and the player switches levels as network conditions change.<\/p>\n<p><code>video_player<\/code> plays HLS natively on Android and iOS. Pass the <code>.m3u8<\/code> URL to <code>networkUrl()<\/code>:<\/p>\n<pre><code class=\"language-dart\">_controller = VideoPlayerController.networkUrl(\r\n  Uri.parse('https:\/\/example.com\/live\/stream\/master.m3u8'),\r\n  formatHint: VideoFormat.hls,\r\n);\r\n<\/code><\/pre>\n<p>The <code>formatHint<\/code> helps ExoPlayer when the URL doesn&#8217;t end in <code>.m3u8<\/code>, which is common with signed or tokenized URLs.<\/p>\n<p>On the web, HLS support depends on the browser. Safari plays it natively. Other browsers may need a JavaScript player like <a href=\"https:\/\/liveapi.com\/blog\/hls-js\/\" target=\"_blank\" rel=\"noopener\">hls.js<\/a>, and at that point many teams render an HTML player through a platform view instead.<\/p>\n<h3>Adaptive quality selection in video_player 2.14<\/h3>\n<p>By default, the native engine handles <a href=\"https:\/\/liveapi.com\/blog\/adaptive-bitrate-streaming\/\" target=\"_blank\" rel=\"noopener\">adaptive bitrate streaming<\/a> on its own. It picks a rendition based on bandwidth and screen size.<\/p>\n<p>Version 2.14.0 added manual control, so you can build a quality menu:<\/p>\n<pre><code class=\"language-dart\">Future&lt;void&gt; pickQuality(int targetHeight) async {\r\n  if (!_controller.isVideoTrackSupportAvailable()) return;\r\n\r\n  final tracks = await _controller.getVideoTracks();\r\n  if (tracks.isEmpty) return;\r\n\r\n  final match = tracks.firstWhere(\r\n    (t) =&gt; t.height == targetHeight,\r\n    orElse: () =&gt; tracks.first,\r\n  );\r\n  await _controller.selectVideoTrack(match);\r\n}\r\n\r\n\/\/ Hand control back to automatic switching:\r\nawait _controller.selectVideoTrack(null);\r\n<\/code><\/pre>\n<p>Each <code>VideoTrack<\/code> carries a label, bitrate, width, height, frame rate, and codec. Under the hood, Android uses an ExoPlayer track selection override and iOS sets <code>preferredPeakBitRate<\/code> on the player item.<\/p>\n<p>Know the limits before you ship it:<\/p>\n<ul>\n<li><strong>iOS 13 and 14<\/strong> return an empty list, because the underlying AVFoundation API needs iOS 15<\/li>\n<li><strong>Web<\/strong> throws <code>UnimplementedError<\/code>, so always check <code>isVideoTrackSupportAvailable()<\/code> first<\/li>\n<li><strong>Single-file MP4s<\/strong> may return one track or none<\/li>\n<\/ul>\n<h3>Live streams<\/h3>\n<p>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.<\/p>\n<p>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 <a href=\"https:\/\/liveapi.com\/blog\/low-latency-hls\/\" target=\"_blank\" rel=\"noopener\">low-latency HLS<\/a>, which gets closer to 2 to 5 seconds when both server and player support it.<\/p>\n<h3>Protected content<\/h3>\n<p>If your content needs DRM, <code>video_player<\/code> can&#8217;t help. Your options are better_player, which supports Widevine and FairPlay, or a commercial player SDK. Either way, you&#8217;ll need a <a href=\"https:\/\/liveapi.com\/blog\/multi-drm\/\" target=\"_blank\" rel=\"noopener\">multi-DRM setup<\/a> on the server side, because Android and iOS use different DRM systems.<\/p>\n<h2>Flutter Video Player Performance: Feeds and Multiple Players<\/h2>\n<p>A single player on a detail screen rarely causes trouble. Problems start when you put video in a scrolling list.<\/p>\n<p>Each <code>VideoPlayerController<\/code> 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.<\/p>\n<p>For a feed or a multiple video player layout, follow these patterns:<\/p>\n<ul>\n<li><strong>Keep a small pool of controllers.<\/strong> Three or four is usually enough: the current video, one or two ahead, one behind.<\/li>\n<li><strong>Dispose controllers that leave the pool.<\/strong> Don&#8217;t keep one per list item.<\/li>\n<li><strong>Mute instead of pausing neighbors.<\/strong> A paused player often has to rebuffer when it resumes. A muted one stays warm.<\/li>\n<li><strong>Preload the next video<\/strong> by initializing its controller before it scrolls into view.<\/li>\n<li><strong>Use <code>visibility_detector<\/code><\/strong> or a <code>PageView<\/code> callback to decide which video plays.<\/li>\n<\/ul>\n<p><code>video_player<\/code> ties one controller to one source, so pooling means creating and disposing controllers as the user scrolls. media_kit lets you call <code>open()<\/code> on an existing <code>Player<\/code> with a new URL, which is why feed-heavy apps often pick it.<\/p>\n<p>Encoding matters as much as player code.<\/p>\n<p>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 <a href=\"https:\/\/liveapi.com\/blog\/how-to-avoid-buffering\/\" target=\"_blank\" rel=\"noopener\">how to avoid buffering<\/a>.<\/p>\n<h2>Common Flutter Video Player Problems and Fixes<\/h2>\n<p>These are the issues that show up over and over in GitHub and Stack Overflow.<\/p>\n<table>\n<thead>\n<tr>\n<th>Problem<\/th>\n<th>Likely cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Black screen<\/strong><\/td>\n<td>Widget built before <code>initialize()<\/code> finished, or size is zero<\/td>\n<td>Show the <code>VideoPlayer<\/code> only after <code>isInitialized<\/code> is true, and wrap it in <code>AspectRatio<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Works in debug, not in release (Android)<\/strong><\/td>\n<td>Missing <code>INTERNET<\/code> permission in the main manifest<\/td>\n<td>Add it to <code>src\/main\/AndroidManifest.xml<\/code>, not only <code>src\/debug<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Nothing plays on iOS<\/strong><\/td>\n<td>Plain <code>http<\/code> URL blocked by App Transport Security<\/td>\n<td>Use HTTPS, or add an ATS exception in <code>Info.plist<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong><code>UnimplementedError<\/code> on web<\/strong><\/td>\n<td><code>VideoPlayerController.file()<\/code> isn&#8217;t supported on web<\/td>\n<td>Use <code>networkUrl()<\/code> or <code>asset()<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Video stretched<\/strong><\/td>\n<td>No aspect ratio wrapper, or anamorphic content<\/td>\n<td>Wrap in <code>AspectRatio<\/code>; update <code>video_player_android<\/code> to 2.12.2+ for anamorphic fixes<\/td>\n<\/tr>\n<tr>\n<td><strong>Plays on Android, not iOS<\/strong><\/td>\n<td>Codec AVPlayer doesn&#8217;t support (VP9, WebM)<\/td>\n<td>Encode to H.264 or HEVC in an MP4 or HLS container<\/td>\n<\/tr>\n<tr>\n<td><strong>Constant buffering<\/strong><\/td>\n<td>Single high-bitrate file on a slow network<\/td>\n<td>Serve HLS with multiple renditions and a CDN<\/td>\n<\/tr>\n<tr>\n<td><strong>Video plays in background<\/strong><\/td>\n<td>App lifecycle not handled<\/td>\n<td>Pause in <code>didChangeAppLifecycleState<\/code>, or set <code>allowBackgroundPlayback<\/code> deliberately<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>When in doubt, read <code>_controller.value.errorDescription<\/code>. The native engine usually tells you exactly what failed.<\/p>\n<h2>Where Your Flutter App&#8217;s Video Comes From<\/h2>\n<p>Everything so far covers the playback side. Every package above, from <code>video_player<\/code> to better_player, expects you to hand it a URL.<\/p>\n<p>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 <a href=\"https:\/\/liveapi.com\/blog\/cdn-for-video-streaming\/\" target=\"_blank\" rel=\"noopener\">CDN for video streaming<\/a>. The player can only switch quality levels that your backend actually produced.<\/p>\n<p>This is the part most Flutter tutorials skip. It&#8217;s also where most of the engineering time goes when you <a href=\"https:\/\/liveapi.com\/blog\/how-to-build-a-video-streaming-app\/\" target=\"_blank\" rel=\"noopener\">build a video streaming app<\/a> from scratch.<\/p>\n<h3>Using LiveAPI as the backend<\/h3>\n<p>LiveAPI handles the server side and gives your Flutter app an HLS URL to play.<\/p>\n<p>For on-demand content, the <a href=\"https:\/\/liveapi.com\/video-api\/\" target=\"_blank\" rel=\"noopener\">LiveAPI video API<\/a> 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.<\/p>\n<p>For live content, the <a href=\"https:\/\/liveapi.com\/live-streaming-api\/\" target=\"_blank\" rel=\"noopener\">live streaming API<\/a> 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 <a href=\"https:\/\/liveapi.com\/blog\/live-to-vod\/\" target=\"_blank\" rel=\"noopener\">live-to-VOD<\/a> files.<\/p>\n<p>On the Flutter side, the code doesn&#8217;t change:<\/p>\n<pre><code class=\"language-dart\">final playbackUrl = await myBackend.getPlaybackUrl(videoId); \/\/ HLS URL from LiveAPI\r\n_controller = VideoPlayerController.networkUrl(\r\n  Uri.parse(playbackUrl),\r\n  formatHint: VideoFormat.hls,\r\n);\r\nawait _controller.initialize();\r\n<\/code><\/pre>\n<p>Because the stream already carries multiple renditions, <code>getVideoTracks()<\/code> returns real options for your quality menu, and ExoPlayer and AVPlayer can adapt on their own when the network drops.<\/p>\n<h2>Which Flutter Video Player Package Should You Use?<\/h2>\n<p>Match the package to your requirements:<\/p>\n<ul>\n<li><strong>Pick <code>video_player<\/code><\/strong> if you need Android, iOS, and web playback, you&#8217;re fine building your own controls, and you want the package least likely to break on a Flutter upgrade.<\/li>\n<li><strong>Add chewie<\/strong> if you want finished Material or Cupertino controls and full screen without writing them.<\/li>\n<li><strong>Pick media_kit<\/strong> if you ship on Windows or Linux, need identical behavior on every platform, or you&#8217;re building a scrolling video feed.<\/li>\n<li><strong>Pick better_player<\/strong> if you need DRM, offline caching, or picture-in-picture, and you can live with a young, fast-moving 1.x release line.<\/li>\n<li><strong>Look at a commercial SDK<\/strong> if you need analytics, ads, or casting with vendor support.<\/li>\n<\/ul>\n<p>Most apps end up with <code>video_player<\/code> plus chewie. It&#8217;s also the setup you&#8217;ll find the most answers for when something breaks.<\/p>\n<p>If you&#8217;re comparing across frameworks, the same tradeoffs show up in <a href=\"https:\/\/liveapi.com\/blog\/react-video-player\/\" target=\"_blank\" rel=\"noopener\">React video players<\/a> and in <a href=\"https:\/\/liveapi.com\/blog\/react-native-video-example\/\" target=\"_blank\" rel=\"noopener\">React Native video<\/a>: a thin official layer over native engines, with community packages adding UI and features.<\/p>\n<h2>Flutter Video Player FAQ<\/h2>\n<h3>What is the best video player for Flutter?<\/h3>\n<p>For most apps, the official <code>video_player<\/code> package paired with chewie for controls. It&#8217;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.<\/p>\n<h3>Can Flutter play video?<\/h3>\n<p>Yes, through plugins. The framework has no built-in video widget, but the official <code>video_player<\/code> plugin renders video inside the widget tree using ExoPlayer on Android, AVPlayer on iOS and macOS, and the HTML video element on web.<\/p>\n<h3>How do I play a video from a URL in Flutter?<\/h3>\n<p>Create a <code>VideoPlayerController.networkUrl(Uri.parse(url))<\/code>, call <code>initialize()<\/code>, and show a <code>VideoPlayer<\/code> widget inside an <code>AspectRatio<\/code> once <code>isInitialized<\/code> is true. On Android, add the internet permission to your manifest.<\/p>\n<h3>Does the Flutter video player support HLS and m3u8?<\/h3>\n<p>Yes. <code>video_player<\/code> plays HLS natively on Android and iOS: pass the <code>.m3u8<\/code> URL to <code>networkUrl()<\/code>, and set <code>formatHint: VideoFormat.hls<\/code> if the URL doesn&#8217;t end in <code>.m3u8<\/code>. On web, support depends on the browser.<\/p>\n<h3>How do I make a Flutter video player full screen?<\/h3>\n<p>The quickest way is chewie, which handles the full screen route, orientation lock, and system UI. With plain <code>video_player<\/code>, push a new route containing the <code>VideoPlayer<\/code>, set landscape orientation with <code>SystemChrome.setPreferredOrientations<\/code>, and hide system overlays.<\/p>\n<h3>Why is my Flutter video player showing a black screen?<\/h3>\n<p>Usually the <code>VideoPlayer<\/code> widget is built before the controller finished initializing, or it has no size. Wait for <code>initialize()<\/code> to complete, check <code>value.isInitialized<\/code>, and wrap the widget in <code>AspectRatio<\/code>. Also check <code>value.errorDescription<\/code> for codec or network errors.<\/p>\n<h3>Can I play YouTube videos with video_player?<\/h3>\n<p>No. YouTube doesn&#8217;t provide direct video file URLs, so general players can&#8217;t load them. Use a package built for YouTube, such as <code>youtube_player_iframe<\/code>, which embeds YouTube&#8217;s own player.<\/p>\n<h3>Does video_player work on Windows and Linux?<\/h3>\n<p>Not officially. The endorsed implementations cover Android, iOS, macOS, and web. For desktop, use media_kit, or add <code>fvp<\/code> or <code>video_player_win<\/code> as a backend so your existing <code>video_player<\/code> code runs there.<\/p>\n<h3>Is better_player still maintained?<\/h3>\n<p>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.<\/p>\n<h2>Build Your Flutter Video Player on a Solid Backend<\/h2>\n<p>A Flutter video player comes down to two choices: the package that handles playback and the backend that produces what it plays.<\/p>\n<p>For playback, start with <code>video_player<\/code> 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.<\/p>\n<p>For the backend, you need encoding, adaptive renditions, and CDN delivery. Your player&#8217;s quality menu and buffering behavior both depend on it.<\/p>\n<p>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. <a href=\"https:\/\/liveapi.com\/\" target=\"_blank\" rel=\"noopener\">Get started with LiveAPI<\/a> and have a playable HLS URL in your app today.<\/p>\n","protected":false},"excerpt":{"rendered":"<p><span class=\"rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">Reading Time: <\/span> <span class=\"rt-time\">11<\/span> <span class=\"rt-label rt-postfix\">minutes<\/span><\/span> Flutter doesn&#8217;t ship with a video widget. There&#8217;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&#8217;s native player. That design is [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1347,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_yoast_wpseo_title":"Flutter Video Player: Packages, Code, and HLS Setup (2026) %%sep%% %%sitename%%","_yoast_wpseo_metadesc":"Build a Flutter video player with video_player, chewie, media_kit, or better_player. Working code, HLS and m3u8 playback, quality selection, and fixes.","inline_featured_image":false,"footnotes":""},"categories":[4],"tags":[],"class_list":["post-1344","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-video-player"],"jetpack_featured_media_url":"https:\/\/liveapi.com\/blog\/wp-content\/uploads\/2026\/09\/Flutter-Video-Player-01.jpg","yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v15.6.2 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<meta name=\"description\" content=\"Build a Flutter video player with video_player, chewie, media_kit, or better_player. Working code, HLS and m3u8 playback, quality selection, and fixes.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/liveapi.com\/blog\/flutter-video-player\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Flutter Video Player: Packages, Code, and HLS Setup (2026) - LiveAPI Blog\" \/>\n<meta property=\"og:description\" content=\"Build a Flutter video player with video_player, chewie, media_kit, or better_player. Working code, HLS and m3u8 playback, quality selection, and fixes.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/liveapi.com\/blog\/flutter-video-player\/\" \/>\n<meta property=\"og:site_name\" content=\"LiveAPI Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-25T04:57:39+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-28T04:06:35+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/liveapi.com\/blog\/wp-content\/uploads\/2026\/09\/Flutter-Video-Player-01.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"2500\" \/>\n\t<meta property=\"og:image:height\" content=\"1308\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\">\n\t<meta name=\"twitter:data1\" content=\"17 minutes\">\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebSite\",\"@id\":\"https:\/\/liveapi.com\/blog\/#website\",\"url\":\"https:\/\/liveapi.com\/blog\/\",\"name\":\"LiveAPI Blog\",\"description\":\"Live Video Streaming API Blog\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":\"https:\/\/liveapi.com\/blog\/?s={search_term_string}\",\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/liveapi.com\/blog\/flutter-video-player\/#primaryimage\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/liveapi.com\/blog\/wp-content\/uploads\/2026\/09\/Flutter-Video-Player-01.jpg\",\"width\":2500,\"height\":1308,\"caption\":\"Flutter Video Player\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/liveapi.com\/blog\/flutter-video-player\/#webpage\",\"url\":\"https:\/\/liveapi.com\/blog\/flutter-video-player\/\",\"name\":\"Flutter Video Player: Packages, Code, and HLS Setup (2026) - LiveAPI Blog\",\"isPartOf\":{\"@id\":\"https:\/\/liveapi.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/liveapi.com\/blog\/flutter-video-player\/#primaryimage\"},\"datePublished\":\"2026-09-25T04:57:39+00:00\",\"dateModified\":\"2026-09-28T04:06:35+00:00\",\"author\":{\"@id\":\"https:\/\/liveapi.com\/blog\/#\/schema\/person\/98f2ee8b3a0bd93351c0d9e8ce490e4a\"},\"description\":\"Build a Flutter video player with video_player, chewie, media_kit, or better_player. Working code, HLS and m3u8 playback, quality selection, and fixes.\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/liveapi.com\/blog\/flutter-video-player\/\"]}]},{\"@type\":\"Person\",\"@id\":\"https:\/\/liveapi.com\/blog\/#\/schema\/person\/98f2ee8b3a0bd93351c0d9e8ce490e4a\",\"name\":\"govz\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/liveapi.com\/blog\/#personlogo\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/ab5cbe0543c0a44dc944c720159323bd001fc39a8ba5b1f137cd22e7578e84c9?s=96&d=mm&r=g\",\"caption\":\"govz\"},\"sameAs\":[\"https:\/\/liveapi.com\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","_links":{"self":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts\/1344","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/comments?post=1344"}],"version-history":[{"count":2,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts\/1344\/revisions"}],"predecessor-version":[{"id":1348,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/posts\/1344\/revisions\/1348"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/media\/1347"}],"wp:attachment":[{"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/media?parent=1344"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/categories?post=1344"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/liveapi.com\/blog\/wp-json\/wp\/v2\/tags?post=1344"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}