objective c - Como usar o VideoToolbox para descompactar o fluxo de vídeo H.264



objective-c ios8 (4)

Conceitos:

NALUs: NALUs são simplesmente um grupo de dados de comprimento variável que possui um cabeçalho de código inicial NALU 0x00 00 00 01 YY onde os 5 primeiros bits de YY informam que tipo de NALU é esse e, portanto, que tipo de dados segue o cabeçalho. (Como você só precisa dos 5 primeiros bits, eu uso YY & 0x1F para obter apenas os bits relevantes.) NSString * const naluTypesStrings[] que todos esses tipos estão no método NSString * const naluTypesStrings[] , mas você não precisa saber o que eles todos são.

Parâmetros: Seu decodificador precisa de parâmetros para saber como os dados de vídeo H.264 são armazenados. Os 2 que você precisa definir são SPS (Sequence Parameter Set) e Picture Parameter Set (PPS) e cada um tem seu próprio número de tipo NALU. Você não precisa saber o que significam os parâmetros, o decodificador sabe o que fazer com eles.

Formato de fluxo H.264: Na maioria dos fluxos H.264, você receberá um conjunto inicial de parâmetros PPS e SPS, seguidos por um NALU de quadro i (também conhecido como quadro IDR ou quadro nivelado). Em seguida, você receberá várias NALUs do quadro P (talvez algumas dezenas), depois outro conjunto de parâmetros (que podem ser os mesmos que os parâmetros iniciais) e um quadro i, mais quadros P etc. Os quadros i são muito maiores que P frames. Conceitualmente, você pode pensar no quadro i como uma imagem inteira do vídeo, e os quadros P são apenas as alterações feitas no quadro i, até você receber o próximo quadro i.

Procedimento:

  1. Gere NALUs individuais a partir do seu fluxo H.264. Não consigo mostrar o código desta etapa, pois depende muito de qual fonte de vídeo você está usando. Fiz este gráfico para mostrar com o que estava trabalhando ("dados" no gráfico são "quadro" no meu código a seguir), mas seu caso pode e provavelmente será diferente. Meu método receivedRawVideoFrame: é chamado toda vez que recebo um quadro ( uint8_t *frame ) que era um dos dois tipos. No diagrama, esses dois tipos de quadros são as duas grandes caixas roxas.

  2. Crie um CMVideoFormatDescriptionRef a partir das NALUs do SPS e PPS com CMVideoFormatDescriptionCreateFromH264ParameterSets () . Você não pode exibir nenhum quadro sem fazer isso primeiro. O SPS e o PPS podem parecer uma confusão de números, mas o VTD sabe o que fazer com eles. Tudo o que você precisa saber é que CMVideoFormatDescriptionRef é uma descrição dos dados do vídeo., Como largura / altura, tipo de formato ( kCMPixelFormat_32BGRA , kCMVideoCodecType_H264 etc.), proporção, espaço de cores etc. Seu decodificador manterá os parâmetros até que um novo conjunto chegue (às vezes os parâmetros são reenviados regularmente, mesmo quando não foram alterados).

  3. Reembale as NALUs de quadro IDR e não IDR, de acordo com o formato "AVCC". Isso significa remover os códigos de início da NALU e substituí-los por um cabeçalho de 4 bytes que indica o comprimento da NALU. Você não precisa fazer isso para as NALUs do SPS e do PPS. (Observe que o cabeçalho NALU de 4 bytes está em big-endian, portanto, se você tiver um valor UInt32 ele deverá ser trocado por byte antes de copiar para o CMBlockBuffer usando CFSwapInt32 . Faço isso no meu código com a chamada de função htonl .)

  4. Empacote os quadros NALU IDR e não IDR no CMBlockBuffer. Não faça isso com o parâmetro NPSS do SPS PPS. Tudo o que você precisa saber sobre o CMBlockBuffers é que eles são um método para CMBlockBuffers blocos de dados arbitrários na mídia principal. (Quaisquer dados de vídeo compactados em um pipeline de vídeo estão incluídos nisso.)

  5. Empacote o CMBlockBuffer no CMSampleBuffer. Tudo o que você precisa saber sobre o CMSampleBuffers é que eles CMBlockBuffers nossos CMBlockBuffers com outras informações (aqui seriam CMVideoFormatDescription e CMTime , se CMTime for usado).

  6. Crie um VTDecompressionSessionRef e alimente os buffers de exemplo em VTDecompressionSessionDecodeFrame (). Como alternativa, você pode usar AVSampleBufferDisplayLayer e seu método enqueueSampleBuffer: e não precisará usar VTDecompSession. É mais simples de configurar, mas não gera erros se algo der errado como o VTD.

  7. No retorno de chamada VTDecompSession, use o CVImageBufferRef resultante para exibir o quadro de vídeo. Se você precisar converter seu CVImageBuffer em UIImage , consulte minha resposta StackOverflow here .

Outras notas:

  • Os fluxos H.264 podem variar muito. Pelo que aprendi, os cabeçalhos de código inicial do NALU às vezes são 3 bytes ( 0x00 00 01 ) e às vezes 4 ( 0x00 00 00 01 ). Meu código funciona para 4 bytes; você precisará alterar algumas coisas se estiver trabalhando com o 3.

  • Se você quiser saber mais sobre as NALUs , achei esta resposta muito útil. No meu caso, descobri que não precisava ignorar os bytes de "prevenção de emulação", conforme descrito, então pulei pessoalmente essa etapa, mas talvez você precise saber sobre isso.

  • Se sua VTDecompressionSession gerar um número de erro (como -12909), procure o código de erro no seu projeto XCode. Encontre a estrutura VideoToolbox no navegador do projeto, abra-a e encontre o cabeçalho VTErrors.h. Se você não encontrar, também incluímos todos os códigos de erro abaixo em outra resposta.

Exemplo de código:

Então, vamos começar declarando algumas variáveis ​​globais e incluindo a estrutura 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;

A matriz a seguir é usada apenas para que você possa imprimir o tipo de quadro NALU que está recebendo. Se você sabe o que todos esses tipos significam, bom para você, você sabe mais sobre o H.264 do que eu :) Meu código lida apenas com os tipos 1, 5, 7 e 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)",
};

Agora é aqui que toda a mágica acontece.

-(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;
    }
}

O método a seguir cria sua sessão VTD. Recrie-o sempre que receber novos parâmetros. (Você não precisa recriá-lo toda vez que receber parâmetros, com certeza.)

Se você deseja definir atributos para o CVPixelBuffer destino, leia os valores CoreVideo PixelBufferAttributes e coloque-os no 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);
}

Agora, esse método é chamado toda vez que o VTD termina de descompactar qualquer quadro enviado a ele. Esse método é chamado mesmo se houver um erro ou se o quadro for descartado.

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];
    }
}

É aqui que realmente enviamos o sampleBuffer para o VTD a ser decodificado.

- (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];
}

Se você estiver usando AVSampleBufferDisplayLayer , certifique-se de AVSampleBufferDisplayLayer a camada assim, em viewDidLoad ou dentro de algum outro método 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];
}

https://ffff65535.com

Eu tive muitos problemas para descobrir como usar a estrutura de vídeo acelerada por hardware da Apple para descomprimir um fluxo de vídeo H.264. Depois de algumas semanas, eu descobri e queria compartilhar um exemplo extenso, pois não conseguia encontrar um.

Meu objetivo é dar um exemplo completo e instrutivo da Video Toolbox introduzida na sessão 513 da WWDC '14 . Meu código não será compilado ou executado, pois precisa ser integrado a um fluxo H.264 elementar (como um vídeo lido de um arquivo ou transmitido online, etc.) e precisa ser aprimorado, dependendo do caso específico.

Devo mencionar que tenho muito pouca experiência com decodificação / vídeo, exceto o que aprendi enquanto pesquisava no assunto. Como não conheço todos os detalhes sobre formatos de vídeo, estrutura de parâmetros etc., incluí apenas o que acho que você precisa saber.

Estou usando o XCode 6.2 e implantado em dispositivos iOS que executam o iOS 8.1 e 8.2.


@Livy para remover vazamentos de memória antes de CMVideoFormatDescriptionCreateFromH264ParameterSets você deve adicionar o seguinte:

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

Se você não conseguir encontrar os códigos de erro do VTD na estrutura, decidi incluí-los aqui. (Novamente, todos esses erros e mais podem ser encontrados dentro do próprio VideoToolbox.framework no navegador do projeto, no arquivo VTErrors.h .)

Você receberá um desses códigos de erro no retorno de chamada do quadro de decodificação VTD ou quando criar sua sessão VTD se tiver feito algo incorretamente.

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,

Um bom exemplo rápido disso pode ser encontrado na biblioteca Avios de Josh Baker: https://github.com/tidwall/Avios

Observe que o Avios atualmente espera que o usuário manipule os dados de chunking nos códigos de início do NAL, mas manipula a decodificação dos dados desse ponto em diante.

Também vale uma olhada na biblioteca RTMP baseada em Swift HaishinKit (anteriormente "LF"), que possui sua própria implementação de decodificação, incluindo uma análise NALU mais robusta: https://github.com/shogo4405/lf.swift





video-toolbox