Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[video_player]fix ios 16 bug where encrypted video stream is not showing #6442

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -61,6 +65,45 @@ @interface VideoPlayerTests : XCTestCase

@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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

@import os.log;
@import XCTest;
@import CoreGraphics;

@interface VideoPlayerUITests : XCTestCase
@property(nonatomic, strong) XCUIApplication *app;
Expand Down Expand Up @@ -46,7 +47,7 @@ - (void)testPlayVideo {
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]);
Expand All @@ -60,4 +61,54 @@ - (void)testPlayVideo {
}
}

- (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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class _App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
length: 3,
child: Scaffold(
key: const ValueKey<String>('home_page'),
appBar: AppBar(
Expand All @@ -30,15 +30,20 @@ class _App extends StatelessWidget {
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(),
],
),
Expand Down Expand Up @@ -156,6 +161,59 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ - (void)onDisplayLink:(CADisplayLink *)link {
@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;
Expand Down Expand Up @@ -132,6 +134,19 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
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 {
Expand Down Expand Up @@ -227,6 +242,14 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item
_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];
Expand Down Expand Up @@ -458,6 +481,9 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
/// 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"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: video_player_avfoundation
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"
Expand Down