1. 程式人生 > >H264解碼輸出yuv檔案

H264解碼輸出yuv檔案

    現在來寫下s5pv210的h264解碼,這一章有些部分我理解的不是很透徹,只能寫個大概了。希望看到的人能給出些意見,有些地方寫錯的還望指正出來!

    解碼過程與編碼過程類似,編碼過程是先初始化編碼器,然後從編碼器輸出buf中讀出h264檔案頭資料,寫入輸出檔案,然後開始不斷地將一幀幀NV12格式的影象寫入到編碼器的輸入buf,啟動編碼,從編碼器輸出buf中將h264視訊資料寫入到輸出檔案。解碼是首先開啟一個h264格式的檔案作為輸入檔案,從這個檔案中先讀出檔案頭資料,寫入到解碼器的輸入buf中,再初始化解碼器,之後就是不斷地將H264格式輸入檔案中的一段段NALU資料寫入到解碼器的輸入buf,啟動解碼,從解碼器輸出buf中讀取NV12格式的資料,然後轉換成YUV420p格式寫入到輸出檔案中。

    上面一段中所提到的H264檔案頭資料其實是一段包含SPS(序列引數集)、PPS(影象引數集)的資料,裡面的引數用來配置解碼器的初始化。與編碼過程中讀取一幀幀NV12格式的影象資料不同,因為NV12格式每一幀長度是一樣的。而H264格式檔案中每一段NALU的長度不是固定的,這就需要在讀取檔案中做判斷。下面給出一個h264格式檔案的前160個位元組(檔案用Hex模式檢視)。

00 00 00 01 67 64 00 28 ac d3 05 07 e4 00 00 00
01 68 ea 40 6f 2c 00 00 00 01 65 b8 40 57 8a b4
03 0e 39 4a 43 8f 20 fb db 09 bb ae 57 d1 94 e4
20 8c e7 8b 44 b0 03 1c 72 59 78 bf 57 a6 f1 f8
9f 33 ce 4a 5c b4 e1 be 52 03 3d 0b 64 74 37 a7
57 42 8e a1 39 75 03 d6 68 a3 2f e0 a3 0b 26 e3
a1 74 5a e5 b6 34 85 e6 10 c9 82 0f 53 12 47 cc
c8 0f 28 1d 9e 26 7c ac ed 4b e4 00 ea 64 ca 8a
3b 2c 4f f4 05 84 8d cd 6f 96 02 d1 92 be 0b dc
1f e5 5a 35 ea ed 87 a9 1b 7f ca 3c b3 53 a1 89

    裡面有幾個特殊的欄位“00 00 00 01”,這個即是h264格式檔案中每一段NALU資料中各個資料單元的頭部,這些資料單元可以是SPS、PPS、SEI等,具體如下。
enum H264NALTYPE{ 
    H264NT_NAL = 0, 
    H264NT_SLICE,        //1 非IDR影象的編碼條帶 
    H264NT_SLICE_DPA,    //2 編碼條帶資料分割塊A
    H264NT_SLICE_DPB,    //3 編碼條帶資料分割塊B
    H264NT_SLICE_DPC,    //4 編碼條帶資料分割塊C
    H264NT_SLICE_IDR,    //5 IDR影象的編碼條帶
    H264NT_SEI,          //6 增強資訊
    H264NT_SPS,          //7 序列引數集
    H264NT_PPS,          //8 影象引數集
}; 
    區分這些資料單元,可以取“00 00 00 01”欄位後一位元組的資料,與0x1f相&獲得。比如上面第一個資料單元:
00 00 00 01 67 64 00 28 ac d3 05 07 e4
    說明這個是一段SPS(67&1f = 7)。既然解碼是是以一段NALU資料為單位的,那麼如何區分一段NALU中有幾個資料單元呢?這是根據資料單元的型別定義的。其中SEI、SPS與PPS如果相鄰則放在一段NALU資料中,給編碼器做初始化用。SLICE和SLICE_IDR分別屬於單獨的NALU資料段,但SLICE_IDR為關鍵幀,SLICE為P幀,P幀為單向預測編碼或幀內預測編碼,依賴於關鍵幀。也即是說,解碼是,在P幀的前面一般至少要有一幀關鍵幀發給解碼器,否則不能正常解碼影象資訊。

    接下來既可以說下這個h264格式的檔案怎麼讀取了。首先是讀取檔案的頭部,從SPS/PPS/SEI資料單元開始讀,遇到SLICE/SLICE_IDR資料單元時停止,將讀到的資料寫入到解碼器的輸入buf中,然後初始化解碼器。之後開始不斷讀取一段段NALU資料(可以是SPS/PPS/SE連續資料單元+SLICE/SLICE_IDR資料單元,也可以是一個SLICE資料單元,或者是一個SLICE_IDR資料單元)。

    下面看h264格式檔案讀取的程式碼。這個函式返回讀取一段NALU資料的長度,資料會拷貝到buf指標處,當header為1是是讀取檔案頭資訊,為0時時正常讀取一段NALU資料。

int read_one_frame(FILE *fp, uint8_t **buf, int header)
{
    static int end_of_file = 0;
    int ustart, uend;
    int cstart, cend;
    int found;
    uint8_t nal_unit_type;
    
    // 一、從檔案中讀取一段資料到fbuf緩衝區中,讀取的長度是緩衝區最大長度的一半
    // fstart==fend : empty
    // we keep fstart<=fend. whenever fend goes beyond fbufsz, we move the data back to [0 ...)
    int rsz;
    if(!end_of_file && fend-fstart<fbufsz/2) { // fbuf is less than half full
        if (fstart>fbufsz/2) { 	// move back to [0 ...)
            memcpy(fbuf,fbuf+fstart, fend-fstart);
            fend-=fstart;
            fstart=0;
        }
        // fill up to half: fbufsz/2-fend+fstart
        rsz = fread(fbuf+fend, 1, fbufsz/2-fend+fstart, fp);
        if(rsz<(int)(fbufsz/2-fend+fstart)) { // end of file
            printf("We have read all data from the input file\n");
            end_of_file = 1;
        }
        if(rsz>0)
            fend += rsz;
    }
    if(fend>fbufsz) {
        fprintf(stderr,"Opps: this should never happen!\n");
        return -1;
    }
    
    // 二、讀取檔案頭資料
    // now either fbuf is half full or it is end of file
    if(header) { // find header
        // find the first SPS,PPS,SEI header
        found = 0;
        cstart = cend = -1;
        while (find_nal_unit(fbuf+fstart, fend-fstart, &ustart, &uend)>0) {

        	nal_unit_type = fbuf[fstart+ustart] & 0x1f;
            if(nal_unit_type==(uint8_t)6 || nal_unit_type==(uint8_t)7 || nal_unit_type==(uint8_t)8) {
                // SEI, SPS or PPS
                if(!found){
                    found = 1;
                    cstart = fstart+ustart-3; // the start of first SPS, PPS or SEI, fbuf[cstart]: 00 00 01
                    if(cstart>0 && !fbuf[cstart-1])
                        cstart--;
                }
            }else {
                if(found) {
                    cend = fstart+ustart-3; // the end of header before the following picture slice NAL. fbuf[cend]: 00 00 01
                    if (!fbuf[cend-1]) { // the following picture slice has a long start code 00 00 00 01
                        cend--;
                    }
                    break;
                }
            }
            fstart+=uend; // now fbuf[fstart] is the first byte of start code of next NAL
        }
        
        if(cstart<0 || cend<0) {
            fprintf(stderr,"Error: cannot find a NAL header.\n");
            buf = NULL;
            if(!end_of_file)
                fprintf(stderr,"You should consider increase fbufsz. Current fbufsz=%d.\n",fbufsz);
            return -1;
        }
        
        fstart = cend;
        
        // now fbuf[cstart,cend) should contain the first SPS,PPS,SEI header
        printf("Header: cstart=%x, cend=%x, length=%d\n",cstart,cend,cend-cstart);
        *buf=fbuf+cstart;
        
        return cend-cstart;
        
    }   
    
    // 三、讀取一段NALU資料
    cstart = cend = -1;
    found = 0;
    while (find_nal_unit(fbuf+fstart, fend-fstart, &ustart, &uend)>0) {
        nal_unit_type = fbuf[fstart+ustart] & 0x1f;
        if(nal_unit_type==(uint8_t)6 || nal_unit_type==(uint8_t)7 || nal_unit_type==(uint8_t)8) {
            // SEI, SPS or PPS
            if(!found){
                found = 1;
                cstart = fstart+ustart-3; // the start of first SPS, PPS or SEI, fbuf[cstart]: 00 00 01
                if(cstart>0 && !fbuf[cstart-1])
                    cstart--;
            }
        }else if(nal_unit_type==(uint8_t)1 || nal_unit_type==(uint8_t)5) { // IDR or non-IDR
            if(!found) { // no header
                cstart = fstart+ustart-3;
                if(cstart>0 && !fbuf[cstart-1])
                    cstart--;
            }
            cend = fstart+uend;
            break;
        }
        fstart+=uend; // now fbuf[fstart] is the first byte of start code of next NAL
    }
    
    if(cstart<0 || cend<0) {
        //printf("No more NALs. Exiting\n");
        buf = NULL;
        if(!end_of_file)
            fprintf(stderr,"You should consider increase fbufsz. Current fbufsz=%d.\n",fbufsz);
        return -1;
    }
    
    fstart = cend;
    
    *buf=fbuf+cstart;
    return cend - cstart;
}

    函式有點長,不過總體上分為三部分。第一部分是從檔案中讀入資料到fbuf緩衝區,並使緩衝區資料保持一半空間存有資料。第二部分是讀取檔案頭資料,find_nal_unit()函式為讀取一個數據單元,即兩個“00 00 00 01”欄位之間的資料,然後判斷資料單元型別,當為SPS(7),PPS(8),SEI(6)時則繼續讀,直到遇到其它型別資料單元時,將fbuf中前面幾個資料單元的起始地址賦給buf,然後返回前面幾個資料單元(不包含其它資料型別)的長度,即完成了檔案頭資料的讀取。

    當header不等於1時,會執行第三部分程式,讀取一段NALU資料。可以看到第三部分程式,先是用find_nal_unit()函式讀取一個數據單元,接著判斷單元型別,是SPS(7),PPS(8),SEI(6)時則繼續讀,讀到SLICE/SLICE_IDR資料單元時停止,將這端NALU資料的起始地址賦給buf,然後返回NALU資料段(包含一個SLICE/SLICE_IDR資料單元)的長度。

    好了,知道檔案怎麼讀取了,接下來解碼就簡單多了。首先是解碼器初始化的程式碼。

    unsigned int buf_type = CACHE;
    void *openHandle;
    SSBSIP_MFC_ERROR_CODE err;
    SSBSIP_MFC_DEC_OUTPUT_INFO oinfo;
    FILE *fpi, *fpo; 					// input and output files
    
    // 開啟輸入輸出檔案
    char *ifile=DEFAULT_INPUT_FILE, *ofile=DEFAULT_OUTPUT_FILE;
    if(!(fpi = fopen(ifile,"rb"))) {
        fprintf(stderr,"Error: open input file %s.\n",ifile);
        return 1;
    }
    if(!(fpo = fopen(ofile,"wb"))) {
        fprintf(stderr,"Error: open output file %s.\n",ofile);
        goto clr_fpi;
    }
    printf("Input file: %s. Output file: %s.\n", ifile,ofile);
    
    //初始化檔案讀入buf
    if(init_frame_parser()<0) {
        fprintf(stderr,"Error: init frame parser\n");
        goto clr_fpo;
    }
    
    // find the first SPS,PPS,SEI header -> 讀取h264檔案頭到frmbuf中
    int frmlen;
	uint8_t * frmbuf;
    if((frmlen=read_one_frame(fpi,&frmbuf,1))<=0) {
        fprintf(stderr,"Error: cannot find header\n");
        goto clr_parser;
    }
    
    // 開啟解碼器
    openHandle = SsbSipMfcDecOpen(&buf_type);
    if(!openHandle) {
        fprintf(stderr,"Error: SsbSipMfcDecOpen.\n");
        goto clr_parser;
    }
    printf("SsbSipMfcDecOpen succeeded.\n");
    
    // 獲得解碼器輸入buf地址->virInBuf
    void * phyInBuf;
	void * virInBuf;
    virInBuf = SsbSipMfcDecGetInBuf(openHandle, &phyInBuf, MAX_DECODER_INPUT_BUFFER_SIZE);
    if(!virInBuf) {
        fprintf(stderr,"Error: SsbSipMfcDecGetInBuf.\n");
        goto clr_mfc;
    }
    printf("SsbSipMfcDecGetInBuf succeeded.\n");
    // 將檔案頭資料拷貝到解碼器輸入buf
    memcpy(virInBuf,frmbuf,frmlen);
    
    // 初始化解碼器
    err = SsbSipMfcDecInit(openHandle, H264_DEC, frmlen);
    if(err<0) {
        fprintf(stderr,"Error: SsbSipMfcDecInit. Code %d\n",err);
        goto clr_mfc;
    }
    printf("SsbSipMfcDecInit succeeded..\n");
    程式首先打開了輸入檔案和輸出檔案,輸出檔案fpo 在解碼部分才會使用。輸入檔案即fpi 就是H264格式檔案了,程式首先通過呼叫read_one_frame(fpi,&frmbuf,1)) 函式讀出檔案頭資料,然後將資料拷貝入解碼器輸入buf,最後初始化了解碼器。
    解碼器初始化完成後,接下來是正式的解碼過程了。程式碼如下。
    // now start decoding
    status = MFC_GETOUTBUF_STATUS_NULL;
    read_cnt = 0;
    show_cnt = 0;
    do {
        if (status != MFC_GETOUTBUF_DISPLAY_ONLY) {
            // read one frame
            if((frmlen = read_one_frame(fpi,&frmbuf,0))<=0) {
                printf("No more NALs. Exiting\n");
                break;
            }else{
            	printf("%d frames len %d!\n", ++read_cnt, frmlen);
            }
            memcpy(virInBuf, frmbuf, frmlen);
        }
        err = SsbSipMfcDecExe(openHandle, frmlen);
        if(err<0) {
            fprintf(stderr,"Error: SsbSipMfcDecExe. Code %d\n",err);
            break;
        }
        
        memset(&oinfo, 0, sizeof(oinfo));
        status = SsbSipMfcDecGetOutBuf(openHandle,&oinfo);

        if(status==MFC_GETOUTBUF_DISPLAY_DECODING || status==MFC_GETOUTBUF_DISPLAY_ONLY) {
            if(!ylin)
                ylin = (uint8_t *)malloc(oinfo.img_width*oinfo.img_height);
            if(!ylin) {
                fprintf(stderr,"Out of memory.\n");
                break;
            }
            // converted tiled to linear nv12 format - Y plane
            csc_tiled_to_linear(ylin, (uint8_t *)oinfo.YVirAddr, oinfo.img_width, oinfo.img_height);
            fwrite(ylin,1, oinfo.img_width*oinfo.img_height, fpo);
            
            if(!clin)
                clin = (uint8_t *)malloc(oinfo.img_width*oinfo.img_height/2);
            if(!clin) {
                fprintf(stderr,"Out of memory.\n");
                break;
            }

            p_U = (uint8_t *)clin;
            p_V = (uint8_t *)clin;
            p_V += ((oinfo.img_width * oinfo.img_height) >> 2);
            // converted tiled to linear uv format - C plane
            csc_tiled_to_linear_deinterleave(p_U, p_V, (uint8_t *)oinfo.CVirAddr, oinfo.img_width, oinfo.img_height/2);

            fwrite(clin,1,oinfo.img_width*oinfo.img_height/2,fpo);
            show_cnt++;
        }
    } while (1);
    
    printf("Decoding completed! Total number of decoded frames: %d.\nThe video has a dimension of: ", show_cnt);
    printf("img %dx%d, buf %dx%d\n",oinfo.img_width,oinfo.img_height, oinfo.buf_width,oinfo.buf_height);
    解碼過程與編碼過程類似,首先read_one_frame(fpi,&frmbuf,0)) 函式讀取一段NALU資料,然後用memcpy(virInBuf, frmbuf, frmlen) 函式將資料拷貝到解碼器輸入buf,接著呼叫SsbSipMfcDecExe(openHandle, frmlen) 函式來啟動一次解碼,最後用SsbSipMfcDecGetOutBuf(openHandle,&oinfo) 函式獲取解碼的輸出資料,由於解碼器輸出的格式是NV12,而且是tiled型別的,這裡需要進行格式轉換。轉換時先轉換Y分量,然後轉換UV分量。
    csc_tiled_to_linear(ylin, (uint8_t *)oinfo.YVirAddr, oinfo.img_width, oinfo.img_height);
    fwrite(ylin,1, oinfo.img_width*oinfo.img_height, fpo);
    csc_tiled_to_linear_deinterleave(p_U, p_V, (uint8_t *)oinfo.CVirAddr, oinfo.img_width, oinfo.img_height/2);             
    fwrite(clin,1,oinfo.img_width*oinfo.img_height/2,fpo);
    這樣就完成了寫一幀解碼後YUV格式影象到輸出檔案,這個檔案可以用YUV格式播放器開啟,播放器下載地址為http://www.yuvplayer.com/。

    要注意的是,測試這個程式是,所選的h264格式檔案不要太大,因為解碼後的yuv格式檔案很大,所以編碼h264格式檔案時,尺寸要小於640*480,幀數小於200幀最好。其實是smart210板子上可用的儲存空間太小了,不到180M,不夠用啊!下面一章我會寫一個解碼後直接用液晶顯示的,不儲存就不會有這個問題了。順便調整下編碼引數,使編碼後的影象足夠清晰。

    整個工程的程式碼我上傳到了http://download.csdn.net/detail/westlor/9396310。

相關推薦

H264解碼輸出yuv檔案

    現在來寫下s5pv210的h264解碼,這一章有些部分我理解的不是很透徹,只能寫個大概了。希望看到的人能給出些意見,有些地方寫錯的還望指正出來!     解碼過程與編碼過程類似,編碼過程是先初始化編碼器,然後從編碼器輸出buf中讀出h264檔案頭資料,寫入輸出檔案,

H264解碼YUV格式轉換及縮放

int video_decoder::swscale(const char* srcbuf, int ntype, int nsrcwidth, int nsrcheight, int ndstwidt

FFmpeg解碼MP4檔案h264YUV檔案

#include <iostream> #ifdef __cplusplus extern "C" { #endif #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #in

FFmpeg把MP4檔案解碼YUV,然後通過SDL播放

#include <iostream> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swsca

FFmpeg In Android - H264碼流解碼/儲存Yuv

本節例子原始碼_NativeH264Android,修改自ffmpeg原始碼目錄/doc/examples/decode_video.c H264的碼流結構 H.264原始碼流(又稱為“裸流”)是由一個一個的NALU組成的,包括I幀,B幀,P幀等等,他們的結構如下圖所示: 其中每個

FFmpeg In Android - 多媒體檔案解封裝/解碼/儲存Yuv

FFMPEG視音訊編解碼零基礎學習方法 100行程式碼實現最簡單的基於FFMPEG+SDL的視訊播放器 本文例子的原始碼_demuxing_decoding.cpp,修改自原始碼自帶的例子ffmpeg原始碼/doc/example/demuxing_decoding.c FFmpeg

H264解碼之讀取本地H264檔案

讀取本地檔案,並逐幀讀取: onReadThread(): void RtspVideo::onReadThread() { // 解碼引數 char *virInBuf = new char[MAX_DECODER_INPUT_BUFFER_SIZE]; //初

使用GStreamer作v4l2攝像頭採集和輸出YUV檔案及螢幕的相關測試

環境:ubuntu 9.10  安裝軟體:gstreamer 基本包 以及gst-plug-ins-bad, mplayer 1、使用mplayer播放yuv檔案 [email protected]:~/yuv_play_test_from_hp$ mplayer

mac:使用VLC播放純視訊YUV檔案(命令列)

有時候,我們需要播放一些純視訊檔案,判斷YUV資料是否可用。舉個例子,我們使用命令列播放/Users/lz目錄下的test_yuv420p_320x180.yuv檔案,命令如下: /Applications/VLC.app/Contents/MacOS/VLC --demux rawvide

輸入輸出流的讀取輸出 .txt 檔案的 中文亂碼問題 未解決

package interview; import java.io.*; public class TestInOrOutStream { public static void main(String[] args) { int c; try { I

log4j不列印sql,不輸出日誌檔案到指定目錄

#log4j.rootLogger=CONSOLE,info,error,DEBUG log4j.rootLogger=infoA,errorA,CONSOLE,DEBUGA //注意這裡不要用INFO,DEBUG等 改個名字 log4j.appender.CONSOLE=org.apach

python log輸出檔案和控制檯

import logging LOG_FILE = 'mylog.log' file_handler = logging.FileHandler(LOG_FILE) #輸出到檔案 console_handler = logging.StreamHandler() #輸出到控制檯 fil

FFmpeg 4.0.2 實現YUV檔案scale大小變換

/* * 功能:實現YUV檔案scale大小變換 * FFmpeg:4.0.2 */ #include <iostream> extern "C" { #include <libswscale/swscale.h> #include <libavutil/f

FFmpeg 4.0.2 + SDL2-2.0.8 實現H264解碼後播放

一、初級版 功能:實現了簡易視訊播放器的功能,能解碼H264後播放 工具:FFmpeg 4.0.2 + SDL2-2.0.8 C++程式碼: /************************************* 功能:H264解碼為YUV序列,通過SDL播放 FFmpeg:

[轉載] js 讀取和輸出txt檔案

https://www.jb51.net/article/46712.htm ActiveXObject is not defined,ActiveXObject是IE瀏覽器獨有支援的外掛,其他瀏覽器可能不支援。     then--前端不好將資料儲存到本地txt檔案,可以通過jq

springboot2.0 @Slf4j log 日誌配置 輸出檔案 彩色日誌

今天做了個日誌記錄: 解決問題: 使用了springboot原生自帶的一個log框架  POM新增一個外掛(不然使用不了@Slf4j註解): <dependency> <groupId>org.projectlombok</

[Qt] 日誌輸出檔案(qDebug\qWarning\qCritical\qFatal)

#ifndef LOGFILE_H #define LOGFILE_H #include <QMutex> #include <QFile> #include <QTextStream> #include <QDateTime> stati

C++隨時輸出檔案-outfile

這裡主要是討論fstream的內容: #include <fstream> ofstream         //檔案寫操作 記憶體寫入儲存裝置  ifstream         //檔

7.基於FFMPEG將video解碼YUV

參考資料: 1.雷博部落格 1)解碼流程 2)轉換 3)linux下編譯命令 4)程式碼 繼續ffmpeg學習之路。。。 前面寫了將音訊解碼為PCM並通過SD

System.out.println將內容輸出檔案儲存

try    {        PrintStream out = new PrintStream(fileName);