Здравствуй!
В одном из проектов мне понадобилось решить задачу объединения видео, в частности, пользователь мог поставить видео на паузу, после чего продолжить запись (количество итераций было неизвестно). Поэтому необходимо было найти способ для решения этой задачи доступными средствами. Конечно, в голову пришло два варианта, либо писать всё сразу в один файл, либо записывать в разные, а склеивать уже после сессии. Я решил остановиться на втором, а что из этого вышло, читайте под катом.
Для записи видео я использовал AVCamCaptureManager, основываясь на приложении AVCam, любезно расположенном на всеми нами любимом сайте. Ну а после нажатия кнопки стоп начиналось самое интересное.
Этап 1. Подготовка.
На этом этапе нужно сделать следующее:
- Объект AVMutableComposition. В нём будут наши дорожки.
AVMutableComposition
AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
- Массив инструкций для трэков и объект AVMutableVideoCompositionInstruction для них.
AVMutableVideoCompositionInstruction
NSMutableArray *arrayInstruction = [[NSMutableArray alloc] init]; AVMutableVideoCompositionInstruction *MainInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
- Общая аудиодорожка. Переменная isSound указывает, нужна ли она здесь (согласно поставленной задаче пользователь мог записывать видео как со звуком, так и без него).
Общая аудиодорожка
AVMutableCompositionTrack *audioTrack; if(self.isSoundOn==YES) audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
- Переменная для хранения длительности (а также она нужна для определения границ склейки).
CMTime duration = kCMTimeZero;
Этап 2. Поиск файлов, создание ассетов, генерация инструкций и трансформация видеодорожки при необходимости.
- Пробегаемся по всем файлам и создаём ассеты для каждого из них. Всё это можно и нужно делать циклом, переменная i-здесь индекс файла.
Создание ассетовВсе видео я записывал в темповую директорию, при этом увеличивая значение i. Соответственно, ассеты создаются из этих файлов.
AVAsset *currentAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@%d%@", NSTemporaryDirectory(), @"Movie",i,@".mov"]]];
- В цикле создаются AVMutableCompositionTrack, в каждый из которых вставляется временной отрезок CMTimeRange из созданного ассета.
Создание треков
//VIDEO TRACK AVMutableCompositionTrack *currentTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; [currentTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, currentAsset.duration) ofTrack:[[currentAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] atTime:duration error:nil];
Особое внимание здесь стоит уделить тому, куда именно нужно вставлять отрезок, если вы хотите избежать чёрных полос во время проигрывания, или что ещё хуже, накладывания одного видео на другое в процессе воспроизведения. И даже не спрашивайте, почему я акцентирую на этом внимание.
- AVMutableVideoCompositionLayerInstruction используется для создания инструкции для конкретного слоя дорожки. Именно в него записывается возможная трансформация.
AVMutableVideoCompositionLayerInstruction
AVMutableVideoCompositionLayerInstruction *currentAssetLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:currentTrack];
При правильном определении положения видео (портретного или лэндскейпного) на выходе вы сможете получить шикарную картинку, в которой горизонтальное или вертикальное видео будет центрироваться в зависимости от того, дорожка в каком режиме у вас идёт первой.
- Объект AVAssetTrack нужен для создания дорожки на основе уже существующего ассета с типом AVMediaTypeVideo.
AVAssetTrack
AVAssetTrack *currentAssetTrack = [[currentAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
- Увеличиваем значение duration.
duration UPCMTime требует даже отдельного поста, конечно.
duration=CMTimeAdd(duration, currentAsset.duration);
Этап 3. Установка параметров MainInstruction и формирование AVMutableVideoComposition
-
timeRangeОбщая длительность видеодорожки.
MainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, duration);
-
layerInstructionsЗдесь нам понадобится массив инструкций, сформированный на втором этапе.
MainInstruction.layerInstructions = arrayInstruction;
-
MainCompositionInstСоздаём экземпляр MainCompositionInst с нужными параметрами
AVMutableVideoComposition *MainCompositionInst = [AVMutableVideoComposition videoComposition]; MainCompositionInst.instructions = [NSArray arrayWithObject:MainInstruction]; MainCompositionInst.frameDuration = CMTimeMake(1, 30); MainCompositionInst.renderSize = CGSizeMake(320.0, 480.0);
-
Куда сохранять-то?
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; NSString *myPathDocs = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"mergeVideo-%d.mov",arc4random() % 1000]]; NSURL *url = [NSURL fileURLWithPath:myPathDocs];
Этап 4. Сохранение полученного видео через AVAssetExportSession
Разница между пресетами Highest и Medium очень даже существенна. Поэтому если вы планируете распространять видео в социальных сетях, настоятельно вам рекомендую использовать именно Medium.
-
Задаём качество и создаём сессию
NSString *quality = AVAssetExportPresetHighestQuality; if(self.isHighQuality==NO) quality = AVAssetExportPresetMediumQuality; AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:quality];
-
Сохраняем!
exporter.outputURL=url; exporter.outputFileType = AVFileTypeQuickTimeMovie; exporter.videoComposition = MainCompositionInst; exporter.shouldOptimizeForNetworkUse = YES; [exporter exportAsynchronouslyWithCompletionHandler:^ { //здесь можно удалить темповые файлы. }];
Возможные статусы при экспорте:
switch (exporter.status)
{
case AVAssetExportSessionStatusCompleted:
NSLog(@"Completed exporting!");
break;
case AVAssetExportSessionStatusFailed:
NSLog(@"Failed:%@", exporter.error.description);
break;
case AVAssetExportSessionStatusCancelled:
NSLog(@"Canceled:%@", exporter.error);
break;
case AVAssetExportSessionStatusExporting:
NSLog(@"Exporting!");
break;
case AVAssetExportSessionStatusWaiting:
NSLog(@"Waiting");
break;
default:
break;
}
Заключение
В статье я постарался акцентировать внимание именно на объединении видео, и думаю, многимам, занимающимся разработкой под iOS, столкнувшимся с такой задачей, этот пост поможет в будущем. В дальнейшем я планирую более подробно рассказать о CMTime, и почему это не просто «время». Кстати, много полезной информации по теме есть на сайте. А сам метод вы можете найти в репозитории
Автор: Viktorianec