Skip to content

Commit 71fec5b

Browse files
authored
Merge pull request flutter#1 from recastrodiaz/clipVideos
Clip local videos + cache http(s) videos
2 parents 432d820 + 4cc2ad4 commit 71fec5b

6 files changed

Lines changed: 252 additions & 28 deletions

File tree

packages/video_player/android/build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ android {
5252
}
5353

5454
dependencies {
55-
implementation 'com.google.android.exoplayer:exoplayer-core:2.10.3'
56-
implementation 'com.google.android.exoplayer:exoplayer-hls:2.10.3'
57-
implementation 'com.google.android.exoplayer:exoplayer-dash:2.10.3'
58-
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.10.3'
55+
implementation 'com.google.android.exoplayer:exoplayer-core:2.10.4'
56+
implementation 'com.google.android.exoplayer:exoplayer-hls:2.10.4'
57+
implementation 'com.google.android.exoplayer:exoplayer-dash:2.10.4'
58+
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.10.4'
5959
}
6060
}

packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.android.exoplayer2.SimpleExoPlayer;
2323
import com.google.android.exoplayer2.audio.AudioAttributes;
2424
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
25+
import com.google.android.exoplayer2.source.ClippingMediaSource;
2526
import com.google.android.exoplayer2.source.ExtractorMediaSource;
2627
import com.google.android.exoplayer2.source.MediaSource;
2728
import com.google.android.exoplayer2.source.dash.DashMediaSource;
@@ -60,6 +61,7 @@ private static class VideoPlayer {
6061
private static final String FORMAT_OTHER = "other";
6162

6263
private SimpleExoPlayer exoPlayer;
64+
private final String dataSource;
6365

6466
private Surface surface;
6567

@@ -70,6 +72,7 @@ private static class VideoPlayer {
7072
private final EventChannel eventChannel;
7173

7274
private boolean isInitialized = false;
75+
private long startPositionMs = 0;
7376

7477
VideoPlayer(
7578
Context context,
@@ -85,6 +88,7 @@ private static class VideoPlayer {
8588
TrackSelector trackSelector = new DefaultTrackSelector();
8689
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
8790

91+
this.dataSource = dataSource;
8892
Uri uri = Uri.parse(dataSource);
8993

9094
DataSource.Factory dataSourceFactory;
@@ -214,7 +218,8 @@ public void onPlayerError(final ExoPlaybackException error) {
214218
private void sendBufferingUpdate() {
215219
Map<String, Object> event = new HashMap<>();
216220
event.put("event", "bufferingUpdate");
217-
List<? extends Number> range = Arrays.asList(0, exoPlayer.getBufferedPosition());
221+
List<? extends Number> range =
222+
Arrays.asList(0, startPositionMs + exoPlayer.getBufferedPosition());
218223
// iOS supports a list of buffered ranges, so here is a list with a single range.
219224
event.put("values", Collections.singletonList(range));
220225
eventSink.success(event);
@@ -248,11 +253,12 @@ void setVolume(double value) {
248253
}
249254

250255
void seekTo(int location) {
251-
exoPlayer.seekTo(location);
256+
long seekToMs = Math.max(0, location - startPositionMs);
257+
exoPlayer.seekTo(seekToMs);
252258
}
253259

254260
long getPosition() {
255-
return exoPlayer.getCurrentPosition();
261+
return startPositionMs + exoPlayer.getCurrentPosition();
256262
}
257263

258264
void setSpeed(double value) {
@@ -300,6 +306,25 @@ void dispose() {
300306
exoPlayer.release();
301307
}
302308
}
309+
310+
public void clip(Context context, long startMs, long endMs, Result result) {
311+
Uri uri = Uri.parse(dataSource);
312+
313+
DataSource.Factory dataSourceFactory;
314+
if (!isHTTP(uri)) {
315+
dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer");
316+
} else {
317+
result.error(
318+
"invalid_datasource", "clipping a video is not supported for http(s) videos", null);
319+
return;
320+
}
321+
322+
startPositionMs = startMs;
323+
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, null, context);
324+
exoPlayer.prepare(
325+
new ClippingMediaSource(mediaSource, 1000 * startPositionMs, 1000L * endMs));
326+
result.success(null);
327+
}
303328
}
304329

305330
public static void registerWith(Registrar registrar) {
@@ -451,6 +476,13 @@ private void onMethodCall(MethodCall call, Result result, long textureId, VideoP
451476
player.setSpeed((Double) call.argument("speed"));
452477
result.success(null);
453478
break;
479+
case "clip":
480+
player.clip(
481+
registrar.context(),
482+
(Integer) call.argument("startMs"),
483+
(Integer) call.argument("endMs"),
484+
result);
485+
break;
454486
default:
455487
result.notImplemented();
456488
break;

packages/video_player/ios/Classes/VideoPlayerPlugin.m

Lines changed: 157 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,47 @@
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

1152
int64_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;
57102
static 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

Comments
 (0)