ios video encoding callback h.264

ios - Extrayendo h264 de CMBlockBuffer



video encoding (2)

Estoy usando Apple VideoTool Box (iOS) para comprimir cuadros sin procesar capturados por la cámara del dispositivo.

Se llama a mi devolución de llamada con un objeto CMSampleBufferRef que contiene CMBlockBuffer.

El objeto CMBlockBuffer contiene la secuencia elemental H264 pero no encontré ninguna forma de obtener un puntero a la transmisión primaria.

Cuando imprimí en la consola el objeto CMSampleBufferRef que obtuve:

(lldb) po blockBufferRef CMBlockBuffer 0x1701193e0 totalDataLength: 4264 retainCount: 1 allocator: 0x1957c2c80 subBlockCapacity: 2 [0] 4264 bytes @ offset 128 Buffer Reference: CMBlockBuffer 0x170119350 totalDataLength: 4632 retainCount: 1 allocator: 0x1957c2c80 subBlockCapacity: 2 [0] 4632 bytes @ offset 0 Memory Block 0x10295c000, 4632 bytes (custom V=0 A=0x0 F=0x18498bb44 R=0x0)

Parece que el objeto CMBlockBuffer al que logré obtener puntero está contactando a otro CMBlockBuferRef (4632 bytes) que no es accesible.

¿Alguien puede publicar cómo acceder al flujo elemantry H264?

¡Gracias!


Gracias Anton por una excelente respuesta! Estoy poniendo un ingenuo puerto Swift de su solución para las personas interesadas en utilizar los conceptos discutidos aquí directamente en sus proyectos basados ​​en Swift.

public func didEncodeFrame(frame: CMSampleBuffer) { print ("Received encoded frame in delegate...") //----AVCC to Elem stream-----// var elementaryStream = NSMutableData() //1. check if CMBuffer had I-frame var isIFrame:Bool = false let attachmentsArray:CFArray = CMSampleBufferGetSampleAttachmentsArray(frame, false)! //check how many attachments if ( CFArrayGetCount(attachmentsArray) > 0 ) { let dict = CFArrayGetValueAtIndex(attachmentsArray, 0) let dictRef:CFDictionaryRef = unsafeBitCast(dict, CFDictionaryRef.self) //get value let value = CFDictionaryGetValue(dictRef, unsafeBitCast(kCMSampleAttachmentKey_NotSync, UnsafePointer<Void>.self)) if ( value != nil ){ print ("IFrame found...") isIFrame = true } } //2. define the start code let nStartCodeLength:size_t = 4 let nStartCode:[UInt8] = [0x00, 0x00, 0x00, 0x01] //3. write the SPS and PPS before I-frame if ( isIFrame == true ){ let description:CMFormatDescriptionRef = CMSampleBufferGetFormatDescription(frame)! //how many params var numParams:size_t = 0 CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, 0, nil, nil, &numParams, nil) //write each param-set to elementary stream print("Write param to elementaryStream ", numParams) for i in 0..<numParams { var parameterSetPointer:UnsafePointer<UInt8> = nil var parameterSetLength:size_t = 0 CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, i, &parameterSetPointer, &parameterSetLength, nil, nil) elementaryStream.appendBytes(nStartCode, length: nStartCodeLength) elementaryStream.appendBytes(parameterSetPointer, length: unsafeBitCast(parameterSetLength, Int.self)) } } //4. Get a pointer to the raw AVCC NAL unit data in the sample buffer var blockBufferLength:size_t = 0 var bufferDataPointer: UnsafeMutablePointer<Int8> = nil CMBlockBufferGetDataPointer(CMSampleBufferGetDataBuffer(frame)!, 0, nil, &blockBufferLength, &bufferDataPointer) print ("Block length = ", blockBufferLength) //5. Loop through all the NAL units in the block buffer var bufferOffset:size_t = 0 let AVCCHeaderLength:Int = 4 while (bufferOffset < (blockBufferLength - AVCCHeaderLength) ) { // Read the NAL unit length var NALUnitLength:UInt32 = 0 memcpy(&NALUnitLength, bufferDataPointer + bufferOffset, AVCCHeaderLength) //Big-Endian to Little-Endian NALUnitLength = CFSwapInt32(NALUnitLength) if ( NALUnitLength > 0 ){ print ( "NALUnitLen = ", NALUnitLength) // Write start code to the elementary stream elementaryStream.appendBytes(nStartCode, length: nStartCodeLength) // Write the NAL unit without the AVCC length header to the elementary stream elementaryStream.appendBytes(bufferDataPointer + bufferOffset + AVCCHeaderLength, length: Int(NALUnitLength)) // Move to the next NAL unit in the block buffer bufferOffset += AVCCHeaderLength + size_t(NALUnitLength); print("Moving to next NALU...") } } print("Read completed...") }


He estado luchando con esto yo mismo desde hace bastante tiempo, y finalmente he descubierto todo.

La función CMBlockBufferGetDataPointer le da acceso a toda la información que necesita, pero hay algunas cosas no muy obvias que debe hacer para convertirla en una transmisión primaria.

AVCC vs formato Anexo B

Los datos en el CMBlockBuffer se almacenan en formato AVCC, mientras que los flujos elementales generalmente siguen la especificación del Anexo B ( aquí hay una excelente descripción de los dos formatos). En el formato AVCC, los 4 primeros bytes contienen la longitud de la unidad NAL (otra palabra para el paquete H264). Debe reemplazar este encabezado con el código de inicio de 4 bytes: 0x00 0x00 0x00 0x01, que funciona como un separador entre unidades NAL en una transmisión primaria del Anexo B (la versión de 3 bytes 0x00 0x00 0x01 también funciona bien).

Múltiples unidades NAL en un solo CMBlockBuffer

La siguiente cosa no muy obvia es que un solo CMBlockBuffer a veces contendrá múltiples unidades NAL. Apple parece agregar una unidad NAL adicional (SEI) que contiene metadatos a cada unidad I-Frame NAL (también llamada IDR). Esta es probablemente la razón por la que está viendo múltiples almacenamientos intermedios en un solo objeto CMBlockBuffer. Sin embargo, la función CMBlockBufferGetDataPointer le proporciona un único puntero con acceso a todos los datos. Dicho esto, la presencia de múltiples unidades NAL complica la conversión de los encabezados AVCC. Ahora, en realidad, tiene que leer el valor de longitud contenido en el encabezado AVCC para encontrar la siguiente unidad NAL y continuar convirtiendo los encabezados hasta que haya llegado al final del búfer.

Big-Endian vs Little-Endian

La siguiente cosa no muy obvia es que el encabezado AVCC se almacena en formato Big-Endian, y iOS es Little-Endian de forma nativa. Entonces, cuando está leyendo el valor de longitud contenido en un encabezado AVCC, CFSwapInt32BigToHost primero a la función CFSwapInt32BigToHost .

Unidades NPS SPS y PPS

La última cosa no muy obvia es que los datos dentro de CMBlockBuffer no contienen el parámetro unidades NAL SPS y PPS, que contiene parámetros de configuración para el decodificador, como perfil, nivel, resolución, velocidad de cuadros. Se almacenan como metadatos en la descripción de formato del búfer de muestra y se puede acceder a través de la función CMVideoFormatDescriptionGetH264ParameterSetAtIndex . Tenga en cuenta que debe agregar los códigos de inicio a estas unidades NAL antes de enviarlas. Las unidades SPS y PPS NAL no tienen que enviarse con cada fotograma nuevo. Un decodificador solo necesita leerlos una vez, pero es común reenviarlos periódicamente, por ejemplo, antes de cada nueva unidad NAL de cuadros I.

Ejemplo de código

A continuación se muestra un ejemplo de código teniendo en cuenta todas estas cosas.

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 **)&notSync); // 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, &parameterSetPointer, &parameterSetLength, 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; } }