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

Commit 446c6f7

Browse files
authored
[video_player]fix ios 16 bug where encrypted video stream is not showing (#6442)
1 parent 88dc5e3 commit 446c6f7

File tree

6 files changed

+185
-6
lines changed

6 files changed

+185
-6
lines changed

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 2.3.6
22

3+
* Fixes a bug in iOS 16 where videos from protected live streams are not shown.
34
* Updates minimum Flutter version to 2.10.
45
* Fixes violations of new analysis option use_named_constants.
56
* Fixes avoid_redundant_argument_values lint warnings and minor typos.

packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
@interface FLTVideoPlayer : NSObject <FlutterStreamHandler>
1313
@property(readonly, nonatomic) AVPlayer *player;
14+
// This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
15+
// video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the
16+
// protection of pixel buffers in those streams.
17+
@property(readonly, nonatomic) AVPlayerLayer *playerLayer;
1418
@end
1519

1620
@interface FLTVideoPlayerPlugin (Test) <FLTAVFoundationVideoPlayerApi>
@@ -61,6 +65,45 @@ @interface VideoPlayerTests : XCTestCase
6165

6266
@implementation VideoPlayerTests
6367

68+
- (void)testIOS16BugWithEncryptedVideoStream {
69+
// This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
70+
// video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the
71+
// protection of pixel buffers in those streams.
72+
// Note that a better unit test is to validate that `copyPixelBuffer` API returns the pixel
73+
// buffers as expected, which requires setting up the video player properly with a protected video
74+
// stream (.m3u8 file).
75+
NSObject<FlutterPluginRegistry> *registry =
76+
(NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
77+
NSObject<FlutterPluginRegistrar> *registrar =
78+
[registry registrarForPlugin:@"testPlayerLayerWorkaround"];
79+
FLTVideoPlayerPlugin *videoPlayerPlugin =
80+
[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];
81+
82+
FlutterError *error;
83+
[videoPlayerPlugin initialize:&error];
84+
XCTAssertNil(error);
85+
86+
FLTCreateMessage *create = [FLTCreateMessage
87+
makeWithAsset:nil
88+
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
89+
packageName:nil
90+
formatHint:nil
91+
httpHeaders:@{}];
92+
FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error];
93+
XCTAssertNil(error);
94+
XCTAssertNotNil(textureMessage);
95+
FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId];
96+
XCTAssertNotNil(player);
97+
98+
if (@available(iOS 16.0, *)) {
99+
XCTAssertNotNil(player.playerLayer, @"AVPlayerLayer should be present for iOS 16.");
100+
XCTAssertNotNil(player.playerLayer.superlayer,
101+
@"AVPlayerLayer should be added on screen for iOS 16.");
102+
} else {
103+
XCTAssertNil(player.playerLayer, @"AVPlayerLayer should not be present before iOS 16.");
104+
}
105+
}
106+
64107
- (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry {
65108
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
66109
OCMProtocolMock(@protocol(FlutterTextureRegistry));

packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
@import os.log;
66
@import XCTest;
7+
@import CoreGraphics;
78

89
@interface VideoPlayerUITests : XCTestCase
910
@property(nonatomic, strong) XCUIApplication *app;
@@ -46,7 +47,7 @@ - (void)testPlayVideo {
4647
XCTAssertTrue(foundPlaybackSpeed5x);
4748

4849
// Cycle through tabs.
49-
for (NSString *tabName in @[ @"Asset", @"Remote" ]) {
50+
for (NSString *tabName in @[ @"Asset mp4", @"Remote mp4" ]) {
5051
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName];
5152
XCUIElement *unselectedTab = [app.staticTexts elementMatchingPredicate:predicate];
5253
XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]);
@@ -60,4 +61,54 @@ - (void)testPlayVideo {
6061
}
6162
}
6263

64+
- (void)testEncryptedVideoStream {
65+
// This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
66+
// video for encrypted video streams.
67+
68+
NSString *tabName = @"Remote enc m3u8";
69+
70+
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName];
71+
XCUIElement *unselectedTab = [self.app.staticTexts elementMatchingPredicate:predicate];
72+
XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]);
73+
XCTAssertFalse(unselectedTab.isSelected);
74+
[unselectedTab tap];
75+
76+
XCUIElement *selectedTab = [self.app.otherElements
77+
elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]];
78+
XCTAssertTrue([selectedTab waitForExistenceWithTimeout:30.0]);
79+
XCTAssertTrue(selectedTab.isSelected);
80+
81+
// Wait until the video is loaded.
82+
[NSThread sleepForTimeInterval:60];
83+
84+
NSMutableSet *frames = [NSMutableSet set];
85+
int numberOfFrames = 60;
86+
for (int i = 0; i < numberOfFrames; i++) {
87+
UIImage *image = self.app.screenshot.image;
88+
89+
// Plugin CI does not support attaching screenshot.
90+
// Convert the image to base64 encoded string, and print it out for debugging purpose.
91+
// NSLog truncates long strings, so need to scale downn image.
92+
CGSize smallerSize = CGSizeMake(100, 200);
93+
UIGraphicsBeginImageContextWithOptions(smallerSize, NO, 0.0);
94+
[image drawInRect:CGRectMake(0, 0, smallerSize.width, smallerSize.height)];
95+
UIImage *smallerImage = UIGraphicsGetImageFromCurrentImageContext();
96+
UIGraphicsEndImageContext();
97+
98+
// 0.5 compression is good enough for debugging purpose.
99+
NSData *imageData = UIImageJPEGRepresentation(smallerImage, 0.5);
100+
NSString *imageString = [imageData base64EncodedStringWithOptions:0];
101+
NSLog(@"frame %d image data:\n%@", i, imageString);
102+
103+
[frames addObject:imageString];
104+
105+
// The sample interval must NOT be the same as video length.
106+
// Otherwise it would always result in the same frame.
107+
[NSThread sleepForTimeInterval:1];
108+
}
109+
110+
// At least 1 loading and 2 distinct frames (3 in total) to validate that the video is playing.
111+
XCTAssert(frames.count >= 3, @"Must have at least 3 distinct frames.");
112+
}
113+
63114
@end

packages/video_player/video_player_avfoundation/example/lib/main.dart

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class _App extends StatelessWidget {
2020
@override
2121
Widget build(BuildContext context) {
2222
return DefaultTabController(
23-
length: 2,
23+
length: 3,
2424
child: Scaffold(
2525
key: const ValueKey<String>('home_page'),
2626
appBar: AppBar(
@@ -30,15 +30,20 @@ class _App extends StatelessWidget {
3030
tabs: <Widget>[
3131
Tab(
3232
icon: Icon(Icons.cloud),
33-
text: 'Remote',
33+
text: 'Remote mp4',
3434
),
35-
Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'),
35+
Tab(
36+
icon: Icon(Icons.favorite),
37+
text: 'Remote enc m3u8',
38+
),
39+
Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset mp4'),
3640
],
3741
),
3842
),
3943
body: TabBarView(
4044
children: <Widget>[
4145
_BumbleBeeRemoteVideo(),
46+
_BumbleBeeEncryptedLiveStream(),
4247
_ButterFlyAssetVideo(),
4348
],
4449
),
@@ -156,6 +161,59 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
156161
}
157162
}
158163

164+
class _BumbleBeeEncryptedLiveStream extends StatefulWidget {
165+
@override
166+
_BumbleBeeEncryptedLiveStreamState createState() =>
167+
_BumbleBeeEncryptedLiveStreamState();
168+
}
169+
170+
class _BumbleBeeEncryptedLiveStreamState
171+
extends State<_BumbleBeeEncryptedLiveStream> {
172+
late MiniController _controller;
173+
174+
@override
175+
void initState() {
176+
super.initState();
177+
_controller = MiniController.network(
178+
'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/encrypted_bee.m3u8',
179+
);
180+
181+
_controller.addListener(() {
182+
setState(() {});
183+
});
184+
_controller.initialize();
185+
186+
_controller.play();
187+
}
188+
189+
@override
190+
void dispose() {
191+
_controller.dispose();
192+
super.dispose();
193+
}
194+
195+
@override
196+
Widget build(BuildContext context) {
197+
return SingleChildScrollView(
198+
child: Column(
199+
children: <Widget>[
200+
Container(padding: const EdgeInsets.only(top: 20.0)),
201+
const Text('With remote encrypted m3u8'),
202+
Container(
203+
padding: const EdgeInsets.all(20),
204+
child: _controller.value.isInitialized
205+
? AspectRatio(
206+
aspectRatio: _controller.value.aspectRatio,
207+
child: VideoPlayer(_controller),
208+
)
209+
: const Text('loading...'),
210+
),
211+
],
212+
),
213+
);
214+
}
215+
}
216+
159217
class _ControlsOverlay extends StatelessWidget {
160218
const _ControlsOverlay({Key? key, required this.controller})
161219
: super(key: key);

packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ - (void)onDisplayLink:(CADisplayLink *)link {
3636
@interface FLTVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
3737
@property(readonly, nonatomic) AVPlayer *player;
3838
@property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput;
39+
/// An invisible player layer used to access the pixel buffers in protected video streams in iOS 16.
40+
@property(readonly, nonatomic) AVPlayerLayer *playerLayer;
3941
@property(readonly, nonatomic) CADisplayLink *displayLink;
4042
@property(nonatomic) FlutterEventChannel *eventChannel;
4143
@property(nonatomic) FlutterEventSink eventSink;
@@ -132,6 +134,19 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
132134
return degrees;
133135
};
134136

137+
NS_INLINE UIViewController *rootViewController() API_AVAILABLE(ios(16.0)) {
138+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
139+
if ([scene isKindOfClass:UIWindowScene.class]) {
140+
for (UIWindow *window in ((UIWindowScene *)scene).windows) {
141+
if (window.isKeyWindow) {
142+
return window.rootViewController;
143+
}
144+
}
145+
}
146+
}
147+
return nil;
148+
}
149+
135150
- (AVMutableVideoComposition *)getVideoCompositionWithTransform:(CGAffineTransform)transform
136151
withAsset:(AVAsset *)asset
137152
withVideoTrack:(AVAssetTrack *)videoTrack {
@@ -227,6 +242,14 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item
227242
_player = [AVPlayer playerWithPlayerItem:item];
228243
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
229244

245+
// This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank
246+
// video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the
247+
// protection of pixel buffers in those streams.
248+
if (@available(iOS 16.0, *)) {
249+
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
250+
[rootViewController().view.layer addSublayer:_playerLayer];
251+
}
252+
230253
[self createVideoOutputAndDisplayLink:frameUpdater];
231254

232255
[self addObservers:item];
@@ -458,6 +481,9 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
458481
/// so the channel is going to die or is already dead.
459482
- (void)disposeSansEventChannel {
460483
_disposed = YES;
484+
if (@available(iOS 16.0, *)) {
485+
[_playerLayer removeFromSuperlayer];
486+
}
461487
[_displayLink invalidate];
462488
AVPlayerItem *currentItem = self.player.currentItem;
463489
[currentItem removeObserver:self forKeyPath:@"status"];

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 implementation of the video_player plugin.
33
repository: https://github.com/flutter/plugins/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.3.5
5+
version: 2.3.6
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"

0 commit comments

Comments
 (0)