Skip to main content

Migrating from media_kit to Better Player

· 6 min read
Jakub Homlala
Senior Mobile Engineer

media_kit is a solid piece of engineering if you're building a desktop app on Windows, Linux, or macOS. Wrapping libmpv gives you an all-in-one codec powerhouse that plays almost any obscure video container you throw at it.

Mobile is a completely different game.

Once your Flutter app targets Android and iOS, bundling libmpv extracts a heavy tax: 30 to 50 MB of native C binaries per ABI in your final APK or IPA. Then your roadmap asks for studio-grade Widevine L1 or FairPlay DRM, native Picture-in-Picture, lock screen media controls, or a scrollable feed like TikTok. That is where libmpv starts fighting the mobile OS, and where Better Player takes over.

Here is why developers make the switch, and how to migrate your codebase in under thirty minutes.


The Mobile Reality Check: libmpv vs Native Engines​

Before changing any code, understand the architectural trade-off:

Requirementmedia_kit (libmpv)Better Player 1.x
Underlying EngineBundled libmpv (C/C++ binary)AndroidX Media3 (Android), AVPlayer via Swift FFI (iOS & macOS), Shaka Player (Web), libmpv (Windows)
App Size Overhead30 to 50+ MB per ABI on mobile~1–2 MB on mobile (uses OS media frameworks; libmpv on Windows)
Studio DRMSoftware-only / limitedWidevine L1/L3, Apple FairPlay & ClearKey via hardware enclaves
Mobile UI & ControlsDIY or basic desktop controlsPrebuilt mobile Material & Cupertino controls with overflow menus
Battery & Hardware DecodersCustom render loop, variable power efficiencyDirect platform hardware decoding and OS power management
Feed Playback & PoolingManual player pooling and visibility trackingBuilt-in BetterPlayerFeed & BetterPlayerPool

If you are shipping to mobile users, downloading 40 MB of extra player libraries when Android and iOS already have world-class hardware media frameworks built into the OS does not make sense.


Step 1: Clean Up Dependencies​

Remove the media_kit packages and add better_player:

flutter pub remove media_kit media_kit_video media_kit_libs_video
flutter pub add better_player

Step 2: Delete MediaKit.ensureInitialized()​

media_kit requires global runtime initialization in your app entrypoint before calling runApp(). Better Player uses the platform's native lifecycle, so you can delete that completely.

In lib/main.dart:

void main() {
WidgetsFlutterBinding.ensureInitialized();
// Delete this line:
// MediaKit.ensureInitialized();

runApp(const MyApp());
}

Step 3: Controller and Widget Migration​

Let's look at the code difference side by side.

The Old media_kit Setup​

With media_kit, you typically maintain a Player instance and a separate VideoController instance, load the media asynchronously, and wrap everything in a Video widget:

// Old media_kit code
class OldMediaKitView extends StatefulWidget {
const OldMediaKitView({super.key});


State<OldMediaKitView> createState() => _OldMediaKitViewState();
}

class _OldMediaKitViewState extends State<OldMediaKitView> {
late final Player _player = Player();
late final VideoController _videoController = VideoController(_player);


void initState() {
super.initState();
_player.open(Media('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'));
}


void dispose() {
_player.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.width * 9.0 / 16.0,
child: Video(controller: _videoController),
);
}
}

The Modern Better Player Setup​

In Better Player, configuration separates cleanly into how the player behaves (PlayerConfiguration) and what content it plays (PlayerDataSource). A single BetterPlayerController manages the lifecycle, and autoDispose defaults to true so BetterPlayer cleans up automatically when unmounted:

// Clean Better Player code
import 'package:better_player/better_player.dart';

class ModernBetterPlayerView extends StatefulWidget {
const ModernBetterPlayerView({super.key});


State<ModernBetterPlayerView> createState() => _ModernBetterPlayerViewState();
}

class _ModernBetterPlayerViewState extends State<ModernBetterPlayerView> {
late final BetterPlayerController _controller;


void initState() {
super.initState();

const config = PlayerConfiguration(
aspectRatio: 16 / 9,
autoPlay: true,
looping: false,
);

final dataSource = PlayerDataSource(
DataSourceType.network,
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
);

_controller = BetterPlayerController(config, betterPlayerDataSource: dataSource);
}


Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 16 / 9,
child: BetterPlayer(controller: _controller),
);
}
}

Notice what you gain immediately:

  • Built-in mobile controls (play/pause, scrubbing, fullscreen toggle, overflow menu) without writing custom overlay widgets.
  • Native aspect ratio constraint handling.
  • Automatic buffering indicators and automatic controller disposal when the widget unmounts.

Step 4: Caching and DRM in Three Lines​

Two features where media_kit requires painful workarounds on mobile are disk caching and hardware DRM. In Better Player, you configure both directly on the PlayerDataSource.

Smart Video Caching​

To cache video segments on the device without re-downloading on every replay:

final dataSource = PlayerDataSource(
DataSourceType.network,
'https://example.com/video.mp4',
cacheConfiguration: const CacheConfiguration(
useCache: true,
maxCacheSize: 200 * 1024 * 1024, // 200 MB cache pool
maxCacheFileSize: 25 * 1024 * 1024, // 25 MB max per file
),
);

Studio Hardware DRM (Widevine & FairPlay)​

If you are streaming protected commercial streams, configure your DRM keys and license endpoint:

final protectedDataSource = PlayerDataSource(
DataSourceType.network,
'https://example.com/live/manifest.mpd',
drmConfiguration: DrmConfiguration(
drmType: DrmType.widevine,
licenseUrl: 'https://proxy.example.com/widevine/license',
headers: {'Authorization': 'Bearer YOUR_TOKEN'},
),
);

On Android, AndroidX Media3 communicates directly with the device's hardware DRM pipeline (Widevine L1 or L3). On iOS and macOS, AVPlayer connects with FairPlay Streaming.


Step 5: Pooled Video Feeds (BetterPlayerFeed & BetterPlayerPool)​

In media_kit, building a vertical swipe feed (think Instagram Reels or TikTok) requires manually tracking scroll view offsets, managing player pools, and hoping you don't exhaust native hardware decoder threads.

Better Player provides BetterPlayerFeed backed by a bounded BetterPlayerPool:

final feedController = BetterPlayerFeedController(
dataSources: videoUrls.map((url) {
return PlayerDataSource(DataSourceType.network, url);
}).toList(),
poolConfiguration: const PlayerPoolConfiguration(
poolSize: 3,
preloadForward: 1,
preloadBackward: 1,
),
);

// Inside your build method:
BetterPlayerFeed(
controller: feedController,
configuration: PlayerFeedConfiguration(
scrollDirection: Axis.vertical,
placeholderBuilder: (context, index, dataSource) =>
const Center(child: CircularProgressIndicator()),
),
);

BetterPlayerPool recycles a fixed number of native decoder instances, preloads upcoming items in the background, and releases out-of-range leases automatically.


Pitfalls to Avoid​

Keep these points in mind when testing:

  1. Don't double-dispose: PlayerConfiguration.autoDispose defaults to true, so BetterPlayer automatically disposes its BetterPlayerController when the widget unmounts. Only call _controller.dispose() manually in State.dispose() if you set autoDispose: false.
  2. Android Cleartext Traffic: If testing against local http:// backend endpoints, ensure android:usesCleartextTraffic="true" is set in android/app/src/main/AndroidManifest.xml.
  3. iOS App Transport Security: Add necessary ATS exceptions in ios/Runner/Info.plist if streaming non-SSL feeds during development.

The Bottom Line​

media_kit is an impressive feat for desktop Flutter apps. But for mobile production apps where download size, battery efficiency, native controls, and studio DRM matter, Better Player is the right tool for the job.

Drop the 50 MB native binary bloat, embrace the OS engines, and keep your Flutter app lightweight.