對(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档痪,這里播放的部分就略去了。