From eed3d2bedbfb4258130ba0f76b5525674687b187 Mon Sep 17 00:00:00 2001 From: Wolfgang Haupt Date: Mon, 17 Aug 2026 22:40:13 +0200 Subject: [PATCH] [video_player_avfoundation] Route video over AirPlay `usesExternalPlaybackWhileExternalScreenIsActive` defaults to NO, so an AVPlayer keeps the picture on the device whenever an external screen is active. Selecting an AirPlay route therefore moved only the audio, while the receiver showed a mirrored copy of the screen rather than playing the stream itself. `allowsExternalPlayback` already defaults to YES, so the two together were incoherent: external playback was permitted but never used. AVKit's own AVPlayerViewController routes to AirPlay out of the box; this brings the plugin in line with it. Verified on an Apple TV from iOS 26.6. --- .../video_player_avfoundation/CHANGELOG.md | 7 +++++++ .../darwin/RunnerTests/VideoPlayerTests.swift | 17 +++++++++++++++++ .../FVPVideoPlayer.m | 16 ++++++++++++++++ .../video_player_avfoundation/pubspec.yaml | 2 +- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index f97f9f1c5e41..f0ca386794cb 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.12.0 + +* Routes video over AirPlay when an external screen is active, by setting + `usesExternalPlaybackWhileExternalScreenIsActive` on the `AVPlayer`. Selecting + an AirPlay route previously moved only the audio, leaving the video on the + device while the receiver showed a mirrored screen. + ## 2.11.0 * Implements `setPreventsDisplaySleepDuringVideoPlayback` using diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift index 72bbe1e44658..f18954d9f65e 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -396,6 +396,23 @@ private let hlsAudioTestURI = #expect(player.player.preventsDisplaySleepDuringVideoPlayback == true) } + #if os(iOS) + @Test func externalPlaybackIsEnabledForAirPlay() { + let stubAVFactory = StubFVPAVFactory(player: AVPlayer()) + let player = FVPVideoPlayer( + playerItem: StubPlayerItem(), + avFactory: stubAVFactory, + viewProvider: StubViewProvider()) + + // usesExternalPlaybackWhileExternalScreenIsActive defaults to NO, which + // keeps the video on the device whenever an external screen is active. An + // AirPlay route then moves only the audio, and the receiver shows a + // mirrored screen rather than playing the stream itself. + #expect(player.player.allowsExternalPlayback == true) + #expect(player.player.usesExternalPlaybackWhileExternalScreenIsActive == true) + } + #endif + /// Sanity checks a video player playing the given URL with the actual AVPlayer. This is essentially /// a mini integration test of the player component. /// diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m index 316aa6237c25..86418642f7d6 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m @@ -145,6 +145,22 @@ - (instancetype)initWithPlayerItem:(NSObject *)item _player = [avFactory playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; +#if TARGET_OS_IOS + // Allow the video to follow an AirPlay route to an external device. + // + // allowsExternalPlayback already defaults to YES, but + // usesExternalPlaybackWhileExternalScreenIsActive defaults to NO, so the + // player deliberately keeps the picture on the device whenever an external + // screen is active. The result is that selecting an AirPlay route changes the + // audio route while the video stays on the phone and the TV only receives a + // mirrored copy of the screen. + // + // Verified on an Apple TV: with both set, the receiver plays the stream + // itself and the local surface is released. + _player.allowsExternalPlayback = YES; + _player.usesExternalPlaybackWhileExternalScreenIsActive = YES; +#endif + // Configure output. AVVideoColorPropertiesKey must be declared on the output settings (not on // pixel buffer attributes, where AVFoundation silently ignores it) so that HDR sources are // tone-mapped to BT.709 SDR for the Flutter texture. diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 9a1e29eab6d1..e0505a96ca8b 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.11.0 +version: 2.12.0 environment: sdk: ^3.10.0