ffmpeg-给视频添加字幕(二十四)

前言

在我们观看电影或者抖音等短视频平台的视频时一般都会出现字幕,有了字幕那视频的表现形式就更加丰富了,所以为一段视频添加字幕也是一个硬需求。本文的目的就是为一段视频添加字幕,了解如何添加字幕前先了解下字幕的类型:

  • 外挂字幕
    外挂字幕是一个单独的外部字幕文件,格式类型一般有srt、vtt、ass等等。播放视频时,需要把外挂字幕和视频放在同一目录下,并在播放器中选择字幕文件才可以在视频中看到字幕。

  • 软字幕
    软字幕也叫内挂字幕、封装字幕、内封字幕,字幕流等,就是把前面的外挂字幕的字幕文件嵌入到视频中作为流的一部分,如果一个视频有多个字幕流那么播放视频是还得选择对应的字幕流

备注:不管是外挂字幕还是软字幕,字幕要正常显示播放器必须要支持字幕的渲染。

  • 硬字幕
    硬字幕就是嵌入到视频帧里面的字幕,它就像视频水印一样作为视频帧的一分部分了,不管再任何平台字幕看起来都是一样的,而且也不再要求播放器单独对字母进行渲染

总结:
1、外挂字幕和软字幕都要求播放器额外支持字幕的渲染,而硬字幕不需要。外挂字幕和软字幕可以随时更换和取消字幕文件,而硬字幕则不可以取消和更改视频中的字幕
2、如果是字幕流或者外挂字幕则还需要播放器支持字幕流的单独渲染
3、此外嵌入字幕流也需要容器格式支持,比如MKV格式就支持各种格式字幕文件,但是MP4对字幕的支持就不太好(只支持苹果的MOV text)

常见字幕格式

不同的字幕文件有其对应的格式(针对外挂字幕和软字幕),常见的字幕格式有:

  • SRT(标准外挂字幕格式):只包含文字和时间码,没有样式,显示效果由播放器决定,不同的播放器显示出的效果可能差别很大
  • ASS(高级外挂字幕格式):支持样式、字体、字幕定位、淡入淡出、简单的特效。如果不缺字体,不同的播放器显示效果基本一致
  • XML+PNG序列:用来导入Premiere、FCP7、Edius、Vegas、AE,不支持FCPX
    Avid DS Cap字幕格式:AVID专用格式,导入后可以修改文字
  • UTF(会声会影专用格式):可以直接导入会声会影使用

推荐一款字幕制作软件Arctime,下载地址,该软件可以制作各种格式的字幕,如下为各种字幕文件的格式:

ass字幕格式


image.png

ttxt字幕格式


image.png

srt字幕格式


image.png

ffmpeg字幕处理流程

image.png

ffmpeg命令行实现添加字幕

  • 将字幕处理滤镜编译到ffmpeg

如果ffmpeg要实现添加字幕的功能需要在编译时开启--enable-filter=subtitles --enable-libass

--enable-filter=subtitles 代表开启字幕滤镜
--enable-libass 则是字幕滤镜需要依赖的外部库,所以编译时还需要指定该外部库的路径(如x264的编译一样)

libass是一个用来进行字幕处理和渲染的开源库,地址https://github.com/libass/libass.git

完整编译脚本参考:包含subtitles滤镜的编译脚本

  • 添加软字幕
ffmpeg -i test_1280x720_3.mp4 -i test_1280x720_3.srt -c copy output.mkv

添加软字幕的原理和流程就跟给视频添加音频一样,这个过程不需要重新编解码,所以速度非常快。

tips:软字幕只有部分容器格式比如(mkv)才支持,MP4/MOV等不支持,而且也只有部分播放器支持软字幕或者外挂字幕(如VLC播放器)

VLC播放器播放上面命令中合成的带有软字幕的mkv视频


image.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中字幕处理的滤镜有两个subtitles和drawtext。
1、要想正确使用subtitles滤镜,编译ffmpeg时需要添加--enable-libass --enable-filter=subtitles配置参数,同时引入libass库。同时由于libass库又引用了freetype,fribidi外部库所以还需要同时编译这两个库,此外
libass库根据操作系统的不同还引入不同的外部库,比如mac os系统则引入了CoreText.framework库,Linux则引入了fontconfig库,windows系统则引入了DirectWrite,或者添加--disable-require-system-font-provider
代表不使用这些系统的库
2、要想正确使用drawtext滤镜,编译ffmpeg时需要添加--enable-filter=drawtext同时要引入freetype和fribidi外部库
3、所以libass和drawtext滤镜从本质上看都是调用freetype生成一张图片,然后再将图片和视频融合
与libass库字幕处理相关的三个库:
1、text shaper相关:用来定义字体形状相关,fribidi和HarfBuzz两个库,其中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(Invalid data found when processing input)"
分析原因:编译ffmpeg库是没有将对应的字幕解析器添加进去比如(ff_ass_demuxer,ff_ass_muxer)
解决方案:添加对应的编译参数

2、遇到问题:封装后生成的mkv文件字幕无法显示,封装时提示"[matroska @ 0x10381c000] Starting new cluster due to timestamp"
分析原因:通过和ffmpeg.c中源码进行比对,后发现mvk对字幕写入的顺序有要求
解决方案:将字幕写入放到音视频之前

3、遇到问题:生成视频前面1秒钟是灰色的
分析原因:直接从源视频流拷贝编码参数到新的编码上下文中(即通过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时 提示"undefined reference to xxxx"
分析原因:此问题为偶然发现,以静态库方式导入可执行程序时(如果引用的库中又引用了其它库或者各个模块之间有相互引用时)那么就一定要注意连接顺序的问题,所以最后一定要按照如下顺序导入到android中(其中ffmpeg库的顺序也要固定)
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、遇到问题:"引入fontconfig时提示"libtool: link: warning: library `/home/admin/usr/lib/freetype.la' was moved." ";因为fontcong依赖freetype,libass也依赖freetype。而fontconfig如果加入了--with-sysroot=参数
则生成的fontconfig.la文件的dependency_libs字段 是-Lxxx/freetype/lib =/user/xxxxx/freetype.la的格式,导致libtool解析错误,所以这里fontconfig不需要添加"--with-root" 参数

7、遇到问题:mac编译时提示"Undefined symbols _libintl_dgettext"
分析原因:因为fontconfig库依赖intl库而编译时未导入
解决方案:通过编译参数"-lintl"导入即可

8、遇到问题:真机使用fontconfig库时奔溃
分析原因:通过查看fontconfig库源码发现头文件fcatomic.h中有宏定义__IPHONE_VERSION_MIN_REQUIRED时才引入<Availability.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三个目录下,可根据需要选择不同平台

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342