Экспорт
Чтобы считать и записать аудиовизуальные активы, необходимо использовать экспорт APIs, предоставленный платформой Основы AV. AVAssetExportSession
класс обеспечивает интерфейс для простых потребностей экспорта, таких как изменение формата файла или обрезка длины актива (см. Обрезку и Транскодирование Фильма). Для большего количества всесторонних потребностей экспорта используйте AVAssetReader
и AVAssetWriter
классы.
Используйте AVAssetReader
когда Вы хотите выполнить работу на содержании актива. Например, Вы могли бы считать аудиотрек актива для создания визуального представления формы волны. Для создания актива из носителей, таких как демонстрационные буферы или неподвижные изображения используйте AVAssetWriter
объект.
Чтение актива
Каждый AVAssetReader
объект может быть связан только с единственным активом за один раз, но этот актив может содержать многократные дорожки. Поэтому необходимо присвоить конкретные подклассы AVAssetReaderOutput
класс Вашего читателя актива, прежде чем Вы начнете читать, чтобы сконфигурировать, как считаны данные носителей. Существует три конкретных подкласса AVAssetReaderOutput
базовый класс, который можно использовать для потребностей чтения актива: AVAssetReaderTrackOutput
, AVAssetReaderAudioMixOutput
, и AVAssetReaderVideoCompositionOutput
.
Создание читателя актива
Все необходимо инициализировать AVAssetReader
объект является активом, который Вы хотите считать.
NSError *outError; |
AVAsset *someAsset = <#AVAsset that you want to read#>; |
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:someAsset error:&outError]; |
BOOL success = (assetReader != nil); |
Установка выводов читателя актива
После создания читателя актива устанавливал по крайней мере один вывод для получения считанных данных носителей. При установке выводов, убедиться установить alwaysCopiesSampleData
свойство к NO
. Таким образом Вы получаете преимущества повышений производительности. Во всех примерах в этой главе это свойство могло и должно быть установлено в NO
.
Если Вы хотите только считать данные носителей из одной или более дорожек и потенциально преобразовать те данные в другой формат, используйте AVAssetReaderTrackOutput
класс, с помощью выходного объекта одноколейного пути для каждого AVAssetTrack
возразите, что Вы хотите читать из Вашего актива. Для распаковки аудиотрека к Линейному PCM с читателем актива Вы устанавливаете свой вывод дорожки следующим образом:
AVAsset *localAsset = assetReader.asset; |
// Get the audio track to read. |
AVAssetTrack *audioTrack = [[localAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0]; |
// Decompression settings for Linear PCM |
NSDictionary *decompressionAudioSettings = @{ AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM] }; |
// Create the output with the audio track and decompression settings. |
AVAssetReaderOutput *trackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:decompressionAudioSettings]; |
// Add the output to the reader if possible. |
if ([assetReader canAddOutput:trackOutput]) |
[assetReader addOutput:trackOutput]; |
Вы используете AVAssetReaderAudioMixOutput
и AVAssetReaderVideoCompositionOutput
данные носителей классов чтения, смешанные или составленные вместе с помощью AVAudioMix
объект или AVVideoComposition
объект, соответственно. Когда Ваш читатель актива читает из, Как правило, эти выводы используются AVComposition
объект.
С единственным аудио выводом соединения можно считать многократные аудиотреки из актива, смешанные вместе с помощью AVAudioMix
объект. Чтобы указать, как аудиотреки смешаны, присвойте соединение AVAssetReaderAudioMixOutput
объект после инициализации. Следующие дисплеи кода, как создать аудио вывод соединения со всеми аудиотреками от Вашего актива, распаковывают аудиотреки к Линейному PCM и присваивают аудио объект соединения выводу. Для получения дополнительной информации о том, как сконфигурировать аудио соединение, посмотрите Редактирование.
AVAudioMix *audioMix = <#An AVAudioMix that specifies how the audio tracks from the AVAsset are mixed#>; |
// Assumes that assetReader was initialized with an AVComposition object. |
AVComposition *composition = (AVComposition *)assetReader.asset; |
// Get the audio tracks to read. |
NSArray *audioTracks = [composition tracksWithMediaType:AVMediaTypeAudio]; |
// Get the decompression settings for Linear PCM. |
NSDictionary *decompressionAudioSettings = @{ AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM] }; |
// Create the audio mix output with the audio tracks and decompression setttings. |
AVAssetReaderOutput *audioMixOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:audioTracks audioSettings:decompressionAudioSettings]; |
// Associate the audio mix used to mix the audio tracks being read with the output. |
audioMixOutput.audioMix = audioMix; |
// Add the output to the reader if possible. |
if ([assetReader canAddOutput:audioMixOutput]) |
[assetReader addOutput:audioMixOutput]; |
Видео вывод состава ведет себя почти таким же способом: можно считать многократные видеотреки из актива, составленные вместе с помощью AVVideoComposition
объект. Чтобы считать данные носителей из многократных составленных видеотреков и распаковать их к ARGB, установите свой вывод следующим образом:
AVVideoComposition *videoComposition = <#An AVVideoComposition that specifies how the video tracks from the AVAsset are composited#>; |
// Assumes assetReader was initialized with an AVComposition. |
AVComposition *composition = (AVComposition *)assetReader.asset; |
// Get the video tracks to read. |
NSArray *videoTracks = [composition tracksWithMediaType:AVMediaTypeVideo]; |
// Decompression settings for ARGB. |
NSDictionary *decompressionVideoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32ARGB], (id)kCVPixelBufferIOSurfacePropertiesKey : [NSDictionary dictionary] }; |
// Create the video composition output with the video tracks and decompression setttings. |
AVAssetReaderOutput *videoCompositionOutput = [AVAssetReaderVideoCompositionOutput assetReaderVideoCompositionOutputWithVideoTracks:videoTracks videoSettings:decompressionVideoSettings]; |
// Associate the video composition used to composite the video tracks being read with the output. |
videoCompositionOutput.videoComposition = videoComposition; |
// Add the output to the reader if possible. |
if ([assetReader canAddOutput:videoCompositionOutput]) |
[assetReader addOutput:videoCompositionOutput]; |
Чтение данных носителей актива
Чтобы начать читать после установки всех выводов, Вы нуждаетесь, вызываете startReading
метод на Вашем читателе актива. Затем, получите данные носителей индивидуально от каждого вывода с помощью copyNextSampleBuffer
метод. Чтобы запустить читателя актива с единственным выводом и считать все его выборки носителей, сделайте следующее:
// Start the asset reader up. |
[self.assetReader startReading]; |
BOOL done = NO; |
while (!done) |
{ |
// Copy the next sample buffer from the reader output. |
CMSampleBufferRef sampleBuffer = [self.assetReaderOutput copyNextSampleBuffer]; |
if (sampleBuffer) |
{ |
// Do something with sampleBuffer here. |
CFRelease(sampleBuffer); |
sampleBuffer = NULL; |
} |
else |
{ |
// Find out why the asset reader output couldn't copy another sample buffer. |
if (self.assetReader.status == AVAssetReaderStatusFailed) |
{ |
NSError *failureError = self.assetReader.error; |
// Handle the error here. |
} |
else |
{ |
// The asset reader output has read all of its samples. |
done = YES; |
} |
} |
} |
Запись актива
AVAssetWriter
данные носителей класса записи от многократных источников до единственного файла указанного формата файла. Вы не должны связывать свой объект писателя актива с определенным активом, но необходимо использовать отдельного писателя актива для каждого выходного файла, который Вы хотите создать. Поскольку писатель актива может записать данные носителей из многократных источников, необходимо создать AVAssetWriterInput
объект для каждой отдельной дорожки, которую Вы хотите записать в выходной файл. Каждый AVAssetWriterInput
объект ожидает получать данные в форме CMSampleBufferRef
объекты, но если Вы хотите добавить CVPixelBufferRef
объекты Вашему писателю актива вводят, используют AVAssetWriterInputPixelBufferAdaptor
класс.
Создание писателя актива
Для создания писателя актива укажите URL для выходного файла и желаемого типа файла. Следующий код выводит на экран, как инициализировать писателя актива для создания фильма в формате QuickTime:
NSError *outError; |
NSURL *outputURL = <#NSURL object representing the URL where you want to save the video#>; |
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outputURL |
fileType:AVFileTypeQuickTimeMovie |
error:&outError]; |
BOOL success = (assetWriter != nil); |
Установка вводов писателя актива
Для Вашего писателя актива, чтобы быть в состоянии записать данные носителей, необходимо установить по крайней мере один ввод писателя актива. Например, если Ваш источник данных носителей уже продает выборки носителей как CMSampleBufferRef
объекты, просто используйте AVAssetWriterInput
класс. Устанавливать ввод писателя актива, сжимающий аудио данные носителей до AAC на 128 Кбит/с и подключающий его к Вашему писателю актива, делающий следующее:
// Configure the channel layout as stereo. |
AudioChannelLayout stereoChannelLayout = { |
.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo, |
.mChannelBitmap = 0, |
.mNumberChannelDescriptions = 0 |
}; |
// Convert the channel layout object to an NSData object. |
NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)]; |
// Get the compression settings for 128 kbps AAC. |
NSDictionary *compressionAudioSettings = @{ |
AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC], |
AVEncoderBitRateKey : [NSNumber numberWithInteger:128000], |
AVSampleRateKey : [NSNumber numberWithInteger:44100], |
AVChannelLayoutKey : channelLayoutAsData, |
AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2] |
}; |
// Create the asset writer input with the compression settings and specify the media type as audio. |
AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:compressionAudioSettings]; |
// Add the input to the writer if possible. |
if ([assetWriter canAddInput:assetWriterInput]) |
[assetWriter addInput:assetWriterInput]; |
Ваш писатель актива ввел, может дополнительно включать некоторые метаданные или указать различное преобразование для определенной дорожки с помощью metadata
и transform
свойства соответственно. Для ввода писателя актива, источник данных которого является видеотреком, можно поддержать исходное преобразование видео в выходном файле путем выполнения следующего:
AVAsset *videoAsset = <#AVAsset with at least one video track#>; |
AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; |
assetWriterInput.transform = videoAssetTrack.preferredTransform; |
При записи данных носителей в выходной файл иногда можно хотеть выделить пиксельные буферы. Для этого используйте AVAssetWriterInputPixelBufferAdaptor
класс. Для самой большой эффективности, вместо того, чтобы добавить пиксельные буферы, выделенные с помощью отдельного пула, используют пиксельный пул буферов, предоставленный пиксельным буферным адаптером. Следующий код создает пиксельный буферный объект, работающий в домене RGB, который будет использовать CGImage
объекты создать его пиксельные буферы.
NSDictionary *pixelBufferAttributes = @{ |
kCVPixelBufferCGImageCompatibilityKey : [NSNumber numberWithBool:YES], |
kCVPixelBufferCGBitmapContextCompatibilityKey : [NSNumber numberWithBool:YES], |
kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_32ARGB] |
}; |
AVAssetWriterInputPixelBufferAdaptor *inputPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.assetWriterInput sourcePixelBufferAttributes:pixelBufferAttributes]; |
Запись данных носителей
При конфигурировании всех вводов, необходимых для писателя актива Вы готовы начать писать данные носителей. Когда Вы сделали с читателем актива, инициируйте запись с вызовом к startWriting
метод. Тогда необходимо запустить пишущий выборку сеанс с вызова к startSessionAtSourceTime:
метод. Все записи, делавшие писателем актива, должны произойти в одном из этих сеансов, и диапазон времени каждого сеанса определяет диапазон времени данных носителей, включенных из источника. Например, если Ваш источник является читателем актива, снабжающим данными носителей, считанными из AVAsset
возразите и Вы не хотите включать данные носителей от первой половины актива, Вы сделали бы следующее:
CMTime halfAssetDuration = CMTimeMultiplyByFloat64(self.asset.duration, 0.5); |
[self.assetWriter startSessionAtSourceTime:halfAssetDuration]; |
//Implementation continues. |
Обычно, для окончания сеанса записи необходимо вызвать endSessionAtSourceTime:
метод. Однако, если Ваш сеанс записи идет право до конца на Ваш файл, можно закончить сеанс записи просто путем вызова finishWriting
метод. Чтобы запустить писателя актива с единственным вводом и записать все его данные носителей, сделайте следующее:
// Prepare the asset writer for writing. |
[self.assetWriter startWriting]; |
// Start a sample-writing session. |
[self.assetWriter startSessionAtSourceTime:kCMTimeZero]; |
// Specify the block to execute when the asset writer is ready for media data and the queue to call it on. |
[self.assetWriterInput requestMediaDataWhenReadyOnQueue:myInputSerialQueue usingBlock:^{ |
while ([self.assetWriterInput isReadyForMoreMediaData]) |
{ |
// Get the next sample buffer. |
CMSampleBufferRef nextSampleBuffer = [self copyNextSampleBufferToWrite]; |
if (nextSampleBuffer) |
{ |
// If it exists, append the next sample buffer to the output file. |
[self.assetWriterInput appendSampleBuffer:nextSampleBuffer]; |
CFRelease(nextSampleBuffer); |
nextSampleBuffer = nil; |
} |
else |
{ |
// Assume that lack of a next sample buffer means the sample buffer source is out of samples and mark the input as finished. |
[self.assetWriterInput markAsFinished]; |
break; |
} |
} |
}]; |
copyNextSampleBufferToWrite
метод в коде выше является просто тупиком. Расположение этого тупика - то, где необходимо было бы вставить некоторую логику для возврата CMSampleBufferRef
объекты, представляющие данные носителей, которые Вы хотите записать. Один возможный источник демонстрационных буферов является выводом читателя актива.
Перекодирование активов
Можно использовать читателя актива и объект писателя актива в тандеме для преобразования актива от одного представления до другого. Используя эти объекты, Вы имеете больше контроля над преобразованием, чем Вы делаете с AVAssetExportSession
объект. Например, можно выбрать, какая из дорожек Вы хотите быть представленными в выходном файле, укажите свой собственный выходной формат или измените актив во время процесса преобразования. Первый шаг в этом процессе должен только установить Ваши выводы читателя актива и вводы писателя актива, как желаемый. После того, как Ваш читатель актива и писатель полностью сконфигурированы, Вы запускаете их обоих с вызовами к startReading
и startWriting
методы, соответственно. Следующие дисплеи фрагмента кода, как использовать единственный ввод писателя актива для записи данных носителей, снабженных единственным читателем актива, выводят:
NSString *serializationQueueDescription = [NSString stringWithFormat:@"%@ serialization queue", self]; |
// Create a serialization queue for reading and writing. |
dispatch_queue_t serializationQueue = dispatch_queue_create([serializationQueueDescription UTF8String], NULL); |
// Specify the block to execute when the asset writer is ready for media data and the queue to call it on. |
[self.assetWriterInput requestMediaDataWhenReadyOnQueue:serializationQueue usingBlock:^{ |
while ([self.assetWriterInput isReadyForMoreMediaData]) |
{ |
// Get the asset reader output's next sample buffer. |
CMSampleBufferRef sampleBuffer = [self.assetReaderOutput copyNextSampleBuffer]; |
if (sampleBuffer != NULL) |
{ |
// If it exists, append this sample buffer to the output file. |
BOOL success = [self.assetWriterInput appendSampleBuffer:sampleBuffer]; |
CFRelease(sampleBuffer); |
sampleBuffer = NULL; |
// Check for errors that may have occurred when appending the new sample buffer. |
if (!success && self.assetWriter.status == AVAssetWriterStatusFailed) |
{ |
NSError *failureError = self.assetWriter.error; |
//Handle the error. |
} |
} |
else |
{ |
// If the next sample buffer doesn't exist, find out why the asset reader output couldn't vend another one. |
if (self.assetReader.status == AVAssetReaderStatusFailed) |
{ |
NSError *failureError = self.assetReader.error; |
//Handle the error here. |
} |
else |
{ |
// The asset reader output must have vended all of its samples. Mark the input as finished. |
[self.assetWriterInput markAsFinished]; |
break; |
} |
} |
} |
}]; |
Соединение всего этого: Используя читателя актива и писателя в тандеме, чтобы повторно закодировать актив
Этот краткий пример кода иллюстрирует, как использовать читателя актива и писателя, чтобы повторно закодировать первый видеотрек и аудиотрек актива в новый файл. Это показывает как:
Используйте очереди сериализации для обработки асинхронной природы чтения и записи аудиовизуальных данных
Инициализируйте читателя актива и сконфигурируйте два вывода читателя актива, один для аудио и один для видео
Инициализируйте писателя актива и сконфигурируйте два ввода писателя актива, один для аудио и один для видео
Используйте читателя актива для асинхронного снабжения данными носителей писателю актива через две различных комбинации вывода/ввода
Используйте группу отгрузки, чтобы быть уведомленными относительно завершения процесса перекодирования
Позвольте пользователю отменять процесс перекодирования, как только он начался
Обработка начальной настройки
Прежде чем Вы создадите своего читателя актива и писателя и сконфигурируете их выводы и вводы, необходимо обработать некоторую начальную настройку. Первая часть этой установки включает создание трех отдельных очередей сериализации для координирования чтения и записи.
NSString *serializationQueueDescription = [NSString stringWithFormat:@"%@ serialization queue", self]; |
// Create the main serialization queue. |
self.mainSerializationQueue = dispatch_queue_create([serializationQueueDescription UTF8String], NULL); |
NSString *rwAudioSerializationQueueDescription = [NSString stringWithFormat:@"%@ rw audio serialization queue", self]; |
// Create the serialization queue to use for reading and writing the audio data. |
self.rwAudioSerializationQueue = dispatch_queue_create([rwAudioSerializationQueueDescription UTF8String], NULL); |
NSString *rwVideoSerializationQueueDescription = [NSString stringWithFormat:@"%@ rw video serialization queue", self]; |
// Create the serialization queue to use for reading and writing the video data. |
self.rwVideoSerializationQueue = dispatch_queue_create([rwVideoSerializationQueueDescription UTF8String], NULL); |
Основная очередь сериализации используется для координирования запуска и остановки читателя актива и писателя (возможно, вследствие отмены), и другие две очереди сериализации используются для сериализации чтения и записи каждой комбинацией вывода/ввода с потенциальной отменой.
Теперь, когда Вы имеете некоторые очереди сериализации, загружаете дорожки Вашего актива и начинаете процесс перекодирования.
self.asset = <#AVAsset that you want to reencode#>; |
self.cancelled = NO; |
self.outputURL = <#NSURL representing desired output URL for file generated by asset writer#>; |
// Asynchronously load the tracks of the asset you want to read. |
[self.asset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^{ |
// Once the tracks have finished loading, dispatch the work to the main serialization queue. |
dispatch_async(self.mainSerializationQueue, ^{ |
// Due to asynchronous nature, check to see if user has already cancelled. |
if (self.cancelled) |
return; |
BOOL success = YES; |
NSError *localError = nil; |
// Check for success of loading the assets tracks. |
success = ([self.asset statusOfValueForKey:@"tracks" error:&localError] == AVKeyValueStatusLoaded); |
if (success) |
{ |
// If the tracks loaded successfully, make sure that no file exists at the output path for the asset writer. |
NSFileManager *fm = [NSFileManager defaultManager]; |
NSString *localOutputPath = [self.outputURL path]; |
if ([fm fileExistsAtPath:localOutputPath]) |
success = [fm removeItemAtPath:localOutputPath error:&localError]; |
} |
if (success) |
success = [self setupAssetReaderAndAssetWriter:&localError]; |
if (success) |
success = [self startAssetReaderAndWriter:&localError]; |
if (!success) |
[self readingAndWritingDidFinishSuccessfully:success withError:localError]; |
}); |
}]; |
Когда процесс загрузки дорожки заканчивается, или успешно или нет, остальная часть работы диспетчеризируется основной очереди сериализации, чтобы гарантировать, что вся эта работа сериализируется с потенциальной отменой. Теперь все, что это оставляют, должно реализовать процесс отмены и эти три пользовательских метода в конце предыдущего листинга кода.
Инициализация читателя актива и писателя
Пользовательское setupAssetReaderAndAssetWriter:
метод инициализирует читателя и писателя и конфигурирует две комбинации вывода/ввода, один для аудиотрека и один для видеотрека. В этом примере аудио распаковывается к Линейному PCM использование читателя актива и сжало назад до AAC на 128 Кбит/с использование писателя актива. Видео распаковано к YUV использование читателя актива и сжато до H.264 с помощью писателя актива.
- (BOOL)setupAssetReaderAndAssetWriter:(NSError **)outError |
{ |
// Create and initialize the asset reader. |
self.assetReader = [[AVAssetReader alloc] initWithAsset:self.asset error:outError]; |
BOOL success = (self.assetReader != nil); |
if (success) |
{ |
// If the asset reader was successfully initialized, do the same for the asset writer. |
self.assetWriter = [[AVAssetWriter alloc] initWithURL:self.outputURL fileType:AVFileTypeQuickTimeMovie error:outError]; |
success = (self.assetWriter != nil); |
} |
if (success) |
{ |
// If the reader and writer were successfully initialized, grab the audio and video asset tracks that will be used. |
AVAssetTrack *assetAudioTrack = nil, *assetVideoTrack = nil; |
NSArray *audioTracks = [self.asset tracksWithMediaType:AVMediaTypeAudio]; |
if ([audioTracks count] > 0) |
assetAudioTrack = [audioTracks objectAtIndex:0]; |
NSArray *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo]; |
if ([videoTracks count] > 0) |
assetVideoTrack = [videoTracks objectAtIndex:0]; |
if (assetAudioTrack) |
{ |
// If there is an audio track to read, set the decompression settings to Linear PCM and create the asset reader output. |
NSDictionary *decompressionAudioSettings = @{ AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM] }; |
self.assetReaderAudioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetAudioTrack outputSettings:decompressionAudioSettings]; |
[self.assetReader addOutput:self.assetReaderAudioOutput]; |
// Then, set the compression settings to 128kbps AAC and create the asset writer input. |
AudioChannelLayout stereoChannelLayout = { |
.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo, |
.mChannelBitmap = 0, |
.mNumberChannelDescriptions = 0 |
}; |
NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)]; |
NSDictionary *compressionAudioSettings = @{ |
AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC], |
AVEncoderBitRateKey : [NSNumber numberWithInteger:128000], |
AVSampleRateKey : [NSNumber numberWithInteger:44100], |
AVChannelLayoutKey : channelLayoutAsData, |
AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2] |
}; |
self.assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:[assetAudioTrack mediaType] outputSettings:compressionAudioSettings]; |
[self.assetWriter addInput:self.assetWriterAudioInput]; |
} |
if (assetVideoTrack) |
{ |
// If there is a video track to read, set the decompression settings for YUV and create the asset reader output. |
NSDictionary *decompressionVideoSettings = @{ |
(id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithUnsignedInt:kCVPixelFormatType_422YpCbCr8], |
(id)kCVPixelBufferIOSurfacePropertiesKey : [NSDictionary dictionary] |
}; |
self.assetReaderVideoOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetVideoTrack outputSettings:decompressionVideoSettings]; |
[self.assetReader addOutput:self.assetReaderVideoOutput]; |
CMFormatDescriptionRef formatDescription = NULL; |
// Grab the video format descriptions from the video track and grab the first one if it exists. |
NSArray *videoFormatDescriptions = [assetVideoTrack formatDescriptions]; |
if ([videoFormatDescriptions count] > 0) |
formatDescription = (__bridge CMFormatDescriptionRef)[formatDescriptions objectAtIndex:0]; |
CGSize trackDimensions = { |
.width = 0.0, |
.height = 0.0, |
}; |
// If the video track had a format description, grab the track dimensions from there. Otherwise, grab them direcly from the track itself. |
if (formatDescription) |
trackDimensions = CMVideoFormatDescriptionGetPresentationDimensions(formatDescription, false, false); |
else |
trackDimensions = [assetVideoTrack naturalSize]; |
NSDictionary *compressionSettings = nil; |
// If the video track had a format description, attempt to grab the clean aperture settings and pixel aspect ratio used by the video. |
if (formatDescription) |
{ |
NSDictionary *cleanAperture = nil; |
NSDictionary *pixelAspectRatio = nil; |
CFDictionaryRef cleanApertureFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_CleanAperture); |
if (cleanApertureFromCMFormatDescription) |
{ |
cleanAperture = @{ |
AVVideoCleanApertureWidthKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureWidth), |
AVVideoCleanApertureHeightKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHeight), |
AVVideoCleanApertureHorizontalOffsetKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHorizontalOffset), |
AVVideoCleanApertureVerticalOffsetKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureVerticalOffset) |
}; |
} |
CFDictionaryRef pixelAspectRatioFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_PixelAspectRatio); |
if (pixelAspectRatioFromCMFormatDescription) |
{ |
pixelAspectRatio = @{ |
AVVideoPixelAspectRatioHorizontalSpacingKey : (id)CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioHorizontalSpacing), |
AVVideoPixelAspectRatioVerticalSpacingKey : (id)CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioVerticalSpacing) |
}; |
} |
// Add whichever settings we could grab from the format description to the compression settings dictionary. |
if (cleanAperture || pixelAspectRatio) |
{ |
NSMutableDictionary *mutableCompressionSettings = [NSMutableDictionary dictionary]; |
if (cleanAperture) |
[mutableCompressionSettings setObject:cleanAperture forKey:AVVideoCleanApertureKey]; |
if (pixelAspectRatio) |
[mutableCompressionSettings setObject:pixelAspectRatio forKey:AVVideoPixelAspectRatioKey]; |
compressionSettings = mutableCompressionSettings; |
} |
} |
// Create the video settings dictionary for H.264. |
NSMutableDictionary *videoSettings = (NSMutableDictionary *) @{ |
AVVideoCodecKey : AVVideoCodecH264, |
AVVideoWidthKey : [NSNumber numberWithDouble:trackDimensions.width], |
AVVideoHeightKey : [NSNumber numberWithDouble:trackDimensions.height] |
}; |
// Put the compression settings into the video settings dictionary if we were able to grab them. |
if (compressionSettings) |
[videoSettings setObject:compressionSettings forKey:AVVideoCompressionPropertiesKey]; |
// Create the asset writer input and add it to the asset writer. |
self.assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:[videoTrack mediaType] outputSettings:videoSettings]; |
[self.assetWriter addInput:self.assetWriterVideoInput]; |
} |
} |
return success; |
} |
Перекодирование актива
При условии, что читатель актива и писатель успешно инициализируются и конфигурируются, startAssetReaderAndWriter:
метод, описанный в Обработке Начальной настройки, вызывают. Этот метод - то, где фактическое чтение и запись актива имеют место.
- (BOOL)startAssetReaderAndWriter:(NSError **)outError |
{ |
BOOL success = YES; |
// Attempt to start the asset reader. |
success = [self.assetReader startReading]; |
if (!success) |
*outError = [self.assetReader error]; |
if (success) |
{ |
// If the reader started successfully, attempt to start the asset writer. |
success = [self.assetWriter startWriting]; |
if (!success) |
*outError = [self.assetWriter error]; |
} |
if (success) |
{ |
// If the asset reader and writer both started successfully, create the dispatch group where the reencoding will take place and start a sample-writing session. |
self.dispatchGroup = dispatch_group_create(); |
[self.assetWriter startSessionAtSourceTime:kCMTimeZero]; |
self.audioFinished = NO; |
self.videoFinished = NO; |
if (self.assetWriterAudioInput) |
{ |
// If there is audio to reencode, enter the dispatch group before beginning the work. |
dispatch_group_enter(self.dispatchGroup); |
// Specify the block to execute when the asset writer is ready for audio media data, and specify the queue to call it on. |
[self.assetWriterAudioInput requestMediaDataWhenReadyOnQueue:self.rwAudioSerializationQueue usingBlock:^{ |
// Because the block is called asynchronously, check to see whether its task is complete. |
if (self.audioFinished) |
return; |
BOOL completedOrFailed = NO; |
// If the task isn't complete yet, make sure that the input is actually ready for more media data. |
while ([self.assetWriterAudioInput isReadyForMoreMediaData] && !completedOrFailed) |
{ |
// Get the next audio sample buffer, and append it to the output file. |
CMSampleBufferRef sampleBuffer = [self.assetReaderAudioOutput copyNextSampleBuffer]; |
if (sampleBuffer != NULL) |
{ |
BOOL success = [self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]; |
CFRelease(sampleBuffer); |
sampleBuffer = NULL; |
completedOrFailed = !success; |
} |
else |
{ |
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 = self.audioFinished; |
self.audioFinished = YES; |
if (oldFinished == NO) |
{ |
[self.assetWriterAudioInput markAsFinished]; |
} |
dispatch_group_leave(self.dispatchGroup); |
} |
}]; |
} |
if (self.assetWriterVideoInput) |
{ |
// If we had video to reencode, enter the dispatch group before beginning the work. |
dispatch_group_enter(self.dispatchGroup); |
// Specify the block to execute when the asset writer is ready for video media data, and specify the queue to call it on. |
[self.assetWriterVideoInput requestMediaDataWhenReadyOnQueue:self.rwVideoSerializationQueue usingBlock:^{ |
// Because the block is called asynchronously, check to see whether its task is complete. |
if (self.videoFinished) |
return; |
BOOL completedOrFailed = NO; |
// If the task isn't complete yet, make sure that the input is actually ready for more media data. |
while ([self.assetWriterVideoInput isReadyForMoreMediaData] && !completedOrFailed) |
{ |
// Get the next video sample buffer, and append it to the output file. |
CMSampleBufferRef sampleBuffer = [self.assetReaderVideoOutput copyNextSampleBuffer]; |
if (sampleBuffer != NULL) |
{ |
BOOL success = [self.assetWriterVideoInput appendSampleBuffer:sampleBuffer]; |
CFRelease(sampleBuffer); |
sampleBuffer = NULL; |
completedOrFailed = !success; |
} |
else |
{ |
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 video work has finished). |
BOOL oldFinished = self.videoFinished; |
self.videoFinished = YES; |
if (oldFinished == NO) |
{ |
[self.assetWriterVideoInput markAsFinished]; |
} |
dispatch_group_leave(self.dispatchGroup); |
} |
}]; |
} |
// Set up the notification that the dispatch group will send when the audio and video work have both finished. |
dispatch_group_notify(self.dispatchGroup, self.mainSerializationQueue, ^{ |
BOOL finalSuccess = YES; |
NSError *finalError = nil; |
// Check to see if the work has finished due to cancellation. |
if (self.cancelled) |
{ |
// If so, cancel the reader and writer. |
[self.assetReader cancelReading]; |
[self.assetWriter cancelWriting]; |
} |
else |
{ |
// If cancellation didn't occur, first make sure that the asset reader didn't fail. |
if ([self.assetReader status] == AVAssetReaderStatusFailed) |
{ |
finalSuccess = NO; |
finalError = [self.assetReader error]; |
} |
// If the asset reader didn't fail, attempt to stop the asset writer and check for any errors. |
if (finalSuccess) |
{ |
finalSuccess = [self.assetWriter finishWriting]; |
if (!finalSuccess) |
finalError = [self.assetWriter error]; |
} |
} |
// Call the method to handle completion, and pass in the appropriate parameters to indicate whether reencoding was successful. |
[self readingAndWritingDidFinishSuccessfully:finalSuccess withError:finalError]; |
}); |
} |
// Return success here to indicate whether the asset reader and writer were started successfully. |
return success; |
} |
Во время перекодирования аудиотреки и видеотреки асинхронно обрабатываются на отдельных очередях сериализации для увеличения общей производительности процесса, но обе очереди содержатся в той же группе отгрузки. Путем размещения работы для каждой дорожки в той же группе отгрузки группа может отправить уведомление, когда вся работа выполнена, и успех процесса перекодирования может быть определен.
Обработка завершения
Обработать завершение чтения и записи, readingAndWritingDidFinishSuccessfully:
метод вызывают — с параметрами, указывающими, завершилось ли перекодирование успешно. Если процесс не заканчивался успешно, читатель актива и писатель и отменяются и связанные задачи любого UI, диспетчеризируются основной очереди.
- (void)readingAndWritingDidFinishSuccessfully:(BOOL)success withError:(NSError *)error |
{ |
if (!success) |
{ |
// If the reencoding process failed, we need to cancel the asset reader and writer. |
[self.assetReader cancelReading]; |
[self.assetWriter cancelWriting]; |
dispatch_async(dispatch_get_main_queue(), ^{ |
// Handle any UI tasks here related to failure. |
}); |
} |
else |
{ |
// Reencoding was successful, reset booleans. |
self.cancelled = NO; |
self.videoFinished = NO; |
self.audioFinished = NO; |
dispatch_async(dispatch_get_main_queue(), ^{ |
// Handle any UI tasks here related to success. |
}); |
} |
} |
Обработка отмены
Используя многократные очереди сериализации, можно позволить пользователю приложения отменять процесс перекодирования легко. На основной очереди сериализации сообщения асинхронно отправляются в каждый актив, повторно кодирующий очереди сериализации для отмены их чтения и записи. Когда эти две очереди сериализации завершают свою отмену, группа отгрузки отправляет уведомление основной очереди сериализации где cancelled
свойство установлено в YES
. Вы могли бы связаться cancel
метод от следующего листинга кода с кнопкой на Вашем UI.
- (void)cancel |
{ |
// Handle cancellation asynchronously, but serialize it with the main queue. |
dispatch_async(self.mainSerializationQueue, ^{ |
// If we had audio data to reencode, we need to cancel the audio work. |
if (self.assetWriterAudioInput) |
{ |
// Handle cancellation asynchronously again, but this time serialize it with the audio queue. |
dispatch_async(self.rwAudioSerializationQueue, ^{ |
// Update the Boolean property indicating the task is complete and mark the input as finished if it hasn't already been marked as such. |
BOOL oldFinished = self.audioFinished; |
self.audioFinished = YES; |
if (oldFinished == NO) |
{ |
[self.assetWriterAudioInput markAsFinished]; |
} |
// Leave the dispatch group since the audio work is finished now. |
dispatch_group_leave(self.dispatchGroup); |
}); |
} |
if (self.assetWriterVideoInput) |
{ |
// Handle cancellation asynchronously again, but this time serialize it with the video queue. |
dispatch_async(self.rwVideoSerializationQueue, ^{ |
// Update the Boolean property indicating the task is complete and mark the input as finished if it hasn't already been marked as such. |
BOOL oldFinished = self.videoFinished; |
self.videoFinished = YES; |
if (oldFinished == NO) |
{ |
[self.assetWriterVideoInput markAsFinished]; |
} |
// Leave the dispatch group, since the video work is finished now. |
dispatch_group_leave(self.dispatchGroup); |
}); |
} |
// Set the cancelled Boolean property to YES to cancel any work on the main queue as well. |
self.cancelled = YES; |
}); |
} |
Выходной ассистент настроек актива
AVOutputSettingsAssistant
класс помогает в создании словарей выходных настроек для читателя актива или писателя. Это делает установку намного более простой, специально для высокой частоты кадров фильмы H264, имеющие много определенных предварительных установок. Перечисление 5-1 показывает пример, использующий выходной ассистент настроек для использования ассистента настроек.
Перечисление 5-1 выборка AVOutputSettingsAssistant
AVOutputSettingsAssistant *outputSettingsAssistant = [AVOutputSettingsAssistant outputSettingsAssistantWithPreset:<some preset>]; |
CMFormatDescriptionRef audioFormat = [self getAudioFormat]; |
if (audioFormat != NULL) |
[outputSettingsAssistant setSourceAudioFormat:(CMAudioFormatDescriptionRef)audioFormat]; |
CMFormatDescriptionRef videoFormat = [self getVideoFormat]; |
if (videoFormat != NULL) |
[outputSettingsAssistant setSourceVideoFormat:(CMVideoFormatDescriptionRef)videoFormat]; |
CMTime assetMinVideoFrameDuration = [self getMinFrameDuration]; |
CMTime averageFrameDuration = [self getAvgFrameDuration] |
[outputSettingsAssistant setSourceVideoAverageFrameDuration:averageFrameDuration]; |
[outputSettingsAssistant setSourceVideoMinFrameDuration:assetMinVideoFrameDuration]; |
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:<some URL> fileType:[outputSettingsAssistant outputFileType] error:NULL]; |
AVAssetWriterInput *audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:[outputSettingsAssistant audioSettings] sourceFormatHint:audioFormat]; |
AVAssetWriterInput *videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:[outputSettingsAssistant videoSettings] sourceFormatHint:videoFormat]; |