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等,具體如下。
區分這些資料單元,可以取“00 00 00 01”欄位後一位元組的資料,與0x1f相&獲得。比如上面第一個資料單元: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 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檔案為h264和YUV檔案
#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);