從零開(kāi)始認(rèn)識(shí)視頻解碼和渲染,需求是用ffmpeg軟解碼和videoToolBox硬解碼,現(xiàn)已完成ffmpeg方面,從開(kāi)始的無(wú)從下手,到現(xiàn)在能夠較好的利用它,總算不辜負(fù)好幾天來(lái)的折騰。如有不當(dāng)?shù)牡胤?,歡迎批評(píng)指正,謝謝。廢話不多說(shuō),上筆記。
一、編譯
- 1、首先,你需要下載
下載gas-preprocessor
下載FFmpeg-iOS-build-script - 2、如果你安裝了多個(gè)xcode,可以先修改下默認(rèn)路徑,否則后邊設(shè)置了路徑還是會(huì)報(bào)#import "avformat.h"錯(cuò)誤
sudo xcode-select -s /Applications/Xcode.app - 3、將 gas-preprocessor文件夾內(nèi)的gas-preprocessor.pl文件拷貝到/usr/sbin/目錄下,并修改/usr/sbin/gas-preprocessor.pl的文件權(quán)限為可執(zhí)行權(quán)限
chmod 777 /usr/sbin/gas-preprocessor.pl - 4、通過(guò)終端進(jìn)入FFmpeg-iOS-build-script文件夾,開(kāi)始編譯
編譯所有的版本arm64、armv7、x86_64的靜態(tài)庫(kù)(如只需編譯部分版本的可在指令后添加特定版本)
./build-ffmpeg.sh - 5、以上算是編譯完成了,此時(shí)你需要在運(yùn)行后的腳本文件夾FFmpeg-iOS-build-script里找到下載編譯后的FFmpeg-iOS文件夾(里邊有包含兩個(gè)文件夾include和lib),拖入工程中,設(shè)置路徑TARGETS→Build Settings→Search Paths→Library Search Paths 設(shè)置為
"$(SRCROOT)/你的工程名/FFmpeg-iOS/lib"
(這里還有個(gè)小技巧,之前無(wú)論這樣設(shè)置都行不通,谷歌許久嘗試了多種設(shè)置方法都行不通,最后直接把庫(kù)放在頭文件下,行得通了!如果你也這樣不妨試試我的辦法O(∩_∩)O~) - 6、另外還需要在工程中加上一些一些庫(kù):libiconv.dylib、libbz.dylib、libz.dylib,到這里算是結(jié)束了。
- 7、最后,在頭文件加上
#import "avformat.h試試看是不是已經(jīng)不會(huì)報(bào)錯(cuò)說(shuō)找不到文件了,那么可以開(kāi)始利用ffmpeg咯。
二、h264解碼
ffmpeg對(duì)視頻文件進(jìn)行解碼的大致流程:
- 注冊(cè)所有容器格式和CODEC: av_register_all()
- 打開(kāi)文件: av_open_input_file()
- 從文件中提取流信息: av_find_stream_info()
- 窮舉所有的流,查找其中種類為CODEC_TYPE_VIDEO
- 查找對(duì)應(yīng)的解碼器: avcodec_find_decoder()
- 打開(kāi)編解碼器: avcodec_open()
- 為解碼幀分配內(nèi)存: avcodec_alloc_frame()
- 不停地從碼流中提取中幀數(shù)據(jù): av_read_frame()
- 判斷幀的類型,對(duì)于視頻幀調(diào)用: avcodec_decode_video()
- 解碼完后,釋放解碼器: avcodec_close()
- 關(guān)閉輸入文件:av_close_input_file()
剛接觸的時(shí)候看到這些流程真不知如何下手,找到的一些Demo也都是.mm文件里用C++編程的,理解起來(lái)還是覺(jué)得頗有難度,后來(lái)干脆直接把這流程放在代碼里,逐個(gè)突破,居然得到了意向不到的效果,后來(lái)再看網(wǎng)上教程,就開(kāi)始明白解碼也就這點(diǎn)流程這點(diǎn)函數(shù)。
這里簡(jiǎn)單梳理下大致流程:
(1)av_register_all() 這個(gè)函數(shù)來(lái)注冊(cè)所有的組件,初始化只需要一次,可以用GCD的方式使其執(zhí)行一次;
(2)avformat_open_input打開(kāi)文件;
(3)當(dāng)文件成功打開(kāi)(所以需要判斷文件是否成功打開(kāi)),用** avformat_find_stream_info從文件中提取流信息;
(4)當(dāng)成功提取了流信息(同理需要判斷流信息是否提取成功),就需要做一個(gè)遍歷在多個(gè)數(shù)據(jù)流中找到視頻流;
(5)當(dāng)找到視頻流(需要判斷是否找到視頻流),需要 avcodec_find_decoder查找視頻流中相對(duì)應(yīng)的解碼器;
(6)如果找到了解碼器(加判斷),則用 avcodec_open2打開(kāi)解碼器;
(7)打開(kāi)解碼器之后用 av_frame_alloc為解碼幀分配內(nèi)存;
(8)接下來(lái)要做一個(gè)循環(huán), av_read_frame是從流中讀取一幀的數(shù)據(jù)到Packet中;
(9)當(dāng)已經(jīng)讀取到數(shù)據(jù)(加判斷),則h264文件解碼中最重要的函數(shù)登場(chǎng), avcodec_decode_video2開(kāi)始解碼,它會(huì)將AVPacket(是個(gè)結(jié)構(gòu)體,里面裝的是h.264)轉(zhuǎn)換為AVFream(里面裝的是yuv數(shù)據(jù)),此時(shí)的yuv數(shù)據(jù)渲染后便是我們?nèi)庋劭吹降囊曨l數(shù)據(jù);
(10)當(dāng)(8)中的 av_read_frame**讀不到數(shù)據(jù),結(jié)束循環(huán)。
上代碼:
// [1]注冊(cè)所支持的所有的文件(容器)格式及其對(duì)應(yīng)的CODEC av_register_all()
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
av_register_all();
});
// [2]打開(kāi)文件 avformat_open_input()
pFormatContext = avformat_alloc_context();
NSString *fileName = [[NSBundle mainBundle] pathForResource:@"zjd.h264" ofType:nil];
if (fileName == nil)
{
NSLog(@"Couldn't open file:%@",fileName);
return;
}
if (avformat_open_input(&pFormatContext, [fileName cStringUsingEncoding:NSASCIIStringEncoding], NULL, NULL) != 0)//[1]函數(shù)調(diào)用成功之后處理過(guò)的AVFormatContext結(jié)構(gòu)體;[2]打開(kāi)的視音頻流的URL;[3]強(qiáng)制指定AVFormatContext中AVInputFormat的。這個(gè)參數(shù)一般情況下可以設(shè)置為NULL,這樣FFmpeg可以自動(dòng)檢測(cè)AVInputFormat;[4]附加的一些選項(xiàng),一般情況下可以設(shè)置為NULL。)
{
NSLog(@"無(wú)法打開(kāi)文件");
return;
}
以上是對(duì)本地H264文件的解碼,當(dāng)然如果你要對(duì)網(wǎng)絡(luò)實(shí)時(shí)傳輸?shù)膆264視頻碼流進(jìn)行解碼,需要初始化網(wǎng)絡(luò)環(huán)境,同時(shí)修改路徑,以RTSP為例
avformat_network_init();
...(其余部分不變)
in_filename = [[NSString stringWithFormat:@"rtsp://192.168.100.1/video/h264"] cStringUsingEncoding:NSASCIIStringEncoding];
// [3]從文件中提取流信息 avformat_find_stream_info()
if (avformat_find_stream_info(pFormatContext, NULL) < 0) {
NSLog(@"無(wú)法提取流信息");
return;
}
// [4]在多個(gè)數(shù)據(jù)流中找到視頻流 video stream(類型為MEDIA_TYPE_VIDEO)
int videoStream = -1;
for (int i = 0; i < pFormatContext -> nb_streams; i++)
{
if (pFormatContext -> streams[i] -> codec -> codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
}
}
if (videoStream == -1) {
NSLog(@"Didn't find a video stream.");
return;
}
// [5]查找video stream 相對(duì)應(yīng)的解碼器 avcodec_find_decoder(獲取視頻解碼器上下文和解碼器)
pCodecCtx = pFormatContext->streams[videoStream]->codec;
AVCodec *pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (pCodec == NULL) {
NSLog(@"pVideoCodec not found.");
return NO;
}
// [6]打開(kāi)解碼器 avcodec_open2()
avcodec_open2(pCodecCtx, pCodec, NULL);
// [7]為解碼幀分配內(nèi)存 av_frame_alloc()
pFrame = av_frame_alloc();
// [8]從流中讀取讀取數(shù)據(jù)到Packet中 av_read_frame()
AVPacket packet;
av_init_packet(&packet);
if (av_read_frame(pFormatContext, &packet) >= 0)
{//已經(jīng)讀取到了數(shù)據(jù)
// [9]對(duì)video 幀進(jìn)行解碼,調(diào)用 avcodec_decode_video2()
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);//作用是解碼一幀視頻數(shù)據(jù)。輸入一個(gè)壓縮編碼的結(jié)構(gòu)體AVPacket,輸出一個(gè)解碼后的結(jié)構(gòu)體AVFrame
av_free_packet(&packet);
}
if (frameFinished)
{
//能夠跳進(jìn)此方法說(shuō)明已經(jīng)解碼成功
}
渲染
介紹一個(gè)很棒的OpenGL直接渲染YUV的代碼,在解碼成功的函數(shù)里只要初始化后調(diào)用外部接口,可用
//初始化
OpenGLView20 *glView = [[OpenGLView20 alloc] initWithFrame:frame];
//設(shè)置視頻原始尺寸
[glView setVideoSize:352 height:288];
//渲染yuv
[glView displayYUV420pData:yuvBuffer width:352height:288;
這里需要注意的是,我們的視頻文件解碼后的AVFrame的data不能直接渲染,因?yàn)锳VFrame里的數(shù)據(jù)是分散的,displayYUV420pData這里指明了數(shù)據(jù)格式為YUV420P,所以我們必須copy到一個(gè)連續(xù)的緩沖區(qū),然后再進(jìn)行渲染。
像這樣:
char *buf = (char *)malloc(_pFrame->width * _pFrame->height * 3 / 2);
AVPicture *pict;
int w, h, i;
char *y, *u, *v;
pict = (AVPicture *)_pFrame;//這里的frame就是解碼出來(lái)的AVFrame
w = _pFrame->width;
h = _pFrame->height;
y = buf;
u = y + w * h;
v = u + w * h / 4;
for (i=0; i<h; i++)
memcpy(y + w * i, pict->data[0] + pict->linesize[0] * i, w);
for (i=0; i<h/2; i++)
memcpy(u + w / 2 * i, pict->data[1] + pict->linesize[1] * i, w / 2);
for (i=0; i<h/2; i++)
memcpy(v + w / 2 * i, pict->data[2] + pict->linesize[2] * i, w / 2);
if (buf == NULL) {
return;
}else {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
[myview displayYUV420pData:buf width:_pFrame -> width height:_pFrame ->height];
free(buf);
});
}