[video_player]fix ios 16 bug where encrypted video stream is not showing (#6442)
diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md
index 3c9cc2d..cc4411c 100644
--- a/packages/video_player/video_player_avfoundation/CHANGELOG.md
+++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 2.3.6
+* Fixes a bug in iOS 16 where videos from protected live streams are not shown.
* Updates minimum Flutter version to 2.10.
* Fixes violations of new analysis option use_named_constants.
* Fixes avoid_redundant_argument_values lint warnings and minor typos.
diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m
index 7decd04..813fca2 100644
--- a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m
+++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m
@@ -11,6 +11,10 @@
@interface FLTVideoPlayer : NSObject <FlutterStreamHandler>
@property(readonly, nonatomic) AVPlayer *player;
+// This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
+// video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the
+// protection of pixel buffers in those streams.
+@property(readonly, nonatomic) AVPlayerLayer *playerLayer;
@end
@interface FLTVideoPlayerPlugin (Test) <FLTAVFoundationVideoPlayerApi>
@@ -61,6 +65,45 @@
@implementation VideoPlayerTests
+- (void)testIOS16BugWithEncryptedVideoStream {
+ // This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
+ // video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the
+ // protection of pixel buffers in those streams.
+ // Note that a better unit test is to validate that `copyPixelBuffer` API returns the pixel
+ // buffers as expected, which requires setting up the video player properly with a protected video
+ // stream (.m3u8 file).
+ NSObject<FlutterPluginRegistry> *registry =
+ (NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
+ NSObject<FlutterPluginRegistrar> *registrar =
+ [registry registrarForPlugin:@"testPlayerLayerWorkaround"];
+ FLTVideoPlayerPlugin *videoPlayerPlugin =
+ [[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];
+
+ FlutterError *error;
+ [videoPlayerPlugin initialize:&error];
+ XCTAssertNil(error);
+
+ FLTCreateMessage *create = [FLTCreateMessage
+ makeWithAsset:nil
+ uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
+ packageName:nil
+ formatHint:nil
+ httpHeaders:@{}];
+ FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error];
+ XCTAssertNil(error);
+ XCTAssertNotNil(textureMessage);
+ FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId];
+ XCTAssertNotNil(player);
+
+ if (@available(iOS 16.0, *)) {
+ XCTAssertNotNil(player.playerLayer, @"AVPlayerLayer should be present for iOS 16.");
+ XCTAssertNotNil(player.playerLayer.superlayer,
+ @"AVPlayerLayer should be added on screen for iOS 16.");
+ } else {
+ XCTAssertNil(player.playerLayer, @"AVPlayerLayer should not be present before iOS 16.");
+ }
+}
+
- (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry {
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
OCMProtocolMock(@protocol(FlutterTextureRegistry));
diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m
index b9f0f16..531d4fd 100644
--- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m
+++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m
@@ -4,6 +4,7 @@
@import os.log;
@import XCTest;
+@import CoreGraphics;
@interface VideoPlayerUITests : XCTestCase
@property(nonatomic, strong) XCUIApplication *app;
@@ -46,7 +47,7 @@
XCTAssertTrue(foundPlaybackSpeed5x);
// Cycle through tabs.
- for (NSString *tabName in @[ @"Asset", @"Remote" ]) {
+ for (NSString *tabName in @[ @"Asset mp4", @"Remote mp4" ]) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName];
XCUIElement *unselectedTab = [app.staticTexts elementMatchingPredicate:predicate];
XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]);
@@ -60,4 +61,54 @@
}
}
+- (void)testEncryptedVideoStream {
+ // This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
+ // video for encrypted video streams.
+
+ NSString *tabName = @"Remote enc m3u8";
+
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName];
+ XCUIElement *unselectedTab = [self.app.staticTexts elementMatchingPredicate:predicate];
+ XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]);
+ XCTAssertFalse(unselectedTab.isSelected);
+ [unselectedTab tap];
+
+ XCUIElement *selectedTab = [self.app.otherElements
+ elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]];
+ XCTAssertTrue([selectedTab waitForExistenceWithTimeout:30.0]);
+ XCTAssertTrue(selectedTab.isSelected);
+
+ // Wait until the video is loaded.
+ [NSThread sleepForTimeInterval:60];
+
+ NSMutableSet *frames = [NSMutableSet set];
+ int numberOfFrames = 60;
+ for (int i = 0; i < numberOfFrames; i++) {
+ UIImage *image = self.app.screenshot.image;
+
+ // Plugin CI does not support attaching screenshot.
+ // Convert the image to base64 encoded string, and print it out for debugging purpose.
+ // NSLog truncates long strings, so need to scale downn image.
+ CGSize smallerSize = CGSizeMake(100, 200);
+ UIGraphicsBeginImageContextWithOptions(smallerSize, NO, 0.0);
+ [image drawInRect:CGRectMake(0, 0, smallerSize.width, smallerSize.height)];
+ UIImage *smallerImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ // 0.5 compression is good enough for debugging purpose.
+ NSData *imageData = UIImageJPEGRepresentation(smallerImage, 0.5);
+ NSString *imageString = [imageData base64EncodedStringWithOptions:0];
+ NSLog(@"frame %d image data:\n%@", i, imageString);
+
+ [frames addObject:imageString];
+
+ // The sample interval must NOT be the same as video length.
+ // Otherwise it would always result in the same frame.
+ [NSThread sleepForTimeInterval:1];
+ }
+
+ // At least 1 loading and 2 distinct frames (3 in total) to validate that the video is playing.
+ XCTAssert(frames.count >= 3, @"Must have at least 3 distinct frames.");
+}
+
@end
diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart
index bca4e29..d385fd0 100644
--- a/packages/video_player/video_player_avfoundation/example/lib/main.dart
+++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart
@@ -20,7 +20,7 @@
@override
Widget build(BuildContext context) {
return DefaultTabController(
- length: 2,
+ length: 3,
child: Scaffold(
key: const ValueKey<String>('home_page'),
appBar: AppBar(
@@ -30,15 +30,20 @@
tabs: <Widget>[
Tab(
icon: Icon(Icons.cloud),
- text: 'Remote',
+ text: 'Remote mp4',
),
- Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'),
+ Tab(
+ icon: Icon(Icons.favorite),
+ text: 'Remote enc m3u8',
+ ),
+ Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset mp4'),
],
),
),
body: TabBarView(
children: <Widget>[
_BumbleBeeRemoteVideo(),
+ _BumbleBeeEncryptedLiveStream(),
_ButterFlyAssetVideo(),
],
),
@@ -156,6 +161,59 @@
}
}
+class _BumbleBeeEncryptedLiveStream extends StatefulWidget {
+ @override
+ _BumbleBeeEncryptedLiveStreamState createState() =>
+ _BumbleBeeEncryptedLiveStreamState();
+}
+
+class _BumbleBeeEncryptedLiveStreamState
+ extends State<_BumbleBeeEncryptedLiveStream> {
+ late MiniController _controller;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = MiniController.network(
+ 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/encrypted_bee.m3u8',
+ );
+
+ _controller.addListener(() {
+ setState(() {});
+ });
+ _controller.initialize();
+
+ _controller.play();
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SingleChildScrollView(
+ child: Column(
+ children: <Widget>[
+ Container(padding: const EdgeInsets.only(top: 20.0)),
+ const Text('With remote encrypted m3u8'),
+ Container(
+ padding: const EdgeInsets.all(20),
+ child: _controller.value.isInitialized
+ ? AspectRatio(
+ aspectRatio: _controller.value.aspectRatio,
+ child: VideoPlayer(_controller),
+ )
+ : const Text('loading...'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
class _ControlsOverlay extends StatelessWidget {
const _ControlsOverlay({Key? key, required this.controller})
: super(key: key);
diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m
index a95779b..645c86d 100644
--- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m
+++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m
@@ -36,6 +36,8 @@
@interface FLTVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
@property(readonly, nonatomic) AVPlayer *player;
@property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput;
+/// An invisible player layer used to access the pixel buffers in protected video streams in iOS 16.
+@property(readonly, nonatomic) AVPlayerLayer *playerLayer;
@property(readonly, nonatomic) CADisplayLink *displayLink;
@property(nonatomic) FlutterEventChannel *eventChannel;
@property(nonatomic) FlutterEventSink eventSink;
@@ -132,6 +134,19 @@
return degrees;
};
+NS_INLINE UIViewController *rootViewController() API_AVAILABLE(ios(16.0)) {
+ for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
+ if ([scene isKindOfClass:UIWindowScene.class]) {
+ for (UIWindow *window in ((UIWindowScene *)scene).windows) {
+ if (window.isKeyWindow) {
+ return window.rootViewController;
+ }
+ }
+ }
+ }
+ return nil;
+}
+
- (AVMutableVideoComposition *)getVideoCompositionWithTransform:(CGAffineTransform)transform
withAsset:(AVAsset *)asset
withVideoTrack:(AVAssetTrack *)videoTrack {
@@ -227,6 +242,14 @@
_player = [AVPlayer playerWithPlayerItem:item];
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
+ // This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
+ // video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the
+ // protection of pixel buffers in those streams.
+ if (@available(iOS 16.0, *)) {
+ _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
+ [rootViewController().view.layer addSublayer:_playerLayer];
+ }
+
[self createVideoOutputAndDisplayLink:frameUpdater];
[self addObservers:item];
@@ -458,6 +481,9 @@
/// so the channel is going to die or is already dead.
- (void)disposeSansEventChannel {
_disposed = YES;
+ if (@available(iOS 16.0, *)) {
+ [_playerLayer removeFromSuperlayer];
+ }
[_displayLink invalidate];
AVPlayerItem *currentItem = self.player.currentItem;
[currentItem removeObserver:self forKeyPath:@"status"];
diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml
index 06042c3..bd88ddf 100644
--- a/packages/video_player/video_player_avfoundation/pubspec.yaml
+++ b/packages/video_player/video_player_avfoundation/pubspec.yaml
@@ -2,7 +2,7 @@
description: iOS implementation of the video_player plugin.
repository: https://github.com/flutter/plugins/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.3.5
+version: 2.3.6
environment:
sdk: ">=2.14.0 <3.0.0"