Skip to content

Commit cd703a9

Browse files
[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.
1 parent ddc9430 commit cd703a9

7 files changed

Lines changed: 53 additions & 15 deletions

File tree

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.9.7
2+
3+
* Forces tone-mapping to SDR on iOS to prevent washed-out HDR video playback.
4+
15
## 2.9.6
26

37
* Adds a Swift version to fix a potential build issue when using CocoaPods.

packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ final class StubFVPAVFactory: NSObject, FVPAVFactory {
200200
let player: AVPlayer
201201
let playerItem: FVPAVPlayerItem
202202
let pixelBufferSource: FVPPixelBufferSource?
203+
private(set) var lastOutputSettings: [String: Any]?
203204
#if os(iOS)
204205
var audioSession: FVPAVAudioSession
205206
#endif
@@ -233,7 +234,8 @@ final class StubFVPAVFactory: NSObject, FVPAVFactory {
233234
return self.player
234235
}
235236

236-
func videoOutput(pixelBufferAttributes attributes: [String: Any]) -> FVPPixelBufferSource {
237+
func videoOutput(outputSettings: [String: Any]) -> FVPPixelBufferSource {
238+
lastOutputSettings = outputSettings
237239
return pixelBufferSource ?? TestPixelBufferSource()
238240
}
239241

packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,28 @@ private let hlsAudioTestURI =
755755
}
756756
}
757757

758+
@Test func videoOutputIsConfiguredWithBT709ColorProperties() throws {
759+
let item = StubPlayerItem()
760+
let stubAVFactory = StubFVPAVFactory(player: nil, playerItem: item, pixelBufferSource: nil)
761+
let stubViewProvider = StubViewProvider()
762+
let _ = FVPVideoPlayer(
763+
playerItem: item, avFactory: stubAVFactory, viewProvider: stubViewProvider)
764+
765+
// BT.709 color properties are required so AVFoundation tone-maps HDR sources
766+
// into the Flutter texture. Without them HDR samples arrive unconverted and
767+
// render washed out. See flutter/flutter#91241.
768+
let settings = try #require(stubAVFactory.lastOutputSettings)
769+
let colorProperties = try #require(
770+
settings[AVVideoColorPropertiesKey] as? [String: Any])
771+
#expect(
772+
colorProperties[AVVideoColorPrimariesKey] as? String == AVVideoColorPrimaries_ITU_R_709_2)
773+
#expect(
774+
colorProperties[AVVideoTransferFunctionKey] as? String
775+
== AVVideoTransferFunction_ITU_R_709_2)
776+
#expect(
777+
colorProperties[AVVideoYCbCrMatrixKey] as? String == AVVideoYCbCrMatrix_ITU_R_709_2)
778+
}
779+
758780
// MARK: - Helper Methods
759781

760782
/// Creates a plugin with the given dependencies, and default stubs for any that aren't provided,

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ @interface FVPDefaultAVPlayerItemVideoOutput : NSObject <FVPPixelBufferSource>
8484
@end
8585

8686
@implementation FVPDefaultAVPlayerItemVideoOutput
87-
- (instancetype)initWithPixelBufferAttributes:(NSDictionary<NSString *, id> *)attributes {
87+
- (instancetype)initWithOutputSettings:(NSDictionary<NSString *, id> *)outputSettings {
8888
self = [super init];
8989
if (self) {
90-
_videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attributes];
90+
_videoOutput = [[AVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings];
9191
}
9292
return self;
9393
}
@@ -150,9 +150,9 @@ - (AVPlayer *)playerWithPlayerItem:(NSObject<FVPAVPlayerItem> *)playerItem {
150150
return [AVPlayer playerWithPlayerItem:((FVPDefaultAVPlayerItem *)playerItem).playerItem];
151151
}
152152

153-
- (NSObject<FVPPixelBufferSource> *)videoOutputWithPixelBufferAttributes:
154-
(NSDictionary<NSString *, id> *)attributes {
155-
return [[FVPDefaultAVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attributes];
153+
- (NSObject<FVPPixelBufferSource> *)videoOutputWithOutputSettings:
154+
(NSDictionary<NSString *, id> *)outputSettings {
155+
return [[FVPDefaultAVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings];
156156
}
157157

158158
#if TARGET_OS_IOS

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,20 @@ - (instancetype)initWithPlayerItem:(NSObject<FVPAVPlayerItem> *)item
138138
_player = [avFactory playerWithPlayerItem:item];
139139
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
140140

141-
// Configure output.
142-
NSDictionary *pixBuffAttributes = @{
141+
// Configure output. AVVideoColorPropertiesKey must be declared on the output settings (not on
142+
// pixel buffer attributes, where AVFoundation silently ignores it) so that HDR sources are
143+
// tone-mapped to BT.709 SDR for the Flutter texture.
144+
// See https://github.com/flutter/flutter/issues/91241
145+
NSDictionary *outputSettings = @{
146+
AVVideoColorPropertiesKey : @{
147+
AVVideoColorPrimariesKey : AVVideoColorPrimaries_ITU_R_709_2,
148+
AVVideoTransferFunctionKey : AVVideoTransferFunction_ITU_R_709_2,
149+
AVVideoYCbCrMatrixKey : AVVideoYCbCrMatrix_ITU_R_709_2,
150+
},
143151
(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
144-
(id)kCVPixelBufferIOSurfacePropertiesKey : @{}
152+
(id)kCVPixelBufferIOSurfacePropertiesKey : @{},
145153
};
146-
_pixelBufferSource = [avFactory videoOutputWithPixelBufferAttributes:pixBuffAttributes];
154+
_pixelBufferSource = [avFactory videoOutputWithOutputSettings:outputSettings];
147155

148156
[asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler];
149157

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,12 @@ NS_ASSUME_NONNULL_BEGIN
9494
/// Creates and returns an AVPlayer instance with the specified player item.
9595
- (AVPlayer *)playerWithPlayerItem:(NSObject<FVPAVPlayerItem> *)playerItem;
9696

97-
/// Creates and returns a wrapped AVPlayerItemVideoOutput instance with the specified pixel buffer
98-
/// attributes.
99-
- (NSObject<FVPPixelBufferSource> *)videoOutputWithPixelBufferAttributes:
100-
(NSDictionary<NSString *, id> *)attributes;
97+
/// Creates and returns a wrapped AVPlayerItemVideoOutput instance with the specified output
98+
/// settings. The dictionary may contain both video output keys (e.g. AVVideoColorPropertiesKey)
99+
/// and CVPixelBuffer attribute keys (e.g. kCVPixelBufferPixelFormatTypeKey), as accepted by
100+
/// -[AVPlayerItemVideoOutput initWithOutputSettings:].
101+
- (NSObject<FVPPixelBufferSource> *)videoOutputWithOutputSettings:
102+
(NSDictionary<NSString *, id> *)outputSettings NS_SWIFT_NAME(videoOutput(outputSettings:));
101103

102104
#if TARGET_OS_IOS
103105
/// Returns the AVAudioSession shared instance, wrapped in the protocol.

packages/video_player/video_player_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_avfoundation
22
description: iOS and macOS implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.9.6
5+
version: 2.9.7
66

77
environment:
88
sdk: ^3.10.0

0 commit comments

Comments
 (0)