77#import < GLKit/GLKit.h>
88
99#import < Photos/Photos.h>
10+ #import " VIMediaCache.h"
11+
12+ @interface VIMediaCacheSingleton : NSObject {
13+ VIResourceLoaderManager* resourceLoaderManager;
14+ }
15+
16+ @property (nonatomic , retain ) VIResourceLoaderManager* resourceLoaderManager;
17+
18+ + (id )sharedVIMediaCache ;
19+
20+ @end
21+
22+ @implementation VIMediaCacheSingleton
23+
24+ @synthesize resourceLoaderManager;
25+
26+ #pragma mark Singleton Methods
27+
28+ + (id )sharedVIMediaCache {
29+ static VIResourceLoaderManager* shared = nil ;
30+ static dispatch_once_t onceToken;
31+ dispatch_once (&onceToken, ^{
32+ shared = [[self alloc ] init ];
33+ });
34+ return shared;
35+ }
36+
37+ - (id )init {
38+ if (self = [super init ]) {
39+ resourceLoaderManager = [VIResourceLoaderManager new ];
40+ }
41+ return self;
42+ }
43+
44+ - (void )dealloc {
45+ // Should never be called, but just here for clarity really.
46+ }
47+
48+ @end
49+
50+ #pragma mark FLTFrameUpdater
1051
1152int64_t FLTCMTimeToMillis (CMTime time) {
1253 if (time.timescale == 0 ) return 0 ;
@@ -32,8 +73,11 @@ - (void)onDisplayLink:(CADisplayLink*)link {
3273}
3374@end
3475
76+ #pragma mark FLTVideoPlayer
77+
3578@interface FLTVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
3679@property (readonly , nonatomic ) AVPlayer* player;
80+ @property (nonatomic ) AVAsset* fullAsset;
3781@property (readonly , nonatomic ) AVPlayerItemVideoOutput* videoOutput;
3882@property (readonly , nonatomic ) CADisplayLink * displayLink;
3983@property (nonatomic ) FlutterEventChannel* eventChannel;
@@ -43,6 +87,7 @@ @interface FLTVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
4387@property (nonatomic , readonly ) bool isPlaying;
4488@property (nonatomic ) bool isLooping;
4589@property (nonatomic , readonly ) bool isInitialized;
90+ @property (nonatomic ) CMTime startPosition;
4691- (instancetype )initWithURL : (NSURL *)url frameUpdater : (FLTFrameUpdater*)frameUpdater ;
4792- (void )play ;
4893- (void )pause ;
@@ -57,6 +102,7 @@ - (void)updatePlayingState;
57102static void * playbackBufferFullContext = &playbackBufferFullContext;
58103
59104@implementation FLTVideoPlayer
105+
60106- (instancetype )initWithAsset : (NSString *)asset frameUpdater : (FLTFrameUpdater*)frameUpdater {
61107 NSString * path = [[NSBundle mainBundle ] pathForResource: asset ofType: nil ];
62108 return [self initWithURL: [NSURL fileURLWithPath: path] frameUpdater: frameUpdater];
@@ -137,11 +183,12 @@ static inline CGFloat radiansToDegrees(CGFloat radians) {
137183};
138184
139185- (AVMutableVideoComposition*)getVideoCompositionWithTransform : (CGAffineTransform)transform
140- withAsset : (AVAsset*) asset
186+ withTimeRange : (CMTimeRange) timeRange
141187 withVideoTrack : (AVAssetTrack*)videoTrack {
142188 AVMutableVideoCompositionInstruction* instruction =
143189 [AVMutableVideoCompositionInstruction videoCompositionInstruction ];
144- instruction.timeRange = CMTimeRangeMake (kCMTimeZero , [asset duration ]);
190+ instruction.timeRange = timeRange;
191+
145192 AVMutableVideoCompositionLayerInstruction* layerInstruction =
146193 [AVMutableVideoCompositionLayerInstruction
147194 videoCompositionLayerInstructionWithAssetTrack: videoTrack];
@@ -183,7 +230,8 @@ - (void)createVideoOutputAndDisplayLink:(FLTFrameUpdater*)frameUpdater {
183230}
184231
185232- (instancetype )initWithURL : (NSURL *)url frameUpdater : (FLTFrameUpdater*)frameUpdater {
186- AVPlayerItem* item = [AVPlayerItem playerItemWithURL: url];
233+ VIMediaCacheSingleton* shared = [VIMediaCacheSingleton sharedVIMediaCache ];
234+ AVPlayerItem* item = [shared.resourceLoaderManager playerItemWithURL: url];
187235 return [self initWithPlayerItem: item frameUpdater: frameUpdater];
188236}
189237
@@ -227,6 +275,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem*)item frameUpdater:(FLTFrameUpd
227275 AVAssetTrack* videoTrack = tracks[0 ];
228276 void (^trackCompletionHandler)(void ) = ^{
229277 if (self->_disposed ) return ;
278+ self.fullAsset = asset;
230279 if ([videoTrack statusOfValueForKey: @" preferredTransform"
231280 error: nil ] == AVKeyValueStatusLoaded) {
232281 // Rotate the video by using a videoComposition and the preferredTransform
@@ -235,9 +284,10 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem*)item frameUpdater:(FLTFrameUpd
235284 // https://developer.apple.com/documentation/avfoundation/avplayeritem/1388818-videocomposition
236285 // Video composition can only be used with file-based media and is not supported for
237286 // use with media served using HTTP Live Streaming.
287+ CMTimeRange timeRange = CMTimeRangeMake (kCMTimeZero , [asset duration ]);
238288 AVMutableVideoComposition* videoComposition =
239289 [self getVideoCompositionWithTransform: self ->_preferredTransform
240- withAsset: asset
290+ withTimeRange: timeRange
241291 withVideoTrack: videoTrack];
242292 item.videoComposition = videoComposition;
243293 }
@@ -251,6 +301,8 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem*)item frameUpdater:(FLTFrameUpd
251301 _player = [AVPlayer playerWithPlayerItem: item];
252302 _player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
253303
304+ _startPosition = kCMTimeZero ;
305+
254306 [self createVideoOutputAndDisplayLink: frameUpdater];
255307
256308 [self addObservers: item];
@@ -269,7 +321,7 @@ - (void)observeValueForKeyPath:(NSString*)path
269321 NSMutableArray <NSArray <NSNumber *>*>* values = [[NSMutableArray alloc ] init ];
270322 for (NSValue * rangeValue in [object loadedTimeRanges ]) {
271323 CMTimeRange range = [rangeValue CMTimeRangeValue ];
272- int64_t start = FLTCMTimeToMillis (range.start );
324+ int64_t start = FLTCMTimeToMillis (range.start ) + FLTCMTimeToMillis (_startPosition) ;
273325 [values addObject: @[ @(start), @(start + FLTCMTimeToMillis (range.duration)) ]];
274326 }
275327 _eventSink (@{@" event" : @" bufferingUpdate" , @" values" : values});
@@ -360,17 +412,22 @@ - (void)pause {
360412}
361413
362414- (int64_t )position {
363- return FLTCMTimeToMillis ([_player currentTime ]);
415+ return FLTCMTimeToMillis ([_player currentTime ]) + FLTCMTimeToMillis (_startPosition) ;
364416}
365417
366418- (int64_t )duration {
367- return FLTCMTimeToMillis ([[_player currentItem ] duration ]);
419+ return FLTCMTimeToMillis ([_fullAsset duration ]);
368420}
369421
370422- (void )seekTo : (int )location {
371- [_player seekToTime: CMTimeMake (location, 1000 )
372- toleranceBefore: kCMTimeZero
373- toleranceAfter: kCMTimeZero ];
423+ CMTime disiredPosition = CMTimeMake (location, 1000 );
424+ CMTime computedPosition = CMTimeSubtract (disiredPosition, _startPosition);
425+ // NSLog(@"Computed positon: %f : %f", CMTimeGetSeconds(computedPosition),
426+ // CMTimeGetSeconds(disiredPosition));
427+ computedPosition =
428+ CMTimeClampToRange (computedPosition, CMTimeRangeMake (kCMTimeZero , kCMTimePositiveInfinity ));
429+ // NSLog(@"Computed positon (2): %f", CMTimeGetSeconds(computedPosition));
430+ [_player seekToTime: computedPosition toleranceBefore: kCMTimeZero toleranceAfter: kCMTimeZero ];
374431}
375432
376433- (void )setIsLooping : (bool )isLooping {
@@ -406,6 +463,84 @@ - (void)setSpeed:(double)speed result:(FlutterResult)result {
406463 }
407464}
408465
466+ - (void )clip : (int )startMs endMs : (int )endMs result : (FlutterResult)result {
467+ if (self->_disposed ) {
468+ result (nil );
469+ return ;
470+ }
471+
472+ CMTime videoDuration = _fullAsset.duration ;
473+ if (CMTIME_IS_INDEFINITE (videoDuration)) {
474+ result ([FlutterError errorWithCode: @" video_not_ready"
475+ message: @" Do not call clip until the video is ready to play"
476+ details: nil ]);
477+ } else if (self.fullAsset == nil ) {
478+ result ([FlutterError errorWithCode: @" video_asset_not_ready"
479+ message: @" Do not call clip until the video is ready to play"
480+ details: nil ]);
481+ } else if (startMs < 0 || endMs <= startMs || endMs > 1000 * CMTimeGetSeconds (videoDuration)) {
482+ result ([FlutterError errorWithCode: @" unsupported_clip_parameters"
483+ message: @" startMs must be >= 0.0 and < endMs and endMs <= duration"
484+ details: nil ]);
485+ } else {
486+ [self removeAvPlayerObservers ];
487+
488+ CMTime start = CMTimeMake (startMs, 1000 );
489+ _startPosition = start;
490+ CMTime duration = CMTimeMake (endMs - startMs, 1000 );
491+
492+ NSError * error = nil ;
493+ AVMutableVideoComposition* videoComposition = nil ;
494+ AVMutableComposition* mutableComposition = [AVMutableComposition composition ];
495+ if ([[self .fullAsset tracksWithMediaType: AVMediaTypeVideo] count ] != 0 ) {
496+ AVAssetTrack* videoTrack =
497+ [[self .fullAsset tracksWithMediaType: AVMediaTypeVideo] objectAtIndex: 0 ];
498+
499+ AVMutableCompositionTrack* videoComTrack =
500+ [mutableComposition addMutableTrackWithMediaType: AVMediaTypeVideo
501+ preferredTrackID: kCMPersistentTrackID_Invalid ];
502+ [videoComTrack insertTimeRange: CMTimeRangeMake (start, duration)
503+ ofTrack: videoTrack
504+ atTime: kCMTimeZero
505+ error: &error];
506+
507+ videoComposition =
508+ [self getVideoCompositionWithTransform: self ->_preferredTransform
509+ withTimeRange: CMTimeRangeMake (kCMTimeZero , duration)
510+ withVideoTrack: videoTrack];
511+ }
512+ if ([[self .fullAsset tracksWithMediaType: AVMediaTypeAudio] count ] != 0 ) {
513+ AVAssetTrack* audioTrack =
514+ [[self .fullAsset tracksWithMediaType: AVMediaTypeAudio] objectAtIndex: 0 ];
515+
516+ AVMutableCompositionTrack* audioComTrack =
517+ [mutableComposition addMutableTrackWithMediaType: AVMediaTypeAudio
518+ preferredTrackID: kCMPersistentTrackID_Invalid ];
519+ [audioComTrack insertTimeRange: CMTimeRangeMake (start, duration)
520+ ofTrack: audioTrack
521+ atTime: kCMTimeZero
522+ error: &error];
523+ }
524+
525+ if (!error) {
526+ AVPlayerItem* newItem = [[AVPlayerItem alloc ] initWithAsset: mutableComposition];
527+
528+ if (videoComposition) {
529+ newItem.videoComposition = videoComposition;
530+ }
531+
532+ [self ->_player replaceCurrentItemWithPlayerItem: newItem];
533+ [self addObservers: newItem];
534+ result (nil );
535+ } else {
536+ result ([FlutterError
537+ errorWithCode: @" clip_error"
538+ message: @" Could not clip video from \( start) with duration \( duration)"
539+ details: error]);
540+ }
541+ }
542+ }
543+
409544- (CVPixelBufferRef)copyPixelBuffer {
410545 CMTime outputItemTime = [_videoOutput itemTimeForHostTime: CACurrentMediaTime ()];
411546 if ([_videoOutput hasNewPixelBufferForItemTime: outputItemTime]) {
@@ -432,9 +567,7 @@ - (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
432567 return nil ;
433568}
434569
435- - (void )dispose {
436- _disposed = true ;
437- [_displayLink invalidate ];
570+ - (void )removeAvPlayerObservers {
438571 [[_player currentItem ] removeObserver: self forKeyPath: @" status" context: statusContext];
439572 [[_player currentItem ] removeObserver: self
440573 forKeyPath: @" loadedTimeRanges"
@@ -450,6 +583,12 @@ - (void)dispose {
450583 context: playbackBufferFullContext];
451584 [_player replaceCurrentItemWithPlayerItem: nil ];
452585 [[NSNotificationCenter defaultCenter ] removeObserver: self ];
586+ }
587+
588+ - (void )dispose {
589+ _disposed = true ;
590+ [_displayLink invalidate ];
591+ [self removeAvPlayerObservers ];
453592 [_eventChannel setStreamHandler: nil ];
454593}
455594
@@ -575,6 +714,11 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
575714 } else if ([@" setSpeed" isEqualToString: call.method]) {
576715 [player setSpeed: [[argsMap objectForKey: @" speed" ] doubleValue ] result: result];
577716 return ;
717+ } else if ([@" clip" isEqualToString: call.method]) {
718+ [player clip: [[argsMap objectForKey: @" startMs" ] intValue ]
719+ endMs: [[argsMap objectForKey: @" endMs" ] intValue ]
720+ result: result];
721+ return ;
578722 } else {
579723 result (FlutterMethodNotImplemented);
580724 }
0 commit comments