ffmpeg-給視頻添加字幕(二十四)

前言

在我們觀看電影或者抖音等短視頻平臺(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字幕格式


image.png

ttxt字幕格式


image.png

srt字幕格式


image.png

ffmpeg字幕處理流程

image.png

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視頻


image.png

默認(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)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末穗酥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惠遏,更是在濱河造成了極大的恐慌砾跃,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件节吮,死亡現(xiàn)場(chǎng)離奇詭異抽高,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)透绩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)翘骂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人渺贤,你說(shuō)我怎么就攤上這事雏胃∏朊” “怎么了志鞍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)方仿。 經(jīng)常有香客問(wèn)我固棚,道長(zhǎng)统翩,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任此洲,我火速辦了婚禮厂汗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呜师。我一直安慰自己娶桦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布汁汗。 她就那樣靜靜地躺著衷畦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪知牌。 梳的紋絲不亂的頭發(fā)上祈争,一...
    開(kāi)封第一講書(shū)人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音角寸,去河邊找鬼菩混。 笑死,一個(gè)胖子當(dāng)著我的面吹牛扁藕,可吹牛的內(nèi)容都是我干的沮峡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼亿柑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼帖烘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起橄杨,我...
    開(kāi)封第一講書(shū)人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤秘症,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后式矫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體乡摹,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年采转,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了聪廉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡故慈,死狀恐怖板熊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情察绷,我是刑警寧澤干签,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站拆撼,受9級(jí)特大地震影響容劳,放射性物質(zhì)發(fā)生泄漏喘沿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一竭贩、第九天 我趴在偏房一處隱蔽的房頂上張望蚜印。 院中可真熱鬧,春花似錦留量、人聲如沸窄赋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寝凌。三九已至,卻和暖如春孝赫,著一層夾襖步出監(jiān)牢的瞬間较木,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工青柄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留伐债,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓致开,卻偏偏與公主長(zhǎng)得像峰锁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子双戳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354