iOS平臺(tái)使用ffmpeg解碼h264視頻流

對(duì)于視頻文件和rtsp之類的主流視頻傳輸協(xié)議椎扬,ffmpeg提供avformat_open_input接口,直接將文件路徑或URL傳入即可打開。讀取視頻數(shù)據(jù)、解碼器初始參數(shù)設(shè)置等皂岔,都可以通過調(diào)用API來完成蹋笼。

但是對(duì)于h264流展姐,沒有任何封裝格式躁垛,也就無法使用libavformat。所以許多工作需要自己手工完成圾笨。

這里的h264流指AnnexB教馆,也就是每個(gè)nal unit以起始碼00 00 00 01 或 00 00 01開始的格式。關(guān)于h264碼流格式擂达,可以參考這篇文章土铺。

首先是手動(dòng)設(shè)定AVCodec和AVCodecContext:

AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);

AVCodecContext *codecCtx = avcodec_alloc_context3(codec);

avcodec_open2(codecCtx, codec, nil);

在AVCodecContext中會(huì)保存很多解碼需要的信息,比如視頻的長和寬板鬓,但是現(xiàn)在我們還不知道悲敷。

這些信息存儲(chǔ)在h264流的SPS(序列參數(shù)集)和PPS(圖像參數(shù)集)中。

對(duì)于每個(gè)nal unit俭令,起始碼后面第一個(gè)字節(jié)的后5位后德,代表這個(gè)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的時(shí)候會(huì)提取出視頻的信息,保存在AVCodecContext中悟耘。但是只把SPS和PPS傳遞進(jìn)去是不行的落蝙,需要把后面的IDR幀一起傳給解碼器,才能夠正確解碼作煌。

可以寫一個(gè)簡單的檢測掘殴,如果接收到SPS,就把后面的PPS和IDR幀都接收過來粟誓,然后一起傳給解碼器奏寨。

初始化一個(gè)AVPacket和AVFrame,然后把SPS鹰服、PPS病瞳、IDR幀連在一起的數(shù)據(jù)塊傳給AVPacket的data指針,再進(jìn)行解碼悲酷。

我們假設(shè)包含SPS套菜、PPS、IDR幀的數(shù)據(jù)塊保存在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){

//進(jìn)行下一步的處理

}

}

這樣就可以順利解碼h264流了,解碼出的數(shù)據(jù)保存在AVFrame中顿肺。

我寫了一個(gè)Objective-C類用來執(zhí)行接收視頻流戏溺、解碼渣蜗、播放一系列步驟。

視頻數(shù)據(jù)的接收采用socket直接接收旷祸,使用了開源項(xiàng)目CocoaAsyncSocket耕拷。

就像項(xiàng)目名稱中指明的,這是一個(gè)異步socket類托享。讀寫socket的動(dòng)作會(huì)在一個(gè)單獨(dú)的dispatch queue中執(zhí)行骚烧,執(zhí)行完畢后對(duì)應(yīng)的delegate方法會(huì)自動(dòng)調(diào)用,在其中進(jìn)行進(jìn)一步的處理闰围。

讀取h264流使用了GCDAsyncSocket 的

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

tag:(long)tag

方法赃绊,也就是當(dāng)讀到和data中的字節(jié)一致的內(nèi)容時(shí)就停止讀取,并調(diào)用delegate方法羡榴。傳入的data參數(shù)是 00 00 01 三個(gè)字節(jié)凭戴。這樣每次讀入的nalu開始是沒有start code的,而最后面有下一個(gè)nalu的start code炕矮。因此每次讀取之后都會(huì)把末尾的start code 暫存么夫,然后把主體接到上一次暫存的start code之后,構(gòu)成完整的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

在項(xiàng)目中播放解碼出來的YUV視頻使用了OPENGL档痪,這里播放的部分就略去了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末邢滑,一起剝皮案震驚了整個(gè)濱河市腐螟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌困后,老刑警劉巖乐纸,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異摇予,居然都是意外死亡汽绢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門侧戴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宁昭,“玉大人,你說我怎么就攤上這事酗宋』蹋” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵蜕猫,是天一觀的道長寂曹。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么隆圆? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任芬失,我火速辦了婚禮,結(jié)果婚禮上匾灶,老公的妹妹穿的比我還像新娘。我一直安慰自己租漂,他們只是感情好阶女,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哩治,像睡著了一般秃踩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上业筏,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天憔杨,我揣著相機(jī)與錄音,去河邊找鬼蒜胖。 笑死消别,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的台谢。 我是一名探鬼主播寻狂,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼朋沮!你這毒婦竟也來了蛇券?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤樊拓,失蹤者是張志新(化名)和其女友劉穎纠亚,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體筋夏,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒂胞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了条篷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啤誊。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拥娄,靈堂內(nèi)的尸體忽然破棺而出蚊锹,到底是詐尸還是另有隱情,我是刑警寧澤稚瘾,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布牡昆,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丢烘。R本人自食惡果不足惜柱宦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望播瞳。 院中可真熱鬧掸刊,春花似錦、人聲如沸赢乓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牌芋。三九已至蚓炬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躺屁,已是汗流浹背肯夏。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留犀暑,地道東北人驯击。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像耐亏,于是被迫代替她去往敵國和親余耽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,700評(píng)論 0 3
  • 在上一篇筆記中我們已經(jīng)完成了使用SDL播放聲音和視頻苹熏,聲音播放沒有什么問題碟贾,而視頻播放太快,很明顯視頻沒有同步轨域。在...
    762683ff5d3d閱讀 1,329評(píng)論 0 1
  • 圖片轉(zhuǎn)視頻 為什么想將圖片轉(zhuǎn)視頻干发? 是這樣的朱巨,我打造的任性動(dòng)圖軟件,在編輯制作GIF動(dòng)圖方面枉长,已經(jīng)基本完善〖叫現(xiàn)在想...
    古典小說閱讀 1,571評(píng)論 1 0
  • 漢水纏綿育靈秀, 峻嶺雄峰藏人杰必峰; 山水云闋弄股掌洪唐, 壯志凌云步青天。
    十二明月夜閱讀 230評(píng)論 0 3
  • 三個(gè)月前我開始了培養(yǎng)自己每天記錄的習(xí)慣吼蚁,如果回想一開始的目的凭需,我希望自己每天都能學(xué)點(diǎn)什么,然后輸出出來,通過這種輸...
    Gzw丶南山閱讀 165評(píng)論 0 0