无人机H264视频流解码

近期开始学习H264的视频流解析。写此文章,作为记录,也梳理下相应的知识点。


1. 解码前我们先看一下H264的部分码流数据(红色部分用于帧类型识别)

图一

图中红色部分:

SPS帧:00000001 67    SPS帧数据:64001e ace80a03 d9 (仅供参考数据)

PPS帧:00000001 68    PPS帧数据:ee3cb0 (仅供参考数据)

I      帧:00000001 65    I      帧数据:8881 4017ff89 c0159b63 2b53a0a2 59a7cd02 5c82be32 39a6db81 efb24d1a 348af4b0 1323bbe1 33044f7e 8ccb735f f4a439a7 4db023df 1b4e0d7d d8b1a15d 56ecfa1a e02debc0 (这里只是展示部分数据)

I 帧数据为主要视频帧,视频图像保存在 I 帧,需要SPS帧和PPS帧配合才能解码。


开始解码准备工作:

需要引入的头文件:

#import<VideoToolbox/VideoToolbox.h>

需要定义的参数:

uint8_t  *_sps;

NSInteger _spsSize;

uint8_t  *_pps;

NSInteger _ppsSize;

VTDecompressionSessionRef        _deocderSession;

CMVideoFormatDescriptionRef    _decoderFormatDescription;




2. 解码前分离SPS、PPS、I帧数据

- (void) decodeVidoeUdp:(uint8_t *)udpBuf udpLen:(int)udpLen {

NSInteger PPSCommand;

NSInteger IFRCommand;

NSInteger spsCommand;

if (udpBuf[4] == 0x67) {

spsCommand = 0x67;

PPSCommand = 0x68;

IFRCommand = 0x65;

}

const uint8_t ppsStartCode[5] = {0, 0, 0, 1, PPSCommand};

const uint8_t IFrStartCode[5] = {0, 0, 0, 1, IFRCommand};

if (udpLen < 5) {

//printf("udp len is too short %d\n", udpLen);

return;

}

// 如果是sps帧, 需要将sps pps I帧分离

if (udpBuf[4] == spsCommand)

{

uint8_t *start = udpBuf, *end = udpBuf;

int count = 0;

while (count < udpLen) {

end++;

count++;

if (end[3] == 0x01) {

//如果找到pps帧,前面的就是sps数据。下面传入的是sps数据

if (memcmp(end, ppsStartCode, 5) == 0)

{

[self decodeFile:start frameLen:(int)(end - start)];

start = end;

}

//IDR帧

                if (memcmp(end, IFrStartCode, 5) == 0) {

                        [self decodeFile:start frameLen:(int)(end - start)];

                        [self decodeFile:end frameLen:(int)(udpBuf + udpLen - end)];

                        break;

              } 

        }

}

}else{

           [self decodeFile:udpBuf frameLen:udpLen];

      }

}

3. 准备解码数据

//初始化H264解码器

- (BOOL) initH264DecoderAndisValid:(BOOL)isVaild {

if(_deocderSession && isVaild) {

//printf("\nalready initH264Decoder\n");

return YES;

}

//printf("\ninitH264Decoder\n");

const uint8_t* const parameterSetPointers[2] = { _sps, _pps };

const size_t parameterSetSizes[2] = { _spsSize, _ppsSize };

OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,2,//param countparameterSetPointers,parameterSetSizes,4, //nal start code size&_decoderFormatDescription);

if(status == noErr) {

CFDictionaryRef attrs = NULL;

const void *keys[] = { kCVPixelBufferPixelFormatTypeKey };

//      kCVPixelFormatType_420YpCbCr8Planar is YUV420

//      kCVPixelFormatType_420YpCbCr8BiPlanarFullRange is NV12

uint32_t v = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;

const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &v) };

attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);

VTDecompressionOutputCallbackRecord callBackRecord

callBackRecord.decompressionOutputCallback = didDecompress

callBackRecord.decompressionOutputRefCon = NULL;

status = VTDecompressionSessionCreate(kCFAllocatorDefault,_decoderFormatDescription,NULL, attrs,&callBackRecord,&_deocderSession);

//NSLog(@"status:%d",status);

CFRelease(attrs);

attrs = NULL;

} else {

//NSLog(@"IOS8VT: reset decoder session failed status=%d", (int)status);

}

return YES;

}

//准备解码需要的数据

- (void) decodeFile:(uint8_t *)frameBuf frameLen:(int)frameLen {

uint8_t udpBuf_test[frameLen];

memcpy(udpBuf_test, frameBuf, frameLen);

const uint8_t KStartCode[4] = {0, 0, 0, 1};

__block CVPixelBufferRef pixelBuffer = NULL;

uint32_t nalSize = (uint32_t)(frameLen - 4);

uint8_t *pNalSize = (uint8_t*)(&nalSize);

//判断是否为帧头

if(memcmp(udpBuf_test, KStartCode, 4) != 0) {

//printf("\nframe buf is not a video frame\n");

return;

}

//将4个字节的帧头填充为帧长度,不包括帧头4个字节

udpBuf_test[0] = *(pNalSize + 3);

udpBuf_test[1] = *(pNalSize + 2);

udpBuf_test[2] = *(pNalSize + 1);

udpBuf_test[3] = *(pNalSize);

int nalType = udpBuf_test[4] & 0x1F;

switch (nalType) {

case 0x05:

//通过sps和pps初始化参数,并开始解码I帧

if([self initH264DecoderAndisValid:YES]) {

pixelBuffer = [self decode:udpBuf_test frameLen:frameLen];

}

break;

case 0x07:

if (_sps) {

break;

}

_spsSize = nalSize;

_sps = malloc(_spsSize);

memcpy(_sps, udpBuf_test + 4, _spsSize);

break;

case 0x08:

if (_pps) {

break;

}

_ppsSize = nalSize;

_pps = malloc(_ppsSize);

memcpy(_pps, udpBuf_test + 4, _ppsSize);

break;

case 0x01:

{

pixelBuffer = [self decode:udpBuf_test frameLen:frameLen];//这里得到视频中的图像数据

}

break;

default:

break;

}

//编码后返回的像素缓存入列到图像显示队列

if(pixelBuffer) {

dispatch_sync(dispatch_get_main_queue(), ^{

self.GLLayer.pixelBuffer = pixelBuffer;

});

CVPixelBufferRelease(pixelBuffer);

pixelBuffer = NULL;

}

}

4. 解码器解码(使用系统自带解码器解码)

- (CVPixelBufferRef) decode:(uint8_t *)frameBuf frameLen:(int)frameLen {

//NSLog(@"frameLen:%d",frameLen);

CVPixelBufferRef outputPixelBuffer =  NULL;

CMBlockBufferRef blockBuffer = NULL;

//创建CMBlockBuffer

OSStatus status  = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,(void*)frameBuf,frameLen,kCFAllocatorNull,NULL,0,frameLen,0,&blockBuffer);

if(status == kCMBlockBufferNoErr) {

CMSampleBufferRef sampleBuffer = NULL;

const size_t sampleSizeArray[] = {frameLen};

//创建CMSampleBuffer

status = CMSampleBufferCreateReady(kCFAllocatorDefault,blockBuffer,_decoderFormatDescription ,1, 0, NULL, 1, sampleSizeArray,&sampleBuffer);

if (status == kCMBlockBufferNoErr && sampleBuffer) {

VTDecodeFrameFlags flags = 0;

VTDecodeInfoFlags flagOut = 0;

if (_deocderSession == NULL || sampleBuffer == NULL) {

//printf("null pointer %p %p", _deocderSession, sampleBuffer);

return NULL;

}

//开始解码,解码后存放在outputPixelBuffer

OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(_deocderSession,sampleBuffer,flags,&outputPixelBuffer,&flagOut);

if(decodeStatus == kVTInvalidSessionErr) {

[self initH264DecoderAndisValid:NO];

//NSLog(@"IOS8VT: Invalid session, reset decoder session");

} else if(decodeStatus == kVTVideoDecoderBadDataErr) {

//NSLog(@"IOS8VT: decode failed status=%d(Bad data)", (int)decodeStatus);

} else if(decodeStatus != noErr) {

[self clearH264Deocder];

[self initH264DecoderAndisValid:NO];

//NSLog(@"IOS8VT: decode failed status=%d", (int)decodeStatus);

return nil;

}

CFRelease(sampleBuffer);

sampleBuffer = NULL;

}

CFRelease(blockBuffer);

blockBuffer = NULL;

}

return outputPixelBuffer;

}

5. 释放解码器

- (void) clearH264Deocder {

[self.lock lock];

if(_deocderSession) {

VTDecompressionSessionInvalidate(_deocderSession);

if (_deocderSession == nil) {

[self.lock unlock];

return;

}

CFRelease(_deocderSession);

_deocderSession = NULL;

}

if(_decoderFormatDescription) {

CFRelease(_decoderFormatDescription);

_decoderFormatDescription = NULL;

}

free(_sps);

free(_pps);

_sps = _pps = NULL;

_spsSize = _ppsSize = 0;

[self.lock unlock];

}


解码流程大致就是这5个步骤,刚接触时心里是懵逼的,接触久了慢慢就熟悉了。本人也是新手一枚,如果有错误的地方还请指正。不胜感激。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容