Skip to content

Commit 24d6d9c

Browse files
authored
[video_player_avfoundation] iOS platform view support (#8237)
This PR adds support for platform views on iOS as a way of displaying a video. When creating a video, it's now possible to choose between texture view approach (rendered using `Texture` widget on the Flutter side) and platform view approach (rendered on the native side, using `AVPlayerLayer`). `FVPVideoPlayer` class now has nothing to do with texture. The texture-related code was moved from it to `FVPTextureBasedVideoPlayer` - a subclass of `FVPVideoPlayer` that adds texture functionality. In the plugin class (`createWithOptions` method) we create either the basic version (for platform view) or the texture subclass (in case of texture approach) based on the parameter passed in from Flutter side. Platform view is only supported on iOS, no MacOS implementation is added in this PR. The functionality is not yet exposed in the app-facing package (only in example app) - it will be done later, once we add the Android implementation. The PR does not introduce breaking changes, I followed the rule "non-breaking changes, even at the expense of a less-clean API" (`buildViewWithOptions` and `createWithOptions` methods). Up to this point, the variable naming relied heavily on the texture (we had a lot of `textureId` variables and properties). Since now you can use a platform view instead of a texture view, these variables and parameters were renamed to just `playerId`. I left some comments in the PR to clarify/discuss some choices. Resolves [#86613](flutter/flutter#86613) (not fully though, as it is not yet available in the app-facing package). ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] page, which explains my responsibilities. - [x] I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use `dart format`.) - [x] I signed the [CLA]. - [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` - [x] I [linked to at least one issue that this PR fixes] in the description above. - [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. - [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or this PR is [exempt from CHANGELOG changes]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md [relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [linked to at least one issue that this PR fixes]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#version [following repository CHANGELOG style]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog-style [exempt from CHANGELOG changes]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog [test-exempt]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests
1 parent 625023a commit 24d6d9c

32 files changed

+1569
-549
lines changed

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.7.0
2+
3+
* Adds support for platform views as an optional way of displaying a video.
4+
15
## 2.6.7
26

37
* Fixes playback speed resetting.

packages/video_player/video_player_avfoundation/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ should add it to your `pubspec.yaml` as usual.
1313

1414
[1]: https://pub.dev/packages/video_player
1515
[2]: https://flutter.dev/to/endorsed-federated-plugin
16+
17+
## Platform limitations
18+
19+
On macOS, the plugin does not currently support platform views. Instead, a texture view is always used to display the video player, even if `VideoViewType.platformView` is specified as a parameter.

packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m

Lines changed: 189 additions & 72 deletions
Large diffs are not rendered by default.

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPFrameUpdater.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ - (void)displayLinkFired {
2525
CMTime outputItemTime = [self.videoOutput itemTimeForHostTime:CACurrentMediaTime()];
2626
if ([self.videoOutput hasNewPixelBufferForItemTime:outputItemTime]) {
2727
_lastKnownAvailableTime = outputItemTime;
28-
[_registry textureFrameAvailable:_textureId];
28+
[_registry textureFrameAvailable:_textureIdentifier];
2929
}
3030
}
3131
@end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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+
#import "./include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h"
6+
#import "./include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h"
7+
8+
@interface FVPTextureBasedVideoPlayer ()
9+
// The CALayer associated with the Flutter view this plugin is associated with, if any.
10+
@property(nonatomic, readonly) CALayer *flutterViewLayer;
11+
// The updater that drives callbacks to the engine to indicate that a new frame is ready.
12+
@property(nonatomic) FVPFrameUpdater *frameUpdater;
13+
// The display link that drives frameUpdater.
14+
@property(nonatomic) FVPDisplayLink *displayLink;
15+
// Whether a new frame needs to be provided to the engine regardless of the current play/pause state
16+
// (e.g., after a seek while paused). If YES, the display link should continue to run until the next
17+
// frame is successfully provided.
18+
@property(nonatomic, assign) BOOL waitingForFrame;
19+
@property(nonatomic, copy) void (^onDisposed)(int64_t);
20+
@end
21+
22+
@implementation FVPTextureBasedVideoPlayer
23+
- (instancetype)initWithAsset:(NSString *)asset
24+
frameUpdater:(FVPFrameUpdater *)frameUpdater
25+
displayLink:(FVPDisplayLink *)displayLink
26+
avFactory:(id<FVPAVFactory>)avFactory
27+
registrar:(NSObject<FlutterPluginRegistrar> *)registrar
28+
onDisposed:(void (^)(int64_t))onDisposed {
29+
return [self initWithURL:[NSURL fileURLWithPath:[FVPVideoPlayer absolutePathForAssetName:asset]]
30+
frameUpdater:frameUpdater
31+
displayLink:displayLink
32+
httpHeaders:@{}
33+
avFactory:avFactory
34+
registrar:registrar
35+
onDisposed:onDisposed];
36+
}
37+
38+
- (instancetype)initWithURL:(NSURL *)url
39+
frameUpdater:(FVPFrameUpdater *)frameUpdater
40+
displayLink:(FVPDisplayLink *)displayLink
41+
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
42+
avFactory:(id<FVPAVFactory>)avFactory
43+
registrar:(NSObject<FlutterPluginRegistrar> *)registrar
44+
onDisposed:(void (^)(int64_t))onDisposed {
45+
NSDictionary<NSString *, id> *options = nil;
46+
if ([headers count] != 0) {
47+
options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers};
48+
}
49+
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options];
50+
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
51+
return [self initWithPlayerItem:item
52+
frameUpdater:frameUpdater
53+
displayLink:displayLink
54+
avFactory:avFactory
55+
registrar:registrar
56+
onDisposed:onDisposed];
57+
}
58+
59+
- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
60+
frameUpdater:(FVPFrameUpdater *)frameUpdater
61+
displayLink:(FVPDisplayLink *)displayLink
62+
avFactory:(id<FVPAVFactory>)avFactory
63+
registrar:(NSObject<FlutterPluginRegistrar> *)registrar
64+
onDisposed:(void (^)(int64_t))onDisposed {
65+
self = [super initWithPlayerItem:item avFactory:avFactory registrar:registrar];
66+
67+
if (self) {
68+
_frameUpdater = frameUpdater;
69+
_displayLink = displayLink;
70+
_frameUpdater.videoOutput = self.videoOutput;
71+
_onDisposed = [onDisposed copy];
72+
73+
// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16
74+
// (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some
75+
// video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An
76+
// invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams
77+
// for issue #1, and restore the correct width and height for issue #2.
78+
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
79+
[self.flutterViewLayer addSublayer:self.playerLayer];
80+
}
81+
return self;
82+
}
83+
84+
- (void)setTextureIdentifier:(int64_t)textureIdentifier {
85+
self.frameUpdater.textureIdentifier = textureIdentifier;
86+
}
87+
88+
- (void)expectFrame {
89+
self.waitingForFrame = YES;
90+
91+
_displayLink.running = YES;
92+
}
93+
94+
#pragma mark - Private methods
95+
96+
- (CALayer *)flutterViewLayer {
97+
#if TARGET_OS_OSX
98+
return self.registrar.view.layer;
99+
#else
100+
#pragma clang diagnostic push
101+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
102+
// TODO(hellohuanlin): Provide a non-deprecated codepath. See
103+
// https://github.com/flutter/flutter/issues/104117
104+
UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;
105+
#pragma clang diagnostic pop
106+
return root.view.layer;
107+
#endif
108+
}
109+
110+
#pragma mark - Overrides
111+
112+
- (void)updatePlayingState {
113+
[super updatePlayingState];
114+
// If the texture is still waiting for an expected frame, the display link needs to keep
115+
// running until it arrives regardless of the play/pause state.
116+
_displayLink.running = self.isPlaying || self.waitingForFrame;
117+
}
118+
119+
- (void)seekTo:(int64_t)location completionHandler:(void (^)(BOOL))completionHandler {
120+
CMTime previousCMTime = self.player.currentTime;
121+
[super seekTo:location
122+
completionHandler:^(BOOL completed) {
123+
if (CMTimeCompare(self.player.currentTime, previousCMTime) != 0) {
124+
// Ensure that a frame is drawn once available, even if currently paused. In theory a
125+
// race is possible here where the new frame has already drawn by the time this code
126+
// runs, and the display link stays on indefinitely, but that should be relatively
127+
// harmless. This must use the display link rather than just informing the engine that a
128+
// new frame is available because the seek completing doesn't guarantee that the pixel
129+
// buffer is already available.
130+
[self expectFrame];
131+
}
132+
133+
if (completionHandler) {
134+
completionHandler(completed);
135+
}
136+
}];
137+
}
138+
139+
- (void)disposeSansEventChannel {
140+
// This check prevents the crash caused by removing the KVO observers twice.
141+
// When performing a Hot Restart, the leftover players are disposed once directly
142+
// by [FVPVideoPlayerPlugin initialize:] method and then disposed again by
143+
// [FVPVideoPlayer onTextureUnregistered:] call leading to possible over-release.
144+
if (self.disposed) {
145+
return;
146+
}
147+
148+
[super disposeSansEventChannel];
149+
150+
[self.playerLayer removeFromSuperlayer];
151+
152+
_displayLink = nil;
153+
}
154+
155+
- (void)dispose {
156+
[super dispose];
157+
158+
_onDisposed(self.frameUpdater.textureIdentifier);
159+
}
160+
161+
#pragma mark - FlutterTexture
162+
163+
- (CVPixelBufferRef)copyPixelBuffer {
164+
CVPixelBufferRef buffer = NULL;
165+
CMTime outputItemTime = [self.videoOutput itemTimeForHostTime:CACurrentMediaTime()];
166+
if ([self.videoOutput hasNewPixelBufferForItemTime:outputItemTime]) {
167+
buffer = [self.videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL];
168+
} else {
169+
// If the current time isn't available yet, use the time that was checked when informing the
170+
// engine that a frame was available (if any).
171+
CMTime lastAvailableTime = self.frameUpdater.lastKnownAvailableTime;
172+
if (CMTIME_IS_VALID(lastAvailableTime)) {
173+
buffer = [self.videoOutput copyPixelBufferForItemTime:lastAvailableTime
174+
itemTimeForDisplay:NULL];
175+
}
176+
}
177+
178+
if (self.waitingForFrame && buffer) {
179+
self.waitingForFrame = NO;
180+
// If the display link was only running temporarily to pick up a new frame while the video was
181+
// paused, stop it again.
182+
if (!self.isPlaying) {
183+
self.displayLink.running = NO;
184+
}
185+
}
186+
187+
return buffer;
188+
}
189+
190+
- (void)onTextureUnregistered:(NSObject<FlutterTexture> *)texture {
191+
dispatch_async(dispatch_get_main_queue(), ^{
192+
if (!self.disposed) {
193+
[self dispose];
194+
}
195+
});
196+
}
197+
198+
@end

0 commit comments

Comments
 (0)