字幕简介
我们在观看电影或短视频时,一般在视频下发会出现字幕,有些视频还会配中英双语字幕。字幕可以帮助观看人更好的了解视频内容。字幕分为以下三种类型:
外挂字幕:独立的字幕文件,播放视频时把视频和字幕放入同一路径下,也可以在播放器中选择外挂字幕。这样在播放视频时就可以看到字幕内容。常见字幕格式srt、vtt、ass等。
软字幕:也叫内挂字幕、封装字幕、字幕流等。通过某种技术将外挂字幕与视频文件打包在一起成一个文件。视频文件也可以同时封装多个字幕文件,播放时通过播放器选择所需字幕或不显示字幕。在需要时,还可以将字幕分离出来,修改后再打包进去。
硬字幕:将字幕内容覆盖叠加到视频画面上。这种字幕与视频画面溶于一体,具有最佳兼容性,只要能播放视频就能显示字幕。缺点是字幕占据视频画面,无法隐藏,破坏原有视频内容。且不可编辑更改。文章来源:https://www.toymoban.com/news/detail-730531.html
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中有其他用途,因此在传入路径时使用相对路径或者加转义字符“\”
链接: 源码下载文章来源地址https://www.toymoban.com/news/detail-730531.html
到了这里,关于通过FFMPEG给视频加字幕的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!