背景
哈囉街貓移動團隊在支撐業務發展過程中,已有的多媒體基礎能力存在一些問題/瓶頸:
- 貓屋直播 - 三方直播sdk,在MTK芯片的機型上存在兼容問題(hevc硬解報錯),導致直播流無法播放,用户無法使用app的核心功能
- 音視頻流合成、濾鏡 - 需要能夠靈活的支持用户去觸發對貓屋直播流的截取、合成、添加濾鏡等,使用系統多媒體Api,在可擴展性,流處理效率,兼容性,以及濾鏡的支持上,都存在問題
- 視頻轉碼 - 貓友圈上傳的用户視頻,需要對其做轉碼後發佈,轉碼最大的問題是,源視頻的格式和參數是不確定的(Android),如果只用系統多媒體Api去處理,會存在各種各樣的兼容性問題,結果要麼是轉碼失敗,要麼視頻發佈後,其他端的用户觀看異常
所以需要自研多媒體框架去解決/優化上述問題,以便後續能夠更好的支撐業務發展。
街貓自研多媒體架構
硬編解碼
支持Android 7.0以後,將media codec部分從media player service裏抽離出來,單獨開了一條新的binder服務media.codec來開放系統的硬編解碼能力,系統要開放,必須要有標準,讓各個硬件平台根據標準來開發其硬編解碼器,然後集成到media.codec中,這個協議就是OpenMax。
OpenMax分為三層:
- DL - 開發層
- IL - 集成層
- AL- 應用層
OpenMax是多媒體框架標準,目前應用最廣泛的是IL層,各個硬件平台只需要依託IL層協議,提供統一的抽象層接口,屏蔽各自在底層適配時存在的差異,最終打包到libstagefrighthw.so,由media codec service加載後,以binder服務的形式,將硬編解碼能力開放給有需要的多媒體應用,包括OpenMax AL,ffmpeg,nuplayer,exoplayer,街貓多媒體組件等等。
多媒體底層框架 - FFMPEG
街貓多媒體底層依託於ffmpeg,從而具備了覆蓋多媒體全應用場景的底層能力,基於ffmpeg 4.2.2源碼,我們目前主要做了如下定製化:
- 修改編解碼配置,使ffmpeg支持flv&h265視頻流
- 添加h264&h265編解碼codec,依託Android Ndk MediaCodec Api,使ffmpeg具備h264&h265格式的硬編解碼能力
- 添加h264軟編碼codec,使ffmpeg具備h264軟編碼能力
自研多媒體組件
| 組件名稱 | 描述 |
|---|---|
| 街貓多媒體核心組件 | 包含街貓多媒體基礎java和c++代碼實現, 包含兩個核心的c++庫:1. libpet-media-core.so 包含ffmpeg4.2.2的代碼和街貓多媒體轉碼,直播,視頻合成的核心實現(平台無關),2. libpet-media-compat.so android和ios的代理庫,包含android和ios平台兼容性c++實現 |
| 街貓多媒體轉碼組件 | 依賴多媒體核心組件,包含轉碼的java層實現核心能力: 支持全格式轉碼,優先使用硬編碼,如果設備不支持,則自動降級為軟編碼 支持碼率,分辨率,fps,gop等參數配置 支持對源視頻指定區間轉碼 轉碼後視頻編碼格式默認為h264,音頻aac 轉碼完成後,對生成視頻做有效性校驗,確保轉碼符合要求 轉碼後MP4視頻全部moov前置 |
| 街貓多媒體直播組件 | 依賴多媒體核心組件,包含flv直播流的java層實現,核心能力: 支持flv hevc格式的直播流的播放,支持軟硬解碼動態切換,相對三方庫純硬解碼,具有更好的兼容性, api的設計跟三方sdk完全保持一致,業務層無縫接入 |
| 街貓多媒體視頻合成組件 | 依賴多媒體核心組件,包含音視頻合成的java層實現, 核心能力: 支持輸入視頻和音頻數據流,合成h264編碼格式的mp4文件 視頻格式支持yuv420p&nv12(格式可擴展) 音頻輸入pcm數據,支持Packed和Planar兩種格式,也可不設置音頻(合成視頻無聲音) 支持添加logo和名稱水印濾鏡(濾鏡可擴展) 支持配置合成視頻的片尾視頻 合成視頻的編碼格式,碼率、軟硬編碼等可配置 |
業務成果
街貓轉碼
- 將轉碼覆蓋率從原先的50%以下,提高到99%以上,統一轉碼後的視頻格式,通過轉碼後視頻格式的有效性校驗,確保99%可播放,從而徹底解決5+線上遺留問題
- 覆蓋率提高後,視頻的平均壓縮率提高30+pp,降低了雲端存儲和流量費用,提高用户的觀看體驗
- 轉碼後視頻moov全部前置,提高轉碼後視頻的秒開率
街貓直播
- 完美解決了由於三方播放器兼容性不足導致部分機型無法播放的問題,播放率提高5+pp
街貓合成
- 合成和水印能力,深度應用於投喂一刻和直播間聊天視頻錄製,功能上線後,大幅提高了內容點擊率,貓友圈轉發率和發言提升率
FFMPEG介紹
核心庫
- libavcodec - 包含音頻和視頻的編解碼器
- libavutil - 包含編碼所需的各種使用工具,包括隨機數生成,常用數據結構,多媒體相關基礎工具等等
- libavformat - 包含封裝和解封裝的實現
- libavfilter - 包含多媒體濾鏡的實現
- libavdevice - 包含設備輸入和輸出相關實現
- libswscale - 包含視頻格式轉碼實現
- libswresample - 包含音頻重採樣的實現
音視頻播放流程
上圖是視頻播放器的基本流程,source、demux、decoder、output
- source:數據源,數據的來源不一定都是本地file,也有可能是網路上的各種協議例如:http、rtsp、HLS等。source的任務就是把數據源抽象出來,為下一個demux模塊提供它需要的穩定的數據流。demux不用關信數據到底是從什麼地方來的。
- demux解複用:視頻文件一般情況下都是把音視頻的encoded streams交織的通過某種規則放在一起。這種規則就是容器規則。現在有很多不同的容器格式。如ts、mp4、flv、mkv、avi、rmvb等等。demux的功能就是把音視頻的ES流從容器中剝離出來,然後分別送到不同的解碼器中。其實音頻和視頻本身就是2個獨立的系統。容器把它們包在了一起。但是他們都是獨立解碼的,所以解碼之前,需要把它分別 獨立出來。demux就是幹這活的,他為下一步decoder解碼提供了數據流。
- decoder解碼:解碼器 - 播放器的核心模塊,分為音頻和視頻解碼器。影像在錄製後, 原始的音視頻都是佔用大量空間, 而且是冗餘度較高的數據. 因此, 通常會在製作的時候就會進行某種壓縮 ( 壓縮技術就是將數據中的冗餘信息去除數據之間的相關性 ). 這就是我們熟知的音視頻編碼格式, 包括MPEG1(VCD)\ MPEG2(DVD)\ MPEG4 \ H.264 等等. 音視頻解碼器的作用就是把這些壓縮了的數據還原成原始的音視頻數據. 當然, 編碼解碼過程基本上都是有損的 .解碼器的作用就是把編碼後的數據還原成原始數據。
- output輸出:輸出部分分為音頻和視頻輸出。解碼後的音頻(pcm)和視頻(yuv)的原始數據需要得到音視頻的output模塊的支持才能真正的讓人的感官系統(眼和耳)辨識到。
市面上絕大多數播放器的基本結構都是如此,不同的是在實現方式上會存在差異。
一個簡單的FFMPEG工程
下面拿ffmpeg/examples/transcoding.c做介紹,這是一個轉碼的參考工程:
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
static AVFormatContext *ifmt_ctx;
static AVFormatContext *ofmt_ctx;
typedef struct FilteringContext {
AVFilterContext *buffersink_ctx;
AVFilterContext *buffersrc_ctx;
AVFilterGraph *filter_graph;
} FilteringContext;
static FilteringContext *filter_ctx;
typedef struct StreamContext {
AVCodecContext *dec_ctx;
AVCodecContext *enc_ctx;
} StreamContext;
static StreamContext *stream_ctx;
static int open_input_file(const char *filename)
{
int ret;
unsigned int i;
ifmt_ctx = NULL;
if ((ret = avformat_open_input(&ifmt_ctx, filename, NULL, NULL)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
return ret;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
return ret;
}
stream_ctx = av_mallocz_array(ifmt_ctx->nb_streams, sizeof(*stream_ctx));
if (!stream_ctx)
return AVERROR(ENOMEM);
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
AVStream *stream = ifmt_ctx->streams[i];
AVCodec *dec = avcodec_find_decoder(stream->codecpar->codec_id);
AVCodecContext *codec_ctx;
if (!dec) {
av_log(NULL, AV_LOG_ERROR, "Failed to find decoder for stream #%u\n", i);
return AVERROR_DECODER_NOT_FOUND;
}
codec_ctx = avcodec_alloc_context3(dec);
if (!codec_ctx) {
av_log(NULL, AV_LOG_ERROR, "Failed to allocate the decoder context for stream #%u\n", i);
return AVERROR(ENOMEM);
}
ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to copy decoder parameters to input decoder context "
"for stream #%u\n", i);
return ret;
}
/* Reencode video & audio and remux subtitles etc. */
if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO
|| codec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
codec_ctx->framerate = av_guess_frame_rate(ifmt_ctx, stream, NULL);
/* Open decoder */
ret = avcodec_open2(codec_ctx, dec, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to open decoder for stream #%u\n", i);
return ret;
}
}
stream_ctx[i].dec_ctx = codec_ctx;
}
av_dump_format(ifmt_ctx, 0, filename, 0);
return 0;
}
static int open_output_file(const char *filename)
{
AVStream *out_stream;
AVStream *in_stream;
AVCodecContext *dec_ctx, *enc_ctx;
AVCodec *encoder;
int ret;
unsigned int i;
ofmt_ctx = NULL;
avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, filename);
if (!ofmt_ctx) {
av_log(NULL, AV_LOG_ERROR, "Could not create output context\n");
return AVERROR_UNKNOWN;
}
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
out_stream = avformat_new_stream(ofmt_ctx, NULL);
if (!out_stream) {
av_log(NULL, AV_LOG_ERROR, "Failed allocating output stream\n");
return AVERROR_UNKNOWN;
}
in_stream = ifmt_ctx->streams[i];
dec_ctx = stream_ctx[i].dec_ctx;
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO
|| dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
/* in this example, we choose transcoding to same codec */
encoder = avcodec_find_encoder(dec_ctx->codec_id);
if (!encoder) {
av_log(NULL, AV_LOG_FATAL, "Necessary encoder not found\n");
return AVERROR_INVALIDDATA;
}
enc_ctx = avcodec_alloc_context3(encoder);
if (!enc_ctx) {
av_log(NULL, AV_LOG_FATAL, "Failed to allocate the encoder context\n");
return AVERROR(ENOMEM);
}
/* In this example, we transcode to same properties (picture size,
* sample rate etc.). These properties can be changed for output
* streams easily using filters */
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
enc_ctx->height = dec_ctx->height;
enc_ctx->width = dec_ctx->width;
enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio;
/* take first format from list of supported formats */
if (encoder->pix_fmts)
enc_ctx->pix_fmt = encoder->pix_fmts[0];
else
enc_ctx->pix_fmt = dec_ctx->pix_fmt;
/* video time_base can be set to whatever is handy and supported by encoder */
enc_ctx->time_base = av_inv_q(dec_ctx->framerate);
} else {
enc_ctx->sample_rate = dec_ctx->sample_rate;
enc_ctx->channel_layout = dec_ctx->channel_layout;
enc_ctx->channels = av_get_channel_layout_nb_channels(enc_ctx->channel_layout);
/* take first format from list of supported formats */
enc_ctx->sample_fmt = encoder->sample_fmts[0];
enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate};
}
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
/* Third parameter can be used to pass settings to encoder */
ret = avcodec_open2(enc_ctx, encoder, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open video encoder for stream #%u\n", i);
return ret;
}
ret = avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder parameters to output stream #%u\n", i);
return ret;
}
out_stream->time_base = enc_ctx->time_base;
stream_ctx[i].enc_ctx = enc_ctx;
} else if (dec_ctx->codec_type == AVMEDIA_TYPE_UNKNOWN) {
av_log(NULL, AV_LOG_FATAL, "Elementary stream #%d is of unknown type, cannot proceed\n", i);
return AVERROR_INVALIDDATA;
} else {
/* if this stream must be remuxed */
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Copying parameters for stream #%u failed\n", i);
return ret;
}
out_stream->time_base = in_stream->time_base;
}
}
av_dump_format(ofmt_ctx, 0, filename, 1);
if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&ofmt_ctx->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", filename);
return ret;
}
}
/* init muxer, write output file header */
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error occurred when opening output file\n");
return ret;
}
return 0;
}
static int init_filter(FilteringContext* fctx, AVCodecContext *dec_ctx,
AVCodecContext *enc_ctx, const char *filter_spec)
{
char args[512];
int ret = 0;
const AVFilter *buffersrc = NULL;
const AVFilter *buffersink = NULL;
AVFilterContext *buffersrc_ctx = NULL;
AVFilterContext *buffersink_ctx = NULL;
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
AVFilterGraph *filter_graph = avfilter_graph_alloc();
if (!outputs || !inputs || !filter_graph) {
ret = AVERROR(ENOMEM);
goto end;
}
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
buffersrc = avfilter_get_by_name("buffer");
buffersink = avfilter_get_by_name("buffersink");
if (!buffersrc || !buffersink) {
av_log(NULL, AV_LOG_ERROR, "filtering source or sink element not found\n");
ret = AVERROR_UNKNOWN;
goto end;
}
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt,
dec_ctx->time_base.num, dec_ctx->time_base.den,
dec_ctx->sample_aspect_ratio.num,
dec_ctx->sample_aspect_ratio.den);
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
goto end;
}
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
NULL, NULL, filter_graph);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
goto end;
}
ret = av_opt_set_bin(buffersink_ctx, "pix_fmts",
(uint8_t*)&enc_ctx->pix_fmt, sizeof(enc_ctx->pix_fmt),
AV_OPT_SEARCH_CHILDREN);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
goto end;
}
} else if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
buffersrc = avfilter_get_by_name("abuffer");
buffersink = avfilter_get_by_name("abuffersink");
if (!buffersrc || !buffersink) {
av_log(NULL, AV_LOG_ERROR, "filtering source or sink element not found\n");
ret = AVERROR_UNKNOWN;
goto end;
}
if (!dec_ctx->channel_layout)
dec_ctx->channel_layout =
av_get_default_channel_layout(dec_ctx->channels);
snprintf(args, sizeof(args),
"time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64,
dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_rate,
av_get_sample_fmt_name(dec_ctx->sample_fmt),
dec_ctx->channel_layout);
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer source\n");
goto end;
}
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
NULL, NULL, filter_graph);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer sink\n");
goto end;
}
ret = av_opt_set_bin(buffersink_ctx, "sample_fmts",
(uint8_t*)&enc_ctx->sample_fmt, sizeof(enc_ctx->sample_fmt),
AV_OPT_SEARCH_CHILDREN);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot set output sample format\n");
goto end;
}
ret = av_opt_set_bin(buffersink_ctx, "channel_layouts",
(uint8_t*)&enc_ctx->channel_layout,
sizeof(enc_ctx->channel_layout), AV_OPT_SEARCH_CHILDREN);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot set output channel layout\n");
goto end;
}
ret = av_opt_set_bin(buffersink_ctx, "sample_rates",
(uint8_t*)&enc_ctx->sample_rate, sizeof(enc_ctx->sample_rate),
AV_OPT_SEARCH_CHILDREN);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot set output sample rate\n");
goto end;
}
} else {
ret = AVERROR_UNKNOWN;
goto end;
}
/* Endpoints for the filter graph. */
outputs->name = av_strdup("in");
outputs->filter_ctx = buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL;
inputs->name = av_strdup("out");
inputs->filter_ctx = buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL;
if (!outputs->name || !inputs->name) {
ret = AVERROR(ENOMEM);
goto end;
}
if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_spec,
&inputs, &outputs, NULL)) < 0)
goto end;
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
goto end;
/* Fill FilteringContext */
fctx->buffersrc_ctx = buffersrc_ctx;
fctx->buffersink_ctx = buffersink_ctx;
fctx->filter_graph = filter_graph;
end:
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return ret;
}
static int init_filters(void)
{
const char *filter_spec;
unsigned int i;
int ret;
filter_ctx = av_malloc_array(ifmt_ctx->nb_streams, sizeof(*filter_ctx));
if (!filter_ctx)
return AVERROR(ENOMEM);
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
filter_ctx[i].buffersrc_ctx = NULL;
filter_ctx[i].buffersink_ctx = NULL;
filter_ctx[i].filter_graph = NULL;
if (!(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO
|| ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO))
continue;
if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
filter_spec = "null"; /* passthrough (dummy) filter for video */
else
filter_spec = "anull"; /* passthrough (dummy) filter for audio */
ret = init_filter(&filter_ctx[i], stream_ctx[i].dec_ctx,
stream_ctx[i].enc_ctx, filter_spec);
if (ret)
return ret;
}
return 0;
}
static int encode_write_frame(AVFrame *filt_frame, unsigned int stream_index, int *got_frame) {
int ret;
int got_frame_local;
AVPacket enc_pkt;
int (*enc_func)(AVCodecContext *, AVPacket *, const AVFrame *, int *) =
(ifmt_ctx->streams[stream_index]->codecpar->codec_type ==
AVMEDIA_TYPE_VIDEO) ? avcodec_encode_video2 : avcodec_encode_audio2;
if (!got_frame)
got_frame = &got_frame_local;
av_log(NULL, AV_LOG_INFO, "Encoding frame\n");
/* encode filtered frame */
enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);
ret = enc_func(stream_ctx[stream_index].enc_ctx, &enc_pkt,
filt_frame, got_frame);
av_frame_free(&filt_frame);
if (ret < 0)
return ret;
if (!(*got_frame))
return 0;
/* prepare packet for muxing */
enc_pkt.stream_index = stream_index;
av_packet_rescale_ts(&enc_pkt,
stream_ctx[stream_index].enc_ctx->time_base,
ofmt_ctx->streams[stream_index]->time_base);
av_log(NULL, AV_LOG_DEBUG, "Muxing frame\n");
/* mux encoded frame */
ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
return ret;
}
static int filter_encode_write_frame(AVFrame *frame, unsigned int stream_index)
{
int ret;
AVFrame *filt_frame;
av_log(NULL, AV_LOG_INFO, "Pushing decoded frame to filters\n");
/* push the decoded frame into the filtergraph */
ret = av_buffersrc_add_frame_flags(filter_ctx[stream_index].buffersrc_ctx,
frame, 0);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error while feeding the filtergraph\n");
return ret;
}
/* pull filtered frames from the filtergraph */
while (1) {
filt_frame = av_frame_alloc();
if (!filt_frame) {
ret = AVERROR(ENOMEM);
break;
}
av_log(NULL, AV_LOG_INFO, "Pulling filtered frame from filters\n");
ret = av_buffersink_get_frame(filter_ctx[stream_index].buffersink_ctx,
filt_frame);
if (ret < 0) {
/* if no more frames for output - returns AVERROR(EAGAIN)
* if flushed and no more frames for output - returns AVERROR_EOF
* rewrite retcode to 0 to show it as normal procedure completion
*/
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
ret = 0;
av_frame_free(&filt_frame);
break;
}
filt_frame->pict_type = AV_PICTURE_TYPE_NONE;
ret = encode_write_frame(filt_frame, stream_index, NULL);
if (ret < 0)
break;
}
return ret;
}
static int flush_encoder(unsigned int stream_index)
{
int ret;
int got_frame;
if (!(stream_ctx[stream_index].enc_ctx->codec->capabilities &
AV_CODEC_CAP_DELAY))
return 0;
while (1) {
av_log(NULL, AV_LOG_INFO, "Flushing stream #%u encoder\n", stream_index);
ret = encode_write_frame(NULL, stream_index, &got_frame);
if (ret < 0)
break;
if (!got_frame)
return 0;
}
return ret;
}
int main(int argc, char **argv)
{
int ret;
AVPacket packet = { .data = NULL, .size = 0 };
AVFrame *frame = NULL;
enum AVMediaType type;
unsigned int stream_index;
unsigned int i;
int got_frame;
int (*dec_func)(AVCodecContext *, AVFrame *, int *, const AVPacket *);
if (argc != 3) {
av_log(NULL, AV_LOG_ERROR, "Usage: %s <input file> <output file>\n", argv[0]);
return 1;
}
if ((ret = open_input_file(argv[1])) < 0)
goto end;
if ((ret = open_output_file(argv[2])) < 0)
goto end;
if ((ret = init_filters()) < 0)
goto end;
/* read all packets */
while (1) {
if ((ret = av_read_frame(ifmt_ctx, &packet)) < 0)
break;
stream_index = packet.stream_index;
type = ifmt_ctx->streams[packet.stream_index]->codecpar->codec_type;
av_log(NULL, AV_LOG_DEBUG, "Demuxer gave frame of stream_index %u\n",
stream_index);
if (filter_ctx[stream_index].filter_graph) {
av_log(NULL, AV_LOG_DEBUG, "Going to reencode&filter the frame\n");
frame = av_frame_alloc();
if (!frame) {
ret = AVERROR(ENOMEM);
break;
}
av_packet_rescale_ts(&packet,
ifmt_ctx->streams[stream_index]->time_base,
stream_ctx[stream_index].dec_ctx->time_base);
dec_func = (type == AVMEDIA_TYPE_VIDEO) ? avcodec_decode_video2 :
avcodec_decode_audio4;
ret = dec_func(stream_ctx[stream_index].dec_ctx, frame,
&got_frame, &packet);
if (ret < 0) {
av_frame_free(&frame);
av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
break;
}
if (got_frame) {
frame->pts = frame->best_effort_timestamp;
ret = filter_encode_write_frame(frame, stream_index);
av_frame_free(&frame);
if (ret < 0)
goto end;
} else {
av_frame_free(&frame);
}
} else {
/* remux this frame without reencoding */
av_packet_rescale_ts(&packet,
ifmt_ctx->streams[stream_index]->time_base,
ofmt_ctx->streams[stream_index]->time_base);
ret = av_interleaved_write_frame(ofmt_ctx, &packet);
if (ret < 0)
goto end;
}
av_packet_unref(&packet);
}
/* flush filters and encoders */
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
/* flush filter */
if (!filter_ctx[i].filter_graph)
continue;
ret = filter_encode_write_frame(NULL, i);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Flushing filter failed\n");
goto end;
}
/* flush encoder */
ret = flush_encoder(i);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Flushing encoder failed\n");
goto end;
}
}
av_write_trailer(ofmt_ctx);
end:
av_packet_unref(&packet);
av_frame_free(&frame);
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
avcodec_free_context(&stream_ctx[i].dec_ctx);
if (ofmt_ctx && ofmt_ctx->nb_streams > i && ofmt_ctx->streams[i] && stream_ctx[i].enc_ctx)
avcodec_free_context(&stream_ctx[i].enc_ctx);
if (filter_ctx && filter_ctx[i].filter_graph)
avfilter_graph_free(&filter_ctx[i].filter_graph);
}
av_free(filter_ctx);
av_free(stream_ctx);
avformat_close_input(&ifmt_ctx);
if (ofmt_ctx && !(ofmt_ctx->oformat->flags & AVFMT_NOFILE))
avio_closep(&ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret < 0)
av_log(NULL, AV_LOG_ERROR, "Error occurred: %s\n", av_err2str(ret));
return ret ? 1 : 0;
}
代碼核心流程介紹:
- 調用open_input_file打開輸入文件,先調用avformat_open_input進行解封裝,然後調用avformat_find_stream_info判斷是否存在音視頻流信息,如果存在,則根據video/audio stream的codec id,調用avcodec_find_decoder->avcodec_alloc_context3->avcodec_open2完成音視頻解碼器的創建
- 接着調用open_output_file,先調用avformat_alloc_output_context2創建mux的上下文,然後根據input stream info,調用avformat_new_stream創建對應的輸出流,接着再調用avcodec_find_encoder->avcodec_alloc_context3->avcodec_open2創建對應的編碼器(上述例子中,mux output stream codec id跟輸入是保持一致,實際轉碼可以不一致)
- 不管是input AVStream還是output AVStream,它們跟對應的編解碼器(AVCodecContext)其實是沒有強關聯的,但是在創建編解codec之前,需要調用avcodec_parameters_from_context,將stream->codecpar和AVCodecContext中編解碼器的配置參數做下同步
- 調用init_filters創建濾鏡(濾鏡的創建下面再介紹)
- 調用av_read_frame讀取待解碼的原始幀數據,接着調用avcodec_decode_video2/avcodec_decode_audio4進行解碼
- 如果存在filter graph,則將解碼後的AVFrame傳入filter graph添加濾鏡
- 再調用avcodec_encode_video2/avcodec_encode_audio2對數據幀進行編碼
- 編碼完成後,再調用av_packet_rescale_ts設置AVPacket的time stamp
- 最後調用av_interleaved_write_frame寫入output file中
幀數據存儲
ffmpeg使用AVPacket來存儲編碼的幀數據,解碼後的音視頻幀數據,統一使用AVFrame來存儲。
音頻幀數據存儲音頻解碼後的pcm數據,在ffmpeg內部有Packed和Planar兩種存儲方式:
- Packed: L R L R L R L
- RPlanar: L L L L R R R
RPacked格式,frame.data[0]或frame.extended_data[0]包含AVFrame保存的所有pcm數據
Planar格式,frame.data[i]或者frame.extended_data[i]表示第i個聲道的數據
AVFrame.data數組大小固定為8,如果聲道數超過8,需要從frame.extended_data獲取聲道數據, extended_data是為了支持更多的聲道數,後期擴展的字段。
Planar是ffmpeg內部的數據格式,常規的都為Packed,從命名上,Planar一般都在Packed命名後+P,比如sample format為16bit,Packed命名:AV_SAMPLE_FMT_S16,Planar:AV_SAMPLE_FMT_S16P。
除了data/extended_data,AVFrame保存音頻數據其他四個核心字段:format(AVSampleFormat),sample_rate,channel_layout, nb_samples。
format - 指的是單幀的存儲格式,可以通過該值來確定是packed還是planar,以及存儲大小,16bit或float等
sample_rate - 採樣率
channel_layout - 聲道數
nb_samples - AVFrame中包含的採樣數(單聲道)
所以,我們通過sample_rate和nb_samples就可以得出AVFrame包含pcm數據的播放時長,通過format channel_layout nb_samples就可以得出AVFrame中buffer的長度
**視頻幀數據存儲
**視頻幀數據採用YUV格式,其中Y指亮度通道,UV指色度通道,主流的採樣格式有:YUV444、YUV422、YUV420
YUV4::, 簡單點理解就是,以4個像素為一採樣組,每個像素固定有一個Y通道,YUV後面UV對應的數值,以2為單位對應一組UV色度通道,注意,UV是一組,不要將其分開,基於這個去理解YUV4:2:2和YUV4:2:0採樣方式,會更容易點
對於大分辨率的視頻幀,相鄰像素的色度通道差異是極小的,所以YUV420格式在保證圖像質量的情況下,又大幅的降低了存儲,是目前主流的幀格式,接下去重點介紹下YUV420在ffmpeg,即AVFrame裏的存儲,YUV420根據Y,U,V數據的存儲方式,又細分出YUV420P,YUV420SP(NV12)等子格式
YUV420P: YYYYYYYY UU VV
Y分量、U分量、V分量分別佔一個平面空間,4個像素的Y分量共用一個UV分量
YUV420SP: YYYYYYYY UU VV
Y分量佔一個平面空間,UV交差存儲佔一個平面空間,4個像素的Y分量共用一個UV分量
二者的差異,就是UV分量的存儲方式,AVFrame保存YUV數據,主要用
uint8_t *data[AV_NUM_DATA_POINTERS];
int linesize[AV_NUM_DATA_POINTERS];
data保存平面對應的向量數據,linesize保存平面對應向量數據的長度,所以yuv420p有三個平面,data數組有效長度為3,yuv420sp只有兩個平面,有效長度就只有2。
時間基 - TimeBase
ffmpeg音視頻處理,有一個很重要的概念,那就是時間基,時間基本質是時間刻度,ffmpeg內部用到的時間戳都是要與對應的time base換算才能拿到準確時間的,ffmpeg內部主要有三種類型的time base
- tbr - 表示幀率,tbr往往跟fps相同,比如幀率25,time base即為 1/25
- tbn - 表示視頻流(AVStream) 的timebase,在解碼時,從視頻source demux後,可以拿到video container的stream time base,作為後續視頻解碼和seek等操作時的基準時間;在編碼時,AVPacket在寫入文件前,必須要正確的設置AVStream的time base和pts,要不編碼完成的視頻播放會異常
- tbc - 表示視頻流 codec timebase,編碼時,需要設置codec context的time base
有了時間基和時間戳,可以很容易計算出對應的時間,比如時間戳50,時間基1/25
50 * 1 / 25 = 2s
ffmpeg也提供了輔助計算函數:
timestamp(秒) = pts * av_q2d(time_base)
不同時間基之間換算:
av_rescale_q
對AVPacket內部時間基的換算:
av_packet_rescale_ts
濾鏡 - Filter
ffmpeg內部filter graph處理流程
filter graph建立後,會監聽source filter的source buffer,如果有AVFrame塞入,filter graph就開始運作,通過filter chain處理完後,從sink filter的sink buffer中取出,格式為AVFrame。
上述工程裏initFilter代碼流程:
- 先調用avfilter_graph_alloc創建filter graph
- 接着調用avfilter_get_by_name創建命名的filter
- 接着基於創建的filter,調用avfilter_graph_create_filter創建AVFilterContext,上下文會關聯filter和filter graph等其他信息
- 最後調用avfilter_graph_parse_ptr,將通過filter spec創建的filter以及咱們創建的input和output filter插入到filter graph
然後在幀處理的時候:
- 調用av_buffersrc_add_frame_flags將待處理幀塞入buffersrc_ctx,觸發filter graph開發工作
- 接着調用av_buffersink_get_frame從buffersink_ctx處理完成的幀數據
步驟2和3創建了source和sink filter,work filter則是使用avfilter_graph_parse_ptr傳入filter spec,ffmpeg會解析spec創建內置的filter,當然,我們也可以實現自定義的filter塞入到filte graph中。
下面是添加水印的filter spec:
static const char* filters[] = {
"movie=/sdcard/0/pet_logo.png[watermark];[in][watermark]overlay=main_w-overlay_w-10:main_h-overlay_h-10[out]"};
filter spec更多介紹,可以去官網查看:https://ffmpeg.org/ffmpeg-filters.html
(本文作者:胡付義)