ffmpeg-ビデオに字幕を追加する (24)

序文

映画やDouyinなどのショートビデオプラットフォームで動画を視聴すると通常字幕が表示されますが、字幕があることで映像表現がより豊かになるため、動画に字幕を追加することも難しい要件となります。この記事の目的は、ビデオに字幕を追加することです。字幕を追加する方法を学ぶ前に、まず字幕の種類を理解してください。

  • 外部字幕
    外部字幕は別個の外部字幕ファイルであり、形式の種類には通常、srt、vtt、ass などが含まれます。ビデオを再生するとき、ビデオ内の字幕を表示するには、外部字幕とビデオを同じディレクトリに置き、プレーヤーで字幕ファイルを選択する必要があります。

  • ソフト字幕
    ソフト字幕は、内部字幕、カプセル化された字幕、内部字幕、字幕ストリームなどとも呼ばれます。以前の外部字幕の字幕ファイルをストリームの一部としてビデオに埋め込むことです。ビデオに複数の字幕ストリームがある場合、はい、対応する字幕ストリームを選択する必要があります

備考: 外部字幕、ソフト字幕を問わず、字幕を正常に表示するには、プレーヤーが字幕レンダリングをサポートしている必要があります。

  • ハード字幕
    ハード字幕はビデオ フレームに埋め込まれた字幕です。ビデオの透かしと同じようにビデオ フレームの一部です。字幕はプラットフォームに関係なく同じように見え、プレーヤーは文字を個別に修正する必要がなくなりました。与える

概要:
1. 外部字幕とソフト字幕の両方では、プレーヤーが追加で字幕レンダリングをサポートする必要がありますが、ハード字幕はサポートしません。外部字幕とソフト字幕はいつでも字幕ファイルを置き換えたりキャンセルしたりできますが、ハード字幕はビデオ内の字幕をキャンセルしたり変更したりすることはできません 2. 字幕ストリームまたは外部字幕の場合、プレーヤーは字幕ストリームの個別のレンダリングをサポートする必要があります

3. さらに、字幕ストリームの埋め込みにはコンテナ形式のサポートも必要です。たとえば、MKV 形式はさまざまな形式の字幕ファイルをサポートしていますが、MP4 は字幕をあまりサポートしていません (Apple の MOV テキストのみがサポートされています)。

一般的な字幕形式

さまざまな字幕ファイルには対応する形式 (外部字幕とソフト字幕) があり、一般的な字幕形式は次のとおりです。

  • SRT (標準外部字幕形式): テキストとタイム コードのみが含まれ、スタイルは含まれません。表示効果はプレーヤーによって決定され、プレーヤーによって表示される効果は大きく異なる場合があります。
  • ASS (高度な外部字幕フォーマット): スタイル、フォント、字幕の位置、フェードインとフェードアウト、および単純な特殊効果をサポートします。フォントが不足していない場合、異なるプレーヤーの表示効果は基本的に同じです
  • XML+PNG シーケンス: Premiere、FCP7、Edius、Vegas、AE のインポートに使用されます。FCPX はサポートされていません
    Avid DS Cap 字幕形式: AVID 特殊形式、インポート後にテキストを変更できます
  • UTF (VideoStudio の特別な形式): VideoStudio に直接インポートして使用できます。

字幕作成ソフトウェア Arctime をお勧めします。ダウンロード アドレス。このソフトウェアはさまざまな形式で字幕を作成できます。さまざまな字幕ファイルの形式は次のとおりです。

お尻の字幕フォーマット

 

画像.png

ttxt 字幕形式

 

画像.png

srt 字幕形式

 

画像.png

ffmpeg字幕処理フロー

画像.png

ffmpegコマンドラインは字幕の追加を実装します

  • 字幕処理フィルターをffmpegにコンパイルする

ffmpeg で字幕を追加する機能を実装したい場合は、コンパイル時に有効にする必要があります --enable-filter=subtitles --enable-libass

--enable-filter=subtitles は、字幕フィルターを有効にすることを意味します
--enable-libass は、字幕フィルターが依存する必要がある外部ライブラリであるため、コンパイル時に外部ライブラリのパスを指定する必要があります (x264 コンパイルなど)。

libass は、字幕の処理とレンダリングのためのオープンソース ライブラリです。アドレスはhttps://github.com/libass/libass.git

完全なコンパイル スクリプト リファレンス:字幕フィルターを含むコンパイル スクリプト

  • ソフト字幕を追加する

 

ffmpeg -i test_1280x720_3.mp4 -i test_1280x720_3.srt -c copy output.mkv

ソフト字幕を追加する原理とプロセスはビデオにオーディオを追加するのと同じであり、このプロセスには再コーディングが必要ないため、速度は非常に高速です。

ヒント: ソフト字幕は、(mkv) などの一部のコンテナ形式でのみサポートされており、MP4/MOV などはサポートされておらず、一部のプレーヤーのみがソフト字幕または外部字幕 (VLC プレーヤーなど) をサポートしています。

VLC プレーヤーは、上記のコマンドで合成されたソフト字幕付きの mkv ビデオを再生します。

 

画像.png

 

デフォルトでは、VLC では字幕がオフになっているため、手動でオンにする必要があります。

コマンドを入力して、ソフト字幕が正常に追加されたことを確認します。

 

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
  • 字幕形式変換
    ffmpegコマンドを使用すると、ass/srt/vttなどの字幕形式の相互変換も実現できます。

 

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 は追加する字幕ファイルのパスを表し、test_1280x720_3.ass、test_1280x720_3.ttext など、他の形式の字幕ファイルとして記述することもできます。最終的に、ffmpeg は字幕形式を ass 字幕ストリームに変換し、ビデオ フレームに字幕を埋め込みますが、このプロセスは再エンコードする必要があるため、速度は比較的遅くなります。

コマンドを入力して、ハード字幕が正常に追加されたことを確認します。

 

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

字幕を追加するコード方法

  • 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;
    // 打开视频流
    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;
        }
    }
    
    // 打开字幕流
    /** 遇到问题:调用avformat_open_input()时提示"avformat_open_input failed -1094995529(Invalid data found when processing input)"
     *  分析原因:编译ffmpeg库是没有将对应的字幕解析器添加进去比如(ff_ass_demuxer,ff_ass_muxer)
     *  解决方案:添加对应的编译参数
     */
    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中该选项可以控制字幕默认是否显示,不过这里貌似不可以,原因未知。
     */
//    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);
    
    /** 遇到问题:封装后生成的mkv文件字幕无法显示,封装时提示"[matroska @ 0x10381c000] Starting new cluster due to timestamp"
     *  分析原因:通过和ffmpeg.c中源码进行比对,后发现mvk对字幕写入的顺序有要求
     *  解决方案:将字幕写入放到音视频之前
     */
    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();
    
}

備考:
mkv のカプセル化とカプセル化解除については、ffmpeg のコンパイル パラメータを --enable-muxer=matroska および --enable-demuxer=matroska に設定する必要があります。異なる形式の字幕 ass/srt がファイルに書き込まれた後
、 , プレーヤーを開いたときに字幕が表示されますが、サイズや位置も異なります

  • 2.ハードサブタイトルを追加する

 

void Subtitles::addSubtitlesForVideo(string vpath, string spath, string dstpath,string confpath)
{
    int ret = 0;
    // 打开视频流
    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;
            }
            // 设置解码参数,从源文件拷贝
            avcodec_parameters_to_context(de_video_ctx, sstream->codecpar);
            // 初始化解码器上下文
            if (avcodec_open2(de_video_ctx, codec, NULL) < 0) {
                LOGD("avcodec_open2 failed");
                releaseInternal();
                return;
            }
            
            // 创建编码器
            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;
            }
            
            // 设置编码相关参数
            /** 遇到问题:生成视频前面1秒钟是灰色的
             *  分析原因:直接从源视频流拷贝编码参数到新的编码上下文中(即通过avcodec_parameters_to_context(en_video_ctx, sstream->codecpar);)而部分重要编码参数(如帧率,时间基)并不在codecpar
             *  中,所以导致参数缺失
             *  解决方案:额外设置时间基和帧率参数
             */
            avcodec_parameters_to_context(en_video_ctx, sstream->codecpar);
            // 设置帧率
            int fps = sstream->r_frame_rate.num;
            en_video_ctx->framerate = (AVRational){fps,1};
            // 设置时间基;
            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;
            }
            
            
            // 设置视频流相关参数
            avcodec_parameters_from_context(nstream->codecpar, en_video_ctx);
            nstream->codecpar->codec_tag = 0;
            
        } else if (sstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            
            // 音频直接进行流拷贝
            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);
    
    // 写入头文件
    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) {
            // 进行时间基的转换
            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库时需要开启如下选项:
 *  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、滤镜选项  --enable-filter=drawtext --enable-libfreetype --enable-libass --enable-filter=subtitles
 *
 *  备注:以上字幕编解码器以及字幕解封装器可以只使用一个即可,代表只能使用一个字幕格式。具体参考编译脚本
 */
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;
    }
    
    /** 遇到问题:当使用libass库来合成字幕时无法生成字幕
     *  分析原因:libass使用fontconfig库来匹配字体,而程序中没有指定字体匹配用的描述文件
     *  解决方案:设置FONTCONFIG_FILE的值
     *
     *  fontconfig工作原理:fontconfig通过环境变量FONTCONFIG_FILE来找到指定的fonts.conf文件(该文件的指定了字体文件(ttf,ttc等)的目录,以及字体fallback的规则),最终选择指定的字体文件
     *  font fallback:如果某个字符在指定的字体库中不存在,那么就需要找到能够显示此字符的备用字体库,fontconfig就是专门做此事的。
     *
     *  备注:
     *  1、mac下 系统字体库的路径为:/System/Library/Fonts
     *  2、iOS下 系统字体库的路径为:ios系统字体不允许访问
     *  3、安卓下 系统字体库的路为:/system/fonts
     *  4、Ubuntu下 系统字体库的路径为:/usr/share/fonts
     *  不同系统支持的字体库可能不一样,由于fontconfig的字体fallback机制,如果不自定义自己的字体库,可能不同系统最终因为选择的字体库不一样导致合成字幕也不一样。
     *  所以解决办法就是统一用于各个平台的字体库,然后自定义fontconfig的字体库的搜索路径
     */
    // 滤镜描述符
    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) {
            // 说明已经没有数据了;清空
            //解码成功送入滤镜进行处理
            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;
        }
        
        //解码成功送入滤镜进行处理
        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) {
                // 说明结束了
                LOGD("avfilter endeof");
                // 清空编码器
                doEncodec(NULL);
                // 释放内存
                av_frame_unref(enframe);
            } else if (ret < 0) {
                // 释放内存
                av_frame_unref(enframe);
                break;
            }

            // 进行重新编码
            doEncodec(enframe);
            // 释放内存
            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;
        }
        
        // 写入数据
        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 の字幕処理フィルターには 2 つの字幕と描画テキストがあります。
1. 字幕フィルターを正しく使用するには、ffmpeg をコンパイルするときに --enable-libass --enable-filter=subtitles 構成パラメーターを追加し、同時に libass ライブラリを導入する必要があります。同時に、libass ライブラリは freetype および fribidi 外部ライブラリを参照するため、これら 2 つのライブラリを同時にコンパイルする必要があります。また、libass ライブラリは、異なるオペレーティング システムに応じて異なる外部ライブラリも導入します
。 Mac OS システムでは CoreText.framework ライブラリが導入され、Linux では fontconfig ライブラリが導入され、Windows システムでは DirectWrite が導入されるか、
これらのシステムを使用しないライブラリを表す--disable-require-system-font-provider を追加します
。ドローテキスト フィルターを正しく使用するには、ffmpeg のコンパイル時に -- を追加する必要があります。enable-filter=drawtext では、freetype と fribidi 外部ライブラリ
3 も導入する必要があるため、libass フィルターとdrawtext フィルターは基本的に freetype を呼び出して画像を生成し、その後融合します。
libass ライブラリの字幕処理による画像とビデオ
ライブラリ: 1. テキスト シェイパー関連: フォント シェイプ関連の定義に使用され、fribidi と HarfBuzz の 2 つのライブラリ (うち fribidi の方が高速です)、フォント ライブラリのシェイプとは関係のないライブラリ、libass のデフォルトなので、HarfBuzz はコンパイルしないことを選択できます 2、フォント
ライブラリ関連: CoreText (ios/mac); fontconfig (linux/android/ios/mac); DirectWrite (windows)、フォントの作成に使用されます。
3. freetype: 上記で指定したフォントとフォント形状に従って文字列をフォント イメージにレンダリングするために使用されます (RGB 形式、注意: RGB 形式を PNG として出力することもできます。libpng ライブラリをコンパイルする必要があります)

問題に遭遇する

1. 問題が発生しました: avformat_open_input() を呼び出すと、「avformat_open_input failed -1094995529 (入力の処理中に無効なデータが見つかりました)」というプロンプトが表示されます。 分析理由: ffmpeg ライブラリをコンパイルしても、(ff_ass_demuxer、ff_ass_muxer) などの対応する字幕パーサーが追加されません

解決策: 対応するコンパイルパラメータを追加します

2. 問題が発生しました: カプセル化後に生成された mkv ファイルの字幕が表示できず、カプセル化中に「[matroska @ 0x10381c000] タイムスタンプのため新しいクラスターを開始しています」というメッセージが表示される 分析理由: のソース コードと比較した後ffmpeg.c では、mvk が正しくないことがわかります。
字幕を書き込む順序は必須です。
解決策: 音声とビデオの前に字幕を書きます。

3. 問題が発生しました: 生成されたビデオの最初の 2 秒が灰色です
分析理由: エンコード パラメータをソース ビデオ ストリームから新しいエンコード コンテキストに直接コピーします (つまり、avcodec_parameters_to_context(en_video_ctx, sstream->codecpar); を通じて)。いくつかの重要なエンコーディング パラメータ (フレーム レート、タイム ベースなど) が codecpar にないため、パラメータが欠落しています。解決策
: タイム ベースとフレーム レートのパラメータを追加設定します。

4. 問題が発生しました: fontconf を静的ライブラリとして ffmpeg にインポートすると、「pkg-conf fontconf not found」というメッセージが表示されます。 分析理由
: fontconf 自体によって生成された PC ファイルには expat ライブラリが含まれていないため、最終的にエラーが発生します
。 : fontconfig ライブラリ PC ファイルを自分で定義します

5. 問題が発生しました: Android Studio を静的ライブラリとしてインポートすると、「xxxx への未定義の参照」というプロンプトが表示されます。 分析
理由: この問題は、実行可能プログラムを静的ライブラリとしてインポートするときに偶然発見されました (参照されたライブラリが他のライブラリを参照している場合)ライブラリやさまざまなモジュール間で相互参照があります)、接続順序に注意する必要があるため、最後に次の順序で Android にインポートする必要があります (ffmpeg ライブラリの順序も固定する必要があります) libavformat.a libavcodec.a libavfilter
. libavutil.a libswresample.a libswscale.a libass.a libfontconfig.a libexpat.a libfreetype.a libfribidi.a libmp3lame.a libx264.a

6. 問題が発生しました: 「fontconfig をインポートすると、「libtool: link: warning: library `/home/admin/usr/lib/freetype.la' が移動されました。」というプロンプトが表示されます。これは、fontcong が freetype に依存しており、libass にも依存しているためです。フリータイプに依存します。また、fontconfig が --with-sysroot= パラメーターを追加すると
、生成された fontconfig.la ファイルの dependency_libs フィールドは -Lxxx/freetype/lib =/user/xxxxx/freetype.la の形式になり、libtool 解析エラーが発生します。したがって、ここではfontconfigは「--with-root」パラメータを追加する必要はありません

7. 問題が発生しました: Mac のコンパイル時に「未定義のシンボル _libintl_dgettext」というプロンプトが表示されます。
理由: fontconfig ライブラリは intl ライブラリに依存しているため、コンパイル中にインポートされません。解決策:
コンパイル パラメータ「-lintl」を使用してインポートするだけです。

8. 問題が発生しました: fontconfig ライブラリを使用すると実マシンがクラッシュします
分析理由: fontconfig ライブラリのソース コードを見ると、ヘッダーにマクロ定義 __IPHONE_VERSION_MIN_REQUIRED がある場合に <Availability.h> が導入されていることがわかりますファイル fcatomic.h があるため、このマクロはコンパイル時に追加されません。定義によりクラッシュが発生します
。 解決策: コンパイル時にマクロ定義 __IPHONE_VERSION_MIN_REQUIRED を追加します。

字幕を追加する機能を完了するための ffmpeg コードはそれほど多くなく、主な時間は libass や fontconfig などの外部ライブラリのコンパイルと導入によって引き起こされる問題の解決に費やされるため、それも上記に記録されています

プロジェクトアドレス

https://github.com/nldzsz/ffmpeg-demo

cppsrc ディレクトリにあるファイル Subtitles.hpp/Subtitles.cpp

プロジェクト内のサンプルは、iOS/android/mac プラットフォームで実行でき、プロジェクトは、demo-ios/demo-android/demo-mac の 3 つのディレクトリにあり、ニーズに応じて異なるプラットフォームを選択できます。

おすすめ

転載: blog.csdn.net/qq_21743659/article/details/109305411#comments_26930204