1. 程式人生 > >在iOS平臺使用ffmpeg解碼h264視訊流

在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>

@interfacevideoPlayer : 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;

@interfacevideoPlayer () <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

@implementationvideoPlayer

- (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;
    sharedUserData *userData = [sharedUserData sharedUserData];
    [socket connectToHost:[userData address]
                   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, NULLNULLNULL);

            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,這裡播放的部分就略去了。