[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"