diff --git a/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo.xcodeproj/project.pbxproj b/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo.xcodeproj/project.pbxproj index 8a40e18..cd786e8 100644 --- a/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo.xcodeproj/project.pbxproj +++ b/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo.xcodeproj/project.pbxproj @@ -33,7 +33,8 @@ 7F52B3FB24C05A440048AE58 /* VMPythonResourceDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F52B3EF24C05A440048AE58 /* VMPythonResourceDownloader.m */; }; 7F52B3FC24C05A440048AE58 /* VMDownloadOperationConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F52B3F224C05A440048AE58 /* VMDownloadOperationConstants.m */; }; 7F52B3FD24C05A440048AE58 /* VMPythonDownloadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F52B3F324C05A440048AE58 /* VMPythonDownloadOperation.m */; }; - 7F92A79924C1E13E00D3A15C /* VMVideoNAudioMerger.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F92A79824C1E13E00D3A15C /* VMVideoNAudioMerger.m */; }; + 7F6D774E24C2F6FA00294B9F /* VMVideoNAudioMerger.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6D774D24C2F6FA00294B9F /* VMVideoNAudioMerger.m */; }; + 7F6D775124C2F73F00294B9F /* VMMergingAssetTrackModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6D775024C2F73F00294B9F /* VMMergingAssetTrackModel.m */; }; 7FFEE24624A883DE0071144F /* AFNetworking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FFEE24524A883DE0071144F /* AFNetworking.framework */; }; 7FFEE24A24A883E10071144F /* MBProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FFEE24924A883E10071144F /* MBProgressHUD.framework */; }; 7FFEE25A24AADB9D0071144F /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FFEE25924AADB9C0071144F /* libbz2.tbd */; }; @@ -105,8 +106,10 @@ 7F52B3F424C05A440048AE58 /* VMDownloadOperationConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VMDownloadOperationConstants.h; sourceTree = ""; }; 7F52B3F524C05A440048AE58 /* VMPythonResourceDownloaderDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VMPythonResourceDownloaderDelegate.h; sourceTree = ""; }; 7F52B3F624C05A440048AE58 /* VMPythonResourceDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VMPythonResourceDownloader.h; sourceTree = ""; }; - 7F92A79724C1E13E00D3A15C /* VMVideoNAudioMerger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMVideoNAudioMerger.h; sourceTree = ""; }; - 7F92A79824C1E13E00D3A15C /* VMVideoNAudioMerger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMVideoNAudioMerger.m; sourceTree = ""; }; + 7F6D774C24C2F6FA00294B9F /* VMVideoNAudioMerger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VMVideoNAudioMerger.h; sourceTree = ""; }; + 7F6D774D24C2F6FA00294B9F /* VMVideoNAudioMerger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VMVideoNAudioMerger.m; sourceTree = ""; }; + 7F6D774F24C2F73F00294B9F /* VMMergingAssetTrackModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMMergingAssetTrackModel.h; sourceTree = ""; }; + 7F6D775024C2F73F00294B9F /* VMMergingAssetTrackModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMMergingAssetTrackModel.m; sourceTree = ""; }; 7FFEE24524A883DE0071144F /* AFNetworking.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AFNetworking.framework; path = ../Carthage/Build/iOS/AFNetworking.framework; sourceTree = ""; }; 7FFEE24924A883E10071144F /* MBProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MBProgressHUD.framework; path = ../Carthage/Build/iOS/MBProgressHUD.framework; sourceTree = ""; }; 7FFEE25924AADB9C0071144F /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; @@ -284,8 +287,7 @@ children = ( 7F52B3E424C05A440048AE58 /* VMFileSizeCalculator.h */, 7F52B3E524C05A440048AE58 /* VMFileSizeCalculator.m */, - 7F92A79724C1E13E00D3A15C /* VMVideoNAudioMerger.h */, - 7F92A79824C1E13E00D3A15C /* VMVideoNAudioMerger.m */, + 7F6D774B24C2F6FA00294B9F /* VMVideoNAudioMerger */, ); path = Others; sourceTree = ""; @@ -332,6 +334,17 @@ path = VMPythonDownloadOperation; sourceTree = ""; }; + 7F6D774B24C2F6FA00294B9F /* VMVideoNAudioMerger */ = { + isa = PBXGroup; + children = ( + 7F6D774C24C2F6FA00294B9F /* VMVideoNAudioMerger.h */, + 7F6D774D24C2F6FA00294B9F /* VMVideoNAudioMerger.m */, + 7F6D774F24C2F73F00294B9F /* VMMergingAssetTrackModel.h */, + 7F6D775024C2F73F00294B9F /* VMMergingAssetTrackModel.m */, + ); + path = VMVideoNAudioMerger; + sourceTree = ""; + }; 7FFEE25824AADB9C0071144F /* Frameworks */ = { isa = PBXGroup; children = ( @@ -510,9 +523,10 @@ files = ( 7F52B3F924C05A440048AE58 /* VMWebResourceOptionModel.m in Sources */, 7F52B3FB24C05A440048AE58 /* VMPythonResourceDownloader.m in Sources */, - 7F92A79924C1E13E00D3A15C /* VMVideoNAudioMerger.m in Sources */, + 7F6D774E24C2F6FA00294B9F /* VMVideoNAudioMerger.m in Sources */, 7F4CB57F24A42AAB00CC0889 /* ViewController.m in Sources */, 7F4CB57924A42AAB00CC0889 /* AppDelegate.m in Sources */, + 7F6D775124C2F73F00294B9F /* VMMergingAssetTrackModel.m in Sources */, 7F52B3FD24C05A440048AE58 /* VMPythonDownloadOperation.m in Sources */, 7F52B3FA24C05A440048AE58 /* VMResourceDownloader.m in Sources */, 7F08BBD924AF1E1700B3F5EB /* Constants.m in Sources */, diff --git a/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo/ViewController.m b/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo/ViewController.m index 1f16d11..db7b97c 100644 --- a/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo/ViewController.m +++ b/PythonForVideoMemos-Demo/PythonForVideoMemos-Demo/ViewController.m @@ -16,6 +16,11 @@ #import "VMPythonResourceDownloader.h" #import "VMWebResourceModel.h" +#define DEBUG_VMVideoNAudioMerger 1 +#ifdef DEBUG_VMVideoNAudioMerger + #import "VMVideoNAudioMerger.h" +#endif // END #ifdef DEBUG_VMVideoNAudioMerger + static NSString * const kVideosFolderName_ = @"videos"; @@ -114,6 +119,27 @@ - (void)viewDidAppear:(BOOL)animated if (![fileManager fileExistsAtPath:savePath]) { [fileManager createDirectoryAtPath:savePath withIntermediateDirectories:NO attributes:nil error:NULL]; } + +#ifdef DEBUG_VMVideoNAudioMerger + /* + NSString *videoFilePath = [savePath stringByAppendingPathComponent:@"id=2455392162450265036[00].mp4"]; + NSString *audioFilePath = [savePath stringByAppendingPathComponent:@"id=2455392162450265036[01].mp4"]; + NSString *resultPath = [savePath stringByAppendingPathComponent:@"id=2455392162450265036.mp4"]; + [VMVideoNAudioMerger mergeVideoFileAtPath:videoFilePath withAudioFileAtPath:audioFilePath intoResultPath:resultPath]; + */ +// [VMVideoNAudioMerger mergeVideoNAudioFilesWithIdentifier:@"id=2455392162450265036" +// atFolderPath:savePath +// completion:^(NSString *mergedFilePath, NSString *mergingErrorMessage) { +// }]; + NSArray *filenames = @[@"id=2455392162450265036[00].mp4", @"id=2455392162450265036[01].mp4"]; + [VMVideoNAudioMerger mergeVideoNAudioFiles:filenames + atFolderPath:savePath + preferredResultName:@"id=2455392162450265036" + completion:^(NSString *mergedFilePath, NSString *mergingErrorMessage) { + + }]; + +#else VMPythonResourceDownloader *downloader = [VMPythonResourceDownloader sharedInstance]; downloader.savePath = savePath; downloader.cacheJSONFile = YES; @@ -149,6 +175,7 @@ - (void)viewDidAppear:(BOOL)animated [weakSelf _presentAlertWithTitle:nil message:errorMessage]; } }]; +#endif // END #ifdef DEBUG_VMVideoNAudioMerger } #pragma mark - Private diff --git a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger.h b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger.h deleted file mode 100644 index 38d3311..0000000 --- a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// VMVideoNAudioMerger.h -// PythonForVideoMemos-Demo -// -// Created by Kjuly on 17/7/2020. -// Copyright © 2020 Kjuly. All rights reserved. -// - -@import Foundation; - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^VMVideoNAudioMergerCompletion)(NSString *_Nullable savedPath, NSString *_Nullable errorMessage); - -@interface VMVideoNAudioMerger : NSObject - -+ (void)mergeVideoFileAtPath:(NSString *)videoFilePath - withAudioFileAtPath:(NSString *)audioFilePath - intoResultPath:(NSString *)resultPath - completion:(VMVideoNAudioMergerCompletion)completion; - -@end - -NS_ASSUME_NONNULL_END diff --git a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger.m b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger.m deleted file mode 100644 index df488e6..0000000 --- a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger.m +++ /dev/null @@ -1,98 +0,0 @@ -// -// VMVideoNAudioMerger.m -// PythonForVideoMemos-Demo -// -// Created by Kjuly on 17/7/2020. -// Copyright © 2020 Kjuly. All rights reserved. -// - -#import "VMVideoNAudioMerger.h" - -#import "VMPythonCommon.h" -// Lib -@import AVFoundation; - - -@implementation VMVideoNAudioMerger - -#pragma mark - Public - -+ (void)mergeVideoFileAtPath:(NSString *)videoFilePath - withAudioFileAtPath:(NSString *)audioFilePath - intoResultPath:(NSString *)resultPath - completion:(VMVideoNAudioMergerCompletion)completion -{ - AVMutableComposition *mixComposition = [AVMutableComposition composition]; - - // Handle Video Track - NSURL *videoFileURL = [NSURL fileURLWithPath:videoFilePath]; - AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoFileURL options:nil]; - CMTimeRange videoTimeRange = CMTimeRangeMake(kCMTimeZero, videoAsset.duration); - - VMPythonLogDebug(@"- videoAsset tracksWithMediaType:AVMediaTypeAudio: %@", [videoAsset tracksWithMediaType:AVMediaTypeAudio]); - VMPythonLogDebug(@"- videoAsset tracksWithMediaType:AVMediaTypeVideo: %@", [videoAsset tracksWithMediaType:AVMediaTypeVideo]); - AVMutableCompositionTrack *compositionVideoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; - AVAssetTrack *videoTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] firstObject]; - NSError *error = nil; - if (![compositionVideoTrack insertTimeRange:videoTimeRange ofTrack:videoTrack atTime:kCMTimeZero error:&error]) { - completion(nil, [error localizedDescription]); - return; - } - - // Handle Audio Track - NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath]; - AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioFileURL options:nil]; - CMTimeRange audioTimeRange = CMTimeRangeMake(kCMTimeZero, audioAsset.duration); - - VMPythonLogDebug(@"- audioAsset tracksWithMediaType:AVMediaTypeAudio: %@", [audioAsset tracksWithMediaType:AVMediaTypeAudio]); - VMPythonLogDebug(@"- audioAsset tracksWithMediaType:AVMediaTypeVideo: %@", [audioAsset tracksWithMediaType:AVMediaTypeVideo]); - AVMutableCompositionTrack *compositionAudioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; - AVAssetTrack *audioTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject]; - if (![compositionAudioTrack insertTimeRange:audioTimeRange ofTrack:audioTrack atTime:kCMTimeZero error:&error]) { - completion(nil, [error localizedDescription]); - return; - } - - // Export Merged File to `resultPath` - NSFileManager *fileManager = [NSFileManager defaultManager]; - if ([fileManager fileExistsAtPath:resultPath]) { - [fileManager removeItemAtPath:resultPath error:nil]; - } - AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality]; - exportSession.outputFileType = AVFileTypeMPEG4; - exportSession.outputURL = [NSURL fileURLWithPath:resultPath]; - - [exportSession exportAsynchronouslyWithCompletionHandler:^{ - dispatch_async(dispatch_get_main_queue(), ^{ - switch (exportSession.status) { - case AVAssetExportSessionStatusCompleted: { - VMPythonLogSuccess(@"exportSession Completed"); - completion(resultPath, nil); - break; - } - - case AVAssetExportSessionStatusFailed: { - VMPythonLogError(@"exportSession Failed: %@", exportSession.error); - completion(nil, [exportSession.error localizedDescription]); - break; - } - - case AVAssetExportSessionStatusCancelled: { - VMPythonLogWarn(@"exportSession Cancelled"); - completion(nil, nil); - break; - } - - case AVAssetExportSessionStatusUnknown: - case AVAssetExportSessionStatusWaiting: - case AVAssetExportSessionStatusExporting: - default: - VMPythonLogWarn(@"exportSession other status: %ld", (long)exportSession.status); - completion(nil, nil); - break; - } - }); - }]; -} - -@end diff --git a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMMergingAssetTrackModel.h b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMMergingAssetTrackModel.h new file mode 100644 index 0000000..b886440 --- /dev/null +++ b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMMergingAssetTrackModel.h @@ -0,0 +1,24 @@ +// +// VMMergingAssetTrackModel.h +// PythonForVideoMemos-Demo +// +// Created by Kjuly on 18/7/2020. +// Copyright © 2020 Kjuly. All rights reserved. +// + +@import Foundation; +@import AVFoundation; + + +NS_ASSUME_NONNULL_BEGIN + +@interface VMMergingAssetTrackModel : NSObject + +@property (nonatomic, strong) AVURLAsset *asset; +@property (nonatomic, assign) AVMediaType mediaType; +@property (nonatomic, strong) AVAssetTrack *track; +@property (nonatomic, assign) CMTime duration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMMergingAssetTrackModel.m b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMMergingAssetTrackModel.m new file mode 100644 index 0000000..b360253 --- /dev/null +++ b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMMergingAssetTrackModel.m @@ -0,0 +1,13 @@ +// +// VMMergingAssetTrackModel.m +// PythonForVideoMemos-Demo +// +// Created by Kjuly on 18/7/2020. +// Copyright © 2020 Kjuly. All rights reserved. +// + +#import "VMMergingAssetTrackModel.h" + +@implementation VMMergingAssetTrackModel + +@end diff --git a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMVideoNAudioMerger.h b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMVideoNAudioMerger.h new file mode 100644 index 0000000..1735045 --- /dev/null +++ b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMVideoNAudioMerger.h @@ -0,0 +1,48 @@ +// +// VMVideoNAudioMerger.h +// PythonForVideoMemos-Demo +// +// Created by Kjuly on 17/7/2020. +// Copyright © 2020 Kjuly. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^VMVideoNAudioMergerCompletion)(NSString *_Nullable mergedFilePath, NSString *_Nullable mergingErrorMessage); + +@interface VMVideoNAudioMerger : NSObject + +/** + * Merge video & audio files + * + * @param identifier The unique filename, generally, same to VMPythonResourceDownloader's `preferredName`. + * @param folderPath The path to folder that hosts video & audio files, and also, the merged file will be exporeted there. + * @param completion The bloack to execute when completed. + */ +//+ (void)mergeVideoNAudioFilesWithIdentifier:(NSString *)identifier +// atFolderPath:(NSString *)folderPath +// completion:(VMVideoNAudioMergerCompletion)completion; + +/** + * Merge video & audio files + * + * @param filenames The video & audio filenames + * @param folderPath The path to folder that hosts video & audio files, and also, the merged file will be exporeted there. + * @param preferredResultName The preferred merging result filename + * @param completion The bloack to execute when completed. + */ ++ (void)mergeVideoNAudioFiles:(NSArray *)filenames + atFolderPath:(NSString *)folderPath + preferredResultName:(NSString *)preferredResultName + completion:(VMVideoNAudioMergerCompletion)completion; + +//+ (void)mergeVideoFileAtPath:(NSString *)videoFilePath +// withAudioFileAtPath:(NSString *)audioFilePath +// intoResultPath:(NSString *)resultPath +// completion:(VMVideoNAudioMergerCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMVideoNAudioMerger.m b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMVideoNAudioMerger.m new file mode 100644 index 0000000..3d3b616 --- /dev/null +++ b/PythonForVideoMemos/VMPython/Others/VMVideoNAudioMerger/VMVideoNAudioMerger.m @@ -0,0 +1,275 @@ +// +// VMVideoNAudioMerger.m +// PythonForVideoMemos-Demo +// +// Created by Kjuly on 17/7/2020. +// Copyright © 2020 Kjuly. All rights reserved. +// + +#import "VMVideoNAudioMerger.h" + +#import "VMPythonCommon.h" +// Model +#import "VMMergingAssetTrackModel.h" +// Lib +//@import AVFoundation; +@import CoreServices.UTType; + + +static NSString * const kVMVideoNAudioMergerAVURLAssetPropertyOfDuration_ = @"duration"; +static NSString * const kVMVideoNAudioMergerAVURLAssetPropertyOfTracks_ = @"tracks"; + + +#ifdef DEBUG + +@interface VMVideoNAudioMerger () + ++ (void)_mergeWithValidTrackItems:(NSArray *)trackItems + withFolderPath:(NSString *)folderPath + preferredResultName:(NSString *)preferredResultName + completion:(VMVideoNAudioMergerCompletion)completion; + +@end + +#endif // END #ifdef DEBUG + + +@implementation VMVideoNAudioMerger + +#pragma mark - Private + ++ (void)_mergeWithValidTrackItems:(NSArray *)trackItems + withFolderPath:(NSString *)folderPath + preferredResultName:(NSString *)preferredResultName + completion:(VMVideoNAudioMergerCompletion)completion +{ + AVMutableComposition *mixComposition = [AVMutableComposition composition]; + BOOL videoTrackMerged = NO; + BOOL audioTrackMerged = NO; + for (VMMergingAssetTrackModel *item in trackItems) { + if (videoTrackMerged && audioTrackMerged) { + break; + } + + if ((videoTrackMerged && AVMediaTypeVideo == item.mediaType) || (audioTrackMerged && AVMediaTypeAudio == item.mediaType)) { + continue; + } + NSError *error = nil; + CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, item.duration); + AVMutableCompositionTrack *compositionTrack = [mixComposition addMutableTrackWithMediaType:item.mediaType preferredTrackID:kCMPersistentTrackID_Invalid]; + //AVAssetTrack *track = [[item.asset tracksWithMediaType:item.mediaType] firstObject]; + if ([compositionTrack insertTimeRange:timeRange ofTrack:item.track atTime:kCMTimeZero error:&error]) { + if (AVMediaTypeVideo == item.mediaType) videoTrackMerged = YES; + else audioTrackMerged = YES; + } else { + VMPythonLogError(@"%@", [error localizedDescription]); + } + } + + // Export merged file + AVFileType outputFileType = AVFileTypeMPEG4; + NSString *extension = (__bridge NSString *)(UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(outputFileType), kUTTagClassFilenameExtension)); + NSString *mergedFilename = [preferredResultName stringByAppendingPathExtension:extension]; + NSString *mergedFilePath = [folderPath stringByAppendingPathComponent:mergedFilename]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:mergedFilePath]) { + [fileManager removeItemAtPath:mergedFilePath error:nil]; + } + + // Export Merged File to `resultPath` + AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality]; + exportSession.outputFileType = outputFileType; + exportSession.outputURL = [NSURL fileURLWithPath:mergedFilePath]; + + [exportSession exportAsynchronouslyWithCompletionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + switch (exportSession.status) { + case AVAssetExportSessionStatusCompleted: { + VMPythonLogSuccess(@"exportSession Completed"); + completion(mergedFilePath, nil); + break; + } + + case AVAssetExportSessionStatusFailed: { + VMPythonLogError(@"exportSession Failed: %@", exportSession.error); + completion(nil, [exportSession.error localizedDescription]); + break; + } + + case AVAssetExportSessionStatusCancelled: { + VMPythonLogWarn(@"exportSession Cancelled"); + completion(nil, nil); + break; + } + + case AVAssetExportSessionStatusUnknown: + case AVAssetExportSessionStatusWaiting: + case AVAssetExportSessionStatusExporting: + default: + VMPythonLogWarn(@"exportSession other status: %ld", (long)exportSession.status); + completion(nil, nil); + break; + } + }); + }]; +} + +#pragma mark - Public + +/* ++ (void)mergeVideoNAudioFilesWithIdentifier:(NSString *)identifier + atFolderPath:(NSString *)folderPath + completion:(VMVideoNAudioMergerCompletion)completion {} + */ + ++ (void)mergeVideoNAudioFiles:(NSArray *)filenames + atFolderPath:(NSString *)folderPath + preferredResultName:(NSString *)preferredResultName + completion:(VMVideoNAudioMergerCompletion)completion +{ + NSMutableArray *validTrackItems = [NSMutableArray array]; + + NSUInteger countOfTotalAssets = [filenames count]; + NSUInteger __block countOfLoadedAssets = 0; + BOOL __block startMerging = NO; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *requiredAssetKeys = @[kVMVideoNAudioMergerAVURLAssetPropertyOfDuration_, + kVMVideoNAudioMergerAVURLAssetPropertyOfTracks_]; + + for (NSString *filename in filenames) { + NSString *filepath = [folderPath stringByAppendingPathComponent:filename]; + if (![fileManager fileExistsAtPath:filepath]) { + --countOfTotalAssets; + continue; + } + + NSURL *fileURL = [NSURL fileURLWithPath:filepath]; + AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:fileURL options:nil]; + [asset loadValuesAsynchronouslyForKeys:requiredAssetKeys completionHandler:^{ + if (startMerging) { + return; + } + ++countOfLoadedAssets; + + NSError *error = nil; + AVKeyValueStatus durationValueStatus = [asset statusOfValueForKey:kVMVideoNAudioMergerAVURLAssetPropertyOfDuration_ error:&error]; + if (AVKeyValueStatusLoaded == durationValueStatus) { + AVKeyValueStatus tracksValueStatus = [asset statusOfValueForKey:kVMVideoNAudioMergerAVURLAssetPropertyOfTracks_ error:&error]; + if (AVKeyValueStatusLoaded == tracksValueStatus) { + AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; + if (videoTrack) { + VMMergingAssetTrackModel *videoTrackItem = [[VMMergingAssetTrackModel alloc] init]; + videoTrackItem.asset = asset; + videoTrackItem.mediaType = AVMediaTypeVideo; + videoTrackItem.track = videoTrack; + videoTrackItem.duration = asset.duration; + [validTrackItems insertObject:videoTrackItem atIndex:0]; + } + + AVAssetTrack *audioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject]; + if (audioTrack) { + VMMergingAssetTrackModel *audioTrackItem = [[VMMergingAssetTrackModel alloc] init]; + audioTrackItem.asset = asset; + audioTrackItem.mediaType = AVMediaTypeAudio; + audioTrackItem.track = audioTrack; + audioTrackItem.duration = asset.duration; + [validTrackItems addObject:audioTrackItem]; + } + + } else if (AVKeyValueStatusFailed == tracksValueStatus) { + VMPythonLogError(@"%@", [error localizedDescription]); + } + } else if (AVKeyValueStatusFailed == durationValueStatus) { + VMPythonLogError(@"%@", [error localizedDescription]); + } + + if (countOfLoadedAssets >= countOfTotalAssets) { + startMerging = YES; + [self _mergeWithValidTrackItems:validTrackItems withFolderPath:folderPath preferredResultName:preferredResultName completion:completion]; + } + }]; + } +} + +/* ++ (void)mergeVideoFileAtPath:(NSString *)videoFilePath + withAudioFileAtPath:(NSString *)audioFilePath + intoResultPath:(NSString *)resultPath + completion:(VMVideoNAudioMergerCompletion)completion +{ + AVMutableComposition *mixComposition = [AVMutableComposition composition]; + + // Handle Video Track + NSURL *videoFileURL = [NSURL fileURLWithPath:videoFilePath]; + AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoFileURL options:nil]; + CMTimeRange videoTimeRange = CMTimeRangeMake(kCMTimeZero, videoAsset.duration); + + VMPythonLogDebug(@"- videoAsset tracksWithMediaType:AVMediaTypeAudio: %@", [videoAsset tracksWithMediaType:AVMediaTypeAudio]); + VMPythonLogDebug(@"- videoAsset tracksWithMediaType:AVMediaTypeVideo: %@", [videoAsset tracksWithMediaType:AVMediaTypeVideo]); + AVAssetTrack *videoTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] firstObject]; + NSError *error = nil; + AVMutableCompositionTrack *compositionVideoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; + if (![compositionVideoTrack insertTimeRange:videoTimeRange ofTrack:videoTrack atTime:kCMTimeZero error:&error]) { + completion(nil, [error localizedDescription]); + return; + } + + // Handle Audio Track + NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath]; + AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioFileURL options:nil]; + CMTimeRange audioTimeRange = CMTimeRangeMake(kCMTimeZero, audioAsset.duration); + + VMPythonLogDebug(@"- audioAsset tracksWithMediaType:AVMediaTypeAudio: %@", [audioAsset tracksWithMediaType:AVMediaTypeAudio]); + VMPythonLogDebug(@"- audioAsset tracksWithMediaType:AVMediaTypeVideo: %@", [audioAsset tracksWithMediaType:AVMediaTypeVideo]); + AVAssetTrack *audioTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject]; + AVMutableCompositionTrack *compositionAudioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; + if (![compositionAudioTrack insertTimeRange:audioTimeRange ofTrack:audioTrack atTime:kCMTimeZero error:&error]) { + completion(nil, [error localizedDescription]); + return; + } + + // Export Merged File to `resultPath` + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:resultPath]) { + [fileManager removeItemAtPath:resultPath error:nil]; + } + AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality]; + exportSession.outputFileType = AVFileTypeMPEG4; + exportSession.outputURL = [NSURL fileURLWithPath:resultPath]; + + [exportSession exportAsynchronouslyWithCompletionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + switch (exportSession.status) { + case AVAssetExportSessionStatusCompleted: { + VMPythonLogSuccess(@"exportSession Completed"); + completion(resultPath, nil); + break; + } + + case AVAssetExportSessionStatusFailed: { + VMPythonLogError(@"exportSession Failed: %@", exportSession.error); + completion(nil, [exportSession.error localizedDescription]); + break; + } + + case AVAssetExportSessionStatusCancelled: { + VMPythonLogWarn(@"exportSession Cancelled"); + completion(nil, nil); + break; + } + + case AVAssetExportSessionStatusUnknown: + case AVAssetExportSessionStatusWaiting: + case AVAssetExportSessionStatusExporting: + default: + VMPythonLogWarn(@"exportSession other status: %ld", (long)exportSession.status); + completion(nil, nil); + break; + } + }); + }]; +}*/ + +@end