Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 2 additions & 1 deletion packages/video_player/video_player_avfoundation/CHANGELOG.md
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 @@ -46,7 +46,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 +60,46 @@ - (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++) {
XCUIScreenshot *screenshot = self.app.screenshot;

// Attach the screenshots for debugging if the test fails.
XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenshot];
attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
[self addAttachment:attachment];

UIImage *image = screenshot.image;
[frames addObject:UIImagePNGRepresentation(image)];
// Take random interval between [1, 2) seconds, since the video length could be the same as a
// fixed interval, which would always result in the same frame.
NSTimeInterval sampleInterval = 1 + ((double)arc4random() / UINT32_MAX);
Comment thread
hellohuanlin marked this conversation as resolved.
Outdated
[NSThread sleepForTimeInterval:sampleInterval];
}

// 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.");
Comment thread
hellohuanlin marked this conversation as resolved.
}

@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