|
| 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