76

Apple のハードウェア アクセラレーション ビデオ フレームワークを使用して H.264 ビデオ ストリームを解凍する方法を理解するのに、かなり苦労しました。数週間後、私はそれを理解し、広範な例を見つけることができなかったので共有したいと思いました.

私の目標は、WWDC '14 セッション 513で紹介された Video Toolbox の完全で有益な例を提供することです。基本的な H.264 ストリーム (ファイルから読み取ったビデオやオンラインからストリーミングしたビデオなど) と統合する必要があり、特定のケースに応じて微調整する必要があるため、私のコードはコンパイルまたは実行されません。

私は、主題をグーグルで調べているときに学んだことを除いて、ビデオのエンコード/デコードの経験がほとんどないことに言及する必要があります. ビデオ形式、パラメーター構造などの詳細をすべて知っているわけではないので、知っておく必要があると思われるものだけを含めました。

XCode 6.2 を使用しており、iOS 8.1 および 8.2 を実行している iOS デバイスにデプロイしました。

4

6 に答える 6

201

コンセプト:

NALU: NALU は単に、NALU 開始コード ヘッダーを持つさまざまな長さのデータのチャンクで0x00 00 00 01 YYあり、最初の 5 ビットで、YYこれがどのタイプの NALU であるか、したがってヘッダーに続くデータのタイプがわかります。(必要なのは最初の 5 ビットだけなYY & 0x1Fので、関連するビットだけを取得するために使用します。) method にこれらすべての型をリストしますがNSString * const naluTypesStrings[]、すべてが何であるかを知る必要はありません。

パラメータ: H.264 ビデオ データがどのように格納されているかをデコーダが認識できるように、デコーダにはパラメータが必要です。設定する必要があるのは、シーケンス パラメーター セット (SPS)画像パラメーター セット (PPS)の2 つです。それぞれに独自の NALU タイプ番号があります。パラメータが何を意味するかを知る必要はありません。デコーダはそれらをどう処理するかを知っています。

H.264 ストリーム形式: ほとんどの H.264 ストリームでは、PPS および SPS パラメータの初期セットとそれに続く i フレーム (別名 IDR フレームまたはフラッシュ フレーム) NALU を受け取ります。次に、いくつかの P フレーム NALU (おそらく数十程度) を受け取り、次に別のパラメーター セット (初期パラメーターと同じである可能性があります) と i フレーム、さらに P フレームなどを受け取ります。i フレームは、 P フレーム。概念的には、i フレームはビデオのイメージ全体と考えることができます。P フレームは、次の i フレームを受信するまで、その i フレームに加えられた変更にすぎません。

手順:

  1. H.264 ストリームから個々の NALU を生成します。 使用しているビデオ ソースに大きく依存するため、この手順のコードを示すことはできません。私が作業しているものを示すためにこのグラフィックを作成しました (グラフィックの「データ」は、次のコードでは「フレーム」です) が、場合によっては異なる場合があります。私が取り組んでいたこと私のメソッドreceivedRawVideoFrame:は、2 つのタイプのうちの 1 つであるフレーム ( ) を受け取るたびに呼び出されuint8_t *frameます。図では、これら 2 つのフレーム タイプは 2 つの大きな紫色のボックスです。

  2. CMVideoFormatDescriptionCreateFromH264ParameterSets( ) を使用して、SPS および PPS NALU から CMVideoFormatDescriptionRef を作成します。最初にこれを行わないと、フレームを表示できません。SPS と PPS は数字の寄せ集めのように見えるかもしれませんが、VTD はそれらをどう処理するかを知っています。知っておく必要があるCMVideoFormatDescriptionRefのは、幅/高さ、フォーマット タイプ ( など)、アスペクト比、色空間などのビデオ データの説明です。kCMPixelFormat_32BGRAデコーダーkCMVideoCodecType_H264は、新しいセットが到着するまでパラメーターを保持します (場合によってはパラメーター変更されていない場合でも、定期的に再送信されます)。

  3. 「AVCC」形式に従って、IDR および非 IDR フレーム NALU を再パッケージ化します。 これは、NALU 開始コードを削除し、NALU の長さを示す 4 バイトのヘッダーに置き換えることを意味します。SPS および PPS NALU では、これを行う必要はありません。(4バイトのNALU長ヘッダーはビッグエンディアンであるため、値がある場合は、 usingUInt32にコピーする前にバイトスワップする必要があることに注意してください。関数呼び出しを使用してコードでこれを行います。)CMBlockBufferCFSwapInt32htonl

  4. IDR および非 IDR NALU フレームを CMBlockBuffer にパッケージ化します。SPS PPS パラメータ NALU でこれを行わないでください。知っておく必要がCMBlockBuffersあるのは、それらが任意のデータ ブロックをコア メディアにラップする方法であるということだけです。(ビデオ パイプライン内の圧縮されたビデオ データはすべて、これにラップされます。)

  5. CMBlockBuffer を CMSampleBuffer にパッケージ化します。 あなたが知る必要があるのCMSampleBuffersは、彼らが私たちCMBlockBuffersを他の情報で締めくくるということだけです(ここでは、使用されている場合はCMVideoFormatDescriptionandになります)。CMTimeCMTime

  6. VTDecompressionSessionRef を作成し、サンプル バッファを VTDecompressionSessionDecodeFrame( ) にフィードします。または、AVSampleBufferDisplayLayerとそのenqueueSampleBuffer:メソッドを使用でき、VTDecompSession を使用する必要はありません。セットアップは簡単ですが、VTD のように問題が発生してもエラーは発生しません。

  7. VTDecompSession コールバックで、結果の CVImageBufferRef を使用してビデオ フレームを表示します。 に変換する必要がある場合CVImageBufferは、こちらUIImageの StackOverflow の回答を参照してください。

その他の注意事項:

  • 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 セッションを作成します。新しいパラメータを受け取るたびに再作成してください。(パラメータを受け取る たびに再作成する必要はありません。確かに。)

宛先の属性を設定する場合は、CoreVideo PixelBufferAttributes の値CVPixelBufferを読み、それらを に入れます。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];
}
于 2015-04-08T20:44:16.433 に答える
20

フレームワークで 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,
于 2015-04-09T15:42:04.763 に答える
11

この多くの Swift の良い例は、Josh Baker の Avios ライブラリにあります: https://github.com/tidwall/Avios

Avios は現在、ユーザーが NAL 開始コードでチャンク データを処理することを想定していますが、その時点からデータのデコードを処理することに注意してください。

Swift ベースの RTMP ライブラリ HaishinKit (以前の "LF") も一見の価値があります。これには、より堅牢な NALU 解析を含む独自のデコード実装があります: https://github.com/shogo4405/lf.swift

于 2016-04-04T13:49:16.927 に答える
2

CMVideoFormatDescriptionCreateFromH264ParameterSets以下を追加する前に、@Livy を使用してメモリ リークを削除します。

if (_formatDesc) {
    CFRelease(_formatDesc);
    _formatDesc = NULL;
}
于 2017-09-21T08:18:28.367 に答える