
В одном из проектов мне понадобилось решить задачу объединения видео, в частности, пользователь мог поставить видео на паузу, после чего продолжить запись (количество итераций было неизвестно). Поэтому необходимо было найти способ для решения этой задачи доступными средствами. Конечно, в голову пришло два варианта, либо писать всё сразу в один файл, либо записывать в разные, а склеивать уже после сессии. Я решил остановиться на втором, а что из этого вышло, читайте под катом.
Для записи видео я использовал 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.
AVAssetTrackAVAssetTrack *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, и почему это не просто «время». Кстати, много полезной информации по теме есть на сайте. А сам метод вы можете найти в репозитории