From c659cb241152d8ad3eb313f28a0e4fe76f6d560f Mon Sep 17 00:00:00 2001 From: "mj.kong" Date: Tue, 2 Jul 2019 17:05:17 +0900 Subject: [PATCH 1/3] Add initWithAsset:trackIndex:timeRange: --- Source/ILABAudioTrackExporter.h | 10 ++++++ Source/ILABAudioTrackExporter.m | 62 +++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/Source/ILABAudioTrackExporter.h b/Source/ILABAudioTrackExporter.h index 1521c4f..3c15e25 100644 --- a/Source/ILABAudioTrackExporter.h +++ b/Source/ILABAudioTrackExporter.h @@ -28,6 +28,16 @@ */ -(instancetype)initWithAsset:(AVAsset *)sourceAsset trackIndex:(NSInteger)trackIndex; +/** + Create a new instance for the track exporter + + @param sourceAsset The `AVAsset` to export the audio from. + @param trackIndex The index of the audio track to export + @prarm timeRange The time range of the asset to be reversed. + @return The new instance + */ +-(instancetype)initWithAsset:(AVAsset *)sourceAsset trackIndex:(NSInteger)trackIndex timeRange:(CMTimeRange)timeRange; + /** Exports the audio track to a .wav audio file diff --git a/Source/ILABAudioTrackExporter.m b/Source/ILABAudioTrackExporter.m index cae2fc0..0820930 100644 --- a/Source/ILABAudioTrackExporter.m +++ b/Source/ILABAudioTrackExporter.m @@ -32,6 +32,10 @@ @interface ILABAudioTrackExporter() { @implementation ILABAudioTrackExporter -(instancetype)initWithAsset:(AVAsset *)sourceAsset trackIndex:(NSInteger)trackIndex { + return [self initWithAsset:sourceAsset trackIndex:0 timeRange:CMTimeRangeMake(kCMTimeZero, sourceAsset.duration)]; +} + +-(instancetype)initWithAsset:(AVAsset *)sourceAsset trackIndex:(NSInteger)trackIndex timeRange:(CMTimeRange)timeRange { if ((self = [super init])) { lastError = nil; @@ -49,7 +53,7 @@ -(instancetype)initWithAsset:(AVAsset *)sourceAsset trackIndex:(NSInteger)trackI audioComp = [AVMutableComposition composition]; for(AVAssetTrack *track in [sourceAsset tracksWithMediaType:AVMediaTypeAudio]) { AVMutableCompositionTrack *atrack = [audioComp addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; - [atrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, sourceAsset.duration) ofTrack:track atTime:kCMTimeZero error:nil]; + [atrack insertTimeRange:timeRange ofTrack:track atTime:kCMTimeZero error:nil]; } } @@ -168,24 +172,29 @@ -(BOOL)startReadingAndWriting { __block BOOL audioFinished=NO; + __weak typeof(self) weakSelf = self; + dispatch_group_enter(dispatchGroup); // Specify the block to execute when the asset writer is ready for audio media data, and specify the queue to call it on. [writerInput requestMediaDataWhenReadyOnQueue:audioQueue usingBlock:^{ + // Because the block is called asynchronously, check to see whether its task is complete. if (audioFinished) return; + ILABAudioTrackExporter *exporter = weakSelf; + BOOL completedOrFailed = NO; // If the task isn't complete yet, make sure that the input is actually ready for more media data. - while ([writerInput isReadyForMoreMediaData] && !completedOrFailed) { + while ([exporter->writerInput isReadyForMoreMediaData] && !completedOrFailed) { // Get the next audio sample buffer, and append it to the output file. - CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer]; + CMSampleBufferRef sampleBuffer = [exporter->trackOutput copyNextSampleBuffer]; if (sampleBuffer != NULL) { if (![self processSampleBuffer:sampleBuffer]) { completedOrFailed=YES; } else { - BOOL success = [writerInput appendSampleBuffer:sampleBuffer]; + BOOL success = [exporter->writerInput appendSampleBuffer:sampleBuffer]; completedOrFailed = !success; } @@ -195,29 +204,32 @@ -(BOOL)startReadingAndWriting { completedOrFailed = YES; } } - + if (completedOrFailed) { // Mark the input as finished, but only if we haven't already done so, and then leave the dispatch group (since the audio work has finished). BOOL oldFinished = audioFinished; audioFinished = YES; if (oldFinished == NO) { - [writerInput markAsFinished]; + [exporter->writerInput markAsFinished]; } - dispatch_group_leave(dispatchGroup); + dispatch_group_leave(exporter->dispatchGroup); } }]; dispatch_group_notify(dispatchGroup, mainQueue, ^{ + + ILABAudioTrackExporter *exporter = weakSelf; + dispatch_group_t finishGroup=dispatch_group_create(); dispatch_group_enter(finishGroup); - [assetWriter finishWritingWithCompletionHandler:^{ + [exporter->assetWriter finishWritingWithCompletionHandler:^{ dispatch_group_leave(finishGroup); }]; dispatch_group_notify(finishGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - dispatch_semaphore_signal(semi); + dispatch_semaphore_signal(exporter->semi); }); }); @@ -268,6 +280,9 @@ -(void)exportToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)completeBlock -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)completeBlock { NSURL *exportAudioURL = [[outputURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:@"exported-audio.wav"]; + + __weak typeof(self) weakSelf = self; + [self exportToURL:exportAudioURL complete:^(BOOL complete, NSError *error) { if (!complete) { if (completeBlock) { @@ -277,15 +292,16 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet return; } + ILABAudioTrackExporter *exporter = weakSelf; OSStatus theErr = noErr; // set up input file AudioFileID inputAudioFile; theErr = AudioFileOpenURL((__bridge CFURLRef)exportAudioURL, kAudioFileReadPermission, 0, &inputAudioFile); if (theErr != noErr) { - lastError = [NSError errorWithAudioFileStatusCode:theErr]; + exporter->lastError = [NSError errorWithAudioFileStatusCode:theErr]; if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; @@ -296,10 +312,10 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet theErr = AudioFileGetProperty(inputAudioFile, kAudioFilePropertyDataFormat, &thePropertySize, &theFileFormat); if (theErr != noErr) { AudioFileClose(inputAudioFile); - lastError = [NSError errorWithAudioFileStatusCode:theErr]; + exporter->lastError = [NSError errorWithAudioFileStatusCode:theErr]; if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; @@ -310,9 +326,9 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet theErr = AudioFileGetProperty(inputAudioFile, kAudioFilePropertyAudioDataByteCount, &thePropertySize, &fileDataSize); if (theErr != noErr) { AudioFileClose(inputAudioFile); - lastError = [NSError errorWithAudioFileStatusCode:theErr]; + exporter->lastError = [NSError errorWithAudioFileStatusCode:theErr]; if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; @@ -326,10 +342,10 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet &outputAudioFile); if (theErr != noErr) { AudioFileClose(inputAudioFile); - lastError = [NSError errorWithAudioFileStatusCode:theErr]; + exporter->lastError = [NSError errorWithAudioFileStatusCode:theErr]; if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; @@ -344,7 +360,7 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet AudioFileClose(outputAudioFile); if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; @@ -356,10 +372,10 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet if (theErr != noErr) { AudioFileClose(inputAudioFile); AudioFileClose(outputAudioFile); - lastError = [NSError errorWithAudioFileStatusCode:theErr]; + exporter->lastError = [NSError errorWithAudioFileStatusCode:theErr]; if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; @@ -374,7 +390,7 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet AudioFileClose(outputAudioFile); if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; @@ -392,10 +408,10 @@ -(void)exportReverseToURL:(NSURL *)outputURL complete:(ILABCompleteBlock)complet AudioFileClose(inputAudioFile); AudioFileClose(outputAudioFile); - lastError = [NSError errorWithAudioFileStatusCode:theErr]; + exporter->lastError = [NSError errorWithAudioFileStatusCode:theErr]; if (completeBlock) { - completeBlock(NO, lastError); + completeBlock(NO, exporter->lastError); } return; From 56638e4246fe4c6342abdc6a85f539af49fa5f5e Mon Sep 17 00:00:00 2001 From: "mj.kong" Date: Tue, 2 Jul 2019 17:05:51 +0900 Subject: [PATCH 2/3] Add initWithAsset:timeRange: and exportSessionWithAsset:timeRange:outputURL: --- Source/ILABReverseVideoExportSession.h | 13 ++++++-- Source/ILABReverseVideoExportSession.m | 41 ++++++++++++++++++-------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/Source/ILABReverseVideoExportSession.h b/Source/ILABReverseVideoExportSession.h index 274a3de..0b6b5a1 100644 --- a/Source/ILABReverseVideoExportSession.h +++ b/Source/ILABReverseVideoExportSession.h @@ -40,7 +40,14 @@ */ -(instancetype)initWithURL:(NSURL *)sourceVideoURL; - +/** + Create a new instance + + @param sourceAsset The source AVAsset to reverse + @prarm timeRange The time range of the asset to be reversed. + @return The new instance + */ +-(instancetype)initWithAsset:(AVAsset *)sourceAsset timeRange:(CMTimeRange)timeRange; /** Creates a new instance @@ -59,15 +66,15 @@ */ +(instancetype)exportSessionWithURL:(NSURL *)sourceVideoURL outputURL:(NSURL *)outputURL; - /** Creates a new export session @param sourceAsset The source AVAsset to reverse + @prarm timeRange The time range of the asset to be reversed. @param outputURL URL to output reverse video to @return The new instance */ -+(instancetype)exportSessionWithAsset:(AVAsset *)sourceAsset outputURL:(NSURL *)outputURL; ++(instancetype)exportSessionWithAsset:(AVAsset *)sourceAsset timeRange:(CMTimeRange)timeRange outputURL:(NSURL *)outputURL; /** Start the export process asynchronously diff --git a/Source/ILABReverseVideoExportSession.m b/Source/ILABReverseVideoExportSession.m index 3193da5..5291664 100644 --- a/Source/ILABReverseVideoExportSession.m +++ b/Source/ILABReverseVideoExportSession.m @@ -25,8 +25,10 @@ @interface ILABReverseVideoExportSession() { AVAsset *sourceAsset; + AVAsset *videoAsset; NSError *lastError; } +@property (nonatomic) CMTimeRange timeRange; @end @@ -34,12 +36,16 @@ @implementation ILABReverseVideoExportSession #pragma mark - Init/Dealloc --(instancetype)initWithAsset:(AVAsset *)sourceAVAsset { +-(instancetype)initWithAsset:(AVAsset *)sourceAsset { + return [self initWithAsset:sourceAsset timeRange:CMTimeRangeMake(kCMTimeZero, sourceAsset.duration)]; +} + +-(instancetype)initWithAsset:(AVAsset *)sourceAVAsset timeRange:(CMTimeRange)timeRange { if ((self = [super init])) { sourceAsset = sourceAVAsset; - _samplesPerPass = 100; + _samplesPerPass = 5; _sourceVideoTracks = 0; _sourceAudioTracks = 0; @@ -60,6 +66,16 @@ -(instancetype)initWithAsset:(AVAsset *)sourceAVAsset { AVLinearPCMBitDepthKey: @(32), AVLinearPCMIsFloatKey: @(YES) }; + + AVAssetTrack *track = [sourceAVAsset tracksWithMediaType:AVMediaTypeVideo].firstObject; + + AVMutableComposition *videoComp = [AVMutableComposition composition]; + AVMutableCompositionTrack *vTrack = [videoComp addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; + [vTrack insertTimeRange:timeRange ofTrack:track atTime:kCMTimeZero error:nil]; + + videoAsset = videoComp; + + self.timeRange = timeRange; dispatch_semaphore_t loadSemi = dispatch_semaphore_create(0); __weak typeof(self) weakSelf = self; @@ -81,8 +97,10 @@ -(instancetype)initWithAsset:(AVAsset *)sourceAVAsset { strongSelf->_sourceAudioTracks = [strongSelf->sourceAsset tracksWithMediaType:AVMediaTypeAudio].count; strongSelf->_sourceFPS = t.nominalFrameRate; - _sourceTransform = t.preferredTransform; - if (_sourceTransform.a == 0 && _sourceTransform.d == 0 && (_sourceTransform.b == 1.0 || _sourceTransform.b == -1.0) && (_sourceTransform.c == 1.0 || _sourceTransform.c == -1.0)) { + strongSelf->_sourceTransform = t.preferredTransform; + if (strongSelf->_sourceTransform.a == 0 && strongSelf->_sourceTransform.d == 0 && + (strongSelf->_sourceTransform.b == 1.0 || strongSelf->_sourceTransform.b == -1.0) && + (strongSelf->_sourceTransform.c == 1.0 || strongSelf->_sourceTransform.c == -1.0)) { strongSelf->_sourceSize = CGSizeMake(t.naturalSize.height, t.naturalSize.width); } else { strongSelf->_sourceSize = CGSizeMake(t.naturalSize.width, t.naturalSize.height); @@ -97,7 +115,6 @@ -(instancetype)initWithAsset:(AVAsset *)sourceAVAsset { while(dispatch_semaphore_wait(loadSemi, DISPATCH_TIME_NOW)) { [[NSRunLoop mainRunLoop] runUntilDate:[NSDate date]]; } - } return self; @@ -118,8 +135,8 @@ +(instancetype)exportSessionWithURL:(NSURL *)sourceVideoURL outputURL:(NSURL *)o return session; } -+(instancetype)exportSessionWithAsset:(AVAsset *)sourceAsset outputURL:(NSURL *)outputURL { - ILABReverseVideoExportSession *session = [[[self class] alloc] initWithAsset:sourceAsset]; ++(instancetype)exportSessionWithAsset:(AVAsset *)sourceAsset timeRange:(CMTimeRange)timeRange outputURL:(NSURL *)outputURL { + ILABReverseVideoExportSession *session = [[[self class] alloc] initWithAsset:sourceAsset timeRange:timeRange]; session.outputURL = outputURL; return session; @@ -311,7 +328,7 @@ -(void)exportReversedAudio:(NSURL *)reversedAudioPath results:(ILABGenerateAsset dispatch_async([[self class] generateQueue], ^{ if (weakSelf) { __strong typeof(weakSelf) strongSelf = weakSelf; - ILABAudioTrackExporter *audioExporter = [[ILABAudioTrackExporter alloc] initWithAsset:sourceAsset trackIndex:0]; + ILABAudioTrackExporter *audioExporter = [[ILABAudioTrackExporter alloc] initWithAsset:strongSelf->sourceAsset trackIndex:0 timeRange:strongSelf->_timeRange]; dispatch_semaphore_t audioExportSemi = dispatch_semaphore_create(0); @@ -352,7 +369,7 @@ -(void)exportReversedVideo:(NSURL *)reversedVideoPath progress:(ILABProgressBloc __strong typeof(weakSelf) strongSelf = weakSelf; // Setup the reader NSError *error = nil; - AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:strongSelf->sourceAsset error:&error]; + AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:strongSelf->videoAsset error:&error]; if (error) { strongSelf->lastError = error; resultsBlock(NO, nil, error); @@ -360,7 +377,7 @@ -(void)exportReversedVideo:(NSURL *)reversedVideoPath progress:(ILABProgressBloc } // Setup the reader output - AVAssetReaderTrackOutput *assetReaderOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:[strongSelf->sourceAsset tracksWithMediaType:AVMediaTypeVideo].firstObject outputSettings:@{ (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) }]; + AVAssetReaderTrackOutput *assetReaderOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:[strongSelf->videoAsset tracksWithMediaType:AVMediaTypeVideo].firstObject outputSettings:@{ (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) }]; assetReaderOutput.supportsRandomAccess = YES; [assetReader addOutput:assetReaderOutput]; if (![assetReader startReading]) { @@ -368,7 +385,7 @@ -(void)exportReversedVideo:(NSURL *)reversedVideoPath progress:(ILABProgressBloc resultsBlock(NO, nil, strongSelf->lastError); return; } - + // Fetch the sample times for the source video NSMutableArray *revSampleTimes = [NSMutableArray new]; CMSampleBufferRef sample; @@ -388,7 +405,7 @@ -(void)exportReversedVideo:(NSURL *)reversedVideoPath progress:(ILABProgressBloc localCount++; } - + // No samples, no bueno if (revSampleTimes.count == 0) { strongSelf->lastError = [NSError reverseVideoExportSessionError:ILABReverseVideoExportSessionNoSamplesError]; From 9aaa848c238c0086627a136e8fb5cd2cd8f0cc10 Mon Sep 17 00:00:00 2001 From: "mj.kong" Date: Tue, 2 Jul 2019 17:08:54 +0900 Subject: [PATCH 3/3] Update layouts --- .../Base.lproj/Main.storyboard | 140 +++++++++++-- .../ViewController.m | 190 +++++++++++++++++- 2 files changed, 306 insertions(+), 24 deletions(-) diff --git a/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/Base.lproj/Main.storyboard b/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/Base.lproj/Main.storyboard index 0a2f802..0661d94 100644 --- a/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/Base.lproj/Main.storyboard +++ b/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/Base.lproj/Main.storyboard @@ -1,12 +1,11 @@ - + - - + @@ -22,22 +21,121 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - - - + + + + + + + + + + + + + + diff --git a/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/ViewController.m b/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/ViewController.m index 4c5708b..e47bf83 100644 --- a/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/ViewController.m +++ b/ILABReverseVideoExporterDemo/ILABReverseVideoExporterDemo/ViewController.m @@ -15,12 +15,32 @@ #import #import -@interface ViewController() { +@interface ViewController() { __weak IBOutlet M13ProgressViewBar *progressBar; __weak IBOutlet UILabel *progressLabel; } +@property (weak, nonatomic) IBOutlet UILabel *startTimeLabel; +@property (weak, nonatomic) IBOutlet UILabel *durationLabel; +@property (weak, nonatomic) IBOutlet UIButton *reverseVideoButton; +@property (weak, nonatomic) IBOutlet UIButton *pickerMediaButton; + +@property (strong, nonatomic) AVAsset *asset; +@property (nonatomic, strong) ILABReverseVideoExportSession *exportSession; + +// picker +@property (nonatomic, strong) UITextField *textField; +@property (nonatomic, strong) UIPickerView *pickerView; + +@property (nonatomic) CMTime startTime; +@property (nonatomic) CMTime duration; + +@property (nonatomic) BOOL pressStartTimeChange; +@property (nonatomic) NSUInteger pickerComponentHour; +@property (nonatomic) NSUInteger pickerComponentMinute; +@property (nonatomic) NSUInteger pickerComponentSecond; +@property (nonatomic) NSUInteger pickerComponentMilliSecond; @end @implementation ViewController @@ -36,21 +56,38 @@ - (void)viewDidLoad { progressLabel.hidden = YES; progressLabel.text = @""; + + [self initPickerView]; } - - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } -- (IBAction)reverseVideoTouched:(id)sender { +- (IBAction)pickerVideoTouched:(id)sender { QBImagePickerController *picker = [QBImagePickerController new]; picker.delegate = self; picker.mediaType = QBImagePickerMediaTypeVideo; [self presentViewController:picker animated:YES completion:nil]; } +- (IBAction)reverseVideoTouched:(id)sender { + [self processAsset:self.asset startTime:self.startTime duration:self.duration]; +} + +- (NSString *)timeFormatted:(CMTime)time +{ + NSUInteger seconds = CMTimeGetSeconds(time); + + NSUInteger hour = seconds / 3600; + NSUInteger minute = (seconds / 60) % 60; + NSUInteger second = seconds % 60; + NSUInteger millisecond = ((time.value % time.timescale) / 1000) > 1 ? (time.value % time.timescale) / 1000 : time.value % time.timescale; + + return [NSString stringWithFormat:@"%02lu:%02lu:%02lu.%03lu", (unsigned long)hour, (unsigned long)minute, (unsigned long)second, (unsigned long)millisecond]; +} + - (void)qb_imagePickerController:(QBImagePickerController *)imagePickerController didFinishPickingItems:(NSArray *)items { PHVideoRequestOptions *reqOpts=[PHVideoRequestOptions new]; reqOpts.version=PHImageRequestOptionsVersionCurrent; @@ -60,16 +97,32 @@ - (void)qb_imagePickerController:(QBImagePickerController *)imagePickerControlle NSLog(@"Video download progress %f", progress); }; + __weak typeof(self) weakSelf = self; + [[PHImageManager defaultManager] requestAVAssetForVideo:[items firstObject] options:reqOpts resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) { - [self processAsset:asset]; + dispatch_async(dispatch_get_main_queue(), ^{ + weakSelf.asset = asset; + weakSelf.startTime = kCMTimeZero; + weakSelf.duration = asset.duration; + [weakSelf.startTimeLabel setText:[self timeFormatted:kCMTimeZero]]; + [weakSelf.durationLabel setText:[self timeFormatted:asset.duration]]; + }); }]; [self dismissViewControllerAnimated:YES completion:nil]; } --(void)processAsset:(AVAsset *)asset { +-(void)processAsset:(AVAsset *)asset startTime:(CMTime)startTime duration:(CMTime)duration { + if (asset == nil) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:@"Please picker media" preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; + [alert addAction:action]; + [self presentViewController:alert animated:YES completion:nil]; + return; + } + [progressBar performAction:M13ProgressViewActionNone animated:NO]; progressBar.hidden = NO; [progressBar setProgress:0. animated:NO]; @@ -81,9 +134,9 @@ -(void)processAsset:(AVAsset *)asset { NSURL *outputURL = [NSURL fileURLWithPath:[cachePath stringByAppendingFormat:@"%@-reversed.mov", [[NSUUID UUID] UUIDString]]]; NSLog(@"Output URL: %@", outputURL.path); - ILABReverseVideoExportSession *exportSession = [ILABReverseVideoExportSession exportSessionWithURL:((AVURLAsset *)asset).URL - outputURL:outputURL]; - + self.exportSession = [ILABReverseVideoExportSession exportSessionWithAsset:asset + timeRange:CMTimeRangeMake(startTime, duration) + outputURL:outputURL]; __weak typeof(self) weakSelf = self; ILABProgressBlock progressBlock = ^(NSString *currentOperation, float progress) { if (weakSelf) { @@ -121,10 +174,15 @@ -(void)processAsset:(AVAsset *)asset { }); }; - [exportSession exportAsynchronously:progressBlock complete:completeBlock]; + [self.exportSession exportAsynchronously:progressBlock complete:completeBlock]; } -(void)showReversedVideoAsset:(AVAsset *)asset { + self.asset = nil; + + [self.startTimeLabel setText:[self timeFormatted:kCMTimeZero]]; + [self.durationLabel setText:[self timeFormatted:kCMTimeZero]]; + progressLabel.hidden = YES; progressBar.hidden = YES; @@ -133,4 +191,118 @@ -(void)showReversedVideoAsset:(AVAsset *)asset { [self presentViewController:avpc animated:YES completion:nil]; } +#pragma mark - picker +- (void)initPickerView { + self.pickerView = [[UIPickerView alloc] init]; + self.pickerView.dataSource = self; + self.pickerView.delegate = self; +} + +- (void)setPickerComponents:(CMTime)time { + NSUInteger seconds = CMTimeGetSeconds(time); + + self.pickerComponentHour = seconds / 3600; + self.pickerComponentMinute = (seconds / 60) % 60; + self.pickerComponentSecond = seconds % 60; + self.pickerComponentMilliSecond = ((time.value % time.timescale) / 1000) > 1 ? (time.value % time.timescale) / 1000 : time.value % time.timescale; +} + +- (void)selectPickerRows { + [self.pickerView selectRow:self.pickerComponentHour inComponent:0 animated:NO]; + [self.pickerView selectRow:self.pickerComponentMinute inComponent:1 animated:NO]; + [self.pickerView selectRow:self.pickerComponentSecond inComponent:2 animated:NO]; + [self.pickerView selectRow:self.pickerComponentMilliSecond inComponent:3 animated:NO]; +} + +- (IBAction)startTimeChange:(id)sender { + self.pressStartTimeChange = YES; + + [self setPickerComponents:self.startTime]; + [self selectPickerRows]; + [self showReverseTimeControllPicker:@"Change Reverse Start Time" message:[NSString stringWithFormat:@"asset duration: %@", [self timeFormatted:self.asset.duration]]]; +} + +- (IBAction)durationChange:(id)sender { + self.pressStartTimeChange = NO; + + [self setPickerComponents:self.duration]; + [self selectPickerRows]; + [self showReverseTimeControllPicker:@"Change Reverse Duration" + message:[NSString stringWithFormat:@"asset duration: %@\nstart time: %@", + [self timeFormatted:self.asset.duration], [self timeFormatted:self.startTime]]]; +} + +- (void)showReverseTimeControllPicker:(NSString *)title message:(NSString *)message { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + Float64 seconds = self.pickerComponentHour * 3600 + self.pickerComponentMinute * 60 + self.pickerComponentSecond + self.pickerComponentMilliSecond / 1000.0; + CMTime time = CMTimeMakeWithSeconds(seconds, 1000); + + BOOL inputTimeError = NO; + if (self.pressStartTimeChange) { + if (CMTimeCompare(self.asset.duration, time) < 0) { + inputTimeError = YES; + } + } else { + if (CMTimeCompare(self.asset.duration, CMTimeAdd(self.startTime, time)) < 0) { + inputTimeError = YES; + } + } + if (inputTimeError) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:@"Inputed time is wrong" preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; + [alert addAction:action]; + [self presentViewController:alert animated:YES completion:nil]; + return; + } + + if (self.pressStartTimeChange) { + self.startTime = time; + self.startTimeLabel.text = [self timeFormatted:time]; + } else { + self.duration = time; + self.durationLabel.text = [self timeFormatted:time]; + } + }]; + [alert addTextFieldWithConfigurationHandler:nil]; + [alert addAction:action]; + self.textField = alert.textFields.firstObject; + self.textField.inputView = self.pickerView; + if (self.pressStartTimeChange) { + self.textField.text = [self timeFormatted:self.startTime]; + } else { + self.textField.text = [self timeFormatted:self.duration]; + } + [self presentViewController:alert animated:TRUE completion:nil]; +} + +#pragma mark - UIPickerViewDataSource +- (NSInteger)numberOfComponentsInPickerView:(nonnull UIPickerView *)pickerView { + return 4; // Hour : Minute : Second : MilliSecond +} + +- (NSInteger)pickerView:(nonnull UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { + if (component == 0) return 24; + else if (component == 1) return 60; + else if (component == 2) return 60; + else return 1000; +} + +#pragma mark - UIPickerViewDelegate +- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component { + if (component == 0) self.pickerComponentHour = row; + else if (component == 1) self.pickerComponentMinute = row; + else if (component == 2) self.pickerComponentSecond = row; + else self.pickerComponentMilliSecond = row; + + [self.textField setText:[NSString stringWithFormat:@"%02lu:%02lu:%02lu.%03lu", (unsigned long)self.pickerComponentHour, + (unsigned long)self.pickerComponentMinute, + (unsigned long)self.pickerComponentSecond, + (unsigned long)self.pickerComponentMilliSecond]]; +} + +- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { + return [NSString stringWithFormat:@"%d", (int)row]; +} + @end