標籤:
來源:http://www.aichengxu.com/view/37145
在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 <Foundation/Foundation.h>@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 () <GCDAsyncSocketDelegate>{ 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,這裡播放的部分就略去了。
在iOS平台使用ffmpeg解碼h264視頻流