通过FFMPEG给视频加字幕

字幕简介

我们在观看电影或短视频时,一般在视频下发会出现字幕,有些视频还会配中英双语字幕。字幕可以帮助观看人更好的了解视频内容。字幕分为以下三种类型:
外挂字幕:独立的字幕文件,播放视频时把视频和字幕放入同一路径下,也可以在播放器中选择外挂字幕。这样在播放视频时就可以看到字幕内容。常见字幕格式srt、vtt、ass等。
软字幕:也叫内挂字幕、封装字幕、字幕流等。通过某种技术将外挂字幕与视频文件打包在一起成一个文件。视频文件也可以同时封装多个字幕文件,播放时通过播放器选择所需字幕或不显示字幕。在需要时,还可以将字幕分离出来,修改后再打包进去。
硬字幕:将字幕内容覆盖叠加到视频画面上。这种字幕与视频画面溶于一体,具有最佳兼容性,只要能播放视频就能显示字幕。缺点是字幕占据视频画面,无法隐藏,破坏原有视频内容。且不可编辑更改。

FFMPEG命令行添加字幕

添加软字幕

ffmpeg -i demo.mp4 -i ass=subtitle.ass -c copy output.mkv
#demo.mp4 原始视频
#subtitle.ass 字幕文件
#output.mkv 输出视频

添加硬字幕

ffmpeg -i demo.mp4 -vf ass=subtitle.ass output.mp4

字幕格式转换

ffmpeg -i src.srt out.vtt
ffmpeg -i src.srt out.ass

代码方式添加字幕

添加软字幕

QString m_videoPath;    //原始视频文件
QString m_srtPath;      //字幕文件
QString m_destPath;     //输出视频文件
AVFormatContext* vfmt = NULL;   //源
AVFormatContext* sfmt = NULL;   //字幕
AVFormatContext* ofmt = NULL;   //输出
void SoftSubtitle()
{
    av_register_all();  //初始化
    avcodec_register_all(); //注册编解码器
    int ret = 0;
    // 打开视频流
    if (avformat_open_input(&vfmt, m_videoPath.toStdString().c_str(), NULL, NULL) < 0) {
        printf("avformat_open_input failed");
        return;
    }
    //读取媒体文件的数据包以获取流信息  查看是否有流信息
    if (avformat_find_stream_info(vfmt, NULL) < 0) {
        printf("avformat_find_stream_info");
        releaseInternal();
        return;
    }
    //分配输出的AVFormatContext
    if ((avformat_alloc_output_context2(&ofmt, NULL, NULL, m_destPath.toStdString().c_str())) < 0) {
        printf("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;
    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, m_destPath.toStdString().c_str(), AVIO_FLAG_WRITE) < 0) {
            printf("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, m_srtPath.toStdString().c_str(), NULL, NULL)) < 0) {
        char errorBuf[256] = { 0 };
        av_strerror(ret, errorBuf, sizeof(errorBuf));
        printf("avformat_open_input failed %d(%s)", ret, errorBuf);
        return;
    }
    if ((ret = avformat_find_stream_info(sfmt, NULL)) < 0) {
        char errorBuf[256] = { 0 };
        av_strerror(ret, errorBuf, sizeof(errorBuf));
        printf("avformat_find_stream_info %d(%s)", ret, errorBuf);
        releaseInternal();
        return;
    }
    //获取字幕文件字幕流索引
    int subTitleIndex = av_find_best_stream(sfmt, AVMEDIA_TYPE_SUBTITLE, -1, -1, NULL, 0);
    if (subTitleIndex < 0) {
        printf("not find subtitle stream 0");
        releaseInternal();
        return;
    }
    //创建一个输出字幕流 并把源字幕流复制给它
    AVStream* nstream = avformat_new_stream(ofmt, NULL);
    ret = avcodec_parameters_copy(nstream->codecpar, sfmt->streams[subTitleIndex]->codecpar);
    nstream->codecpar->codec_tag = 0;
    int ou_subtitle_index = nstream->index; //输出字幕流索引值

    //写头文件
    if (avformat_write_header(ofmt, NULL) < 0) {
        printf("avformat_write_header failed");
        releaseInternal();
        return;
    }
    //打印媒体信息  将 AVFormatContext 中的媒体信息转存到输出
    av_dump_format(ofmt, 0, m_destPath.toStdString().c_str(), 1);

    /** 遇到问题:封装后生成的mkv文件字幕无法显示,封装时提示"[matroska @ 0x10381c000] Starting new cluster due to timestamp"
     *  分析原因:通过和ffmpeg.c中源码进行比对,后发现mvk对字幕写入的顺序有要求
     *  解决方案:将字幕写入放到音视频之前
     */
    //创建AVPacket
    AVPacket* inpkt2 = av_packet_alloc();
    while (av_read_frame(sfmt, inpkt2) >= 0) {  //读一帧字幕数据
        AVStream* srcstream = sfmt->streams[0];
        AVStream* dststream = ofmt->streams[ou_subtitle_index];
        //用于将AVPacket中各种时间值从一种时间基转换为另一种时间基
        av_packet_rescale_ts(inpkt2, srcstream->time_base, dststream->time_base);   
        inpkt2->stream_index = ou_subtitle_index;
        inpkt2->pos = -1;
        printf("pts %d", inpkt2->pts);
        if (av_write_frame(ofmt, inpkt2) < 0) { //输出一帧字幕数据
            printf("subtitle av_write_frame failed");
            releaseInternal();
            av_packet_unref(inpkt2);
            return;
        }
    }
    //释放AVPacket
    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;
            printf("inpkt %d", inpkt->pts);
            if (av_write_frame(ofmt, inpkt) < 0) {  //写视频数据到
                printf("video av_write_frame failed");
                av_packet_unref(inpkt);
                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) {  //写视频数据到
                printf("audio av_write_frame failed");
                av_packet_unref(inpkt);
                releaseInternal();
                return;
            }
        }
    }
    av_packet_unref(inpkt);

    //字幕、视音频数据全部读写完毕
    //输出文件尾
    av_write_trailer(ofmt);
    if (vfmt) {
        avformat_close_input(&vfmt);
        avformat_free_context(vfmt);
    }
    if (sfmt) {
        avformat_close_input(&sfmt);
        avformat_free_context(sfmt);
    }
    if (ofmt) {
        avformat_close_input(&ofmt);
        avformat_free_context(ofmt);
    }
}

添加硬字幕

QString m_videoPath;
QString m_srtPath;
QString m_fontsConf;    
QString m_destPath;
AVFormatContext* vfmt = NULL;   //源文件
AVFormatContext* sfmt = NULL;   //字幕文件
AVFormatContext* ofmt = NULL;   //输出文件
int in_video_index, in_audio_index;
int ou_video_index, ou_audio_index;
AVCodecContext* de_video_ctx = NULL;
AVCodecContext* en_video_ctx = NULL;
AVFrame* de_frame = NULL;
AVFilterGraph* graph = NULL;
AVFilterContext* src_filter_ctx = NULL;
AVFilterContext* sink_filter_ctx = NULL;

//编码
void 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;
        //printf("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);
    }
}
//解码
void 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) {
                //printf("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) {
            //printf("av_buffersrc_add_frame_flags failed");
            break;
        }

        while (true) {
            AVFrame* enframe = av_frame_alloc();
            ret = av_buffersink_get_frame(sink_filter_ctx, enframe);    //从FilterGraph中取出一个AVFrame
            if (ret == AVERROR_EOF) {
                // 说明结束了
                //printf("avfilter endeof");
                // 清空编码器
                doEncodec(NULL);
                // 释放内存
                av_frame_unref(enframe);
            }
            else if (ret < 0) {
                // 释放内存
                av_frame_unref(enframe);
                break;
            }

            // 进行重新编码
            doEncodec(enframe);
            // 释放内存
            av_frame_unref(enframe);
        }
    }
}
//初始化过滤器
bool initFilterGraph(QString srtPath)
{
    graph = avfilter_graph_alloc();     //为FilterGraph分配内存
    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:pixel_aspect=%d/%d", stream->codecpar->width, stream->codecpar->height, stream->codecpar->format, stream->time_base.num, stream->time_base.den, stream->codecpar->sample_aspect_ratio.num, stream->codecpar->sample_aspect_ratio.den);
    ret = avfilter_graph_create_filter(&src_filter_ctx, src_filter, "in", desc, NULL, graph);  //创建并向FilterGraph中添加一个Filter
    if (ret < 0) {
        //printf("init src filter failed");
        return false;
    }

    // 输出滤镜
    enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
    AVBufferSinkParams* buffersink_params;
    buffersink_params = av_buffersink_params_alloc();
    buffersink_params->pixel_fmts = pix_fmts;
    const AVFilter* sink_filter = avfilter_get_by_name("buffersink");
    ret = avfilter_graph_create_filter(&sink_filter_ctx, sink_filter, "out", NULL, buffersink_params, graph);
    //av_free(buffersink_params);
    if (ret < 0) {
        char errorBuf[256] = { 0 };
        av_strerror(ret, errorBuf, sizeof(errorBuf));
        //printf("buffersink init failed");
        return false;
    }
     // 滤镜描述符
    char filter_des[400];
    //sprintf(filter_des, "drawbox=x=100:y=100:w=100:h=100:color=pink@0.5");    //加一个方框 可以运行
    //sprintf(filter_des, "drawtext=fontfile=arial.ttf:fontcolor=white:fontsize=30:text='Aispeech'");
    sprintf(filter_des, "subtitles=filename='123.srt'");        //只能使用当前路径的srt文件
    srtPath.replace(":", "\\:");
    //QString srtPath = QString("D\\:\\123.srt");
    //sprintf(filter_des, "subtitles=filename='%s'", srtPath.toStdString().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;

    ret = avfilter_graph_parse_ptr(graph, filter_des, &inputs, &ouputs, NULL);  //将一串通过字符串描述的Graph添加到FilterGraph中
    if (ret < 0) {
        char errorBuf[256] = { 0 };
        av_strerror(ret, errorBuf,sizeof(errorBuf));
        return false;
    }

    av_buffersink_set_frame_size(sink_filter_ctx, en_video_ctx->frame_size);

    // 初始化滤镜
    if (avfilter_graph_config(graph, NULL) < 0) {   //检查FilterGraph的配置
        //printf("avfilter_graph_config failed");
        return false;
    }

    avfilter_inout_free(&inputs);
    avfilter_inout_free(&ouputs);

    return true;
}
void HardSubtitle()
{
    av_register_all();
    avcodec_register_all();
    const char* ver = av_version_info();
    int ret = 0;
    // 打开视频流
    if (avformat_open_input(&vfmt, m_videoPath.toStdString().c_str(), NULL, NULL) < 0) {
        printf("avformat_open_input failed");
        return;
    }
    //判断文件是否有音视频流
    if (avformat_find_stream_info(vfmt, NULL) < 0) {
        printf("avformat_find_stream_info");
        releaseInternal();
        return;
    }
    //分配输出的AVFormatContext
    if ((ret = avformat_alloc_output_context2(&ofmt, NULL, NULL, m_destPath.toStdString().c_str())) < 0) {
        printf("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) {
                printf("not surport codec!");
                releaseInternal();
                return;
            }
            de_video_ctx = avcodec_alloc_context3(codec);
            if (!de_video_ctx) {
                printf("avcodec_alloc_context3 failed");
                releaseInternal();
                return;
            }
            // 设置解码参数,从源文件拷贝
            avcodec_parameters_to_context(de_video_ctx, sstream->codecpar);
            // 初始化解码器上下文
            if (avcodec_open2(de_video_ctx, codec, NULL) < 0) {
                printf("avcodec_open2 failed");
                releaseInternal();
                return;
            }

            // 创建编码器
            AVCodec* encodec = avcodec_find_encoder(sstream->codecpar->codec_id);
            if (!encodec) {
                printf("not surport encodec!");
                releaseInternal();
                return;
            }
            en_video_ctx = avcodec_alloc_context3(encodec);
            if (!en_video_ctx) {
                printf("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->framerate.num = fps;
            en_video_ctx->framerate.den = 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) {
                printf("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) {
        printf("not has video stream");
        releaseInternal();
        return;
    }

    if (!(ofmt->flags & AVFMT_NOFILE)) {
        if (avio_open(&ofmt->pb, m_destPath.toStdString().c_str(), AVIO_FLAG_WRITE) < 0) {
            printf("avio_open() failed");
            releaseInternal();
            return;
        }
    }
    //打印媒体信息  将 AVFormatContext 中的媒体信息转存到输出
    av_dump_format(ofmt, -1, m_destPath.toStdString().c_str(), 1);

    // 写入头文件
    if (avformat_write_header(ofmt, NULL) < 0) {
        printf("avformat_write_header failed");
        releaseInternal();
        return;
    }

    // 初始化过滤器
    if (!initFilterGraph(m_srtPath)) {
        printf("");
        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;
            //printf("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);

    printf("finish !");
    doDecodec(NULL);
    av_write_trailer(ofmt);
    if (vfmt) {
        avformat_close_input(&vfmt);
        avformat_free_context(vfmt);
    }
    if (sfmt) {
        avformat_close_input(&sfmt);
        avformat_free_context(sfmt);
    }
    if (ofmt) {
        avformat_close_input(&ofmt);
        avformat_free_context(ofmt);
    }
    releaseInternal();
}

注意:“:”在FFMPEG中有其他用途,因此在传入路径时使用相对路径或者加转义字符“\”
链接: 源码下载

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值