Ответ 1
Я уже довольно долго боролся с этим сам, и, наконец, понял все.
Функция CMBlockBufferGetDataPointer
предоставляет вам доступ ко всем необходимым вам данным, но есть несколько не очень очевидных вещей, которые вам нужно сделать, чтобы преобразовать их в элементарный поток.
Формат AVCC и Приложение B
Данные в CMBlockBuffer хранятся в формате AVCC, тогда как элементарные потоки, как правило, следуют спецификации приложения B (здесь - отличный обзор два формата). В формате AVCC 4 первых байта содержат длину блока NAL (другое слово для пакета H264). Вам нужно заменить этот заголовок на 4-байтовый стартовый код: 0x00 0x00 0x00 0x01, который функционирует как разделитель между единицами NAL в элементарном потоке приложения B (3-байтная версия 0x00 0x00 0x01 тоже отлично работает).
Несколько блоков NAL в одном CMBlockBuffer
Следующая не очень очевидная вещь заключается в том, что один CMBlockBuffer будет иногда содержать несколько блоков NAL. Кажется, что Apple добавляет дополнительный блок NAL (SEI), содержащий метаданные для каждого блока NAL I-Frame (также называемого IDR). Вероятно, поэтому вы видите несколько буферов в одном объекте CMBlockBuffer. Однако функция CMBlockBufferGetDataPointer
предоставляет вам один указатель с доступом ко всем данным. При этом наличие множества блоков NAL затрудняет преобразование заголовков AVCC. Теперь вам действительно нужно прочитать значение длины, содержащееся в заголовке AVCC, чтобы найти следующий блок NAL и продолжить преобразование заголовков, пока вы не достигнете конца буфера.
Big-Endian vs Little-Endian
Следующая не очень очевидная вещь: заголовок AVCC хранится в формате Big-Endian, а iOS - Little-Endian. Поэтому, когда вы читаете значение длины, содержащееся в заголовке AVCC, сначала передайте его функции CFSwapInt32BigToHost
.
Единицы SPS и PPS NAL
Конечная не очень очевидная вещь заключается в том, что данные внутри CMBlockBuffer не содержат параметры NAL единиц SPS и PPS, которые содержат параметры конфигурации для декодера, такие как профиль, уровень, разрешение, частота кадров. Они хранятся в виде метаданных в описании формата буфера образца и могут быть доступны через функцию CMVideoFormatDescriptionGetH264ParameterSetAtIndex
. Обратите внимание, что перед отправкой необходимо добавить начальные коды к этим блокам NAL. Блоки SPS и PPS NAL не должны отправляться с каждым новым фреймом. Декодер должен только читать их один раз, но обычно их периодически пересылать, например, перед каждым новым NAL-модулем I-кадра.
Пример кода
Ниже приведен пример кода, учитывающий все эти вещи.
static void videoFrameFinishedEncoding(void *outputCallbackRefCon,
void *sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer) {
// Check if there were any errors encoding
if (status != noErr) {
NSLog(@"Error encoding video, err=%lld", (int64_t)status);
return;
}
// In this example we will use a NSMutableData object to store the
// elementary stream.
NSMutableData *elementaryStream = [NSMutableData data];
// Find out if the sample buffer contains an I-Frame.
// If so we will write the SPS and PPS NAL units to the elementary stream.
BOOL isIFrame = NO;
CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, 0);
if (CFArrayGetCount(attachmentsArray)) {
CFBooleanRef notSync;
CFDictionaryRef dict = CFArrayGetValueAtIndex(attachmentsArray, 0);
BOOL keyExists = CFDictionaryGetValueIfPresent(dict,
kCMSampleAttachmentKey_NotSync,
(const void **)¬Sync);
// An I-Frame is a sync frame
isIFrame = !keyExists || !CFBooleanGetValue(notSync);
}
// This is the start code that we will write to
// the elementary stream before every NAL unit
static const size_t startCodeLength = 4;
static const uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
// Write the SPS and PPS NAL units to the elementary stream before every I-Frame
if (isIFrame) {
CMFormatDescriptionRef description = CMSampleBufferGetFormatDescription(sampleBuffer);
// Find out how many parameter sets there are
size_t numberOfParameterSets;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description,
0, NULL, NULL,
&numberOfParameterSets,
NULL);
// Write each parameter set to the elementary stream
for (int i = 0; i < numberOfParameterSets; i++) {
const uint8_t *parameterSetPointer;
size_t parameterSetLength;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description,
i,
¶meterSetPointer,
¶meterSetLength,
NULL, NULL);
// Write the parameter set to the elementary stream
[elementaryStream appendBytes:startCode length:startCodeLength];
[elementaryStream appendBytes:parameterSetPointer length:parameterSetLength];
}
}
// Get a pointer to the raw AVCC NAL unit data in the sample buffer
size_t blockBufferLength;
uint8_t *bufferDataPointer = NULL;
CMBlockBufferGetDataPointer(CMSampleBufferGetDataBuffer(sampleBuffer),
0,
NULL,
&blockBufferLength,
(char **)&bufferDataPointer);
// Loop through all the NAL units in the block buffer
// and write them to the elementary stream with
// start codes instead of AVCC length headers
size_t bufferOffset = 0;
static const int AVCCHeaderLength = 4;
while (bufferOffset < blockBufferLength - AVCCHeaderLength) {
// Read the NAL unit length
uint32_t NALUnitLength = 0;
memcpy(&NALUnitLength, bufferDataPointer + bufferOffset, AVCCHeaderLength);
// Convert the length value from Big-endian to Little-endian
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
// Write start code to the elementary stream
[elementaryStream appendBytes:startCode length:startCodeLength];
// Write the NAL unit without the AVCC length header to the elementary stream
[elementaryStream appendBytes:bufferDataPointer + bufferOffset + AVCCHeaderLength
length:NALUnitLength];
// Move to the next NAL unit in the block buffer
bufferOffset += AVCCHeaderLength + NALUnitLength;
}
}