记ffmpeg subtitles滤镜切换字幕卡顿
1.subtitles卡顿
偷懒在项目中使用ffmpeg的subtitles滤镜进行字幕渲染。后来发现,使用滤镜切换字幕时会出现卡顿。经过进一步测试与代码调式确认,在播放一个时长在一个小时以上的视频文件的内挂字幕时,滤镜初始化花费了较长的时间。
使用ffplay + subtitles滤镜播放该文件并显示字幕存在同样的问题。(播放窗口出现前需执行较长时间)(ps:好,第三方开源库问题,下班喽)
注:该问题在ffmpeg5.1存在。6.1不记得了,回头再测下看看。
2.调试ffmpeg滤镜
ffmpeg代码不是很熟,好在手里有跑通的源码可以用来调试。大致定位了下,找到了vf_subtitles.c这个文件,subtitles滤镜的相关代码都在这个文件内。接下来就好找了,我们阅读下代码或者直接调试都可以确定,花费了大量时间的是init_subtitles函数的以下代码:
while (av_read_frame(fmt, &pkt) >= 0) {
int i, got_subtitle;
AVSubtitle sub = {0};
if (pkt.stream_index == sid) {
ret = avcodec_decode_subtitle2(dec_ctx, &sub, &got_subtitle, &pkt);
if (ret < 0) {
av_log(ctx, AV_LOG_WARNING, "Error decoding: %s (ignored)\n",
av_err2str(ret));
} else if (got_subtitle) {
const int64_t start_time = av_rescale_q(sub.pts, AV_TIME_BASE_Q, av_make_q(1, 1000));
const int64_t duration = sub.end_display_time;
for (i = 0; i < sub.num_rects; i++) {
char *ass_line = sub.rects[i]->ass;
if (!ass_line)
break;
ass_process_chunk(ass->track, ass_line, strlen(ass_line),
start_time, duration);
}
}
}
av_packet_unref(&pkt);
avsubtitle_free(&sub);
}
这段代码很简单,调用的ffmpeg api解码字幕并将字幕文本传入libass。但是再往上找能看到以下代码:
ret = avformat_open_input(&fmt, ass->filename, NULL, NULL);
if (ret < 0) {
av_log(ctx, AV_LOG_ERROR, "Unable to open %s\n", ass->filename);
goto end;
}
ret = avformat_find_stream_info(fmt, NULL);
if (ret < 0)
goto end;
/* Locate subtitles stream */
if (ass->stream_index < 0)
ret = av_find_best_stream(fmt, AVMEDIA_TYPE_SUBTITLE, -1, -1, NULL, 0);
else {
ret = -1;
if (ass->stream_index < fmt->nb_streams) {
for (j = 0; j < fmt->nb_streams; j++) {
if (fmt->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
if (ass->stream_index == k) {
ret = j;
break;
}
k++;
}
}
}
}
这里对字幕文件解封装,如果是针对外挂字幕文件,这两段代码是可行的。但如果是内挂字幕,解码前的av_read_frame方法会遍历输入文件的所有包,包括视频流与音频流的包。耗费了大量的时间。
3.解决方法
代码修改如下:
/* Locate subtitles stream */
if (ass->stream_index < 0)
ret = av_find_best_stream(fmt, AVMEDIA_TYPE_SUBTITLE, -1, -1, NULL, 0);
else {
ret = -1;
if (ass->stream_index < fmt->nb_streams) {
for (j = 0; j < fmt->nb_streams; j++) {
if (fmt->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
if (ass->stream_index == k) {
ret = j;
// break;
}
k++;
}
}
}
}
删除找到字幕流后的的break,并设置除字幕流外每一条流的flag为discard_all,避免遍历非选定字幕流的包导致耗时。然后重新编译ffmpeg即可。
if (ass->stream_index < 0)
ret = av_find_best_stream(fmt, AVMEDIA_TYPE_SUBTITLE, -1, -1, NULL, 0);
else {
ret = -1;
if (ass->stream_index < fmt->nb_streams) {
for (j = 0; j < fmt->nb_streams; j++) {
auto discard = fmt->streams[j]->discard;
fmt->streams[j]->discard = AVDISCARD_ALL;
if (fmt->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
if (ass->stream_index == k) {
ret = j;
// break;
fmt->streams[j]->discard = discard;
}
k++;
}
}
}
}