iOS平台使用ffmpeg解码h264视频流

对于视频文件和rtsp之类的主流视频传输协议,ffmpeg提供avformat_open_input接口,直接将文件路径或URL传入即可打开。读取视频数据、解码器初始参数设置等,都可以通过调用API来完成。

但是对于h264流,没有任何封装格式,也就无法使用libavformat。所以许多工作需要自己手工完成。

这里的h264流指AnnexB,也就是每个nal unit以起始码00 00 00 01 或 00 00 01开始的格式。关于h264码流格式,可以参考这篇文章

首先是手动设定AVCodec和AVCodecContext:

AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);

AVCodecContext *codecCtx = avcodec_alloc_context3(codec);

avcodec_open2(codecCtx, codec, nil);

在AVCodecContext中会保存很多解码需要的信息,比如视频的长和宽,但是现在我们还不知道。

这些信息存储在h264流的SPS(序列参数集)和PPS(图像参数集)中。

对于每个nal unit,起始码后面第一个字节的后5位,代表这个nal unit的类型。7代表SPS,8代表PPS。一般在SPS和PPS后面的是IDR帧,无需前面帧的信息就可以解码,用5来代表。

检测nal unit类型的方法:

- (int)typeOfNalu:(NSData *)data

{

char first = *(char *)[data bytes];

return first & 0x1f;

}

264解码器在解码SPS和PPS的时候会提取出视频的信息,保存在AVCodecContext中。但是只把SPS和PPS传递进去是不行的,需要把后面的IDR帧一起传给解码器,才能够正确解码。

可以写一个简单的检测,如果接收到SPS,就把后面的PPS和IDR帧都接收过来,然后一起传给解码器。

初始化一个AVPacket和AVFrame,然后把SPS、PPS、IDR帧连在一起的数据块传给AVPacket的data指针,再进行解码。

我们假设包含SPS、PPS、IDR帧的数据块保存在videoData中,长度为len。

char *videoData;

int len;

AVFrame *frame = av_frame_alloc();

AVPacket packet;

av_new_packet(&packet, len);

memcpy(packet.data, videoData, len);

int ret, got_picture;

ret = avcodec_decode_video2(codecCtx, frame, &got_picture, &packet);

if (ret > 0){

if(got_picture){

//进行下一步的处理

}

}

这样就可以顺利解码h264流了,解码出的数据保存在AVFrame中。

我写了一个Objective-C类用来执行接收视频流、解码、播放一系列步骤。

视频数据的接收采用socket直接接收,使用了开源项目CocoaAsyncSocket

就像项目名称中指明的,这是一个异步socket类。读写socket的动作会在一个单独的dispatch queue中执行,执行完毕后对应的delegate方法会自动调用,在其中进行进一步的处理。

读取h264流使用了GCDAsyncSocket 的

- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout

tag:(long)tag

方法,也就是当读到和data中的字节一致的内容时就停止读取,并调用delegate方法。传入的data参数是 00 00 01 三个字节。这样每次读入的nalu开始是没有start code的,而最后面有下一个nalu的start code。因此每次读取之后都会把末尾的start code 暂存,然后把主体接到上一次暂存的start code之后,构成完整的nalu。

videoPlayer.h:

//videoPlayer.h

#import

@interface videoPlayer : NSObject

- (void)startup;

- (void)shutdown;

@end

videoPlayer.m:

//videoPlayer.m

#import "videoPlayer.h"

#import "GCDAsyncSocket.h"

#import "libavcodec/avcodec.h"

#import "libswscale/swscale.h"

const int Header = 101;

const int Data = 102;

@interface videoPlayer ()

{

GCDAsyncSocket *socket;

NSData *startcodeData;

NSData *lastStartCode;

//ffmpeg

AVFrame *frame;

AVPicture picture;

AVCodec *codec;

AVCodecContext *codecCtx;

AVPacket packet;

struct SwsContext *img_convert_ctx;

NSMutableData *keyFrame;

int outputWidth;

int outputHeight;

}

@end

@implementation videoPlayer

- (id)init

{

self = [super init];

if (self) {

avcodec_register_all();

frame = av_frame_alloc();

codec = avcodec_find_decoder(AV_CODEC_ID_H264);

codecCtx = avcodec_alloc_context3(codec);

int ret = avcodec_open2(codecCtx, codec, nil);

if (ret != 0){

NSLog(@"open codec failed :%d",ret);

}

socket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

keyFrame = [[NSMutableData alloc]init];

outputWidth = 320;

outputHeight = 240;

unsigned char startcode[] = {0,0,1};

startcodeData = [NSData dataWithBytes:startcode length:3];

}

return self;

}

- (void)startup

{

NSError *error = nil;

[socket connectToHost:@"192.168.1.100"

onPort:9982

withTimeout:-1

error:&error];

NSLog(@"%@",error);

if (!error) {

[socket readDataToData:startcodeData withTimeout:-1 tag:0];

}

}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag

{

[socket readDataToData:startcodeData withTimeout:-1 tag:Data];

if(tag == Data){

int type = [self typeOfNalu:data];

if (type == 7 || type == 8 || type == 6 || type == 5) { //SPS PPS SEI IDR

[keyFrame appendData:lastStartCode];

[keyFrame appendBytes:[data bytes] length:[data length] - [self startCodeLenth:data]];

}

if (type == 5 || type == 1) {//IDR P frame

if (type == 5) {

int nalLen = (int)[keyFrame length];

av_new_packet(&packet, nalLen);

memcpy(packet.data, [keyFrame bytes], nalLen);

keyFrame = [[NSMutableData alloc] init];//reset keyframe

}else{

NSMutableData *nalu = [[NSMutableData alloc]initWithData:lastStartCode];

[nalu appendBytes:[data bytes] length:[data length] - [self startCodeLenth:data]];

int nalLen = (int)[nalu length];

av_new_packet(&packet, nalLen);

memcpy(packet.data, [nalu bytes], nalLen);

}

int ret, got_picture;

//NSLog(@"decode start");

ret = avcodec_decode_video2(codecCtx, frame, &got_picture, &packet);

//NSLog(@"decode finish");

if (ret < 0) {

NSLog(@"decode error");

return;

}

if (!got_picture) {

NSLog(@"didn't get picture");

return;

}

static int sws_flags =  SWS_FAST_BILINEAR;

//outputWidth = codecCtx->width;

//outputHeight = codecCtx->height;

if (!img_convert_ctx)

img_convert_ctx = sws_getContext(codecCtx->width,

codecCtx->height,

codecCtx->pix_fmt,

outputWidth,

outputHeight,

PIX_FMT_YUV420P,

sws_flags, NULL, NULL, NULL);

avpicture_alloc(&picture, PIX_FMT_YUV420P, outputWidth, outputHeight);

ret = sws_scale(img_convert_ctx, (const uint8_t* const*)frame->data, frame->linesize, 0, frame->height, picture.data, picture.linesize);

[self display];

//NSLog(@"show frame finish");

avpicture_free(&picture);

av_free_packet(&packet);

}

}

[self saveStartCode:data];

}

- (void)display

{

}

- (int)typeOfNalu:(NSData *)data

{

char first = *(char *)[data bytes];

return first & 0x1f;

}

- (int)startCodeLenth:(NSData *)data

{

char temp = *((char *)[data bytes] + [data length] - 4);

return temp == 0x00 ? 4 : 3;

}

- (void)saveStartCode:(NSData *)data

{

int startCodeLen = [self startCodeLenth:data];

NSRange startCodeRange = {[data length] - startCodeLen, startCodeLen};

lastStartCode = [data subdataWithRange:startCodeRange];

}

- (void)shutdown

{

if(socket)[socket disconnect];

}

- (void)dealloc

{

// Free scaler

if(img_convert_ctx)sws_freeContext(img_convert_ctx);

// Free the YUV frame

if(frame)av_frame_free(&frame);

// Close the codec

if (codecCtx) avcodec_close(codecCtx);

}

@end

在项目中播放解码出来的YUV视频使用了OPENGL,这里播放的部分就略去了。

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

推荐阅读更多精彩内容

  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,648评论 0 3
  • 在上一篇笔记中我们已经完成了使用SDL播放声音和视频,声音播放没有什么问题,而视频播放太快,很明显视频没有同步。在...
    762683ff5d3d阅读 1,299评论 0 1
  • 图片转视频 为什么想将图片转视频? 是这样的,我打造的任性动图软件,在编辑制作GIF动图方面,已经基本完善。现在想...
    古典小说阅读 1,540评论 1 0
  • 汉水缠绵育灵秀, 峻岭雄峰藏人杰; 山水云阕弄股掌, 壮志凌云步青天。
    十二明月夜阅读 228评论 0 3
  • 三个月前我开始了培养自己每天记录的习惯,如果回想一开始的目的,我希望自己每天都能学点什么,然后输出出来,通过这种输...
    Gzw丶南山阅读 152评论 0 0