From d2d245e404a41a501735571a35b1d9533f3cf132 Mon Sep 17 00:00:00 2001 From: Stefan Gilligan Date: Tue, 21 Apr 2026 17:20:12 +0100 Subject: [PATCH] [video_player] Fix washed-out HDR video playback on iOS AVPlayerItemVideoOutput was being constructed with only pixel buffer attributes and no AVVideoColorPropertiesKey. For HDR sources (HLG, PQ, Dolby Vision) AVFoundation then hands the decoder's BT.2020 samples through to the Flutter texture unconverted, which samples them as sRGB. The result is washed-out highlights, raised blacks, and desaturated midtones. Switch to -initWithOutputSettings: and declare BT.709 color properties on the output. AVFoundation's pixel transfer session now tone-maps and gamut-converts HDR into BT.709 SDR on the way into the pixel buffer, at no per-frame CPU cost. For SDR (BT.709) sources this is a no-op. AVVideoColorPropertiesKey must live in the output settings dictionary passed to -initWithOutputSettings:. Color keys placed under the pixel buffer attributes dictionary are silently ignored by AVFoundation; this is the trap that has kept the underlying issue unresolved. See WWDC22 "Display HDR video in EDR with AVFoundation and Metal" and Apple Developer Forum thread 686044 for the prescribed approach. Addresses flutter/flutter#91241 and flutter/flutter#143080. --- .../video_player_avfoundation/CHANGELOG.md | 4 ++++ .../darwin/RunnerTests/TestClasses.swift | 4 +++- .../darwin/RunnerTests/VideoPlayerTests.swift | 22 +++++++++++++++++++ .../video_player_avfoundation/FVPAVFactory.m | 10 ++++----- .../FVPVideoPlayer.m | 15 +++++++++---- .../video_player_avfoundation/FVPAVFactory.h | 10 +++++---- .../video_player_avfoundation/pubspec.yaml | 2 +- 7 files changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 1508c0bbfe22..a6e14b8ee319 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.9.5 + +* Fixes washed-out HDR video playback on iOS by declaring BT.709 color properties on the video output so AVFoundation tone-maps HDR sources into the Flutter texture. + ## 2.9.4 * Ensures that the display link does not continue requesting frames after a player is disposed. diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift index 3856995b855c..1bb988b1b7e2 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift @@ -198,6 +198,7 @@ final class StubFVPAVFactory: NSObject, FVPAVFactory { let player: AVPlayer let playerItem: FVPAVPlayerItem let pixelBufferSource: FVPPixelBufferSource? + private(set) var lastOutputSettings: [String: Any]? #if os(iOS) var audioSession: FVPAVAudioSession #endif @@ -231,7 +232,8 @@ final class StubFVPAVFactory: NSObject, FVPAVFactory { return self.player } - func videoOutput(pixelBufferAttributes attributes: [String: Any]) -> FVPPixelBufferSource { + func videoOutput(outputSettings: [String: Any]) -> FVPPixelBufferSource { + lastOutputSettings = outputSettings return pixelBufferSource ?? TestPixelBufferSource() } 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 c965a70a8430..b64477914963 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -802,6 +802,28 @@ private let hlsAudioTestURI = } } + @Test func videoOutputIsConfiguredWithBT709ColorProperties() throws { + let item = StubPlayerItem() + let stubAVFactory = StubFVPAVFactory(player: nil, playerItem: item, pixelBufferSource: nil) + let stubViewProvider = StubViewProvider() + let _ = FVPVideoPlayer( + playerItem: item, avFactory: stubAVFactory, viewProvider: stubViewProvider) + + // BT.709 color properties are required so AVFoundation tone-maps HDR sources + // into the Flutter texture. Without them HDR samples arrive unconverted and + // render washed out. See flutter/flutter#91241. + let settings = try #require(stubAVFactory.lastOutputSettings) + let colorProperties = try #require( + settings[AVVideoColorPropertiesKey] as? [String: Any]) + #expect( + colorProperties[AVVideoColorPrimariesKey] as? String == AVVideoColorPrimaries_ITU_R_709_2) + #expect( + colorProperties[AVVideoTransferFunctionKey] as? String + == AVVideoTransferFunction_ITU_R_709_2) + #expect( + colorProperties[AVVideoYCbCrMatrixKey] as? String == AVVideoYCbCrMatrix_ITU_R_709_2) + } + // MARK: - Helper Methods /// Creates a plugin with the given dependencies, and default stubs for any that aren't provided, diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPAVFactory.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPAVFactory.m index 4cee990adb1f..907072928d39 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPAVFactory.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPAVFactory.m @@ -84,10 +84,10 @@ @interface FVPDefaultAVPlayerItemVideoOutput : NSObject @end @implementation FVPDefaultAVPlayerItemVideoOutput -- (instancetype)initWithPixelBufferAttributes:(NSDictionary *)attributes { +- (instancetype)initWithOutputSettings:(NSDictionary *)outputSettings { self = [super init]; if (self) { - _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attributes]; + _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings]; } return self; } @@ -150,9 +150,9 @@ - (AVPlayer *)playerWithPlayerItem:(NSObject *)playerItem { return [AVPlayer playerWithPlayerItem:((FVPDefaultAVPlayerItem *)playerItem).playerItem]; } -- (NSObject *)videoOutputWithPixelBufferAttributes: - (NSDictionary *)attributes { - return [[FVPDefaultAVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attributes]; +- (NSObject *)videoOutputWithOutputSettings: + (NSDictionary *)outputSettings { + return [[FVPDefaultAVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings]; } #if TARGET_OS_IOS diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 2270120378d5..45e972137804 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -138,12 +138,19 @@ - (instancetype)initWithPlayerItem:(NSObject *)item _player = [avFactory playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - // Configure output. - NSDictionary *pixBuffAttributes = @{ + // 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. + NSDictionary *outputSettings = @{ + AVVideoColorPropertiesKey : @{ + AVVideoColorPrimariesKey : AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey : AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey : AVVideoYCbCrMatrix_ITU_R_709_2, + }, (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), - (id)kCVPixelBufferIOSurfacePropertiesKey : @{} + (id)kCVPixelBufferIOSurfacePropertiesKey : @{}, }; - _pixelBufferSource = [avFactory videoOutputWithPixelBufferAttributes:pixBuffAttributes]; + _pixelBufferSource = [avFactory videoOutputWithOutputSettings:outputSettings]; [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler]; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h index 90d4fa3ed1b9..6e80b8a5b67f 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPAVFactory.h @@ -94,10 +94,12 @@ NS_ASSUME_NONNULL_BEGIN /// Creates and returns an AVPlayer instance with the specified player item. - (AVPlayer *)playerWithPlayerItem:(NSObject *)playerItem; -/// Creates and returns a wrapped AVPlayerItemVideoOutput instance with the specified pixel buffer -/// attributes. -- (NSObject *)videoOutputWithPixelBufferAttributes: - (NSDictionary *)attributes; +/// Creates and returns a wrapped AVPlayerItemVideoOutput instance with the specified output +/// settings. The dictionary may contain both video output keys (e.g. AVVideoColorPropertiesKey) +/// and CVPixelBuffer attribute keys (e.g. kCVPixelBufferPixelFormatTypeKey), as accepted by +/// -[AVPlayerItemVideoOutput initWithOutputSettings:]. +- (NSObject *)videoOutputWithOutputSettings: + (NSDictionary *)outputSettings NS_SWIFT_NAME(videoOutput(outputSettings:)); #if TARGET_OS_IOS /// Returns the AVAudioSession shared instance, wrapped in the protocol. diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index f14fefb73326..f7c5c9edf5e3 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.9.4 +version: 2.9.5 environment: sdk: ^3.10.0