Как использовать VideoToolbox для распаковки видеопотока H.264

У меня было много проблем с выяснением того, как использовать аппаратное ускорение видео от Apple для распаковки видеопотока H.264. Через несколько недель я понял это и хотел поделиться обширным примером, так как я не мог найти один.

Моя цель - дать подробный, поучительный пример Video Toolbox, представленного в сессии 513 WWDC '14. Мой код не будет компилироваться или выполняться, так как он должен быть интегрирован с элементарным потоком H.264 (например, видео, прочитанным из файла или переданным из Интернета и т. Д.) И должен быть настроен в зависимости от конкретного случая.

Я должен отметить, что у меня очень мало опыта работы с видео en / decoding, за исключением того, что я узнал, когда гуглял предмет. Я не знаю всех подробностей о видеоформатах, структуре параметров и т. Д., Поэтому я включил только то, что, как мне кажется, вам нужно знать.

Я использую XCode 6.2 и развернул на устройствах iOS под управлением iOS 8.1 и 8.2.

7 ответов

Решение

Основные понятия:

NALU: NALU - это просто фрагмент данных различной длины, имеющий заголовок начального кода NALU. 0x00 00 00 01 YY где первые 5 бит YY говорит вам, что это за тип NALU и, следовательно, какой тип данных следует за заголовком. (Так как вам нужны только первые 5 бит, я использую YY & 0x1F просто получить соответствующие биты.) Я перечисляю, что все эти типы в методе NSString * const naluTypesStrings[], но вам не нужно знать, что они все.

Параметры: Вашему декодеру нужны параметры, чтобы он знал, как хранятся видеоданные H.264. 2, которые вам нужно установить, это набор параметров последовательности (SPS) и набор параметров изображения (PPS), и каждый из них имеет свой собственный номер типа NALU. Вам не нужно знать, что означают параметры, декодер знает, что с ними делать.

Формат потока H.264: В большинстве потоков H.264 вы получите исходный набор параметров PPS и SPS, за которым следует i-кадр (или IDR-кадр или флеш-кадр) NALU. Затем вы получите несколько NALU P-кадров (возможно, несколько десятков или около того), затем другой набор параметров (которые могут совпадать с исходными параметрами) и i-кадр, больше P-кадров и т. Д. I-кадры намного больше, чем P кадров. Концептуально вы можете рассматривать i-кадр как целое изображение видео, а P-кадры - это просто изменения, внесенные в этот i-кадр, пока вы не получите следующий i-кадр.

Процедура:

  1. Создайте отдельные NALU из вашего потока H.264. Я не могу показать код для этого шага, так как он во многом зависит от того, какой источник видео вы используете. Я сделал этот рисунок, чтобы показать, с чем я работал ("данные" на рисунке - это "кадр" в моем следующем коде), но ваш случай может и, вероятно, будет отличаться. С чем я работал Мой метод receivedRawVideoFrame: вызывается каждый раз, когда я получаю кадр (uint8_t *frame) который был одним из 2 типов. На диаграмме эти два типа рамок представляют собой две большие фиолетовые рамки.

  2. Создайте CMVideoFormatDescriptionRef из ваших NALU SPS и PPS с помощью CMVideoFormatDescriptionCreateFromH264ParameterSets (). Вы не можете отобразить какие-либо кадры без этого в первую очередь. SPS и PPS могут выглядеть как смесь чисел, но VTD знает, что с ними делать. Все, что вам нужно знать, это CMVideoFormatDescriptionRef описание видеоданных, таких как ширина / высота, тип формата (kCMPixelFormat_32BGRA, kCMVideoCodecType_H264 и т. д.), соотношение сторон, цветовое пространство и т. д. Ваш декодер будет удерживать параметры до тех пор, пока не прибудет новый набор (иногда параметры регулярно отправляются повторно, даже если они не изменились).

  3. Перепакуйте ваши NALU IDR и NIDU кадров в соответствии с форматом "AVCC". Это означает удаление стартовых кодов NALU и их замену 4-байтовым заголовком, в котором указана длина NALU. Вам не нужно делать это для NPSU SPS и PPS. (Обратите внимание, что 4-байтовый заголовок длины NALU находится в порядке байтов, так что если у вас есть UInt32 значение должно быть заменено байтами перед копированием в CMBlockBuffer с помощью CFSwapInt32, Я делаю это в моем коде с htonl вызов функции.)

  4. Упакуйте кадры IDR и NALU не-IDR в CMBlockBuffer. Не делайте этого с NALU параметра SPS PPS. Все, что вам нужно знать о CMBlockBuffers заключается в том, что они представляют собой метод обертывания произвольных блоков данных в основной носитель. (Любые сжатые видеоданные в конвейере видео оборачиваются в это.)

  5. Упакуйте CMBlockBuffer в CMSampleBuffer. Все, что вам нужно знать о CMSampleBuffers в том, что они завернули нашу CMBlockBuffers с другой информацией (здесь это будет CMVideoFormatDescription а также CMTime, если CMTime используется).

  6. Создайте VTDecompressionSessionRef и передайте образцы буферов в VTDecompressionSessionDecodeFrame (). Кроме того, вы можете использовать AVSampleBufferDisplayLayer И его enqueueSampleBuffer: метод, и вам не нужно будет использовать VTDecompSession. Проще настроить, но не будет выдавать ошибки, если что-то пойдет не так, как VTD.

  7. В обратном вызове VTDecompSession используйте результирующий CVImageBufferRef для отображения видеокадра. Если вам нужно конвертировать ваши CVImageBuffer к UIImageсм. мой ответ Stackru здесь.

Другие заметки:

  • Потоки H.264 могут сильно различаться. Из того, что я узнал, заголовки стартового кода NALU иногда составляют 3 байта (0x00 00 01) а иногда 4 (0x00 00 00 01). Мой код работает на 4 байта; вам нужно будет изменить несколько вещей, если вы работаете с 3.

  • Если вы хотите узнать больше о NALU, я нашел этот ответ очень полезным. В моем случае я обнаружил, что мне не нужно игнорировать байты "предотвращения эмуляции", как описано, поэтому я лично пропустил этот шаг, но вам, возможно, потребуется знать об этом.

  • Если ваша VTDecompressionSession выдает номер ошибки (например, -12909), найдите код ошибки в вашем проекте XCode. Найдите каркас VideoToolbox в навигаторе проекта, откройте его и найдите заголовок VTErrors.h. Если вы не можете найти его, я также включил все коды ошибок ниже в другом ответе.

Пример кода:

Итак, начнем с объявления некоторых глобальных переменных и включения инфраструктуры VT (VT = Video Toolbox).

#import <VideoToolbox/VideoToolbox.h>

@property (nonatomic, assign) CMVideoFormatDescriptionRef formatDesc;
@property (nonatomic, assign) VTDecompressionSessionRef decompressionSession;
@property (nonatomic, retain) AVSampleBufferDisplayLayer *videoLayer;
@property (nonatomic, assign) int spsSize;
@property (nonatomic, assign) int ppsSize;

Следующий массив используется только для того, чтобы вы могли распечатать, какой тип кадра NALU вы получаете. Если вы знаете, что означают все эти типы, хорошо для вас, вы знаете больше о H.264, чем я:) Мой код обрабатывает только типы 1, 5, 7 и 8.

NSString * const naluTypesStrings[] =
{
    @"0: Unspecified (non-VCL)",
    @"1: Coded slice of a non-IDR picture (VCL)",    // P frame
    @"2: Coded slice data partition A (VCL)",
    @"3: Coded slice data partition B (VCL)",
    @"4: Coded slice data partition C (VCL)",
    @"5: Coded slice of an IDR picture (VCL)",      // I frame
    @"6: Supplemental enhancement information (SEI) (non-VCL)",
    @"7: Sequence parameter set (non-VCL)",         // SPS parameter
    @"8: Picture parameter set (non-VCL)",          // PPS parameter
    @"9: Access unit delimiter (non-VCL)",
    @"10: End of sequence (non-VCL)",
    @"11: End of stream (non-VCL)",
    @"12: Filler data (non-VCL)",
    @"13: Sequence parameter set extension (non-VCL)",
    @"14: Prefix NAL unit (non-VCL)",
    @"15: Subset sequence parameter set (non-VCL)",
    @"16: Reserved (non-VCL)",
    @"17: Reserved (non-VCL)",
    @"18: Reserved (non-VCL)",
    @"19: Coded slice of an auxiliary coded picture without partitioning (non-VCL)",
    @"20: Coded slice extension (non-VCL)",
    @"21: Coded slice extension for depth view components (non-VCL)",
    @"22: Reserved (non-VCL)",
    @"23: Reserved (non-VCL)",
    @"24: STAP-A Single-time aggregation packet (non-VCL)",
    @"25: STAP-B Single-time aggregation packet (non-VCL)",
    @"26: MTAP16 Multi-time aggregation packet (non-VCL)",
    @"27: MTAP24 Multi-time aggregation packet (non-VCL)",
    @"28: FU-A Fragmentation unit (non-VCL)",
    @"29: FU-B Fragmentation unit (non-VCL)",
    @"30: Unspecified (non-VCL)",
    @"31: Unspecified (non-VCL)",
};

Теперь здесь происходит вся магия.

-(void) receivedRawVideoFrame:(uint8_t *)frame withSize:(uint32_t)frameSize isIFrame:(int)isIFrame
{
    OSStatus status;

    uint8_t *data = NULL;
    uint8_t *pps = NULL;
    uint8_t *sps = NULL;

    // I know what my H.264 data source's NALUs look like so I know start code index is always 0.
    // if you don't know where it starts, you can use a for loop similar to how i find the 2nd and 3rd start codes
    int startCodeIndex = 0;
    int secondStartCodeIndex = 0;
    int thirdStartCodeIndex = 0;

    long blockLength = 0;

    CMSampleBufferRef sampleBuffer = NULL;
    CMBlockBufferRef blockBuffer = NULL;

    int nalu_type = (frame[startCodeIndex + 4] & 0x1F);
    NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);

    // if we havent already set up our format description with our SPS PPS parameters, we
    // can't process any frames except type 7 that has our parameters
    if (nalu_type != 7 && _formatDesc == NULL)
    {
        NSLog(@"Video error: Frame is not an I Frame and format description is null");
        return;
    }

    // NALU type 7 is the SPS parameter NALU
    if (nalu_type == 7)
    {
        // find where the second PPS start code begins, (the 0x00 00 00 01 code)
        // from which we also get the length of the first SPS code
        for (int i = startCodeIndex + 4; i < startCodeIndex + 40; i++)
        {
            if (frame[i] == 0x00 && frame[i+1] == 0x00 && frame[i+2] == 0x00 && frame[i+3] == 0x01)
            {
                secondStartCodeIndex = i;
                _spsSize = secondStartCodeIndex;   // includes the header in the size
                break;
            }
        }

        // find what the second NALU type is
        nalu_type = (frame[secondStartCodeIndex + 4] & 0x1F);
        NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);
    }

    // type 8 is the PPS parameter NALU
    if(nalu_type == 8)
    {
        // find where the NALU after this one starts so we know how long the PPS parameter is
        for (int i = _spsSize + 4; i < _spsSize + 30; i++)
        {
            if (frame[i] == 0x00 && frame[i+1] == 0x00 && frame[i+2] == 0x00 && frame[i+3] == 0x01)
            {
                thirdStartCodeIndex = i;
                _ppsSize = thirdStartCodeIndex - _spsSize;
                break;
            }
        }

        // allocate enough data to fit the SPS and PPS parameters into our data objects.
        // VTD doesn't want you to include the start code header (4 bytes long) so we add the - 4 here
        sps = malloc(_spsSize - 4);
        pps = malloc(_ppsSize - 4);

        // copy in the actual sps and pps values, again ignoring the 4 byte header
        memcpy (sps, &frame[4], _spsSize-4);
        memcpy (pps, &frame[_spsSize+4], _ppsSize-4);

        // now we set our H264 parameters
        uint8_t*  parameterSetPointers[2] = {sps, pps};
        size_t parameterSetSizes[2] = {_spsSize-4, _ppsSize-4};

        // suggestion from @Kris Dude's answer below
        if (_formatDesc) 
        {
            CFRelease(_formatDesc);
            _formatDesc = NULL;
        }

        status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, 
                                                (const uint8_t *const*)parameterSetPointers, 
                                                parameterSetSizes, 4, 
                                                &_formatDesc);

        NSLog(@"\t\t Creation of CMVideoFormatDescription: %@", (status == noErr) ? @"successful!" : @"failed...");
        if(status != noErr) NSLog(@"\t\t Format Description ERROR type: %d", (int)status);

        // See if decomp session can convert from previous format description 
        // to the new one, if not we need to remake the decomp session.
        // This snippet was not necessary for my applications but it could be for yours
        /*BOOL needNewDecompSession = (VTDecompressionSessionCanAcceptFormatDescription(_decompressionSession, _formatDesc) == NO);
         if(needNewDecompSession)
         {
             [self createDecompSession];
         }*/

        // now lets handle the IDR frame that (should) come after the parameter sets
        // I say "should" because that's how I expect my H264 stream to work, YMMV
        nalu_type = (frame[thirdStartCodeIndex + 4] & 0x1F);
        NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);
    }

    // create our VTDecompressionSession.  This isnt neccessary if you choose to use AVSampleBufferDisplayLayer
    if((status == noErr) && (_decompressionSession == NULL))
    {
        [self createDecompSession];
    }

    // type 5 is an IDR frame NALU.  The SPS and PPS NALUs should always be followed by an IDR (or IFrame) NALU, as far as I know
    if(nalu_type == 5)
    {
        // find the offset, or where the SPS and PPS NALUs end and the IDR frame NALU begins
        int offset = _spsSize + _ppsSize;
        blockLength = frameSize - offset;
        data = malloc(blockLength);
        data = memcpy(data, &frame[offset], blockLength);

        // replace the start code header on this NALU with its size.
        // AVCC format requires that you do this.  
        // htonl converts the unsigned int from host to network byte order
        uint32_t dataLength32 = htonl (blockLength - 4);
        memcpy (data, &dataLength32, sizeof (uint32_t));

        // create a block buffer from the IDR NALU
        status = CMBlockBufferCreateWithMemoryBlock(NULL, data,  // memoryBlock to hold buffered data
                                                    blockLength,  // block length of the mem block in bytes.
                                                    kCFAllocatorNull, NULL,
                                                    0, // offsetToData
                                                    blockLength,   // dataLength of relevant bytes, starting at offsetToData
                                                    0, &blockBuffer);

        NSLog(@"\t\t BlockBufferCreation: \t %@", (status == kCMBlockBufferNoErr) ? @"successful!" : @"failed...");
    }

    // NALU type 1 is non-IDR (or PFrame) picture
    if (nalu_type == 1)
    {
        // non-IDR frames do not have an offset due to SPS and PSS, so the approach
        // is similar to the IDR frames just without the offset
        blockLength = frameSize;
        data = malloc(blockLength);
        data = memcpy(data, &frame[0], blockLength);

        // again, replace the start header with the size of the NALU
        uint32_t dataLength32 = htonl (blockLength - 4);
        memcpy (data, &dataLength32, sizeof (uint32_t));

        status = CMBlockBufferCreateWithMemoryBlock(NULL, data,  // memoryBlock to hold data. If NULL, block will be alloc when needed
                                                    blockLength,  // overall length of the mem block in bytes
                                                    kCFAllocatorNull, NULL,
                                                    0,     // offsetToData
                                                    blockLength,  // dataLength of relevant data bytes, starting at offsetToData
                                                    0, &blockBuffer);

        NSLog(@"\t\t BlockBufferCreation: \t %@", (status == kCMBlockBufferNoErr) ? @"successful!" : @"failed...");
    }

    // now create our sample buffer from the block buffer,
    if(status == noErr)
    {
        // here I'm not bothering with any timing specifics since in my case we displayed all frames immediately
        const size_t sampleSize = blockLength;
        status = CMSampleBufferCreate(kCFAllocatorDefault,
                                      blockBuffer, true, NULL, NULL,
                                      _formatDesc, 1, 0, NULL, 1,
                                      &sampleSize, &sampleBuffer);

        NSLog(@"\t\t SampleBufferCreate: \t %@", (status == noErr) ? @"successful!" : @"failed...");
    }

    if(status == noErr)
    {
        // set some values of the sample buffer's attachments
        CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
        CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
        CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);

        // either send the samplebuffer to a VTDecompressionSession or to an AVSampleBufferDisplayLayer
        [self render:sampleBuffer];
    }

    // free memory to avoid a memory leak, do the same for sps, pps and blockbuffer
    if (NULL != data)
    {
        free (data);
        data = NULL;
    }
}

Следующий метод создает сеанс VTD. Воссоздайте его всякий раз, когда вы получаете новые параметры. (Вы не должны пересоздавать его каждый раз, когда получаете параметры, уверен.)

Если вы хотите установить атрибуты для места назначения CVPixelBuffer, прочитайте значения CoreVideo PixelBufferAttributes и поместите их в NSDictionary *destinationImageBufferAttributes,

-(void) createDecompSession
{
    // make sure to destroy the old VTD session
    _decompressionSession = NULL;
    VTDecompressionOutputCallbackRecord callBackRecord;
    callBackRecord.decompressionOutputCallback = decompressionSessionDecodeFrameCallback;

    // this is necessary if you need to make calls to Objective C "self" from within in the callback method.
    callBackRecord.decompressionOutputRefCon = (__bridge void *)self;

    // you can set some desired attributes for the destination pixel buffer.  I didn't use this but you may
    // if you need to set some attributes, be sure to uncomment the dictionary in VTDecompressionSessionCreate
    NSDictionary *destinationImageBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
                                                      [NSNumber numberWithBool:YES],
                                                      (id)kCVPixelBufferOpenGLESCompatibilityKey,
                                                      nil];

    OSStatus status =  VTDecompressionSessionCreate(NULL, _formatDesc, NULL,
                                                    NULL, // (__bridge CFDictionaryRef)(destinationImageBufferAttributes)
                                                    &callBackRecord, &_decompressionSession);
    NSLog(@"Video Decompression Session Create: \t %@", (status == noErr) ? @"successful!" : @"failed...");
    if(status != noErr) NSLog(@"\t\t VTD ERROR type: %d", (int)status);
}

Теперь этот метод вызывается каждый раз, когда VTD завершает распаковку любого кадра, который вы ему отправили. Этот метод вызывается даже в случае ошибки или пропуска фрейма.

void decompressionSessionDecodeFrameCallback(void *decompressionOutputRefCon,
                                             void *sourceFrameRefCon,
                                             OSStatus status,
                                             VTDecodeInfoFlags infoFlags,
                                             CVImageBufferRef imageBuffer,
                                             CMTime presentationTimeStamp,
                                             CMTime presentationDuration)
{
    THISCLASSNAME *streamManager = (__bridge THISCLASSNAME *)decompressionOutputRefCon;

    if (status != noErr)
    {
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSLog(@"Decompressed error: %@", error);
    }
    else
    {
        NSLog(@"Decompressed sucessfully");

        // do something with your resulting CVImageBufferRef that is your decompressed frame
        [streamManager displayDecodedFrame:imageBuffer];
    }
}

Именно здесь мы на самом деле отправляем sampleBuffer в VTD для декодирования.

- (void) render:(CMSampleBufferRef)sampleBuffer
{
    VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression;
    VTDecodeInfoFlags flagOut;
    NSDate* currentTime = [NSDate date];
    VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flags,
                                      (void*)CFBridgingRetain(currentTime), &flagOut);

    CFRelease(sampleBuffer);

    // if you're using AVSampleBufferDisplayLayer, you only need to use this line of code
    // [videoLayer enqueueSampleBuffer:sampleBuffer];
}

Если вы используете AVSampleBufferDisplayLayer, обязательно инициируйте слой, как это, в viewDidLoad или внутри какого-либо другого метода init.

-(void) viewDidLoad
{
    // create our AVSampleBufferDisplayLayer and add it to the view
    videoLayer = [[AVSampleBufferDisplayLayer alloc] init];
    videoLayer.frame = self.view.frame;
    videoLayer.bounds = self.view.bounds;
    videoLayer.videoGravity = AVLayerVideoGravityResizeAspect;

    // set Timebase, you may need this if you need to display frames at specific times
    // I didn't need it so I haven't verified that the timebase is working
    CMTimebaseRef controlTimebase;
    CMTimebaseCreateWithMasterClock(CFAllocatorGetDefault(), CMClockGetHostTimeClock(), &controlTimebase);

    //videoLayer.controlTimebase = controlTimebase;
    CMTimebaseSetTime(self.videoLayer.controlTimebase, kCMTimeZero);
    CMTimebaseSetRate(self.videoLayer.controlTimebase, 1.0);

    [[self.view layer] addSublayer:videoLayer];
}

Если вы не можете найти коды ошибок VTD в структуре, я решил просто включить их здесь. (Опять же, все эти ошибки и многое другое можно найти внутри VideoToolbox.framework сам в навигаторе проекта, в файле VTErrors.h.)

Один из этих кодов ошибок вы получите либо в обратном вызове кадра декодирования VTD, либо при создании сеанса VTD, если вы что-то сделали неправильно.

kVTPropertyNotSupportedErr              = -12900,
kVTPropertyReadOnlyErr                  = -12901,
kVTParameterErr                         = -12902,
kVTInvalidSessionErr                    = -12903,
kVTAllocationFailedErr                  = -12904,
kVTPixelTransferNotSupportedErr         = -12905, // c.f. -8961
kVTCouldNotFindVideoDecoderErr          = -12906,
kVTCouldNotCreateInstanceErr            = -12907,
kVTCouldNotFindVideoEncoderErr          = -12908,
kVTVideoDecoderBadDataErr               = -12909, // c.f. -8969
kVTVideoDecoderUnsupportedDataFormatErr = -12910, // c.f. -8970
kVTVideoDecoderMalfunctionErr           = -12911, // c.f. -8960
kVTVideoEncoderMalfunctionErr           = -12912,
kVTVideoDecoderNotAvailableNowErr       = -12913,
kVTImageRotationNotSupportedErr         = -12914,
kVTVideoEncoderNotAvailableNowErr       = -12915,
kVTFormatDescriptionChangeNotSupportedErr   = -12916,
kVTInsufficientSourceColorDataErr       = -12917,
kVTCouldNotCreateColorCorrectionDataErr = -12918,
kVTColorSyncTransformConvertFailedErr   = -12919,
kVTVideoDecoderAuthorizationErr         = -12210,
kVTVideoEncoderAuthorizationErr         = -12211,
kVTColorCorrectionPixelTransferFailedErr    = -12212,
kVTMultiPassStorageIdentifierMismatchErr    = -12213,
kVTMultiPassStorageInvalidErr           = -12214,
kVTFrameSiloInvalidTimeStampErr         = -12215,
kVTFrameSiloInvalidTimeRangeErr         = -12216,
kVTCouldNotFindTemporalFilterErr        = -12217,
kVTPixelTransferNotPermittedErr         = -12218,

Хороший пример Swift многих из этого можно найти в библиотеке Avios Джоша Бейкера: https://github.com/tidwall/Avios

Обратите внимание, что в настоящее время Avios ожидает, что пользователь будет обрабатывать данные по частям при начальных кодах NAL, но обрабатывает декодирование данных с этого момента.

Также стоит обратить внимание на основанную на Swift RTMP-библиотеку HaishinKit (ранее "LF"), которая имеет собственную реализацию декодирования, включая более надежный анализ NALU: https://github.com/shogo4405/lf.swift

В дополнение к вышеперечисленным ошибкам VTE, я подумал, что стоит добавить ошибки CMFormatDescription, CMBlockBuffer, CMSampleBuffer, с которыми вы можете столкнуться при попытке попробовать пример Ливи.

kCMFormatDescriptionError_InvalidParameter  = -12710,
kCMFormatDescriptionError_AllocationFailed  = -12711,
kCMFormatDescriptionError_ValueNotAvailable = -12718,

kCMBlockBufferNoErr                             = 0,
kCMBlockBufferStructureAllocationFailedErr      = -12700,
kCMBlockBufferBlockAllocationFailedErr          = -12701,
kCMBlockBufferBadCustomBlockSourceErr           = -12702,
kCMBlockBufferBadOffsetParameterErr             = -12703,
kCMBlockBufferBadLengthParameterErr             = -12704,
kCMBlockBufferBadPointerParameterErr            = -12705,
kCMBlockBufferEmptyBBufErr                      = -12706,
kCMBlockBufferUnallocatedBlockErr               = -12707,
kCMBlockBufferInsufficientSpaceErr              = -12708,

kCMSampleBufferError_AllocationFailed             = -12730,
kCMSampleBufferError_RequiredParameterMissing     = -12731,
kCMSampleBufferError_AlreadyHasDataBuffer         = -12732,
kCMSampleBufferError_BufferNotReady               = -12733,
kCMSampleBufferError_SampleIndexOutOfRange        = -12734,
kCMSampleBufferError_BufferHasNoSampleSizes       = -12735,
kCMSampleBufferError_BufferHasNoSampleTimingInfo  = -12736,
kCMSampleBufferError_ArrayTooSmall                = -12737,
kCMSampleBufferError_InvalidEntryCount            = -12738,
kCMSampleBufferError_CannotSubdivide              = -12739,
kCMSampleBufferError_SampleTimingInfoInvalid      = -12740,
kCMSampleBufferError_InvalidMediaTypeForOperation = -12741,
kCMSampleBufferError_InvalidSampleData            = -12742,
kCMSampleBufferError_InvalidMediaFormat           = -12743,
kCMSampleBufferError_Invalidated                  = -12744,
kCMSampleBufferError_DataFailed                   = -16750,
kCMSampleBufferError_DataCanceled                 = -16751,

Спасибо Оливии за этот отличный и подробный пост! Недавно я начал программировать потоковое приложение на iPad Pro с формами Xamarin, и эта статья мне очень помогла, и я нашел много ссылок на нее в Интернете.

Я полагаю, многие люди уже переписали пример Оливии в Xamarin, и я не претендую на звание лучшего программиста в мире. Но поскольку здесь еще никто не разместил версию C# / Xamarin, и я хотел бы дать кое-что сообществу за отличный пост выше, вот моя версия C# / Xamarin. Может быть, это помогает кому-то ускорить прогресс в его или его проекте.

Я придерживался примера Оливии, я даже сохранил большинство ее комментариев.

Во-первых, поскольку я предпочитаю иметь дело с перечислениями, а не числами, я объявил это перечисление NALU. Для полноты картины я также добавил несколько «экзотических» типов NALU, которые нашел в Интернете:

      public enum NALUnitType : byte
{
    NALU_TYPE_UNKNOWN = 0,
    NALU_TYPE_SLICE = 1,
    NALU_TYPE_DPA = 2,
    NALU_TYPE_DPB = 3,
    NALU_TYPE_DPC = 4,
    NALU_TYPE_IDR = 5,
    NALU_TYPE_SEI = 6,
    NALU_TYPE_SPS = 7,
    NALU_TYPE_PPS = 8,
    NALU_TYPE_AUD = 9,
    NALU_TYPE_EOSEQ = 10,
    NALU_TYPE_EOSTREAM = 11,
    NALU_TYPE_FILL = 12,

    NALU_TYPE_13 = 13,
    NALU_TYPE_14 = 14,
    NALU_TYPE_15 = 15,
    NALU_TYPE_16 = 16,
    NALU_TYPE_17 = 17,
    NALU_TYPE_18 = 18,
    NALU_TYPE_19 = 19,
    NALU_TYPE_20 = 20,
    NALU_TYPE_21 = 21,
    NALU_TYPE_22 = 22,
    NALU_TYPE_23 = 23,

    NALU_TYPE_STAP_A = 24,
    NALU_TYPE_STAP_B = 25,
    NALU_TYPE_MTAP16 = 26,
    NALU_TYPE_MTAP24 = 27,
    NALU_TYPE_FU_A = 28,
    NALU_TYPE_FU_B = 29,
}

Более или менее из соображений удобства я также определил дополнительный словарь для описаний NALU:

      public static Dictionary<NALUnitType, string> GetDescription { get; } =
new Dictionary<NALUnitType, string>()
{
    { NALUnitType.NALU_TYPE_UNKNOWN, "Unspecified (non-VCL)" },
    { NALUnitType.NALU_TYPE_SLICE, "Coded slice of a non-IDR picture (VCL) [P-frame]" },
    { NALUnitType.NALU_TYPE_DPA, "Coded slice data partition A (VCL)" },
    { NALUnitType.NALU_TYPE_DPB, "Coded slice data partition B (VCL)" },
    { NALUnitType.NALU_TYPE_DPC, "Coded slice data partition C (VCL)" },
    { NALUnitType.NALU_TYPE_IDR, "Coded slice of an IDR picture (VCL) [I-frame]" },
    { NALUnitType.NALU_TYPE_SEI, "Supplemental Enhancement Information [SEI] (non-VCL)" },
    { NALUnitType.NALU_TYPE_SPS, "Sequence Parameter Set [SPS] (non-VCL)" },
    { NALUnitType.NALU_TYPE_PPS, "Picture Parameter Set [PPS] (non-VCL)" },
    { NALUnitType.NALU_TYPE_AUD, "Access Unit Delimiter [AUD] (non-VCL)" },
    { NALUnitType.NALU_TYPE_EOSEQ, "End of Sequence (non-VCL)" },
    { NALUnitType.NALU_TYPE_EOSTREAM, "End of Stream (non-VCL)" },
    { NALUnitType.NALU_TYPE_FILL, "Filler data (non-VCL)" },
    { NALUnitType.NALU_TYPE_13, "Sequence Parameter Set Extension (non-VCL)" },
    { NALUnitType.NALU_TYPE_14, "Prefix NAL Unit (non-VCL)" },
    { NALUnitType.NALU_TYPE_15, "Subset Sequence Parameter Set (non-VCL)" },
    { NALUnitType.NALU_TYPE_16, "Reserved (non-VCL)" },
    { NALUnitType.NALU_TYPE_17, "Reserved (non-VCL)" },
    { NALUnitType.NALU_TYPE_18, "Reserved (non-VCL)" },
    { NALUnitType.NALU_TYPE_19, "Coded slice of an auxiliary coded picture without partitioning (non-VCL)" },
    { NALUnitType.NALU_TYPE_20, "Coded Slice Extension (non-VCL)" },
    { NALUnitType.NALU_TYPE_21, "Coded Slice Extension for Depth View Components (non-VCL)" },
    { NALUnitType.NALU_TYPE_22, "Reserved (non-VCL)" },
    { NALUnitType.NALU_TYPE_23, "Reserved (non-VCL)" },
    { NALUnitType.NALU_TYPE_STAP_A, "STAP-A Single-time Aggregation Packet (non-VCL)" },
    { NALUnitType.NALU_TYPE_STAP_B, "STAP-B Single-time Aggregation Packet (non-VCL)" },
    { NALUnitType.NALU_TYPE_MTAP16, "MTAP16 Multi-time Aggregation Packet (non-VCL)" },
    { NALUnitType.NALU_TYPE_MTAP24, "MTAP24 Multi-time Aggregation Packet (non-VCL)" },
    { NALUnitType.NALU_TYPE_FU_A, "FU-A Fragmentation Unit (non-VCL)" },
    { NALUnitType.NALU_TYPE_FU_B, "FU-B Fragmentation Unit (non-VCL)" }
};

Вот моя основная процедура декодирования. Я предполагаю, что полученный кадр представляет собой необработанный массив байтов:

          public void Decode(byte[] frame)
    {
        uint frameSize = (uint)frame.Length;
        SendDebugMessage($"Received frame of {frameSize} bytes.");

        // I know how my H.264 data source's NALUs looks like so I know start code index is always 0.
        // if you don't know where it starts, you can use a for loop similar to how I find the 2nd and 3rd start codes
        uint firstStartCodeIndex = 0;
        uint secondStartCodeIndex = 0;
        uint thirdStartCodeIndex = 0;

        // length of NALU start code in bytes.
        // for h.264 the start code is 4 bytes and looks like this: 0 x 00 00 00 01
        const uint naluHeaderLength = 4;

        // check the first 8bits after the NALU start code, mask out bits 0-2, the NALU type ID is in bits 3-7
        uint startNaluIndex = firstStartCodeIndex + naluHeaderLength;
        byte startByte = frame[startNaluIndex];
        int naluTypeId = startByte & 0x1F; // 0001 1111
        NALUnitType naluType = (NALUnitType)naluTypeId;
        SendDebugMessage($"1st Start Code Index: {firstStartCodeIndex}");
        SendDebugMessage($"1st NALU Type: '{NALUnit.GetDescription[naluType]}' ({(int)naluType})");

        // bits 1 and 2 are the NRI
        int nalRefIdc = startByte & 0x60; // 0110 0000
        SendDebugMessage($"1st NRI (NAL Ref Idc): {nalRefIdc}");

        // IF the very first NALU type is an IDR -> handle it like a slice frame (-> re-cast it to type 1 [Slice])
        if (naluType == NALUnitType.NALU_TYPE_IDR)
        {
            naluType = NALUnitType.NALU_TYPE_SLICE;
        }

        // if we haven't already set up our format description with our SPS PPS parameters,
        // we can't process any frames except type 7 that has our parameters
        if (naluType != NALUnitType.NALU_TYPE_SPS && this.FormatDescription == null)
        {
            SendDebugMessage("Video Error: Frame is not an I-Frame and format description is null.");
            return;
        }
        
        // NALU type 7 is the SPS parameter NALU
        if (naluType == NALUnitType.NALU_TYPE_SPS)
        {
            // find where the second PPS 4byte start code begins (0x00 00 00 01)
            // from which we also get the length of the first SPS code
            for (uint i = firstStartCodeIndex + naluHeaderLength; i < firstStartCodeIndex + 40; i++)
            {
                if (frame[i] == 0x00 && frame[i + 1] == 0x00 && frame[i + 2] == 0x00 && frame[i + 3] == 0x01)
                {
                    secondStartCodeIndex = i;
                    this.SpsSize = secondStartCodeIndex;   // includes the header in the size
                    SendDebugMessage($"2nd Start Code Index: {secondStartCodeIndex} -> SPS Size: {this.SpsSize}");
                    break;
                }
            }

            // find what the second NALU type is
            startByte = frame[secondStartCodeIndex + naluHeaderLength];
            naluType = (NALUnitType)(startByte & 0x1F);
            SendDebugMessage($"2nd NALU Type: '{NALUnit.GetDescription[naluType]}' ({(int)naluType})");
            
            // bits 1 and 2 are the NRI
            nalRefIdc = startByte & 0x60; // 0110 0000
            SendDebugMessage($"2nd NRI (NAL Ref Idc): {nalRefIdc}");
        }

        // type 8 is the PPS parameter NALU
        if (naluType == NALUnitType.NALU_TYPE_PPS)
        {
            // find where the NALU after this one starts so we know how long the PPS parameter is
            for (uint i = this.SpsSize + naluHeaderLength; i < this.SpsSize + 30; i++)
            {
                if (frame[i] == 0x00 && frame[i + 1] == 0x00 && frame[i + 2] == 0x00 && frame[i + 3] == 0x01)
                {
                    thirdStartCodeIndex = i;
                    this.PpsSize = thirdStartCodeIndex - this.SpsSize;
                    SendDebugMessage($"3rd Start Code Index: {thirdStartCodeIndex} -> PPS Size: {this.PpsSize}");
                    break;
                }
            }

            // allocate enough data to fit the SPS and PPS parameters into our data objects.
            // VTD doesn't want you to include the start code header (4 bytes long) so we subtract 4 here
            byte[] sps = new byte[this.SpsSize - naluHeaderLength];
            byte[] pps = new byte[this.PpsSize - naluHeaderLength];

            // copy in the actual sps and pps values, again ignoring the 4 byte header
            Array.Copy(frame, naluHeaderLength, sps, 0, sps.Length);
            Array.Copy(frame, this.SpsSize + naluHeaderLength, pps,0, pps.Length);
            
            // create video format description
            List<byte[]> parameterSets = new List<byte[]> { sps, pps };
            this.FormatDescription = CMVideoFormatDescription.FromH264ParameterSets(parameterSets, (int)naluHeaderLength, out CMFormatDescriptionError formatDescriptionError);
            SendDebugMessage($"Creation of CMVideoFormatDescription: {((formatDescriptionError == CMFormatDescriptionError.None)? $"Successful! (Video Codec = {this.FormatDescription.VideoCodecType}, Dimension = {this.FormatDescription.Dimensions.Height} x {this.FormatDescription.Dimensions.Width}px, Type = {this.FormatDescription.MediaType})" : $"Failed ({formatDescriptionError})")}");

            // re-create the decompression session whenever new PPS data was received
            this.DecompressionSession = this.CreateDecompressionSession(this.FormatDescription);

            // now lets handle the IDR frame that (should) come after the parameter sets
            // I say "should" because that's how I expect my H264 stream to work, YMMV
            startByte = frame[thirdStartCodeIndex + naluHeaderLength];
            naluType = (NALUnitType)(startByte & 0x1F);
            SendDebugMessage($"3rd NALU Type: '{NALUnit.GetDescription[naluType]}' ({(int)naluType})");

            // bits 1 and 2 are the NRI
            nalRefIdc = startByte & 0x60; // 0110 0000
            SendDebugMessage($"3rd NRI (NAL Ref Idc): {nalRefIdc}");
        }

        // type 5 is an IDR frame NALU.
        // The SPS and PPS NALUs should always be followed by an IDR (or IFrame) NALU, as far as I know.
        if (naluType == NALUnitType.NALU_TYPE_IDR || naluType == NALUnitType.NALU_TYPE_SLICE)
        {
            // find the offset or where IDR frame NALU begins (after the SPS and PPS NALUs end) 
            uint offset = (naluType == NALUnitType.NALU_TYPE_SLICE)? 0 : this.SpsSize + this.PpsSize;
            uint blockLength = frameSize - offset;
            SendDebugMessage($"Block Length (NALU type '{naluType}'): {blockLength}");

            var blockData = new byte[blockLength];
            Array.Copy(frame, offset, blockData, 0, blockLength);

            // write the size of the block length (IDR picture data) at the beginning of the IDR block.
            // this means we replace the start code header (0 x 00 00 00 01) of the IDR NALU with the block size.
            // AVCC format requires that you do this.

            // This next block is very specific to my application and wasn't in Olivia's example:
            // For my stream is encoded by NVIDEA NVEC I had to deal with additional 3-byte start codes within my IDR/SLICE frame.
            // These start codes must be replaced by 4 byte start codes adding the block length as big endian.
            // ======================================================================================================================================================

            // find all 3 byte start code indices (0x00 00 01) within the block data (including the first 4 bytes of NALU header)
            uint startCodeLength = 3;
            List<uint> foundStartCodeIndices = new List<uint>();
            for (uint i = 0; i < blockData.Length; i++)
            {
                if (blockData[i] == 0x00 && blockData[i + 1] == 0x00 && blockData[i + 2] == 0x01)
                {
                    foundStartCodeIndices.Add(i);
                    byte naluByte = blockData[i + startCodeLength];
                    var tmpNaluType = (NALUnitType)(naluByte & 0x1F);
                    SendDebugMessage($"3-Byte Start Code (0x000001) found at index: {i} (NALU type {(int)tmpNaluType} '{NALUnit.GetDescription[tmpNaluType]}'");
                }
            }

            // determine the byte length of each slice
            uint totalLength = 0;
            List<uint> sliceLengths = new List<uint>();
            for (int i = 0; i < foundStartCodeIndices.Count; i++)
            {
                // for convenience only
                bool isLastValue = (i == foundStartCodeIndices.Count-1);

                // start-index to bit right after the start code
                uint startIndex = foundStartCodeIndices[i] + startCodeLength;
                
                // set end-index to bit right before beginning of next start code or end of frame
                uint endIndex = isLastValue ? (uint) blockData.Length : foundStartCodeIndices[i + 1];
                
                // now determine slice length including NALU header
                uint sliceLength = (endIndex - startIndex) + naluHeaderLength;

                // add length to list
                sliceLengths.Add(sliceLength);

                // sum up total length of all slices (including NALU header)
                totalLength += sliceLength;
            }

            // Arrange slices like this: 
            // [4byte slice1 size][slice1 data][4byte slice2 size][slice2 data]...[4byte slice4 size][slice4 data]
            // Replace 3-Byte Start Code with 4-Byte start code, then replace the 4-Byte start codes with the length of the following data block (big endian).
            // https://stackoverflow.com/questions/65576349/nvidia-nvenc-media-foundation-encoded-h-264-frames-not-decoded-properly-using

            byte[] finalBuffer = new byte[totalLength];
            uint destinationIndex = 0;
            
            // create a buffer for each slice and append it to the final block buffer
            for (int i = 0; i < sliceLengths.Count; i++)
            {
                // create byte vector of size of current slice, add additional bytes for NALU start code length
                byte[] sliceData = new byte[sliceLengths[i]];

                // now copy the data of current slice into the byte vector,
                // start reading data after the 3-byte start code
                // start writing data after NALU start code,
                uint sourceIndex = foundStartCodeIndices[i] + startCodeLength;
                long dataLength = sliceLengths[i] - naluHeaderLength;
                Array.Copy(blockData, sourceIndex, sliceData, naluHeaderLength, dataLength);

                // replace the NALU start code with data length as big endian
                byte[] sliceLengthInBytes = BitConverter.GetBytes(sliceLengths[i] - naluHeaderLength);
                Array.Reverse(sliceLengthInBytes);
                Array.Copy(sliceLengthInBytes, 0, sliceData, 0, naluHeaderLength);

                // add the slice data to final buffer
                Array.Copy(sliceData, 0, finalBuffer, destinationIndex, sliceData.Length);
                destinationIndex += sliceLengths[i];
            }
            
            // ======================================================================================================================================================

            // from here we are back on track with Olivia's code:

            // now create block buffer from final byte[] buffer
            CMBlockBufferFlags flags = CMBlockBufferFlags.AssureMemoryNow | CMBlockBufferFlags.AlwaysCopyData;
            var finalBlockBuffer = CMBlockBuffer.FromMemoryBlock(finalBuffer, 0, flags, out CMBlockBufferError blockBufferError);
            SendDebugMessage($"Creation of Final Block Buffer: {(blockBufferError == CMBlockBufferError.None ? "Successful!" : $"Failed ({blockBufferError})")}");
            if (blockBufferError != CMBlockBufferError.None) return;

            // now create the sample buffer
            nuint[] sampleSizeArray = new nuint[] { totalLength };
            CMSampleBuffer sampleBuffer = CMSampleBuffer.CreateReady(finalBlockBuffer, this.FormatDescription, 1, null, sampleSizeArray, out CMSampleBufferError sampleBufferError);
            SendDebugMessage($"Creation of Final Sample Buffer: {(sampleBufferError == CMSampleBufferError.None ? "Successful!" : $"Failed ({sampleBufferError})")}");
            if (sampleBufferError != CMSampleBufferError.None) return;

            // if sample buffer was successfully created -> pass sample to decoder

            // set sample attachments
            CMSampleBufferAttachmentSettings[] attachments = sampleBuffer.GetSampleAttachments(true);
            var attachmentSetting = attachments[0];
            attachmentSetting.DisplayImmediately = true;

            // enable async decoding
            VTDecodeFrameFlags decodeFrameFlags = VTDecodeFrameFlags.EnableAsynchronousDecompression;

            // add time stamp
            var currentTime = DateTime.Now;
            var currentTimePtr = new IntPtr(currentTime.Ticks);

            // send the sample buffer to a VTDecompressionSession
            var result = DecompressionSession.DecodeFrame(sampleBuffer, decodeFrameFlags, currentTimePtr, out VTDecodeInfoFlags decodeInfoFlags);

            if (result == VTStatus.Ok)
            {
                SendDebugMessage($"Executing DecodeFrame(..): Successful! (Info: {decodeInfoFlags})");
            }
            else
            {
                NSError error = new NSError(CFErrorDomain.OSStatus, (int)result);
                SendDebugMessage($"Executing DecodeFrame(..): Failed ({(VtStatusEx)result} [0x{(int)result:X8}] - {error}) -  Info: {decodeInfoFlags}");
            }
        }
    }

Моя функция для создания сеанса декомпрессии выглядит так:

          private VTDecompressionSession CreateDecompressionSession(CMVideoFormatDescription formatDescription)
    {
        VTDecompressionSession.VTDecompressionOutputCallback callBackRecord = this.DecompressionSessionDecodeFrameCallback;

        VTVideoDecoderSpecification decoderSpecification = new VTVideoDecoderSpecification
        {
            EnableHardwareAcceleratedVideoDecoder = true
        };

        CVPixelBufferAttributes destinationImageBufferAttributes = new CVPixelBufferAttributes();

        try
        {
            var decompressionSession = VTDecompressionSession.Create(callBackRecord, formatDescription, decoderSpecification, destinationImageBufferAttributes);
            SendDebugMessage("Video Decompression Session Creation: Successful!");
            return decompressionSession;
        }
        catch (Exception e)
        {
            SendDebugMessage($"Video Decompression Session Creation: Failed ({e.Message})");
            return null;
        }
    }

Процедура обратного вызова сеанса декомпрессии:

          private void DecompressionSessionDecodeFrameCallback(
        IntPtr sourceFrame,
        VTStatus status,
        VTDecodeInfoFlags infoFlags,
        CVImageBuffer imageBuffer,
        CMTime presentationTimeStamp,
        CMTime presentationDuration)
    {
        
        if (status != VTStatus.Ok)
        {
            NSError error = new NSError(CFErrorDomain.OSStatus, (int)status);
            SendDebugMessage($"Decompression: Failed ({(VtStatusEx)status} [0x{(int)status:X8}] - {error})");
        }
        else
        {
            SendDebugMessage("Decompression: Successful!");

            try
            {
                var image = GetImageFromImageBuffer(imageBuffer);

                // In my application I do not use a display layer but send the decoded image directly by an event:
                
                ImageSource imgSource = ImageSource.FromStream(() => image.AsPNG().AsStream());
                OnImageFrameReady?.Invoke(imgSource);
            }
            catch (Exception e)
            {
                SendDebugMessage(e.ToString());
            }

        }
    }

Я использую эту функцию для преобразования CVImageBuffer в UIImage. Это также относится к одному из сообщений Оливии, упомянутых выше (как преобразовать CVImageBufferRef в UIImage ):

          private UIImage GetImageFromImageBuffer(CVImageBuffer imageBuffer)
    {
        if (!(imageBuffer is CVPixelBuffer pixelBuffer)) return null;
        
        var ciImage = CIImage.FromImageBuffer(pixelBuffer);
        var temporaryContext = new CIContext();

        var rect = CGRect.FromLTRB(0, 0, pixelBuffer.Width, pixelBuffer.Height);
        CGImage cgImage = temporaryContext.CreateCGImage(ciImage, rect);
        if (cgImage == null) return null;
        
        var uiImage = UIImage.FromImage(cgImage);
        cgImage.Dispose();
        return uiImage;
    }

И последнее, но не менее важное: моя крошечная маленькая функция для вывода отладки, не стесняйтесь сутенерствовать по мере необходимости для ваших целей ;-)

          private void SendDebugMessage(string msg)
    {
        Debug.WriteLine($"VideoDecoder (iOS) - {msg}");
    }

Наконец, давайте посмотрим на пространства имен, используемые для кода выше:

      using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using AvcLibrary;
using CoreFoundation;
using CoreGraphics;
using CoreImage;
using CoreMedia;
using CoreVideo;
using Foundation;
using UIKit;
using VideoToolbox;
using Xamarin.Forms;

@Livy, чтобы удалить утечки памяти, прежде чем CMVideoFormatDescriptionCreateFromH264ParameterSets Вы должны добавить следующее:

if (_formatDesc) {
    CFRelease(_formatDesc);
    _formatDesc = NULL;
}

Этот пост очень помог мне с отправкой видео H264 с одного устройства на другое, но переключение между устройствами вызывало функциюreceivedRawVideoFrameработать некорректно из-за некоторых изменений в данных кадра.

Вот моя последняя функция, которая декодирует единицы NAL напрямую из данных, но не полагается на порядок во фрейме данных.

      - (void)receivedRawVideoFrame:(NSData*)frameData {
    NSUInteger frameSize = [frameData length];
    const uint8_t * frame = [frameData bytes];
    
    NSMutableDictionary* nalUnitsStart = [NSMutableDictionary dictionary];
    NSMutableDictionary* nalUnitsEnd = [NSMutableDictionary dictionary];
    
    uint8_t previousNalUnitType = 0;
    for ( NSUInteger offset = 0; offset < frameSize - 4; offset++ ) {
        // Find the start on NAL unit
        if (frame[offset] == 0x00 && frame[offset+1] == 0x00 && frame[offset+2] == 0x00 && frame[offset+3] == 0x01) {
            uint8_t nalType = frame[offset + 4] & 0x1F;
            
            // Record the end of previous NAL unit
            nalUnitsEnd[@(previousNalUnitType)] = @(offset);
            previousNalUnitType = nalType;
            nalUnitsStart[@(nalType)] = @(offset + 4);
        }
    }
    // Record the end of the last NAL unit
    nalUnitsEnd[@(previousNalUnitType)] = @(frameSize);
    
    // Let's check if our data contains SPS && PPS NAL Units
    NSNumber* spsOffset = nalUnitsStart[@(NAL_TYPE_SPS)];
    NSNumber* ppsOffset = nalUnitsStart[@(NAL_TYPE_PPS)];
    if ( spsOffset && ppsOffset ) {
        NSNumber* spsEnd = nalUnitsEnd[@(NAL_TYPE_SPS)];
        NSNumber* ppsEnd = nalUnitsEnd[@(NAL_TYPE_PPS)];
        NSAssert(spsEnd && ppsEnd, @" [DECODE]: Missing the end of NAL unit(s)");
        
        uint8_t *pps = NULL;
        uint8_t *sps = NULL;

        int spsSize = (int)(spsEnd.unsignedIntegerValue - spsOffset.unsignedIntegerValue);
        int ppsSize = (int)(ppsEnd.unsignedIntegerValue - ppsOffset.unsignedIntegerValue);
        
        // allocate enough data to fit the SPS and PPS parameters into our data objects.
        // VTD doesn't want you to include the start code header (4 bytes long) so we add the - 4 here
        sps = malloc(spsSize);
        pps = malloc(ppsSize);

        // copy in the actual sps and pps values, again ignoring the 4 byte header
        memcpy(sps, &frame[spsOffset.unsignedIntegerValue], spsSize);
        memcpy(pps, &frame[ppsOffset.unsignedIntegerValue], ppsSize);

        // now we set our H264 parameters
        uint8_t*  parameterSetPointers[2] = {sps, pps};
        size_t parameterSetSizes[2] = {spsSize, ppsSize};
        
        OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
                                                                              2,
                                                                              (const uint8_t *const*)parameterSetPointers,
                                                                              parameterSetSizes,
                                                                              4,
                                                                              &_formatDesc);
        
        if (sps != NULL) free(sps);
        if (pps != NULL) free(pps);
        
        DebugAssert(status == noErr, @" [DECODE]: Failed to create CMVideoFormatDescription for H264");
        if ( status != noErr ) {
            NSLog(@" [DECODE]: Failed to create CMVideoFormatDescription for H264");
        } else {
            // Good place to re-create our decompression session
            [self destroySession];
        }
    }
    
    // Loop over all NAL units we have while ignoring everything with type < 5
    for ( NSNumber* nalType in nalUnitsStart.allKeys ) {
        if ( nalType.intValue > 5 ) {
            continue;
        }
        
        // Get the header too (0x00000001), that will be replaced with the NAL unit size
        NSNumber* nalStart = nalUnitsStart[nalType];
        NSNumber* nalEnd = nalUnitsEnd[nalType];
        
        size_t blockLength = nalEnd.unsignedIntegerValue - (nalStart.unsignedIntegerValue - sizeof(uint32_t));
        uint8_t *data = malloc(blockLength);
        memcpy(data, &frame[nalStart.unsignedIntegerValue - sizeof(uint32_t)], blockLength);

        // replace the start code header on this NALU with its size.
        // AVCC format requires that you do this.
        // htonl converts the unsigned int from host to network byte order
        uint32_t dataLength32 = htonl(blockLength - 4);
        memcpy(data, &dataLength32, sizeof(uint32_t));

        CMBlockBufferRef blockBuffer;
        OSStatus status = CMBlockBufferCreateWithMemoryBlock(NULL,
                                                             data,
                                                             blockLength,
                                                             kCFAllocatorNull,
                                                             NULL,
                                                             0,
                                                             blockLength,
                                                             0,
                                                             &blockBuffer);
                
        DebugAssert(status == noErr, @" [DECODE]: Failed to create CMBlockBufferRef for %@", nalType);
        if ( status != noErr ) {
            NSLog(@" [DECODE]: Failed to create CMBlockBufferRef for H264 for %@", nalType);
        } else {
            const size_t sampleSize = blockLength;
            
            /* NOTE:
             We are not responsible for releasing sample buffer,
             it will be released by the decompress frame function
             after it has been decoded!
            */
            CMSampleBufferRef sampleBuffer;
            status = CMSampleBufferCreate(kCFAllocatorDefault,
                                          blockBuffer,
                                          true,
                                          NULL,
                                          NULL,
                                          _formatDesc,
                                          1,
                                          0,
                                          NULL,
                                          1,
                                          &sampleSize,
                                          &sampleBuffer);
            
            DebugAssert(status == noErr, @" [DECODE]: Failed to create CMSampleBufferRef for %@", nalType);
            if ( status != noErr ) {
                NSLog(@" [DECODE]: Failed to create CMSampleBufferRef for H264 for %@", nalType);
                if ( sampleBuffer ) {
                    CFRelease(sampleBuffer);
                    sampleBuffer = NULL;
                }
            } else {
                // set some values of the sample buffer's attachments
                CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
                CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
                CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
                [self decompressFrame:sampleBuffer];
            }
        }
        if ( blockBuffer ) {
            CFRelease(blockBuffer);
            blockBuffer = NULL;
        }
        if ( data != NULL ) {
            free(data);
            data = NULL;
        }
    }
}

decompressFrameФункция отвечает за создание нового сеанса декомпрессии, когда это необходимо, на основе последнейCMVideoFormatDescriptionRefданные, которые мы получили из нашего потока.

Другие вопросы по тегам