native_cast_player
Native video player for Flutter with YouTube-style AirPlay casting on iOS. Playback is 100% native (AVPlayer + AVPlayerLayer through a platform view) — no video rendering in Dart, no third-party dependencies.
Android Chromecast support is planned; the API is deliberately cast-generic.
Why this exists
AirPlay done the way YouTube does it is surprisingly easy to get wrong:
- External playback, not mirroring. The stream URL is handed off to the receiver; the TV decodes it itself at full quality and the phone becomes a remote control.
- The paused frame stays on the TV. Pause never deactivates the audio session, never releases the player item, never detaches the player layer — the three classic mistakes that black out the TV.
- Third-party TVs don't drop the session. Samsung-class receivers kill paused AirPlay sessions after ~30 s of idle (the #54 failure mode). A silent native keep-alive (25 s micro-nudge at 0.003×, invisible on the TV and to your UI) resets their idle timer for as long as you stay paused.
Quick start
import 'package:native_cast_player/native_cast_player.dart';
final controller = CastPlayerController();
await controller.load('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
await controller.play();
// Video surface (goes blank while casting — overlay your "Playing on TV" card):
AspectRatio(aspectRatio: 16 / 9, child: CastVideoView()),
// The system cast picker (AVRoutePickerView):
CastButton(),
// React to everything:
ValueListenableBuilder<CastPlayerValue>(
valueListenable: controller,
builder: (context, value, _) {
if (value.isExternalPlaybackActive) {
return Text('Playing on ${value.externalRouteName ?? 'AirPlay'}');
}
return Text('${value.position} / ${value.duration}');
},
),
Controller API: load(url), play(), pause(), seekTo(position)
(frame-accurate), setSpeed(speed) (survives pause/resume), retry(url),
dispose(). State arrives as an immutable CastPlayerValue (position,
duration, buffering, playback state, cast state + route name, errors).
The example/ app is a complete YouTube-style player screen: controls
overlay, seek bar, speed menu, fullscreen, cast placeholder.
Required app setup (iOS)
Add the background mode to your app's ios/Runner/Info.plist — without it,
casting stops when the app is backgrounded:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
That is the "Audio, AirPlay, and Picture in Picture" capability in Xcode. Minimum iOS: 13.0. Testing AirPlay requires a physical device (the simulator cannot cast) and a receiver on the same Wi-Fi.
The plugin configures the shared AVAudioSession once at startup
(.playback / .moviePlayback / .longFormVideo) and never deactivates it —
if your app also manages the audio session, don't deactivate it while casting.
Known limitations
- One player at a time. The native side owns a single shared AVPlayer;
create one
CastPlayerControllerand dispose it before creating another. - Long paused background sessions. If the app is suspended while paused in the background, no app can keep firing the keep-alive; a third-party TV may then drop after its idle timeout. (YouTube has the same limit.)
- Idle timeouts only. The keep-alive defeats receiver idle timeouts; TV
power-off or Wi-Fi loss still ends the session — the player then falls
back to local rendering, paused at the same position, and reports the
disconnect via
isExternalPlaybackActive. - iOS only today. On other platforms
CastVideoViewshows a placeholder andCastButtonrenders nothing.
How the keep-alive works
While isExternalPlaybackActive && paused, a native DispatchSourceTimer
fires every 25 s: it records the exact playhead, plays at 0.003× for 200 ms
via playImmediately(atRate:) (≈0.6 ms of media time), sets the rate back to
0, and seeks back frame-accurately. Every event to Flutter is suppressed
during the nudge, so your UI never flickers out of "paused" — but the
receiver sees activity and resets its idle countdown. On Apple TV (which has
no idle timeout) the nudge is a harmless no-op.
License
MIT — see LICENSE.
Libraries
- native_cast_player
- A video player with native casting: AVPlayer + AirPlay external playback on iOS, implemented 100% natively — no video rendering in Dart.