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

Commit e6c34fd

Browse files
committed
[video_player]fix ios 16 bug where encrypted video stream is not showing
1 parent 5e7c779 commit e6c34fd

File tree

5 files changed

+105
-196
lines changed

5 files changed

+105
-196
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));
Lines changed: 33 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,234 +1,73 @@
1-
// Copyright 2013 The Flutter Authors. All rights reserved.
2-
// Use of this source code is governed by a BSD-style license that can be
3-
// found in the LICENSE file.
4-
5-
// ignore_for_file: public_member_api_docs
6-
71
import 'package:flutter/material.dart';
8-
2+
import 'package:video_player_avfoundation/video_player_avfoundation.dart';
3+
import 'dart:async';
4+
import 'dart:io';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter/services.dart';
7+
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
98
import 'mini_controller.dart';
109

1110
void main() {
1211
runApp(
13-
MaterialApp(
14-
home: _App(),
12+
const MaterialApp(
13+
home: MyVideo(),
1514
),
1615
);
1716
}
1817

19-
class _App extends StatelessWidget {
20-
@override
21-
Widget build(BuildContext context) {
22-
return DefaultTabController(
23-
length: 2,
24-
child: Scaffold(
25-
key: const ValueKey<String>('home_page'),
26-
appBar: AppBar(
27-
title: const Text('Video player example'),
28-
bottom: const TabBar(
29-
isScrollable: true,
30-
tabs: <Widget>[
31-
Tab(
32-
icon: Icon(Icons.cloud),
33-
text: 'Remote',
34-
),
35-
Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'),
36-
],
37-
),
38-
),
39-
body: TabBarView(
40-
children: <Widget>[
41-
_BumbleBeeRemoteVideo(),
42-
_ButterFlyAssetVideo(),
43-
],
44-
),
45-
),
46-
);
47-
}
48-
}
18+
class MyVideo extends StatefulWidget {
19+
const MyVideo({Key? key}) : super(key: key);
4920

50-
class _ButterFlyAssetVideo extends StatefulWidget {
5121
@override
52-
_ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState();
22+
_MyVideoState createState() => _MyVideoState();
5323
}
5424

55-
class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> {
56-
late MiniController _controller;
25+
class _MyVideoState extends State<MyVideo> {
26+
MiniController? _controller;
5727

5828
@override
5929
void initState() {
6030
super.initState();
61-
_controller = MiniController.asset('assets/Butterfly-209.mp4');
62-
63-
_controller.addListener(() {
64-
setState(() {});
65-
});
66-
_controller.initialize().then((_) => setState(() {}));
67-
_controller.play();
31+
init();
6832
}
6933

7034
@override
7135
void dispose() {
72-
_controller.dispose();
36+
_controller?.dispose();
7337
super.dispose();
7438
}
7539

7640
@override
7741
Widget build(BuildContext context) {
78-
return SingleChildScrollView(
79-
child: Column(
80-
children: <Widget>[
81-
Container(
82-
padding: const EdgeInsets.only(top: 20.0),
83-
),
84-
const Text('With assets mp4'),
85-
Container(
86-
padding: const EdgeInsets.all(20),
87-
child: AspectRatio(
88-
aspectRatio: _controller.value.aspectRatio,
89-
child: Stack(
90-
alignment: Alignment.bottomCenter,
91-
children: <Widget>[
92-
VideoPlayer(_controller),
93-
_ControlsOverlay(controller: _controller),
94-
VideoProgressIndicator(_controller),
95-
],
42+
return Scaffold(
43+
appBar: AppBar(
44+
title: const Text("My Video"),
45+
),
46+
body: Center(
47+
child: _controller!.value.isInitialized
48+
? AspectRatio(
49+
aspectRatio: _controller!.value.aspectRatio,
50+
child: VideoPlayer(_controller!),
51+
)
52+
: Container(
53+
child: const Text('loading...'),
9654
),
97-
),
98-
),
99-
],
10055
),
10156
);
10257
}
103-
}
10458

105-
class _BumbleBeeRemoteVideo extends StatefulWidget {
106-
@override
107-
_BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState();
108-
}
109-
110-
class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
111-
late MiniController _controller;
112-
113-
@override
114-
void initState() {
115-
super.initState();
59+
Future<void> init() async {
11660
_controller = MiniController.network(
117-
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
61+
// 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
62+
'https://service.beta.sanjieke.cn/video/media/11244355/608p.m3u8?user_id=18942194&class_id=33254014&time=1663055202&nonce=868849&token=73daa6bce6e2643a3a9d2f28f3385ce86fa5ec82',
11863
);
11964

120-
_controller.addListener(() {
65+
_controller?.addListener(() {
12166
setState(() {});
12267
});
123-
_controller.initialize();
124-
}
12568

126-
@override
127-
void dispose() {
128-
_controller.dispose();
129-
super.dispose();
130-
}
69+
await _controller?.initialize();
13170

132-
@override
133-
Widget build(BuildContext context) {
134-
return SingleChildScrollView(
135-
child: Column(
136-
children: <Widget>[
137-
Container(padding: const EdgeInsets.only(top: 20.0)),
138-
const Text('With remote mp4'),
139-
Container(
140-
padding: const EdgeInsets.all(20),
141-
child: AspectRatio(
142-
aspectRatio: _controller.value.aspectRatio,
143-
child: Stack(
144-
alignment: Alignment.bottomCenter,
145-
children: <Widget>[
146-
VideoPlayer(_controller),
147-
_ControlsOverlay(controller: _controller),
148-
VideoProgressIndicator(_controller),
149-
],
150-
),
151-
),
152-
),
153-
],
154-
),
155-
);
156-
}
157-
}
158-
159-
class _ControlsOverlay extends StatelessWidget {
160-
const _ControlsOverlay({Key? key, required this.controller})
161-
: super(key: key);
162-
163-
static const List<double> _examplePlaybackRates = <double>[
164-
0.25,
165-
0.5,
166-
1.0,
167-
1.5,
168-
2.0,
169-
3.0,
170-
5.0,
171-
10.0,
172-
];
173-
174-
final MiniController controller;
175-
176-
@override
177-
Widget build(BuildContext context) {
178-
return Stack(
179-
children: <Widget>[
180-
AnimatedSwitcher(
181-
duration: const Duration(milliseconds: 50),
182-
reverseDuration: const Duration(milliseconds: 200),
183-
child: controller.value.isPlaying
184-
? const SizedBox.shrink()
185-
: Container(
186-
color: Colors.black26,
187-
child: const Center(
188-
child: Icon(
189-
Icons.play_arrow,
190-
color: Colors.white,
191-
size: 100.0,
192-
semanticLabel: 'Play',
193-
),
194-
),
195-
),
196-
),
197-
GestureDetector(
198-
onTap: () {
199-
controller.value.isPlaying ? controller.pause() : controller.play();
200-
},
201-
),
202-
Align(
203-
alignment: Alignment.topRight,
204-
child: PopupMenuButton<double>(
205-
initialValue: controller.value.playbackSpeed,
206-
tooltip: 'Playback speed',
207-
onSelected: (double speed) {
208-
controller.setPlaybackSpeed(speed);
209-
},
210-
itemBuilder: (BuildContext context) {
211-
return <PopupMenuItem<double>>[
212-
for (final double speed in _examplePlaybackRates)
213-
PopupMenuItem<double>(
214-
value: speed,
215-
child: Text('${speed}x'),
216-
)
217-
];
218-
},
219-
child: Padding(
220-
padding: const EdgeInsets.symmetric(
221-
// Using less vertical padding as the text is also longer
222-
// horizontally, so it feels like it would need more spacing
223-
// horizontally (matching the aspect ratio of the video).
224-
vertical: 12,
225-
horizontal: 16,
226-
),
227-
child: Text('${controller.value.playbackSpeed}x'),
228-
),
229-
),
230-
),
231-
],
232-
);
71+
_controller?.play();
23372
}
23473
}

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)