前言
在我們觀看電影或者抖音等短視頻平臺(tái)的視頻時(shí)一般都會(huì)出現(xiàn)字幕毅戈,有了字幕那視頻的表現(xiàn)形式就更加豐富了江场,所以為一段視頻添加字幕也是一個(gè)硬需求。本文的目的就是為一段視頻添加字幕蟹演,了解如何添加字幕前先了解下字幕的類(lèi)型:
外掛字幕
外掛字幕是一個(gè)單獨(dú)的外部字幕文件阴颖,格式類(lèi)型一般有srt、vtt云头、ass等等捐友。播放視頻時(shí),需要把外掛字幕和視頻放在同一目錄下溃槐,并在播放器中選擇字幕文件才可以在視頻中看到字幕匣砖。軟字幕
軟字幕也叫內(nèi)掛字幕、封裝字幕、內(nèi)封字幕猴鲫,字幕流等对人,就是把前面的外掛字幕的字幕文件嵌入到視頻中作為流的一部分,如果一個(gè)視頻有多個(gè)字幕流那么播放視頻是還得選擇對(duì)應(yīng)的字幕流
備注:不管是外掛字幕還是軟字幕拂共,字幕要正常顯示播放器必須要支持字幕的渲染牺弄。
- 硬字幕
硬字幕就是嵌入到視頻幀里面的字幕,它就像視頻水印一樣作為視頻幀的一分部分了匣缘,不管再任何平臺(tái)字幕看起來(lái)都是一樣的猖闪,而且也不再要求播放器單獨(dú)對(duì)字母進(jìn)行渲染
總結(jié):
1、外掛字幕和軟字幕都要求播放器額外支持字幕的渲染肌厨,而硬字幕不需要培慌。外掛字幕和軟字幕可以隨時(shí)更換和取消字幕文件,而硬字幕則不可以取消和更改視頻中的字幕
2柑爸、如果是字幕流或者外掛字幕則還需要播放器支持字幕流的單獨(dú)渲染
3吵护、此外嵌入字幕流也需要容器格式支持,比如MKV格式就支持各種格式字幕文件表鳍,但是MP4對(duì)字幕的支持就不太好(只支持蘋(píng)果的MOV text)
常見(jiàn)字幕格式
不同的字幕文件有其對(duì)應(yīng)的格式(針對(duì)外掛字幕和軟字幕)馅而,常見(jiàn)的字幕格式有:
- SRT(標(biāo)準(zhǔn)外掛字幕格式):只包含文字和時(shí)間碼,沒(méi)有樣式譬圣,顯示效果由播放器決定瓮恭,不同的播放器顯示出的效果可能差別很大
- ASS(高級(jí)外掛字幕格式):支持樣式、字體厘熟、字幕定位屯蹦、淡入淡出、簡(jiǎn)單的特效绳姨。如果不缺字體登澜,不同的播放器顯示效果基本一致
- XML+PNG序列:用來(lái)導(dǎo)入Premiere、FCP7飘庄、Edius脑蠕、Vegas、AE跪削,不支持FCPX
Avid DS Cap字幕格式:AVID專(zhuān)用格式祟昭,導(dǎo)入后可以修改文字 - UTF(會(huì)聲會(huì)影專(zhuān)用格式):可以直接導(dǎo)入會(huì)聲會(huì)影使用
推薦一款字幕制作軟件Arctime双泪,下載地址传货,該軟件可以制作各種格式的字幕授霸,如下為各種字幕文件的格式:
ass字幕格式
ttxt字幕格式
srt字幕格式
ffmpeg字幕處理流程
ffmpeg命令行實(shí)現(xiàn)添加字幕
- 將字幕處理濾鏡編譯到ffmpeg
如果ffmpeg要實(shí)現(xiàn)添加字幕的功能需要在編譯時(shí)開(kāi)啟--enable-filter=subtitles --enable-libass
--enable-filter=subtitles 代表開(kāi)啟字幕濾鏡
--enable-libass 則是字幕濾鏡需要依賴(lài)的外部庫(kù),所以編譯時(shí)還需要指定該外部庫(kù)的路徑(如x264的編譯一樣)
libass是一個(gè)用來(lái)進(jìn)行字幕處理和渲染的開(kāi)源庫(kù)廓旬,地址https://github.com/libass/libass.git
完整編譯腳本參考:包含subtitles濾鏡的編譯腳本
- 添加軟字幕
ffmpeg -i test_1280x720_3.mp4 -i test_1280x720_3.srt -c copy output.mkv
添加軟字幕的原理和流程就跟給視頻添加音頻一樣哼审,這個(gè)過(guò)程不需要重新編解碼谐腰,所以速度非常快涩盾。
tips:軟字幕只有部分容器格式比如(mkv)才支持十气,MP4/MOV等不支持,而且也只有部分播放器支持軟字幕或者外掛字幕(如VLC播放器)
VLC播放器播放上面命令中合成的帶有軟字幕的mkv視頻
默認(rèn)VLC是關(guān)閉字幕的春霍,需要手動(dòng)打開(kāi)砸西。
輸入命令可以看到成功添加了軟字幕
ffprobe out.mkv
Input #0, matroska,webm, from '/Users/apple/devoloper/mine/ffmpeg/ffmpeg-demo/filesources/test_1280x720_3_Video_Export/out.mkv':
Metadata:
DESCRIPTION : Generated by Arctime Pro 2.4
ENCODER : Lavf58.31.101
Duration: 00:01:11.05, start: 0.000000, bitrate: 1435 kb/s
Stream #0:0: Video: mpeg4 (Simple Profile), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 49.97 fps, 49.97 tbr, 1k tbn, 26635 tbc (default)
Metadata:
ENCODER : Lavc58.55.100 mpeg4
DURATION : 00:01:11.046000000
Stream #0:1: Audio: ac3, 44100 Hz, stereo, fltp, 192 kb/s (default)
Metadata:
ENCODER : Lavc58.55.100 ac3
DURATION : 00:01:10.949000000
Stream #0:2: Subtitle: ass
Metadata:
ENCODER : Lavc58.55.100 ssa
DURATION : 00:00:18.406000000
- 字幕格式轉(zhuǎn)換
利用ffmpeg命令也可以實(shí)現(xiàn)字幕格式ass/srt/vtt等等的相互轉(zhuǎn)換
ffmpeg -i test_1280x720_3.srt test_1280x720_3_1.vtt
ffmpeg -i test_1280x720_3.srt test_1280x720_3_1.ass
- 添加硬字幕
ffmpeg -i test_1280x720_3.mkv -vf subtitles=test_1280x720_3.srt out.mp4
test_1280x720_3.srt代表要添加的字幕文件路徑,這里也可以寫(xiě)成其它格式字幕文件址儒,比如test_1280x720_3.ass,test_1280x720_3.ttext等等芹枷。ffmpeg最終都會(huì)將字幕格式先轉(zhuǎn)換成ass字幕流再將字幕嵌入到視頻幀中,這個(gè)過(guò)程需要重新編解碼莲趣,所以速度比較慢鸳慈。
輸入命令可以看到成功添加了硬字幕
ffprobe out.mp4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/apple/devoloper/mine/ffmpeg/ffmpeg-demo/filesources/test_1280x720_3_Video_Export/out.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2mp41
encoder : Lavf58.31.101
description : Generated by Arctime Pro 2.4
Duration: 00:01:11.06, start: 0.000000, bitrate: 1374 kb/s
Stream #0:0(und): Video: mpeg4 (Simple Profile) (mp4v / 0x7634706D), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 1238 kb/s, 49.97 fps, 49.97 tbr, 26635 tbn, 26635 tbc (default)
Metadata:
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s (default)
Metadata:
handler_name : SoundHandler
代碼方式實(shí)現(xiàn)添加字幕
- 1、添加軟字幕
void Subtitles::addSubtitleStream(string videopath, string spath, string dstpath)
{
if (dstpath.rfind(".mkv") != dstpath.length() - 4) {
LOGD("can only suport .mkv file");
return;
}
int ret = 0;
// 打開(kāi)視頻流
if (avformat_open_input(&vfmt,videopath.c_str(), NULL, NULL) < 0) {
LOGD("avformat_open_input failed");
return;
}
if (avformat_find_stream_info(vfmt, NULL) < 0) {
LOGD("avformat_find_stream_info");
releaseInternal();
return;
}
if ((avformat_alloc_output_context2(&ofmt, NULL, NULL, dstpath.c_str())) < 0) {
LOGD("avformat_alloc_output_context2() failed");
releaseInternal();
return;
}
int in_video_index = -1,in_audio_index = -1;
int ou_video_index = -1,ou_audio_index = -1,ou_subtitle_index = -1;
for (int i=0; i<vfmt->nb_streams; i++) {
AVStream *stream = vfmt->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
in_video_index = i;
AVStream *newstream = avformat_new_stream(ofmt, NULL);
avcodec_parameters_copy(newstream->codecpar, stream->codecpar);
newstream->codecpar->codec_tag = 0;
ou_video_index = newstream->index;
} else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
AVStream *newstream = avformat_new_stream(ofmt, NULL);
avcodec_parameters_copy(newstream->codecpar, stream->codecpar);
newstream->codecpar->codec_tag = 0;
in_audio_index = i;
ou_audio_index = newstream->index;
}
}
if (!(ofmt->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&ofmt->pb, dstpath.c_str(), AVIO_FLAG_WRITE) < 0) {
LOGD("avio_open failed");
releaseInternal();
return;
}
}
// 打開(kāi)字幕流
/** 遇到問(wèn)題:調(diào)用avformat_open_input()時(shí)提示"avformat_open_input failed -1094995529(Invalid data found when processing input)"
* 分析原因:編譯ffmpeg庫(kù)是沒(méi)有將對(duì)應(yīng)的字幕解析器添加進(jìn)去比如(ff_ass_demuxer,ff_ass_muxer)
* 解決方案:添加對(duì)應(yīng)的編譯參數(shù)
*/
if ((ret = avformat_open_input(&sfmt,spath.c_str(), NULL, NULL)) < 0) {
LOGD("avformat_open_input failed %d(%s)",ret,av_err2str(ret));
return;
}
if ((ret = avformat_find_stream_info(sfmt, NULL)) < 0) {
LOGD("avformat_find_stream_info %d(%s)",ret,av_err2str(ret));
releaseInternal();
return;
}
if((ret = av_find_best_stream(sfmt, AVMEDIA_TYPE_SUBTITLE, -1, -1, NULL, 0))<0){
LOGD("not find subtitle stream 0");
releaseInternal();
return;
}
AVStream *nstream = avformat_new_stream(ofmt, NULL);
ret = avcodec_parameters_copy(nstream->codecpar, sfmt->streams[0]->codecpar);
nstream->codecpar->codec_tag = 0;
/** todo:zsz AV_DISPOSITION_xxx:ffmpeg.c中該選項(xiàng)可以控制字幕默認(rèn)是否顯示喧伞,不過(guò)這里貌似不可以走芋,原因未知。
*/
// nstream->disposition = sfmt->streams[0]->disposition;
ou_subtitle_index = nstream->index;
if(avformat_write_header(ofmt, NULL)<0){
LOGD("avformat_write_header failed");
releaseInternal();
return;
}
av_dump_format(ofmt, 0, dstpath.c_str(), 1);
/** 遇到問(wèn)題:封裝后生成的mkv文件字幕無(wú)法顯示潘鲫,封裝時(shí)提示"[matroska @ 0x10381c000] Starting new cluster due to timestamp"
* 分析原因:通過(guò)和ffmpeg.c中源碼進(jìn)行比對(duì)翁逞,后發(fā)現(xiàn)mvk對(duì)字幕寫(xiě)入的順序有要求
* 解決方案:將字幕寫(xiě)入放到音視頻之前
*/
AVPacket *inpkt2 = av_packet_alloc();
while (av_read_frame(sfmt, inpkt2) >= 0) {
AVStream *srcstream = sfmt->streams[0];
AVStream *dststream = ofmt->streams[ou_subtitle_index];
av_packet_rescale_ts(inpkt2, srcstream->time_base, dststream->time_base);
inpkt2->stream_index = ou_subtitle_index;
inpkt2->pos = -1;
LOGD("pts %d",inpkt2->pts);
if (av_write_frame(ofmt, inpkt2) < 0) {
LOGD("subtitle av_write_frame failed");
releaseInternal();
return;
}
av_packet_unref(inpkt2);
}
AVPacket *inpkt = av_packet_alloc();
while (av_read_frame(vfmt, inpkt) >= 0) {
if (inpkt->stream_index == in_video_index) {
AVStream *srcstream = vfmt->streams[in_video_index];
AVStream *dststream = ofmt->streams[ou_video_index];
av_packet_rescale_ts(inpkt, srcstream->time_base, dststream->time_base);
inpkt->stream_index = ou_video_index;
LOGD("inpkt %d",inpkt->pts);
if (av_write_frame(ofmt, inpkt) < 0) {
LOGD("video av_write_frame failed");
releaseInternal();
return;
}
} else if (inpkt->stream_index == in_audio_index) {
AVStream *srcstream = vfmt->streams[in_audio_index];
AVStream *dststream = ofmt->streams[ou_audio_index];
av_packet_rescale_ts(inpkt, srcstream->time_base, dststream->time_base);
inpkt->stream_index = ou_audio_index;
if (av_write_frame(ofmt, inpkt) < 0) {
LOGD("audio av_write_frame failed");
releaseInternal();
return;
}
}
av_packet_unref(inpkt);
}
LOGD("over");
av_write_trailer(ofmt);
releaseInternal();
}
備注:
對(duì)于mkv的封裝和解封裝要開(kāi)啟ffmpeg的編譯參數(shù) --enable-muxer=matroska和--enable-demuxer=matroska
不同格式的字幕ass/srt寫(xiě)入文件后,當(dāng)用播放器打開(kāi)的時(shí)候字幕的大小以及位置也有區(qū)別
- 2溉仑、添加硬字幕
void Subtitles::addSubtitlesForVideo(string vpath, string spath, string dstpath,string confpath)
{
int ret = 0;
// 打開(kāi)視頻流
if (avformat_open_input(&vfmt,vpath.c_str(), NULL, NULL) < 0) {
LOGD("avformat_open_input failed");
return;
}
if (avformat_find_stream_info(vfmt, NULL) < 0) {
LOGD("avformat_find_stream_info");
releaseInternal();
return;
}
if((ret = avformat_alloc_output_context2(&ofmt, NULL, NULL, dstpath.c_str())) < 0) {
LOGD("avformat_alloc_output_context2 failed");
return;
}
for (int i=0; i<vfmt->nb_streams; i++) {
AVStream *sstream = vfmt->streams[i];
if (sstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
in_video_index = i;
// 添加新的視頻流
AVStream *nstream = avformat_new_stream(ofmt, NULL);
ou_video_index = nstream->index;
// 由于視頻需要添加字幕挖函,所以需要重新編解碼,但是編碼信息和源文件中一樣
AVCodec *codec = avcodec_find_decoder(sstream->codecpar->codec_id);
if (!codec) {
LOGD("not surport codec!");
releaseInternal();
return;
}
de_video_ctx = avcodec_alloc_context3(codec);
if (!de_video_ctx) {
LOGD("avcodec_alloc_context3 failed");
releaseInternal();
return;
}
// 設(shè)置解碼參數(shù)浊竟,從源文件拷貝
avcodec_parameters_to_context(de_video_ctx, sstream->codecpar);
// 初始化解碼器上下文
if (avcodec_open2(de_video_ctx, codec, NULL) < 0) {
LOGD("avcodec_open2 failed");
releaseInternal();
return;
}
// 創(chuàng)建編碼器
AVCodec *encodec = avcodec_find_encoder(sstream->codecpar->codec_id);
if (!encodec) {
LOGD("not surport encodec!");
releaseInternal();
return;
}
en_video_ctx = avcodec_alloc_context3(encodec);
if (!en_video_ctx) {
LOGD("avcodec_alloc_context3 failed");
releaseInternal();
return;
}
// 設(shè)置編碼相關(guān)參數(shù)
/** 遇到問(wèn)題:生成視頻前面1秒鐘是灰色的
* 分析原因:直接從源視頻流拷貝編碼參數(shù)到新的編碼上下文中(即通過(guò)avcodec_parameters_to_context(en_video_ctx, sstream->codecpar);)而部分重要編碼參數(shù)(如幀率怨喘,時(shí)間基)并不在codecpar
* 中,所以導(dǎo)致參數(shù)缺失
* 解決方案:額外設(shè)置時(shí)間基和幀率參數(shù)
*/
avcodec_parameters_to_context(en_video_ctx, sstream->codecpar);
// 設(shè)置幀率
int fps = sstream->r_frame_rate.num;
en_video_ctx->framerate = (AVRational){fps,1};
// 設(shè)置時(shí)間基;
en_video_ctx->time_base = sstream->time_base;
// I幀間隔逐沙,決定了壓縮率
en_video_ctx->gop_size = 12;
if (ofmt->oformat->flags & AVFMT_GLOBALHEADER) {
en_video_ctx->flags = AV_CODEC_FLAG_GLOBAL_HEADER;
}
// 初始化編碼器上下文
if (avcodec_open2(en_video_ctx, encodec, NULL) < 0) {
LOGD("avcodec_open2 failed");
releaseInternal();
return;
}
// 設(shè)置視頻流相關(guān)參數(shù)
avcodec_parameters_from_context(nstream->codecpar, en_video_ctx);
nstream->codecpar->codec_tag = 0;
} else if (sstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
// 音頻直接進(jìn)行流拷貝
in_audio_index = i;
AVStream *nstream = avformat_new_stream(ofmt, NULL);
avcodec_parameters_copy(nstream->codecpar, sstream->codecpar);
ou_audio_index = nstream->index;
nstream->codecpar->codec_tag = 0;
}
}
if (in_video_index == -1) {
LOGD("not has video stream");
releaseInternal();
return;
}
if (!(ofmt->flags & AVFMT_NOFILE)) {
if (avio_open(&ofmt->pb, dstpath.c_str(), AVIO_FLAG_WRITE) < 0) {
LOGD("avio_open() failed");
releaseInternal();
return;
}
}
av_dump_format(ofmt, -1, dstpath.c_str(), 1);
// 寫(xiě)入頭文件
if (avformat_write_header(ofmt, NULL) < 0) {
LOGD("avformat_write_header failed");
releaseInternal();
return;
}
// 初始化濾鏡
if (!initFilterGraph(spath,confpath)) {
LOGD("");
releaseInternal();
return;
}
AVPacket *inpkt = av_packet_alloc();
while (av_read_frame(vfmt, inpkt) >= 0) {
if (inpkt->stream_index == in_video_index) {
doDecodec(inpkt);
} else if (inpkt->stream_index == in_audio_index) {
// 進(jìn)行時(shí)間基的轉(zhuǎn)換
av_packet_rescale_ts(inpkt, vfmt->streams[in_audio_index]->time_base, ofmt->streams[ou_audio_index]->time_base);
inpkt->stream_index = ou_audio_index;
LOGD("audio pts %d(%s)",inpkt->pts,av_ts2timestr(inpkt->pts,&ofmt->streams[ou_audio_index]->time_base));
av_write_frame(ofmt, inpkt);
}
av_packet_unref(inpkt);
}
LOGD("finish !");
doDecodec(NULL);
av_write_trailer(ofmt);
releaseInternal();
}
/** 要使用subtitles和drawtext濾鏡到ffmpeg中哲思,則編譯ffmpeg庫(kù)時(shí)需要開(kāi)啟如下選項(xiàng):
* 1洼畅、字幕編解碼器 --enable-encoder=ass --enable-decoder=ass --enable-encoder=srt --enable-decoder=srt --enable-encoder=webvtt --enable-decoder=webvtt吩案;
* 2、字幕解封裝器 --enable-muxer=ass --enable-demuxer=ass --enable-muxer=srt --enable-demuxer=srt --enable-muxer=webvtt --enable-demuxer=webvtt
* 3帝簇、濾鏡選項(xiàng) --enable-filter=drawtext --enable-libfreetype --enable-libass --enable-filter=subtitles
*
* 備注:以上字幕編解碼器以及字幕解封裝器可以只使用一個(gè)即可徘郭,代表只能使用一個(gè)字幕格式。具體參考編譯腳本
*/
bool Subtitles::initFilterGraph(string spath,string confpath)
{
graph = avfilter_graph_alloc();
int ret = 0;
AVStream *stream = vfmt->streams[in_video_index];
// 輸入濾鏡
const AVFilter *src_filter = avfilter_get_by_name("buffer");
char desc[400];
sprintf(desc,"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d",stream->codecpar->width,stream->codecpar->height,stream->codecpar->format,stream->time_base.num,stream->time_base.den);
ret = avfilter_graph_create_filter(&src_filter_ctx, src_filter, "buffer0", desc, NULL, graph);
if (ret < 0) {
LOGD("init src filter failed");
return false;
}
// 輸出濾鏡
const AVFilter *sink_filter = avfilter_get_by_name("buffersink");
ret = avfilter_graph_create_filter(&sink_filter_ctx, sink_filter, "buffersink0", NULL, NULL, graph);
if (ret < 0) {
LOGD("buffersink init failed");
return false;
}
/** 遇到問(wèn)題:當(dāng)使用libass庫(kù)來(lái)合成字幕時(shí)無(wú)法生成字幕
* 分析原因:libass使用fontconfig庫(kù)來(lái)匹配字體丧肴,而程序中沒(méi)有指定字體匹配用的描述文件
* 解決方案:設(shè)置FONTCONFIG_FILE的值
*
* fontconfig工作原理:fontconfig通過(guò)環(huán)境變量FONTCONFIG_FILE來(lái)找到指定的fonts.conf文件(該文件的指定了字體文件(ttf,ttc等)的目錄残揉,以及字體fallback的規(guī)則),最終選擇指定的字體文件
* font fallback:如果某個(gè)字符在指定的字體庫(kù)中不存在芋浮,那么就需要找到能夠顯示此字符的備用字體庫(kù)抱环,fontconfig就是專(zhuān)門(mén)做此事的。
*
* 備注:
* 1、mac下 系統(tǒng)字體庫(kù)的路徑為:/System/Library/Fonts
* 2镇草、iOS下 系統(tǒng)字體庫(kù)的路徑為:ios系統(tǒng)字體不允許訪(fǎng)問(wèn)
* 3眶痰、安卓下 系統(tǒng)字體庫(kù)的路為:/system/fonts
* 4、Ubuntu下 系統(tǒng)字體庫(kù)的路徑為:/usr/share/fonts
* 不同系統(tǒng)支持的字體庫(kù)可能不一樣梯啤,由于fontconfig的字體fallback機(jī)制竖伯,如果不自定義自己的字體庫(kù),可能不同系統(tǒng)最終因?yàn)檫x擇的字體庫(kù)不一樣導(dǎo)致合成字幕也不一樣因宇。
* 所以解決辦法就是統(tǒng)一用于各個(gè)平臺(tái)的字體庫(kù)七婴,然后自定義fontconfig的字體庫(kù)的搜索路徑
*/
// 濾鏡描述符
setenv("FONTCONFIG_FILE",confpath.c_str(), 0);
char filter_des[400];
sprintf(filter_des, "subtitles=filename=%s",spath.c_str());
AVFilterInOut *inputs = avfilter_inout_alloc();
AVFilterInOut *ouputs = avfilter_inout_alloc();
inputs->name = av_strdup("out");
inputs->filter_ctx = sink_filter_ctx;
inputs->next = NULL;
inputs->pad_idx = 0;
ouputs->name = av_strdup("in");
ouputs->filter_ctx = src_filter_ctx;
ouputs->next = NULL;
ouputs->pad_idx = 0;
if (avfilter_graph_parse_ptr(graph, filter_des, &inputs, &ouputs, NULL) < 0) {
LOGD("avfilter_graph_parse_ptr failed");
return false;
}
av_buffersink_set_frame_size(sink_filter_ctx, en_video_ctx->frame_size);
// 初始化濾鏡
if (avfilter_graph_config(graph, NULL) < 0) {
LOGD("avfilter_graph_config failed");
return false;
}
avfilter_inout_free(&inputs);
avfilter_inout_free(&ouputs);
return true;
}
void Subtitles::doDecodec(AVPacket *pkt)
{
if (!de_frame) {
de_frame = av_frame_alloc();
}
int ret = avcodec_send_packet(de_video_ctx, pkt);
while (true) {
ret = avcodec_receive_frame(de_video_ctx, de_frame);
if (ret == AVERROR_EOF) {
// 說(shuō)明已經(jīng)沒(méi)有數(shù)據(jù)了;清空
//解碼成功送入濾鏡進(jìn)行處理
if((ret = av_buffersrc_add_frame_flags(src_filter_ctx, NULL, AV_BUFFERSRC_FLAG_KEEP_REF)) < 0) {
LOGD("av_buffersrc_add_frame_flags failed");
break;
}
break;
} else if (ret < 0) {
break;
}
//解碼成功送入濾鏡進(jìn)行處理
if((ret = av_buffersrc_add_frame_flags(src_filter_ctx, de_frame, AV_BUFFERSRC_FLAG_KEEP_REF)) < 0) {
LOGD("av_buffersrc_add_frame_flags failed");
break;
}
while (true) {
AVFrame *enframe = av_frame_alloc();
ret = av_buffersink_get_frame(sink_filter_ctx, enframe);
if (ret == AVERROR_EOF) {
// 說(shuō)明結(jié)束了
LOGD("avfilter endeof");
// 清空編碼器
doEncodec(NULL);
// 釋放內(nèi)存
av_frame_unref(enframe);
} else if (ret < 0) {
// 釋放內(nèi)存
av_frame_unref(enframe);
break;
}
// 進(jìn)行重新編碼
doEncodec(enframe);
// 釋放內(nèi)存
av_frame_unref(enframe);
}
}
}
void Subtitles::doEncodec(AVFrame *frame)
{
int ret = avcodec_send_frame(en_video_ctx, frame);
while (true) {
AVPacket *pkt = av_packet_alloc();
ret = avcodec_receive_packet(en_video_ctx, pkt);
if (ret < 0) {
av_packet_unref(pkt);
break;
}
// 寫(xiě)入數(shù)據(jù)
av_packet_rescale_ts(pkt, en_video_ctx->time_base, ofmt->streams[ou_video_index]->time_base);
pkt->stream_index = ou_video_index;
LOGD("video pts %d(%s)",pkt->pts,av_ts2timestr(pkt->pts,&ofmt->streams[ou_video_index]->time_base));
av_write_frame(ofmt, pkt);
av_packet_unref(pkt);
}
}
ffmpeg中字幕處理的濾鏡有兩個(gè)subtitles和drawtext。
1察滑、要想正確使用subtitles濾鏡打厘,編譯ffmpeg時(shí)需要添加--enable-libass --enable-filter=subtitles配置參數(shù),同時(shí)引入libass庫(kù)杭棵。同時(shí)由于libass庫(kù)又引用了freetype,fribidi外部庫(kù)所以還需要同時(shí)編譯這兩個(gè)庫(kù)婚惫,此外
libass庫(kù)根據(jù)操作系統(tǒng)的不同還引入不同的外部庫(kù),比如mac os系統(tǒng)則引入了CoreText.framework庫(kù),Linux則引入了fontconfig庫(kù)魂爪,windows系統(tǒng)則引入了DirectWrite先舷,或者添加--disable-require-system-font-provider
代表不使用這些系統(tǒng)的庫(kù)
2、要想正確使用drawtext濾鏡滓侍,編譯ffmpeg時(shí)需要添加--enable-filter=drawtext同時(shí)要引入freetype和fribidi外部庫(kù)
3蒋川、所以libass和drawtext濾鏡從本質(zhì)上看都是調(diào)用freetype生成一張圖片,然后再將圖片和視頻融合
與libass庫(kù)字幕處理相關(guān)的三個(gè)庫(kù):
1撩笆、text shaper相關(guān):用來(lái)定義字體形狀相關(guān)捺球,fribidi和HarfBuzz兩個(gè)庫(kù),其中fribidi速度較快夕冲,與字體庫(kù)形狀無(wú)關(guān)的一個(gè)庫(kù)氮兵,libass默認(rèn),故HarfBuzz可以選擇不編譯
2歹鱼、字體庫(kù)相關(guān):CoreText(ios/mac)泣栈;fontconfig(linux/android/ios/mac);DirectWrite(windows),用來(lái)創(chuàng)建字體弥姻。
3南片、freetype:用于將字符串按照前面指定的字體以及字體形狀渲染為字體圖像(RGB格式,備注:它還可以將RGB格式最終輸出為PNG庭敦,則需要編譯libpng庫(kù))
遇到問(wèn)題
1疼进、遇到問(wèn)題:調(diào)用avformat_open_input()時(shí)提示"avformat_open_input failed -1094995529(Invalid data found when processing input)"
分析原因:編譯ffmpeg庫(kù)是沒(méi)有將對(duì)應(yīng)的字幕解析器添加進(jìn)去比如(ff_ass_demuxer,ff_ass_muxer)
解決方案:添加對(duì)應(yīng)的編譯參數(shù)
2、遇到問(wèn)題:封裝后生成的mkv文件字幕無(wú)法顯示秧廉,封裝時(shí)提示"[matroska @ 0x10381c000] Starting new cluster due to timestamp"
分析原因:通過(guò)和ffmpeg.c中源碼進(jìn)行比對(duì)伞广,后發(fā)現(xiàn)mvk對(duì)字幕寫(xiě)入的順序有要求
解決方案:將字幕寫(xiě)入放到音視頻之前
3拣帽、遇到問(wèn)題:生成視頻前面1秒鐘是灰色的
分析原因:直接從源視頻流拷貝編碼參數(shù)到新的編碼上下文中(即通過(guò)avcodec_parameters_to_context(en_video_ctx, sstream->codecpar);)而部分重要編碼參數(shù)(如幀率,時(shí)間基)并不在codecpar中嚼锄,所以導(dǎo)致參數(shù)缺失
解決方案:額外設(shè)置時(shí)間基和幀率參數(shù)
4诞外、遇到問(wèn)題:當(dāng)以靜態(tài)庫(kù)方式引入fontconf到ffmpeg中時(shí)提示"pkg-conf fontconf not found"
分析原因:fontconf自己生成的pc文件不包含expat庫(kù),最終導(dǎo)致了錯(cuò)誤
解決方案:自己定義fontconfig庫(kù)的pc文件
5灾票、遇到問(wèn)題:以靜態(tài)庫(kù)的方式引入android studio時(shí) 提示"undefined reference to xxxx"
分析原因:此問(wèn)題為偶然發(fā)現(xiàn)峡谊,以靜態(tài)庫(kù)方式導(dǎo)入可執(zhí)行程序時(shí)(如果引用的庫(kù)中又引用了其它庫(kù)或者各個(gè)模塊之間有相互引用時(shí))那么就一定要注意連接順序的問(wèn)題,所以最后一定要按照如下順序?qū)氲絘ndroid中(其中ffmpeg庫(kù)的順序也要固定)
libavformat.a libavcodec.a libavfilter.a libavutil.a libswresample.a libswscale.a libass.a libfontconfig.a libexpat.a libfreetype.a libfribidi.a libmp3lame.a libx264.a
6刊苍、遇到問(wèn)題:"引入fontconfig時(shí)提示"libtool: link: warning: library `/home/admin/usr/lib/freetype.la' was moved." ";因?yàn)閒ontcong依賴(lài)freetype既们,libass也依賴(lài)freetype。而fontconfig如果加入了--with-sysroot=參數(shù)
則生成的fontconfig.la文件的dependency_libs字段 是-Lxxx/freetype/lib =/user/xxxxx/freetype.la的格式正什,導(dǎo)致libtool解析錯(cuò)誤啥纸,所以這里fontconfig不需要添加"--with-root" 參數(shù)
7、遇到問(wèn)題:mac編譯時(shí)提示"Undefined symbols _libintl_dgettext"
分析原因:因?yàn)閒ontconfig庫(kù)依賴(lài)intl庫(kù)而編譯時(shí)未導(dǎo)入
解決方案:通過(guò)編譯參數(shù)"-lintl"導(dǎo)入即可
8婴氮、遇到問(wèn)題:真機(jī)使用fontconfig庫(kù)時(shí)奔潰
分析原因:通過(guò)查看fontconfig庫(kù)源碼發(fā)現(xiàn)頭文件fcatomic.h中有宏定義__IPHONE_VERSION_MIN_REQUIRED時(shí)才引入<Availability.h>斯棒,所以編譯時(shí)不加此宏定義就會(huì)導(dǎo)致崩潰
解決方案:編譯時(shí)添加宏定義__IPHONE_VERSION_MIN_REQUIRED
完成添加字幕的功能的ffmpeg代碼本身不多,主要的時(shí)間都花在解決引入libass主经、fontconfig等外部庫(kù)的編譯及引入產(chǎn)生的問(wèn)題上了荣暮,所以上面也記錄了一下
項(xiàng)目地址
https://github.com/nldzsz/ffmpeg-demo
位于cppsrc目錄下文件Subtitles.hpp/Subtitles.cpp
項(xiàng)目下示例可運(yùn)行于iOS/android/mac平臺(tái),工程分別位于demo-ios/demo-android/demo-mac三個(gè)目錄下罩驻,可根據(jù)需要選擇不同平臺(tái)