使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

視音頻的基本概念

我們常說的視頻文件(例如 avi 文件,MP4 文件等)本質上是一種“容器”,其內部存放一幀幀的視頻信息和音頻信息。因此,視頻文件內部常常包含不止一個“信息流”,而是包含一組“信息流”(若干視頻流和若干音頻流)。

所謂的“信息流”,其實就是隨時間分佈的信息而已。比如視頻可以看成是一組隨時間分佈的“圖片”。

視頻流中的一個數據元通常被稱作“一幀(frame)”,每一種視頻流都有屬於自己的編解碼器(enCOder/DECoder,在FFmpeg中被簡寫為 codec),用於說明該種視頻流是如何編碼和解碼的。數據包(packets)則常常指從裸數據幀解析而來的數據片段。

使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

處理音視頻流是非常簡單的

總體來說,處理音視頻流是非常簡單的,通常包含以下幾個步驟:

step1. 打開音視頻文件,獲取音視頻流
step2. 從數據流讀取數據幀
step3. 如果數據幀不完整,就回到 step2
step4. 處理數據幀
step5. 回到 step2

事實上,使用 FFmpeg 處理多媒體音視頻的基本步驟和上述“偽代碼”沒有太多不同,當然了,“step4. 處理數據幀”是一個曖昧的說法,畢竟這短短几個字背後的工作量可能非常巨大。

本節將嘗試使用 FFmpeg 處理一段視音頻文件,這裡所謂的“處理”,其實就是將視頻分解為若干個 ppm 圖片,並存儲到磁盤。

打開文件

首先,我們來看看如何打開一個視音頻文件。使用 FFmpeg 之前,首先需要註冊相關的庫,這一過程是簡單的,請參考下面的C語言代碼:

#include <libavcodec>
#include <libavformat>
#include <libswscale>


...
int main(int argc, char *argv[])
{
if (argc < 2){
printf("usage:\\n\\t %s filename\\n", argv[0]);
return -1;
}

av_register_all();
.../<libswscale>/<libavformat>/<libavcodec>
使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

調用av_register_all()函數

av_register_all()函數可以註冊 FFmpeg 中所有可用的文件格式和編解碼庫 codecs,因為這個函數在項目中只需要也只應該調用一次,所以將其放在 main() 函數中了,這不是必須的,當然也可以將其放在項目中的其他地方。

現在我們可以打開相應的文件了:

AVFormatContext *pctx = NULL;
// 打開文件
if (avformat_open_input(&pctx, argv[1], NULL, NULL)!=0) {
return -1;
}

從這段C語言代碼可以看出,我們將要打開的文件名通過程序的第一個參數(argv[1])指定,avformat_open_input() 函數可以讀取文件頭信息,並將其放在 pctx 中。後面的兩個參數用於指定視頻文件的格式,以及選項配置信息的,我們將其設置為 NULL,FFmpeg 庫將自動探測這些信息。

只獲取視頻文件的頭信息是不夠的,因此需要進一步的探測視頻文件的流信息,這一步可以通過下面這個函數實現,請看相關C語言代碼:

// 進一步探測信息
assert(avformat_find_stream_info(pctx, NULL)>=0);

這個函數主要填充 pctx->streams 成員,可以使用下面這個函數顯示 FFmpeg 的一些中間過程信息到終端:

// 顯示中間過程信息
av_dump_format(pctx, 0, argv[1], 0);

下圖是一箇中間過程信息實例:

使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

中間過程信息實例

pctx->streams 本質上是一組指針,每一個指針都對應著視頻容器中存儲的一種流,它的 size 等於 pctx->nb_streams,所以可以通過遍歷對比的方式從這一組流中找到視頻流,相關的C語言代碼可以如下寫:

 int i, video_stream = -1;
for (i=0; i<pctx->nb_streams; i++) {
// 查找第一個視頻流
if (pctx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
video_stream =i;
break;
}
}
if (-1==video_stream) {
printf("no video stream detected\\n");
return -1;
}
// pcodec_ctx 指向第一個視頻流
AVCodecContext *pcodec_ctx =
pctx->streams[video_stream]->codec;/<pctx->
使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

通過遍歷對比的方式從這一組流中找到視頻流

流信息的編解碼器 codec 就存放在我們稱作“codec context(編解碼上下文)”中,它包含對應流信息使用的 codec 的所有信息,上述代碼的最後定義了pcodec_ctx指針,並讓其指向了對打開視頻容器中的第一個視頻流的 codec 上下文,現在可以根據上下文查找對應視頻流的實際編解碼器 codec 了,相應的C語言代碼可以如下寫:

 AVCodec *pcodec = NULL;
// 查找視頻流對應的解碼器
pcodec = avcodec_find_decoder(pcodec_ctx->codec_id);
if (NULL == pcodec) {
printf("unsupported codec.\\n");
return -1;
}
// 拷貝上下文
AVCodecContext *pcodec_ctx_orig =
avcodec_alloc_context3(pcodec);
if (avcodec_copy_context(pcodec_ctx_orig, pcodec_ctx) != 0) {
printf("couldn't copy codec context\\n");
return -1;
}
// 打開編解碼器
if (avcodec_open2(pcodec_ctx, pcodec, NULL) < 0) {
printf("couldn't open codec\\n");
return -1;
}
使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

根據上下文查找對應視頻流的實際編解碼器 codec

應注意,我們一定不能直接使用視頻流的 AVCodecContext,所以不得不使用 avcodec_copy_context() 拷貝了一份上下文。當然了,在拷貝之前,需要先調用 avcodec_alloc_context3() 為其分配相應的內存。

存儲數據幀

存儲數據幀之前,肯定需要先分配一塊內存,這一過程的C語言代碼可以如下寫:

 AVFrame *pframe = av_frame_alloc();
AVFrame *pframe_rgb = av_frame_alloc();
assert(pframe && pframe_rgb);

既然我們計劃輸出 24-bit RGB 格式的 PPM 文件,那麼必須先將打開的輸入視頻文件從它原來的格式轉換為 RGB 格式,因此上面的C語言代碼還預先分配了額外的一塊內存,用於存儲轉換後的數據。

上面的C語言代碼分配的是輸出數據的內存,我們還需要分配一塊內存供原始數據使用,為此,首先要現知道需要多少內存,這一過程可以調用 avpicture_get_size() 函數得到,相關的C語言代碼如下,請看:

 int num_bytes = avpicture_get_size(AV_PIX_FMT_RGB24, 
pcodec_ctx->width, pcodec_ctx->height);
uint8_t *buffer = av_malloc(num_bytes * sizeof(uint8_t));

av_malloc() 函數是 FFmpeg 的內存分配函數,它其實不過是 malloc() 函數的簡單封裝而已,只不過確保了內存地址對齊以提升程序的效率。使用它和使用 malloc() 是類似的,應注意避免內存洩漏,多重釋放等問題。

使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

應注意避免內存洩漏,多重釋放等問題

現在我們可以使用 avpicture_fill() 函數將視頻幀數據填充到新分配的 buffer 裡了,這一過程的C語言代碼是簡單的:

avpicture_fill(
(AVPicture *)pframe_rgb,
buffer,
AV_PIX_FMT_RGB24,
pcodec_ctx->width,
pcodec_ctx->height
);

終於,我們準備好從視頻流裡讀取數據了!

讀取數據

現在要做的就是從視頻流中讀取數據到 packet,然後解碼成幀,將其轉換為我們需要的格式,再保存到磁盤,相應的C語言代碼如下,請看:

 int frame_finished;
AVPacket pkt;
// 初始化 sws 上下文,用於轉換數據格式
struct SwsContext *sws_ctx = sws_getContext(
pcodec_ctx->width,
pcodec_ctx->height,
pcodec_ctx->pix_fmt,
pcodec_ctx->width,
pcodec_ctx->height,
AV_PIX_FMT_RGB24,
SWS_BILINEAR,
NULL,
NULL,
NULL
);
i = 0; // 作為實例,只保存前 5 幀
while (av_read_frame(pctx, &pkt) >= 0) {
if (pkt.stream_index != video_stream) {
continue;
}
avcodec_decode_video2(pcodec_ctx, pframe, &frame_finished, &pkt);
if (!frame_finished)
continue;

sws_scale(sws_ctx, pframe->data, pframe->linesize,
0, pcodec_ctx->height, pframe_rgb->data, pframe_rgb->linesize);
if (++i<=5) {
save_frame(pframe_rgb, pcodec_ctx->width, pcodec_ctx->height,i);
}

}

av_free_packet(&pkt);
使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

轉換格式,保存到磁盤

這一過程的代碼雖然稍稍長了點,但是很簡單:av_read_frame()函數讀取視頻流信息,並將其存放到 AVPacket 結構的 pkt 變量中,應注意,我們只需分配 AVPacket 結構體的內存,數據(pkt->data)的內存則由 FFmpeg 在其內部自動分配,不過使用完畢後,要調用 av_free_packet()函數釋放。

avcodec_decode_video()函數可以將 packet 轉換成 frame,不過,解碼一個 packet 不一定能夠獲得 frame 的全部信息,所以需要藉助 frame_finished 標誌位用於判斷這一過程。

得到一個 frame 後,便可調用 sws_scale() 函數將 frame 從其原始的格式(pctx->pix_fmt)轉換到我們期望的 RGB 格式,轉換完畢後,就可以調用 save_frame() 函數將其保存到磁盤了。

save_frame()是一個自己定義的函數,它的相關C語言代碼可以按照下面這樣寫,請看:

void save_frame(AVFrame *pframe, int width, int height, int iframe)
{
char filename[32];
int y;

sprintf(filename, "frame%d.ppm", iframe);
FILE *fp = fopen(filename, "w+");
assert(fp!=NULL);

fprintf(fp, "P6\\n%d %d\\n255\\n", width, height); // header

for (y=0; y<height> fwrite(pframe->data[0]+y*pframe->linesize[0], 1, width*3, fp);
fclose(fp);
}/<height>
使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

save_frame()函數的C語言代碼

save_frame()函數的C語言代碼大都是基礎庫的使用,唯一需要說明的是下面這行代碼:

fprintf(fp, "P6\\n%d %d\\n255\\n", width, height);

它為 PPM 文件添加了固定的頭部信息。

關閉使用完畢的資源

現在文章開頭計劃的工作完成了,可以關閉所有使用完畢的資源了,具體的C語言代碼如下,請看:

 // 釋放內存
av_free(buffer);
av_free(pframe_rgb);
av_free(pframe);
// 關閉 codec
avcodec_close(pcodec_ctx);
avcodec_close(pcodec_ctx_orig);
// 關閉打開的文件
avformat_close_input(&pctx);

編譯並執行

相應的 FFmpeg 庫的編譯安裝請參考上一節FFmpeg的編譯安裝 ,編譯時應指定 FFmpeg 的頭文件以及庫所在路徑:

$ gcc t.c -I <ffmpeg>/include/ -L <ffmpeg>/lib/ -lavutil -lavformat -lavcodec -lavutil -lm -g -lswscale/<ffmpeg>/<ffmpeg>

在執行編譯生成的C語言程序時,在命令行指定視頻文件所在的路徑,我在工程目錄裡放入了一個名為“test.avi”的視頻文件,因此可以如下執行程序:

$ a.out ./test.avi

最終輸出如下:

使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

輸出信息

這說明程序正常運行了,查看程序所在目錄,的確有若干 PPM 文件生成,並且可以通過圖片瀏覽器打開:

使用C語言製作視頻播放器(2),將視頻拆分成圖片組,並存到磁盤

PPM 文件,點個關注吧

歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。


分享到:


相關文章: