寫在前面
學習雷神的博客,向雷神致敬~
看了雷神的小學期視頻課,在Github上下載了simplest_ffmpeg_player的代碼,爲代碼加上了註釋,作爲留存。
2019.07.16
視頻中的前置知識點
simple_ffmpeg_decoder.cpp註釋
simple_ffmpeg_decoder_pure.cpp註釋
鏈接及參考資料
《基於 FFmpeg + SDL 的視頻播放器的製作》課程的視頻
FFmpeg Documentation
FFmpeg源代碼簡單分析
知識點
封裝、編碼格式
FFmpeg解碼流程及數據結構
FFmpeg數據結構簡介
AVFormatContext:封裝格式上下文結構體,也是統領全局的結構體,保存了視頻文件封裝格式相關信息
AVInputFormat:每種封裝格式對應一個該結構體
AVStream:視頻文件每個視頻(音頻)流對應一個該結構體
AVCodecContext:編碼器上下文結構體,保存了視頻(音頻)編解碼相關信息
AVCodec:每種視頻(音頻)編解碼器對應一個該結構體
AVPacket:存儲一幀壓縮編碼數據
AVFrame:存儲一幀解碼後像素(採樣)數據
AVFormatContext
- iformat:輸入視頻的AVIputFormat
- nb_streams:輸入視頻的AVStream個數
- streams:輸入視頻的AVStream[]數組
- duration:輸入視頻的時長(以微秒爲單位)
- bit_rate:輸入視頻的碼率
AVIputFormat
- name:封裝格式名稱
- long_name:封裝格式的長名稱
- extensions:封裝格式的擴展名
- id:封裝格式ID
- 一些封裝格式處理的接口函數
AVStream
- id:序號
- codec:該流對應的AVCodecContext
- time_base:該流的時基
- r_frame_rate:該流的幀率
AVCodecContext
- codec:編解碼器的AVCodec
- width,height:圖像的寬高(只針對視頻)
- pix_fmt:像素格式(只針對視頻)
- sample_rate:採樣率(只針對音頻)
- channels:聲道數(只針對音頻)
- sample_fmt:採樣格式(只針對音頻)
AVCodec
- name:編解碼器名稱
- long_name:編解碼器長名稱
- type:編解碼器類型
- id:編解碼器ID
- 一些編解碼的接口函數
1.simplest_ffmpeg_decoder.cpp
/**
* 最簡單的基於FFmpeg的視頻解碼器
* Simplest FFmpeg Decoder
*
* 雷霄驊 Lei Xiaohua
* [email protected]
* 中國傳媒大學/數字電視技術
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
*
* 本程序實現了視頻文件解碼爲YUV數據。它使用了libavcodec和
* libavformat。是最簡單的FFmpeg視頻解碼方面的教程。
* 通過學習本例子可以瞭解FFmpeg的解碼流程。
* This software is a simplest decoder based on FFmpeg.
* It decodes video to YUV pixel data.
* It uses libavcodec and libavformat.
* Suitable for beginner of FFmpeg.
*
*/
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#ifdef __cplusplus
};
#endif
#endif
/**
* 將視頻解封裝、解碼,轉換爲yuv格式
**/
int main(int argc, char* argv[])
{
// 封裝格式上下文的結構體,也是統領全局的結構體,保存了視頻文件封裝格式的相關信息
AVFormatContext *pFormatCtx;
// 視頻流在文件中的位置
int i, videoindex;
// 編碼器上下文結構體,保存了視頻(音頻)編解碼相關信息
AVCodecContext *pCodecCtx;
// 每種視頻(音頻)編解碼器(例如H.264解碼器)對應一個該結構體
AVCodec *pCodec;
// 存儲一幀解碼後像素(採樣)數據
AVFrame *pFrame,*pFrameYUV;
//
unsigned char *out_buffer;
// 存儲一幀壓縮編碼數據
AVPacket *packet;
// width×height,用於計算YUV數據分佈
int y_size;
// 視頻是否解碼成功的返回
int ret, got_picture;
// libswsscale 上下文
struct SwsContext *img_convert_ctx;
// 輸入文件
char filepath[]="Titanic.mkv";
// 輸出文件
FILE *fp_yuv=fopen("output.yuv","wb+");
// 註冊複用器,編碼器等(參考FFmpeg解碼流程圖)
av_register_all();
avformat_network_init();
pFormatCtx = avformat_alloc_context();
// 打開多媒體數據並且獲得一些相關的信息(參考FFmpeg解碼流程圖)
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
printf("Couldn't open input stream.\n");
return -1;
}
// 讀取一部分視音頻數據並且獲得一些相關的信息(參考FFmpeg解碼流程圖)
if(avformat_find_stream_info(pFormatCtx,NULL)<0){
printf("Couldn't find stream information.\n");
return -1;
}
// 每個視頻文件中有多個流(視頻流、音頻流、字幕流等,而且可有多個),循環遍歷找到視頻流
// 判斷方式:AVFormatContext->AVStream->AVCodecContext->codec_type是否爲AVMEDIA_TYPE_VIDEO
videoindex=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
// 如果沒有視頻流,返回
if(videoindex==-1){
printf("Didn't find a video stream.\n");
return -1;
}
// 保存視頻流中的AVCodecContext
pCodecCtx=pFormatCtx->streams[videoindex]->codec;
// 用於查找FFmpeg的解碼器(參考FFmpeg解碼流程圖)
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL){
printf("Codec not found.\n");
return -1;
}
// (參考FFmpeg解碼流程圖)
if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
printf("Could not open codec.\n");
return -1;
}
// 創建一個AVFrame,用來存放解碼後的一幀的數據
pFrame=av_frame_alloc();
pFrameYUV=av_frame_alloc();
// av_image_get_buffer_size:返回使用給定參數存儲圖像所需的數據量的字節大小
out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height,1));
// 根據指定的圖像參數和提供的數組設置數據指針和線條(data pointers and linesizes)
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,
AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1);
// 創建一個AVPacket,用來存放下面循環獲取到的未解碼幀
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
//Output Info-----------------------------
printf("--------------- File Information ----------------\n");
av_dump_format(pFormatCtx,0,filepath,0);
printf("-------------------------------------------------\n");
// sws_getContext():初始化一個SwsContext
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
// 循環讀取幀數據
while(av_read_frame(pFormatCtx, packet)>=0){
// 取出視頻流,
if(packet->stream_index==videoindex){
// 解碼一幀視頻數據:輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼後的結構體AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if(ret < 0){
printf("Decode Error.\n");
return -1;
}
if(got_picture){
// sws_scale():處理圖像數據,用於轉換像素
sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
// 根據YUV數據格式,分離Y、U、V數據
// 如果視頻幀的寬和高分別爲w和h,那麼一幀YUV420P像素數據一共佔用w*h*3/2 Byte的數據
// 其中前w*h Byte存儲Y,接着的w*h*1/4 Byte存儲U,最後w*h*1/4 Byte存儲V
y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
printf("Succeed to decode 1 frame!\n");
}
}
av_free_packet(packet);
}
//flush decoder
//FIX: Flush Frames remained in Codec
while (1) {
// 解碼一幀視頻數據:輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼後的結構體AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if (ret < 0)
break;
if (!got_picture)
break;
// // sws_scale():處理圖像數據,用於轉換像素
sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
int y_size=pCodecCtx->width*pCodecCtx->height;
fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y
fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U
fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V
printf("Flush Decoder: Succeed to decode 1 frame!\n");
}
// sws_freeContext():釋放一個SwsContext
sws_freeContext(img_convert_ctx);
// close and free
fclose(fp_yuv);
av_frame_free(&pFrameYUV);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
return 0;
}
2.simplest_ffmpeg_decoder_pure
/**
* 最簡單的基於FFmpeg的視頻解碼器(純淨版)
* Simplest FFmpeg Decoder Pure
*
* 雷霄驊 Lei Xiaohua
* [email protected]
* 中國傳媒大學/數字電視技術
* Communication University of China / Digital TV Technology
* http://blog.csdn.net/leixiaohua1020
*
*
* 本程序實現了視頻碼流(支持HEVC,H.264,MPEG2等)解碼爲YUV數據。
* 它僅僅使用了libavcodec(而沒有使用libavformat)。
* 是最簡單的FFmpeg視頻解碼方面的教程。
* 通過學習本例子可以瞭解FFmpeg的解碼流程。
* This software is a simplest decoder based on FFmpeg.
* It decode bitstreams to YUV pixel data.
* It just use libavcodec (do not contains libavformat).
* Suitable for beginner of FFmpeg.
*/
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#ifdef __cplusplus
};
#endif
#endif
//test different codec
#define TEST_H264 1
#define TEST_HEVC 0
int main(int argc, char* argv[])
{
// 每種視頻(音頻)編解碼器(例如H.264解碼器)對應一個該結構體
AVCodec *pCodec;
// 編碼器上下文結構體,保存了視頻(音頻)編解碼相關信息
AVCodecContext *pCodecCtx= NULL;
// 保存了當前幀的信息,包括offset、dts、pts、寬高等
AVCodecParserContext *pCodecParserCtx=NULL;
FILE *fp_in;
FILE *fp_out;
// 存儲一幀解碼後像素(採樣)數據
AVFrame *pFrame;
const int in_buffer_size=4096;
// FF_INPUT_BUFFER_PADDING_SIZE:在輸入比特流的末尾用於解碼的額外分配字節的所需數量。
// 這主要是因爲一些優化的比特流讀取器一次讀取32位或64位並且可以讀取結束。
// 注意:如果附加字節的前23位不爲0,則損壞的MPEG比特流可能導致過度讀取和段錯誤。
unsigned char in_buffer[in_buffer_size + FF_INPUT_BUFFER_PADDING_SIZE]={0};
unsigned char *cur_ptr;
int cur_size;
// 存儲一幀壓縮編碼數據
AVPacket packet;
// 視頻是否解碼成功的返回
int ret, got_picture;
// hevc h264 m2v -> yuv
#if TEST_HEVC
enum AVCodecID codec_id=AV_CODEC_ID_HEVC;
char filepath_in[]="bigbuckbunny_480x272.hevc";
#elif TEST_H264
AVCodecID codec_id=AV_CODEC_ID_H264;
char filepath_in[]="bigbuckbunny_480x272.h264";
#else
AVCodecID codec_id=AV_CODEC_ID_MPEG2VIDEO;
char filepath_in[]="bigbuckbunny_480x272.m2v";
#endif
char filepath_out[]="bigbuckbunny_480x272.yuv";
int first_time=1;
//av_log_set_level(AV_LOG_DEBUG);
// 註冊複用器,編碼器等(參考FFmpeg解碼流程圖)
avcodec_register_all();
// 用於查找FFmpeg的解碼器(參考FFmpeg解碼流程圖)
pCodec = avcodec_find_decoder(codec_id);
if (!pCodec) {
printf("Codec not found\n");
return -1;
}
// 創建AVCodecContext結構體
pCodecCtx = avcodec_alloc_context3(pCodec);
if (!pCodecCtx){
printf("Could not allocate video codec context\n");
return -1;
}
// 初始化AVCodecParserContext。其參數是codec_id,所以同時只能解析一種
// AVCodecParser用於解析輸入的數據流並把它們分成一幀一幀的壓縮編碼數據。
// 比較形象的說法就是把長長的一段連續的數據“切割”成一段段的數據。
// 核心函數是av_parser_parse2()
pCodecParserCtx=av_parser_init(codec_id);
if (!pCodecParserCtx){
printf("Could not allocate video parser context\n");
return -1;
}
//if(pCodec->capabilities&CODEC_CAP_TRUNCATED)
// pCodecCtx->flags|= CODEC_FLAG_TRUNCATED;
// 使用給定的AVCodec初始化AVCodecContext;
// 在使用這個函數之前需要使用avcodec_alloc_context3()分配的context
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
printf("Could not open codec\n");
return -1;
}
//Input File
fp_in = fopen(filepath_in, "rb");
if (!fp_in) {
printf("Could not open input stream\n");
return -1;
}
//Output File
fp_out = fopen(filepath_out, "wb");
if (!fp_out) {
printf("Could not open output YUV file\n");
return -1;
}
pFrame = av_frame_alloc();
// 把packet的參數設爲默認值,要求packet的內存已經分配好了
av_init_packet(&packet);
while (1) {
// 獲取視頻文件的總長度
cur_size = fread(in_buffer, 1, in_buffer_size, fp_in);
if (cur_size == 0)
break;
cur_ptr=in_buffer;
while (cur_size>0){
/**
* 解析數據獲得一個Packet, 從輸入的數據流中分離出一幀一幀的壓縮編碼數據
* Parse a packet.
*
* @param s parser context.
* @param avctx codec context.
* @param poutbuf set to pointer to parsed buffer or NULL if not yet finished.
* @param poutbuf_size set to size of parsed buffer or zero if not yet finished.
* @param buf input buffer.
* @param buf_size input length, to signal EOF, this should be 0 (so that the last frame can be output).
* @param pts input presentation timestamp.
* @param dts input decoding timestamp.
* @param pos input byte position in stream.
* @return the number of bytes of the input bitstream used.
*
* Example:
* @code
* while(in_len){
* len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
* in_data, in_len,
* pts, dts, pos);
* in_data += len;
* in_len -= len;
*
* if(size)
* decode_frame(data, size);
* }
* @endcode
*
* 其中poutbuf指向解析後輸出的壓縮編碼數據幀,buf指向輸入的壓縮編碼數據。
* 如果函數執行完後輸出數據爲空(poutbuf_size爲0),則代表解析還沒有完成,還需要再次調用av_parser_parse2()解析一部分數據纔可以得到解析後的數據幀。
* 當函數執行完後輸出數據不爲空的時候,代表解析完成,可以將poutbuf中的這幀數據取出來做後續處理。
*/
int len = av_parser_parse2(
pCodecParserCtx, pCodecCtx,
&packet.data, &packet.size,
cur_ptr , cur_size ,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);
cur_ptr += len;
cur_size -= len;
// 如果函數執行完後輸出數據爲空(poutbuf_size爲0),則代表解析還沒有完成,還需要再次調用av_parser_parse2()解析一部分數據纔可以得到解析後的數據幀。
if(packet.size==0)
continue;
//Some Info from AVCodecParserContext
printf("[Packet]Size:%6d\t",packet.size);
switch(pCodecParserCtx->pict_type){
case AV_PICTURE_TYPE_I: printf("Type:I\t");break;
case AV_PICTURE_TYPE_P: printf("Type:P\t");break;
case AV_PICTURE_TYPE_B: printf("Type:B\t");break;
default: printf("Type:Other\t");break;
}
printf("Number:%4d\n",pCodecParserCtx->output_picture_number);
// 解碼一幀視頻數據:輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼後的結構體AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
if (ret < 0) {
printf("Decode Error.\n");
return ret;
}
if (got_picture) {
if(first_time){
printf("\nCodec Full Name:%s\n",pCodecCtx->codec->long_name);
printf("width:%d\nheight:%d\n\n",pCodecCtx->width,pCodecCtx->height);
first_time=0;
}
//Y, U, V
for(int i=0;i<pFrame->height;i++){
fwrite(pFrame->data[0]+pFrame->linesize[0]*i,1,pFrame->width,fp_out);
}
for(int i=0;i<pFrame->height/2;i++){
fwrite(pFrame->data[1]+pFrame->linesize[1]*i,1,pFrame->width/2,fp_out);
}
for(int i=0;i<pFrame->height/2;i++){
fwrite(pFrame->data[2]+pFrame->linesize[2]*i,1,pFrame->width/2,fp_out);
}
printf("Succeed to decode 1 frame!\n");
}
}
}
//Flush Decoder
packet.data = NULL;
packet.size = 0;
while(1){
// 解碼一幀視頻數據:輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼後的結構體AVFrame
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
if (ret < 0) {
printf("Decode Error.\n");
return ret;
}
if (!got_picture){
break;
}else {
//Y, U, V
for(int i=0;i<pFrame->height;i++){
fwrite(pFrame->data[0]+pFrame->linesize[0]*i,1,pFrame->width,fp_out);
}
for(int i=0;i<pFrame->height/2;i++){
fwrite(pFrame->data[1]+pFrame->linesize[1]*i,1,pFrame->width/2,fp_out);
}
for(int i=0;i<pFrame->height/2;i++){
fwrite(pFrame->data[2]+pFrame->linesize[2]*i,1,pFrame->width/2,fp_out);
}
printf("Flush Decoder: Succeed to decode 1 frame!\n");
}
}
// close and free
fclose(fp_in);
fclose(fp_out);
av_parser_close(pCodecParserCtx);
av_frame_free(&pFrame);
avcodec_close(pCodecCtx);
av_free(pCodecCtx);
return 0;
}