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 CastPlayerController and 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 CastVideoView shows a placeholder and CastButton renders 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.