使用AVAssetWriter与原始NAL单元

9
我注意到在iOS的AVAssetWriterInput文档中,你可以传递nil来指定输入数据不需要重新编码。

用于编码附加到输出的媒体的设置。传递nil以指定追加的样本不需要重新编码。

我想利用这个功能来传递一串原始的H.264 NALs流,但是我遇到了麻烦,无法将我的原始字节流调整为CMSampleBuffer,以便我可以将其传递到AVAssetWriterInput的appendSampleBuffer方法中。我的NALs流仅包含SPS / PPS / IDR / P NALs(1,5,7,8)。我找不到有关如何在AVAssetWriter中使用预编码H264数据的文档或确定性答案。生成的视频文件无法播放。我应该如何正确地将NAL单元打包成CMSampleBuffers?我需要使用起始码前缀吗?长度前缀?我需要确保每个CMSampleBuffer只放置一个NAL吗?我的最终目标是创建一个带有H264 / AAC的MP4或MOV容器。下面是我正在尝试的代码:
-(void)addH264NAL:(NSData *)nal
{
    dispatch_async(recordingQueue, ^{
        //Adapting the raw NAL into a CMSampleBuffer
        CMSampleBufferRef sampleBuffer = NULL;
        CMBlockBufferRef blockBuffer = NULL;
        CMFormatDescriptionRef formatDescription = NULL;
        CMItemCount numberOfSampleTimeEntries = 1;
        CMItemCount numberOfSamples = 1;


        CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCMVideoCodecType_H264, 480, 360, nil, &formatDescription);
        OSStatus result = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, NULL, [nal length], kCFAllocatorDefault, NULL, 0, [nal length], kCMBlockBufferAssureMemoryNowFlag, &blockBuffer);
        if(result != noErr)
        {
            NSLog(@"Error creating CMBlockBuffer");
            return;
        }
        result = CMBlockBufferReplaceDataBytes([nal bytes], blockBuffer, 0, [nal length]);
        if(result != noErr)
        {
            NSLog(@"Error filling CMBlockBuffer");
            return;
        }
        const size_t sampleSizes = [nal length];
        CMSampleTimingInfo timing = { 0 };
        result = CMSampleBufferCreate(kCFAllocatorDefault, blockBuffer, YES, NULL, NULL, formatDescription, numberOfSamples, numberOfSampleTimeEntries, &timing, 1, &sampleSizes, &sampleBuffer);

        if(result != noErr)
        {
            NSLog(@"Error creating CMSampleBuffer");
        }
        [self writeSampleBuffer:sampleBuffer ofType:AVMediaTypeVideo];
    });
}

请注意,我正在writeSampleBuffer方法中对样本缓冲区调用CMSampleBufferSetOutputPresentationTimeStamp,并使用我认为有效的时间戳尝试追加它。任何帮助都将不胜感激。

我的问题至少部分原因在于我处理CMSampleTimingInfo的方式。我提到我使用setOutputPresentationTimeStamp来填充实际时间戳。现在我意识到我还需要填充CMSampleTimingInfo的其他字段。我将decodeTimeStamp设置为kCMTimeInvalid,将duration设置为CMTimeMake(1, 30)。现在我得到了一个可寻址的视频容器,具有适当的总时间,但是没有视频(在VLC中测试)。 - bsirang
1个回答

3
我已成功在VLC中实现了视频播放,但QuickTime却无法。我使用类似于上面发布的代码将H.264 NALs传入CMSampleBuffers中。
我遇到了两个主要问题:
  1. 我没有正确设置CMSampleTimingInfo(如上面我的评论所述)。
  2. 我没有正确打包原始NAL数据(如果有的话,我不确定这个在哪里记录)。
为了解决#1,我设置了 timing.duration = CMTimeMake(1, fps); ,其中fps是预期帧率。然后我设置 timing.decodeTimeStamp = kCMTimeInvalid; ,表示样本将按解码顺序给出。最后,我通过计算绝对时间来设置 timing.presentationTimeStamp,我还使用 startSessionAtSourceTime
为了解决#2,通过试错,我发现以以下形式提供我的NAL单元可行:
[7 8 5] [1] [1] [1]..... [7 8 5] [1] [1] [1]..... (repeating)

每个NAL单元前缀都是32位的起始码,等于0x00000001
可能由于同样的原因,它无法在QuickTime中播放,我仍然无法将结果为.mov文件移动到照片相册(ALAssetLibrary方法videoAtPathIsCompatibleWithSavedPhotosAlbum失败,指出“无法播放电影”)。希望有了解情况的人能发表评论。谢谢!

因为这仅适用于VLC,所以有可能我需要改变将NALs打包到缓冲区中的方式才能使QuickTime工作。有人有任何想法吗? - bsirang
我对生成的mov文件进行了十六进制转储,看起来文件顶部存在一个mdat原子以及底部存在一些avc1原子,但没有avcC原子。在mdat原子内部,似乎是我在这个答案中描述的NAL流(以0x00000001前缀分隔)。我猜想QuickTime拒绝播放该文件,是因为mdat下面的数据采用Annex-B格式,并且不存在avcC原子。我需要找出如何正确将数据供给AVAssetWriterInput的方法。 - bsirang
我成功让所有东西都能够工作了。我正在将NAL长度附加到每个NAL之前(而不是0x00000001),并且我还成功地将avcC数据传递到mov容器中。在这里可以查看我的问题和答案:https://dev59.com/unDXa4cB1Zd3GeqP9zmf - bsirang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接