ffmpeg 字幕解码

原文:

https://blog.csdn.net/u011283226/article/details/102241233

【写在前面】

在前一篇,我已经讲过了读取外挂字幕并显示的方法:理解过滤图并使用字幕过滤器

但是,全字幕不仅仅是外挂字幕,还有内封字幕和内嵌字幕,因此我们还得考虑其他两种字幕。

不过,对于内嵌字幕,我们根本不需要解码,因为它是直接绘制在视频图像上的。

所以,本篇只需要讲解内封字幕的解码方法,主要内容有:

1、ass 等格式内封字幕解码。

2、sub+idx 格式内封字幕解码。

3、同步视频和字幕。


【正文开始】

  • 首先是内封字幕:

我们知道,所谓内封字幕,就是将字幕文件(可能是srt, ass)封装在视频容器中,成为字幕流。

因此只要确定视频存在字幕流( ass等 ),就可以使用和外挂字幕一样的方法进行解码。

当然了,略微有些不同,先来看看代码:

  1.  
    AVFormatContext *formatContext = nullptr;
  2.  
    AVCodecContext *videoCodecContext = nullptr, *subCodecContext = nullptr;
  3.  
    AVStream *videoStream = nullptr, *subStream = nullptr;
  4.  
    int videoIndex = -1, subIndex = -1;
  5.  
     
  6.  
    //打开输入文件,并分配格式上下文
  7.  
    avformat_open_input(&formatContext, m_filename.toStdString().c_str(), nullptr, nullptr);
  8.  
    avformat_find_stream_info(formatContext, nullptr);
  9.  
     
  10.  
    //找到视频流,字幕流的索引
  11.  
    for (size_t i = 0; i < formatContext->nb_streams; ++i) {
  12.  
    if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
  13.  
    videoIndex = int(i);
  14.  
    videoStream = formatContext->streams[i];
  15.  
    } else if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
  16.  
    subIndex = int(i);
  17.  
    subStream = formatContext->streams[i];
  18.  
    }
  19.  
    }
  20.  
     
  21.  
    //打印相关信息,在 stderr
  22.  
    av_dump_format(formatContext, 0, "format", 0);
  23.  
    fflush(stderr);
  24.  
     
  25.  
    if (!open_codec_context(videoCodecContext, videoStream)) {
  26.  
    qDebug() << "Open Video Context Failed!";
  27.  
    return;
  28.  
    }
  29.  
     
  30.  
    if (!open_codec_context(subCodecContext, subStream)) {
  31.  
    //字幕流打开失败,也可能是没有,但无影响,接着处理
  32.  
    qDebug() << "Open Subtitle Context Failed!";
  33.  
    }

这块代码就是简单的找到视频流和字幕流,并打开相关上下文( Context ),如果不懂,可以前往第一篇 视频解码

然后我们继续往下看:

  1.  
    m_fps = videoStream->avg_frame_rate.num / videoStream->avg_frame_rate.den;
  2.  
    m_width = videoCodecContext->width;
  3.  
    m_height = videoCodecContext->height;
  4.  
     
  5.  
    //初始化filter相关
  6.  
    AVRational time_base = videoStream->time_base;
  7.  
    QString args = QString::asprintf("video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
  8.  
    m_width, m_height, videoCodecContext->pix_fmt, time_base.num, time_base.den,
  9.  
    videoCodecContext->sample_aspect_ratio.num, videoCodecContext->sample_aspect_ratio.den);
  10.  
    qDebug() << "Video Args: " << args;
  11.  
     
  12.  
    AVFilterContext *buffersrcContext = nullptr;
  13.  
    AVFilterContext *buffersinkContext = nullptr;
  14.  
    bool subtitleOpened = false;
  15.  
     
  16.  
    //如果有字幕流
  17.  
    if (subCodecContext) {
  18.  
    //字幕流直接用视频名即可
  19.  
    QString subtitleFilename = m_filename;
  20.  
    subtitleFilename.replace('/', "\\\\");
  21.  
    subtitleFilename.insert(subtitleFilename.indexOf(":\\"), char('\\'));
  22.  
    QString filterDesc = QString("subtitles=filename='%1':original_size=%2x%3")
  23.  
    .arg(subtitleFilename).arg(m_width).arg(m_height);
  24.  
    qDebug() << "Filter Description:" << filterDesc.toStdString().c_str();
  25.  
    subtitleOpened = init_subtitle_filter(buffersrcContext, buffersinkContext, args, filterDesc);
  26.  
    if (!subtitleOpened) {
  27.  
    qDebug() << "字幕打开失败!";
  28.  
    }
  29.  
    } else {
  30.  
    //没有字幕流时,在同目录下寻找字幕文件
  31.  
    //字幕相关,使用subtitles,目前测试的是ass,但srt, ssa, ass, lrc都行,改后缀名即可
  32.  
    int suffixLength = QFileInfo(m_filename).suffix().length();
  33.  
    QString subtitleFilename = m_filename.mid(0, m_filename.length() - suffixLength - 1) + ".ass";
  34.  
    if (QFile::exists(subtitleFilename)) {
  35.  
    //初始化subtitle filter
  36.  
    //绝对路径必须转成D\:\\xxx\\test.ass这种形式, 记住,是[D\:\\]这种形式
  37.  
    //toNativeSeparator()无用,因为只是 / -> \ 的转换
  38.  
    subtitleFilename.replace('/', "\\\\");
  39.  
    subtitleFilename.insert(subtitleFilename.indexOf(":\\"), char('\\'));
  40.  
    QString filterDesc = QString("subtitles=filename='%1':original_size=%2x%3")
  41.  
    .arg(subtitleFilename).arg(m_width).arg(m_height);
  42.  
    qDebug() << "Filter Description:" << filterDesc.toStdString().c_str();
  43.  
    subtitleOpened = init_subtitle_filter(buffersrcContext, buffersinkContext, args, filterDesc);
  44.  
    if (!subtitleOpened) {
  45.  
    qDebug() << "字幕打开失败!";
  46.  
    }
  47.  
    }
  48.  
    }

1、如果存在字幕流( if (subCodecContext) ),那么就初始化一个字幕过滤器,字幕过滤器的参数是:

要注意,对于外挂字幕而言,filename 即为字幕文件名,而对于内封字幕,fliename 为视频文件名,格式为:[ D\:\\ ]。

2、如果不存在存在字幕流,那么就寻找同目录下的外挂字幕。

  • 然而,这只是 ass 等格式的内封字幕,对于 sub+idx 格式的内嵌字幕,就需要我们自己解码、绘制了。

我们知道,sub+idx 是图形字幕格式,sub 包含了一系列的字幕位图,idx 则是其索引。

当然,对于内部如何我们无需知晓,因为 ffmpeg 会将其解码,具体如下:

  1.  
    SubtitleFrame subFrame;
  2.  
     
  3.  
    //读取下一帧
  4.  
    while (m_runnable && av_read_frame(formatContext, packet) >= 0) {
  5.  
    if (packet->stream_index == videoIndex) {
  6.  
    //发送给解码器
  7.  
    int ret = avcodec_send_packet(videoCodecContext, packet);
  8.  
     
  9.  
    while (ret >= 0) {
  10.  
    //从解码器接收解码后的帧
  11.  
    ret = avcodec_receive_frame(videoCodecContext, frame);
  12.  
     
  13.  
    frame->pts = frame->best_effort_timestamp;
  14.  
     
  15.  
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
  16.  
    else if (ret < 0) goto Run_End;
  17.  
     
  18.  
    //如果字幕成功打开,则输出使用subtitle filter过滤后的图像
  19.  
    if (subtitleOpened) {
  20.  
    if (av_buffersrc_add_frame_flags(buffersrcContext, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0)
  21.  
    break;
  22.  
     
  23.  
    while (true) {
  24.  
    ret = av_buffersink_get_frame(buffersinkContext, filter_frame);
  25.  
     
  26.  
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
  27.  
    else if (ret < 0) goto Run_End;
  28.  
     
  29.  
    QImage videoImage = convert_image(filter_frame);
  30.  
    m_frameQueue.enqueue(videoImage);
  31.  
     
  32.  
    av_frame_unref(filter_frame);
  33.  
    }
  34.  
    } else {
  35.  
    //未打开字幕过滤器或无字幕
  36.  
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
  37.  
    else if (ret < 0) goto Run_End;
  38.  
     
  39.  
    QImage videoImage = convert_image(frame);
  40.  
    //如果需要显示字幕,就将字幕覆盖上去
  41.  
    if (frame->pts >= subFrame.pts && frame->pts <= (subFrame.pts + subFrame.duration)) {
  42.  
    videoImage = overlay_subtitle(videoImage, subFrame.image);
  43.  
    }
  44.  
    m_frameQueue.enqueue(videoImage);
  45.  
    }
  46.  
    av_frame_unref(frame);
  47.  
    }
  48.  
    } else if (packet->stream_index == subIndex) {
  49.  
    AVSubtitle subtitle;
  50.  
    int got_frame;
  51.  
    int ret = avcodec_decode_subtitle2(subCodecContext, &subtitle, &got_frame, packet);
  52.  
     
  53.  
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
  54.  
    else if (ret < 0) goto Run_End;
  55.  
     
  56.  
    if (got_frame > 0) {
  57.  
    //如果是图像字幕,即sub + idx
  58.  
    //实际上,只需要处理这种即可
  59.  
    if (subtitle.format == 0) {
  60.  
    for (size_t i = 0; i < subtitle.num_rects; i++) {
  61.  
    AVSubtitleRect *sub_rect = subtitle.rects[i];
  62.  
     
  63.  
    int dst_linesize[4];
  64.  
    uint8_t *dst_data[4];
  65.  
    //注意,这里是RGBA格式,需要Alpha
  66.  
    av_image_alloc(dst_data, dst_linesize, sub_rect->w, sub_rect->h, AV_PIX_FMT_RGBA, 1);
  67.  
    SwsContext *swsContext = sws_getContext(sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8,
  68.  
    sub_rect->w, sub_rect->h, AV_PIX_FMT_RGBA,
  69.  
    SWS_BILINEAR, nullptr, nullptr, nullptr);
  70.  
    sws_scale(swsContext, sub_rect->data, sub_rect->linesize, 0, sub_rect->h, dst_data, dst_linesize);
  71.  
    sws_freeContext(swsContext);
  72.  
    //这里也使用RGBA
  73.  
    QImage image = QImage(dst_data[0], sub_rect->w, sub_rect->h, QImage::Format_RGBA8888).copy();
  74.  
    av_freep(&dst_data[0]);
  75.  
     
  76.  
    //subFrame存储当前的字幕
  77.  
    //只有图像字幕才有start_display_time和start_display_time
  78.  
    subFrame.pts = packet->pts;
  79.  
    subFrame.duration = subtitle.end_display_time - subtitle.start_display_time;
  80.  
    subFrame.image = image;
  81.  
    }
  82.  
    } else {
  83.  
    //如果是文本格式字幕:srt, ssa, ass, lrc
  84.  
    //可以直接输出文本,实际上已经添加到过滤器中
  85.  
    qreal pts = packet->pts * av_q2d(subStream->time_base);
  86.  
    qreal duration = packet->duration * av_q2d(subStream->time_base);
  87.  
    const char *text = const_int8_ptr(packet->data);
  88.  
    qDebug() << "[PTS: " << pts << "]" << endl
  89.  
    << "[Duration: " << duration << "]" << endl
  90.  
    << "[Text: " << text << "]" << endl;
  91.  
    }
  92.  
    }
  93.  
    }
  • 先来看 else if (packet->stream_index == subIndex) 部分:

1、使用 avcodec_decode_subtitle2() 获取一帧字幕。

2、subtilte.format 存储字幕格式,为0代表图像字幕。

3、subtitle.rects 存储了字幕位图,因此我们只需要将其转换成想要的图像格式,然后覆盖( overlay )在视频图像上即可。

4、这里需要小小的注意一下,因为视频和字幕并不是同时解码的,并且,字幕会持续一段时间,也就是说,可能很多帧视频使用同一帧字幕,所以我们要同步视频和字幕,这里使用了一个 SubtitleFrame,它的定义如下:

  1.  
    struct SubtitleFrame {
  2.  
    QImage image;
  3.  
    int64_t pts;
  4.  
    int64_t duration;
  5.  
    };

我的同步方法是: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() 很简单,所以直接看源码就好了。

至此,内封字幕讲解完毕。

posted @ 2020-12-09 14:21  IWing  阅读(1182)  评论(0编辑  收藏  举报