概念:
的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。然后,您将收到几个P帧的NALU(可能几十个左右),然后是另一组参数(可能与初始参数相同)和一个i帧,更多的P帧等。i帧比P帧。从概念上讲,您可以将i帧视为视频的整个图像,P帧只是对该i帧所做的更改,直到收到下一个i帧为止。
程序:
从您的H.264流中生成单个NALU。 我无法显示此步骤的代码,因为它在很大程度上取决于您所使用的视频源。我制作了此图形以显示我正在使用的图形(以下代码中图形中的“数据”为“框架”),但是您的情况可能并且可能会有所不同。每当我收到属于2种类型之一的frame()时,都会调用我的方法。在该图中,这2种帧类型是2个大紫色框。receivedRawVideoFrame:
uint8_t *frame
使用CMVideoFormatDescriptionCreateFromH264ParameterSets()从您的SPS和PPS NALU中创建一个CMVideoFormatDescriptionRef。您必须先这样做才能显示任何帧。SPS和PPS看起来像是一堆数字,但是VTD知道如何处理它们。所有你需要知道的是,CMVideoFormatDescriptionRef
就是视频数据的描述中,相同的宽度/高度,格式类型(kCMPixelFormat_32BGRA
,kCMVideoCodecType_H264
等),宽高比,色彩空间等你的解码器将不放参数,直到一个新的组到达时(有时参数即使没有改变,也会经常感到不满)。
根据“ AVCC”格式重新包装IDR和非IDR帧NALU。 这意味着删除NALU起始代码,并用一个表示NALU长度的4字节标头替换它们。对于SPS和PPS NALU,您不需要这样做。(请注意,4字节的NALU长度标头是big-endian格式的,因此,如果您有一个UInt32
值,则必须在复制到CMBlockBuffer
using之前对它进行字节交换CFSwapInt32
。我使用htonl
函数调用在代码中进行此操作。)
将IDR和非IDR NALU帧打包到CMBlockBuffer中。请勿使用SPS PPS参数NALUs执行此操作。您需要了解的CMBlockBuffers
是它们是一种将任意数据块包装在核心媒体中的方法。(视频管道中的任何压缩视频数据都包含在其中。)
将CMBlockBuffer打包到CMSampleBuffer中。 您需要知道的CMSampleBuffers
是,它们将我们的CMBlockBuffers
信息与其他信息一起包装起来(此处将使用CMVideoFormatDescription
和CMTime
,如果CMTime
使用的话)。
创建一个VTDecompressionSessionRef并将示例缓冲区馈入VTDecompressionSessionDecodeFrame()。另外,您可以使用AVSampleBufferDisplayLayer
及其enqueueSampleBuffer:
方法,而无需使用VTDecompSession。设置起来比较简单,但是如果出现像VTD这样的错误,则不会抛出错误。
在VTDecompSession回调中,使用生成的CVImageBufferRef显示视频帧。 如果需要将其转换CVImageBuffer
为a 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)",
@"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)",
@"6: Supplemental enhancement information (SEI) (non-VCL)",
@"7: Sequence parameter set (non-VCL)",
@"8: Picture parameter set (non-VCL)",
@"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;
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 (nalu_type != 7 && _formatDesc == NULL)
{
NSLog(@"Video error: Frame is not an I Frame and format description is null");
return;
}
if (nalu_type == 7)
{
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;
break;
}
}
nalu_type = (frame[secondStartCodeIndex + 4] & 0x1F);
NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);
}
if(nalu_type == 8)
{
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;
}
}
sps = malloc(_spsSize - 4);
pps = malloc(_ppsSize - 4);
memcpy (sps, &frame[4], _spsSize-4);
memcpy (pps, &frame[_spsSize+4], _ppsSize-4);
uint8_t* parameterSetPointers[2] = {sps, pps};
size_t parameterSetSizes[2] = {_spsSize-4, _ppsSize-4};
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);
nalu_type = (frame[thirdStartCodeIndex + 4] & 0x1F);
NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);
}
if((status == noErr) && (_decompressionSession == NULL))
{
[self createDecompSession];
}
if(nalu_type == 5)
{
int offset = _spsSize + _ppsSize;
blockLength = frameSize - offset;
data = malloc(blockLength);
data = memcpy(data, &frame[offset], blockLength);
uint32_t dataLength32 = htonl (blockLength - 4);
memcpy (data, &dataLength32, sizeof (uint32_t));
status = CMBlockBufferCreateWithMemoryBlock(NULL, data,
blockLength,
kCFAllocatorNull, NULL,
0,
blockLength,
0, &blockBuffer);
NSLog(@"\t\t BlockBufferCreation: \t %@", (status == kCMBlockBufferNoErr) ? @"successful!" : @"failed...");
}
if (nalu_type == 1)
{
blockLength = frameSize;
data = malloc(blockLength);
data = memcpy(data, &frame[0], blockLength);
uint32_t dataLength32 = htonl (blockLength - 4);
memcpy (data, &dataLength32, sizeof (uint32_t));
status = CMBlockBufferCreateWithMemoryBlock(NULL, data,
blockLength,
kCFAllocatorNull, NULL,
0,
blockLength,
0, &blockBuffer);
NSLog(@"\t\t BlockBufferCreation: \t %@", (status == kCMBlockBufferNoErr) ? @"successful!" : @"failed...");
}
if(status == noErr)
{
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)
{
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
[self render:sampleBuffer];
}
if (NULL != data)
{
free (data);
data = NULL;
}
}
以下方法创建您的VTD会话。收到新参数时,请重新创建它。(可以肯定的是,您不必在每次接收参数时都重新创建它。)
如果要设置目标的属性CVPixelBuffer
,请读取CoreVideo PixelBufferAttributes值并将其放入NSDictionary *destinationImageBufferAttributes
。
-(void) createDecompSession
{
_decompressionSession = NULL;
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = decompressionSessionDecodeFrameCallback;
callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
NSDictionary *destinationImageBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES],
(id)kCVPixelBufferOpenGLESCompatibilityKey,
nil];
OSStatus status = VTDecompressionSessionCreate(NULL, _formatDesc, NULL,
NULL,
&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");
[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);
}
如果您使用AVSampleBufferDisplayLayer
,请确保在viewDidLoad或其他init方法内部初始化这样的图层。
-(void) viewDidLoad
{
videoLayer = [[AVSampleBufferDisplayLayer alloc] init];
videoLayer.frame = self.view.frame;
videoLayer.bounds = self.view.bounds;
videoLayer.videoGravity = AVLayerVideoGravityResizeAspect;
CMTimebaseRef controlTimebase;
CMTimebaseCreateWithMasterClock(CFAllocatorGetDefault(), CMClockGetHostTimeClock(), &controlTimebase);
CMTimebaseSetTime(self.videoLayer.controlTimebase, kCMTimeZero);
CMTimebaseSetRate(self.videoLayer.controlTimebase, 1.0);
[[self.view layer] addSublayer:videoLayer];
}