ffmpeg 字幕解码
原文:
https://blog.csdn.net/u011283226/article/details/102241233
【写在前面】
在前一篇,我已经讲过了读取外挂字幕并显示的方法:理解过滤图并使用字幕过滤器
但是,全字幕不仅仅是外挂字幕,还有内封字幕和内嵌字幕,因此我们还得考虑其他两种字幕。
不过,对于内嵌字幕,我们根本不需要解码,因为它是直接绘制在视频图像上的。
所以,本篇只需要讲解内封字幕的解码方法,主要内容有:
1、ass 等格式内封字幕解码。
2、sub+idx 格式内封字幕解码。
3、同步视频和字幕。
【正文开始】
- 首先是内封字幕:
我们知道,所谓内封字幕,就是将字幕文件(可能是srt, ass)封装在视频容器中,成为字幕流。
因此只要确定视频存在字幕流( ass等 ),就可以使用和外挂字幕一样的方法进行解码。
当然了,略微有些不同,先来看看代码:
- AVFormatContext *formatContext = nullptr;
- AVCodecContext *videoCodecContext = nullptr, *subCodecContext = nullptr;
- AVStream *videoStream = nullptr, *subStream = nullptr;
- int videoIndex = -1, subIndex = -1;
- //打开输入文件,并分配格式上下文
- avformat_open_input(&formatContext, m_filename.toStdString().c_str(), nullptr, nullptr);
- avformat_find_stream_info(formatContext, nullptr);
- //找到视频流,字幕流的索引
- for (size_t i = 0; i < formatContext->nb_streams; ++i) {
- if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
- videoIndex = int(i);
- videoStream = formatContext->streams[i];
- } else if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
- subIndex = int(i);
- subStream = formatContext->streams[i];
- }
- }
- //打印相关信息,在 stderr
- av_dump_format(formatContext, 0, "format", 0);
- fflush(stderr);
- if (!open_codec_context(videoCodecContext, videoStream)) {
- qDebug() << "Open Video Context Failed!";
- return;
- }
- if (!open_codec_context(subCodecContext, subStream)) {
- //字幕流打开失败,也可能是没有,但无影响,接着处理
- qDebug() << "Open Subtitle Context Failed!";
- }
这块代码就是简单的找到视频流和字幕流,并打开相关上下文( Context ),如果不懂,可以前往第一篇 视频解码。
然后我们继续往下看:
- m_fps = videoStream->avg_frame_rate.num / videoStream->avg_frame_rate.den;
- m_width = videoCodecContext->width;
- m_height = videoCodecContext->height;
- //初始化filter相关
- AVRational time_base = videoStream->time_base;
- QString args = QString::asprintf("video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
- m_width, m_height, videoCodecContext->pix_fmt, time_base.num, time_base.den,
- videoCodecContext->sample_aspect_ratio.num, videoCodecContext->sample_aspect_ratio.den);
- qDebug() << "Video Args: " << args;
- AVFilterContext *buffersrcContext = nullptr;
- AVFilterContext *buffersinkContext = nullptr;
- bool subtitleOpened = false;
- //如果有字幕流
- if (subCodecContext) {
- //字幕流直接用视频名即可
- QString subtitleFilename = m_filename;
- subtitleFilename.replace('/', "\\\\");
- subtitleFilename.insert(subtitleFilename.indexOf(":\\"), char('\\'));
- QString filterDesc = QString("subtitles=filename='%1':original_size=%2x%3")
- .arg(subtitleFilename).arg(m_width).arg(m_height);
- qDebug() << "Filter Description:" << filterDesc.toStdString().c_str();
- subtitleOpened = init_subtitle_filter(buffersrcContext, buffersinkContext, args, filterDesc);
- if (!subtitleOpened) {
- qDebug() << "字幕打开失败!";
- }
- } else {
- //没有字幕流时,在同目录下寻找字幕文件
- //字幕相关,使用subtitles,目前测试的是ass,但srt, ssa, ass, lrc都行,改后缀名即可
- int suffixLength = QFileInfo(m_filename).suffix().length();
- QString subtitleFilename = m_filename.mid(0, m_filename.length() - suffixLength - 1) + ".ass";
- if (QFile::exists(subtitleFilename)) {
- //初始化subtitle filter
- //绝对路径必须转成D\:\\xxx\\test.ass这种形式, 记住,是[D\:\\]这种形式
- //toNativeSeparator()无用,因为只是 / -> \ 的转换
- subtitleFilename.replace('/', "\\\\");
- subtitleFilename.insert(subtitleFilename.indexOf(":\\"), char('\\'));
- QString filterDesc = QString("subtitles=filename='%1':original_size=%2x%3")
- .arg(subtitleFilename).arg(m_width).arg(m_height);
- qDebug() << "Filter Description:" << filterDesc.toStdString().c_str();
- subtitleOpened = init_subtitle_filter(buffersrcContext, buffersinkContext, args, filterDesc);
- if (!subtitleOpened) {
- qDebug() << "字幕打开失败!";
- }
- }
- }
1、如果存在字幕流( if (subCodecContext) ),那么就初始化一个字幕过滤器,字幕过滤器的参数是:

要注意,对于外挂字幕而言,filename 即为字幕文件名,而对于内封字幕,fliename 为视频文件名,格式为:[ D\:\\ ]。
2、如果不存在存在字幕流,那么就寻找同目录下的外挂字幕。
- 然而,这只是 ass 等格式的内封字幕,对于 sub+idx 格式的内嵌字幕,就需要我们自己解码、绘制了。
我们知道,sub+idx 是图形字幕格式,sub 包含了一系列的字幕位图,idx 则是其索引。
当然,对于内部如何我们无需知晓,因为 ffmpeg 会将其解码,具体如下:
- SubtitleFrame subFrame;
- //读取下一帧
- while (m_runnable && av_read_frame(formatContext, packet) >= 0) {
- if (packet->stream_index == videoIndex) {
- //发送给解码器
- int ret = avcodec_send_packet(videoCodecContext, packet);
- while (ret >= 0) {
- //从解码器接收解码后的帧
- ret = avcodec_receive_frame(videoCodecContext, frame);
- frame->pts = frame->best_effort_timestamp;
- if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
- else if (ret < 0) goto Run_End;
- //如果字幕成功打开,则输出使用subtitle filter过滤后的图像
- if (subtitleOpened) {
- if (av_buffersrc_add_frame_flags(buffersrcContext, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0)
- break;
- while (true) {
- ret = av_buffersink_get_frame(buffersinkContext, filter_frame);
- if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
- else if (ret < 0) goto Run_End;
- QImage videoImage = convert_image(filter_frame);
- m_frameQueue.enqueue(videoImage);
- av_frame_unref(filter_frame);
- }
- } else {
- //未打开字幕过滤器或无字幕
- if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
- else if (ret < 0) goto Run_End;
- QImage videoImage = convert_image(frame);
- //如果需要显示字幕,就将字幕覆盖上去
- if (frame->pts >= subFrame.pts && frame->pts <= (subFrame.pts + subFrame.duration)) {
- videoImage = overlay_subtitle(videoImage, subFrame.image);
- }
- m_frameQueue.enqueue(videoImage);
- }
- av_frame_unref(frame);
- }
- } else if (packet->stream_index == subIndex) {
- AVSubtitle subtitle;
- int got_frame;
- int ret = avcodec_decode_subtitle2(subCodecContext, &subtitle, &got_frame, packet);
- if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
- else if (ret < 0) goto Run_End;
- if (got_frame > 0) {
- //如果是图像字幕,即sub + idx
- //实际上,只需要处理这种即可
- if (subtitle.format == 0) {
- for (size_t i = 0; i < subtitle.num_rects; i++) {
- AVSubtitleRect *sub_rect = subtitle.rects[i];
- int dst_linesize[4];
- uint8_t *dst_data[4];
- //注意,这里是RGBA格式,需要Alpha
- av_image_alloc(dst_data, dst_linesize, sub_rect->w, sub_rect->h, AV_PIX_FMT_RGBA, 1);
- SwsContext *swsContext = sws_getContext(sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8,
- sub_rect->w, sub_rect->h, AV_PIX_FMT_RGBA,
- SWS_BILINEAR, nullptr, nullptr, nullptr);
- sws_scale(swsContext, sub_rect->data, sub_rect->linesize, 0, sub_rect->h, dst_data, dst_linesize);
- sws_freeContext(swsContext);
- //这里也使用RGBA
- QImage image = QImage(dst_data[0], sub_rect->w, sub_rect->h, QImage::Format_RGBA8888).copy();
- av_freep(&dst_data[0]);
- //subFrame存储当前的字幕
- //只有图像字幕才有start_display_time和start_display_time
- subFrame.pts = packet->pts;
- subFrame.duration = subtitle.end_display_time - subtitle.start_display_time;
- subFrame.image = image;
- }
- } else {
- //如果是文本格式字幕:srt, ssa, ass, lrc
- //可以直接输出文本,实际上已经添加到过滤器中
- qreal pts = packet->pts * av_q2d(subStream->time_base);
- qreal duration = packet->duration * av_q2d(subStream->time_base);
- const char *text = const_int8_ptr(packet->data);
- qDebug() << "[PTS: " << pts << "]" << endl
- << "[Duration: " << duration << "]" << endl
- << "[Text: " << text << "]" << endl;
- }
- }
- }
- 先来看 else if (packet->stream_index == subIndex) 部分:
1、使用 avcodec_decode_subtitle2() 获取一帧字幕。
2、subtilte.format 存储字幕格式,为0代表图像字幕。
3、subtitle.rects 存储了字幕位图,因此我们只需要将其转换成想要的图像格式,然后覆盖( overlay )在视频图像上即可。
4、这里需要小小的注意一下,因为视频和字幕并不是同时解码的,并且,字幕会持续一段时间,也就是说,可能很多帧视频使用同一帧字幕,所以我们要同步视频和字幕,这里使用了一个 SubtitleFrame,它的定义如下:
- struct SubtitleFrame {
- QImage image;
- int64_t pts;
- int64_t duration;
- };
我的同步方法是:videoFrame.pts >= subFrame.pts && videoFrame.pts <= subFrame.pts + subFrame.duration,即 视频帧的显示时间戳处于[字幕开始, 字幕结束]之间时,就显示字幕。
现在我们回到 if (subtitleOpened) 这里。
1、如果字幕已经成功打开( ass等格式的外挂字幕或内封字幕 ),我们就直接使用字幕过滤器将字幕添加到视频帧。
2、如果字幕未能成功打开( 为sub+idx格式或没有字幕 ),我们就将 subFrame 覆盖到视频帧上,注意,subFrame 我们在 else if (packet->stream_index == subIndex) 中已经得到了,当然,如果没有则其为空。
其中,conver_image() 和 overlay_subtitle() 很简单,所以直接看源码就好了。
至此,内封字幕讲解完毕。
ffmpeg 字幕解码的更多相关文章
- ffmpeg编解码视频导致噪声增大的一种解决方法
一.前言 ffmpeg在视音频编解码领域算是一个比较成熟的解决方案了.公司的一款视频编辑软件正是基于ffmpeg做了二次封装,并在此基础上进行音视频的编解码处理.然而,在观察编码后的视频质量时,发现图 ...
- FFmpeg编解码处理4-音频编码
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584948.html FFmpeg编解码处理系列笔记: [0]. FFmpeg时间戳详 ...
- FFmpeg编解码处理3-视频编码
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584937.html FFmpeg编解码处理系列笔记: [0]. FFmpeg时间戳详 ...
- FFmpeg编解码处理2-编解码API详解
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584925.html FFmpeg编解码处理系列笔记: [0]. FFmpeg时间戳详 ...
- FFmpeg编解码处理1-转码全流程简介
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584901.html FFmpeg编解码处理系列笔记: [0]. FFmpeg时间戳详 ...
- 最近在研究FFmpeg编解码
好几年没上CNBLOGS了, 最近在研究FFmpeg编解码,一个人研究感到很寂寞,所以想通过博客来和大家分享和交流,呵呵. 最近研究的主题是: ANDROID手机同屏技术: 需要用到ANDROID截屏 ...
- ffmpeg 编解码详细过程
ffmpeg编解码详细过程 bobbypollo 转:ffmpeg编解码详细过程 原文地址:ffmpeg编解码详细过程(转)作者:心在飞翔原文出处: http://www.360doc.com ...
- 视频编解码---x264用于编码,ffmpeg用于解码
项目要用到视频编解码,最近半个月都在搞,说实话真是走了很多弯路,浪费了很多时间.将自己的最终成果记录于此,期望会给其他人提供些许帮助. 参考教程: http://ffmpeg.org/trac/ffm ...
- 学习FFmpeg API – 解码视频
本文转载 视频播放过程 首先简单介绍以下视频文件的相关知识.我们平时看到的视频文件有许多格式,比如 avi, mkv, rmvb, mov, mp4等等,这些被称为容器(Container), 不同的 ...
- 【转】学习FFmpeg API – 解码视频
ffmpeg是编解码的利器,用了很久,以前看过dranger 的教程,非常精彩,受益颇多,是学习ffmpeg api很好的材料.可惜的是其针对的ffmpeg版本已经比较老了,而ffmpeg的更新又很快 ...
随机推荐
- kafka源码阅读之MacBook Pro M1搭建Kafka2.7版本源码运行环境
原创/朱季谦 最近在阅读Kafka的源码,想可以在阅读过程当中,在代码写一些注释,便决定将源码部署到本地运行. 日常开发过程中,用得比较多一个版本是Kafka2.7版本,故而在MacBook Pro笔 ...
- javaGUI入门之swing(一)
javaGUI入门之swing(一) 前面学过javafx,发现他们有"异曲同工"之处,demo写起来也类似,无非是类名不一样.个人觉得实现一个桌面应用不应该只看用一种语言一种框架 ...
- 揭秘Spring事务失效场景分析与解决方案
在Spring框架中,事务管理是一个核心功能,然而有时候会遇到事务失效的情况,这可能导致数据一致性问题.本文将深入探讨一些Spring事务失效的常见场景,并提供详细的例子以及解决方案. 1. 跨方法调 ...
- 华为IoT首席架构师王启军:全栈工程师“养成记”
在王启军的公众号里,有一篇<My Team>的文章,里面记录了早年他所带团队成长的心得. 这个被他称为完美组合的团队,并不是来自大厂名企,彼时王启军给不起高待遇,团队核心成员中还有很多人是 ...
- 秋风到,ModelArts“ AI市场算法Fast-SCNN指南”秋膘贴起来
本文分享自华为云社区<带你来秋日尝鲜 | ModelArts AI市场算法Fast-SCNN使用指导>,作者:Tianyi_Li 摘要:送小伙伴们一份新鲜出炉的ModelArts AI市场 ...
- 初识GaussDB(for Cassandra)
摘要:GaussDB(for Cassandra)是一款基于华为自主研发的计算存储分离架构的分布式云数据库服务. "local quorum查询某个分区键的条数,每次查询,条数都不一样.&q ...
- 印尼Widya Robotics携手华为云,让建筑工地安全看得见
摘要:AI落地效率提升30倍,实时智慧安全检测,全天候保障劳工人身安全. 本文分享自华为云社区<云上360行 丨 印尼Widya Robotics携手华为云,让建筑工地安全看得见>,作者: ...
- 跟我读论文丨ACL2021 NER BERT化隐马尔可夫模型用于多源弱监督命名实体识别
摘要:本文是对ACL2021 NER BERT化隐马尔可夫模型用于多源弱监督命名实体识别这一论文工作进行初步解读. 本文分享自华为云社区<ACL2021 NER | BERT化隐马尔可夫模型用于 ...
- 教你用ab命令进行并发与压力测试
摘要:今天给大家分享一篇如何使用ab进行并发与压力测试的文章 本文分享自华为云社区<[高并发]如何使用ab进行并发与压力测试?>,作者:冰 河. 今天给大家分享一篇如何使用ab进行并发与压 ...
- 聊聊Hive数据血缘——从Atlas没有列级血缘的Bug讲起
前几天,Datahub提供了最新的字段级别数据血缘功能,很多朋友迫不及待想对比一下Datahub的字段级血缘与Atlas的区别. 这个时候问题来了,在Atlas收集Hive血缘的时候,由于部分版本问题 ...