使用FFMpeg 提取MKV文件中的字幕

原创
2022-11-30
4625
7

MKV封装格式是万能封装格式,可以封装几乎所有的视频和音频编码格式。可以包含多个视频流、音频流和字幕流。本文将介绍使用FFMpeg 解码视频文件,提去字幕内容并保存。这里仅提取ASS格式的字幕文件。

使用FFMpeg解MKV封装,获取字幕流信息

void FFMpegAssThread::openVideoFile(QString fileName)
{
    // 打开视频文件
    int result = avformat_open_input(&m_FormatContext, fileName.toLocal8Bit().data(), nullptr, nullptr);
    if (result < 0)
        return;

    // 查找流信息
    result = avformat_find_stream_info(m_FormatContext, nullptr);
    if (result < 0)
        return;

    // 获取字幕流
    int streamCount = m_FormatContext->nb_streams;
    m_SubtitleCount = 0;
    for (int i=0; i<streamCount; ++i)
    {
        if (m_FormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_SUBTITLE)
        {
            m_SubtitleStream[m_SubtitleCount++] = i;
            continue;
        }
    }

    if (m_SubtitleCount == 0)
    {
        avformat_close_input(&m_FormatContext);
        return;
    }

    // 获取视频总时长
    m_TotalTime = m_FormatContext->duration * 1.0 / AV_TIME_BASE * 1000;

    // 获取解码器
    for (int i=0; i<m_SubtitleCount; ++i)
    {
        AVCodecContext *codecContext = m_FormatContext->streams[m_SubtitleStream[i]]->codec;
        if (codecContext->codec_id == AV_CODEC_ID_ASS)
        {
            AVCodec *codec = avcodec_find_decoder(codecContext->codec_id);
            result = avcodec_open2(codecContext, codec, nullptr);
            if (result < 0)
                continue;

            m_SubtitleCodecContext[i] = codecContext;
        }
    }
}

使用函数avcodec_decode_subtitle2解码字幕Packet,AVSubtitle中存储字幕的具体内容信息。下面是在线程中解码函数

void FFMpegAssThread::run(void)
{
    while (!this->isInterruptionRequested())
    {
        if (m_FormatContext != nullptr)
        {
            AVPacket pkt;
            av_init_packet(&pkt);

            // 获取一帧数据
            int result = av_read_frame(m_FormatContext, &pkt);
            if (result < 0)
            {
                emit sendCurrentProgress(100);
                av_packet_unref(&pkt);
                break;
            }

            bool needDecodec = false;
            AVCodecContext *codecContext = nullptr;
            for (int i=0; i<m_SubtitleCount; ++i)
            {
                if (m_SubtitleStream[i] == pkt.stream_index)
                {
                    needDecodec = true;
                    codecContext = m_SubtitleCodecContext[i];
                    break;
                }
            }

            if (!needDecodec)
            {
                av_packet_unref(&pkt);
                continue;
            }

            int streamIndex = pkt.stream_index;
            AVRational rational = m_FormatContext->streams[streamIndex]->time_base;
            qreal value = pkt.pts * 1.0 / rational.den * rational.num * 1000 / m_TotalTime * 100;
            emit sendCurrentProgress(value);

            // 解码
            AVSubtitle subtitle;
            int gotSub = 0;
            result = avcodec_decode_subtitle2(codecContext, &subtitle, &gotSub, &pkt);
            if (result < 0)
            {
                av_packet_unref(&pkt);
                continue;
            }

            if (gotSub > 0)
            {
                int number = subtitle.num_rects;
                for (int i=0; i<number; ++i)
                {
                    QFile *file = m_FileList.at(0);
                    file->write(subtitle.rects[i]->ass, strlen(subtitle.rects[0]->ass));
                }
                avsubtitle_free(&subtitle);
            }

            av_packet_unref(&pkt);
        }
        else
            QThread::msleep(10);
    }
}

下面是完整的代码:
界面-FFMpegAssGetWidget.h

#ifndef FFMPEG_ASS_GET_H
#define FFMPEG_ASS_GET_H

#include "UIBase/UIBaseWindow.h"
#include "FFMpegASSThread.h"
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QProgressBar>

class FFMpegAssGetWidget : public UIBaseWindow
{
    Q_OBJECT

public:
    FFMpegAssGetWidget(QWidget *parent = nullptr);
    ~FFMpegAssGetWidget();

private:
    void initUi(void);
    // 设置ASS路径
    void setAssPathCount(int count);

    QLineEdit *m_SrcFileNamePathLineEdit = nullptr;
    QPushButton *m_BrowseButton = nullptr;

    QList<QLineEdit*> m_DecodecLineEditList;
    QList<QPushButton*> m_DestBrowseButtonList;

    QPushButton *m_ConvertButton = nullptr;
    QProgressBar *m_ProgressBar = nullptr;

private slots:
    void onClickedBrowseButton(void);
    void onClickedDestBrowseButton(void);
    void onClickedConvertButton(void);
    void onRecvConvertProgress(qreal);

private:
    FFMpegAssThread *m_FFMpegAssThread = nullptr;
    QWidget *m_AssSubtitleWidget = nullptr;
};

#endif

界面-FFMpegAssGetWidget.cpp

#include "FFMpegASSGet.h"
#include <QFileDialog>
#include <QVBoxLayout>
#include <QLabel>
#include <QDebug>
#include "UIBase/UIGlobalTool.h"

FFMpegAssGetWidget::FFMpegAssGetWidget(QWidget *parent)
    :UIBaseWindow(parent)
{
    av_register_all();
    avcodec_register_all();

    initUi();

    m_FFMpegAssThread = new FFMpegAssThread;
    QObject::connect(m_FFMpegAssThread, SIGNAL(sendCurrentProgress(qreal)), \
                     this, SLOT(onRecvConvertProgress(qreal)));
}

FFMpegAssGetWidget::~FFMpegAssGetWidget()
{

}

void FFMpegAssGetWidget::setAssPathCount(int count)
{
    QVBoxLayout *layout = new QVBoxLayout(m_AssSubtitleWidget);

    for (int i=0; i<count; ++i)
    {
        QLabel *destVideoTag = new QLabel(tr("字幕文件目录:"));
        QLineEdit *destFileNamePathLineEdit = new QLineEdit;
        QPushButton *destBrowseButton = new QPushButton(tr("浏览"));
        destBrowseButton->setObjectName(QString::number(i));
        QObject::connect(destBrowseButton, SIGNAL(clicked()), this, SLOT(onClickedDestBrowseButton()));
        m_DecodecLineEditList.push_back(destFileNamePathLineEdit);
        m_DestBrowseButtonList.push_back(destBrowseButton);

        QHBoxLayout *row2Layout = new QHBoxLayout;
        row2Layout->addWidget(destVideoTag, 1);
        row2Layout->addWidget(destFileNamePathLineEdit, 4);
        row2Layout->addWidget(destBrowseButton, 1);
        g_GlobalTool->addShadowEffect(destBrowseButton);

        layout->addLayout(row2Layout);
    }
}

void FFMpegAssGetWidget::initUi(void)
{
    m_SrcFileNamePathLineEdit = new QLineEdit;
    m_BrowseButton = new QPushButton(tr("浏览"));
    QObject::connect(m_BrowseButton, SIGNAL(clicked()), this, SLOT(onClickedBrowseButton()));
    QLabel *srcVideoTag = new QLabel(tr("视频文件目录:"));

    m_DecodecLineEditList.clear();
    m_DestBrowseButtonList.clear();

    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    mainLayout->addSpacing(30);

    // Row1 Layout
    QHBoxLayout *row1Layout = new QHBoxLayout;
    row1Layout->addWidget(srcVideoTag, 1);
    row1Layout->addWidget(m_SrcFileNamePathLineEdit, 4);
    row1Layout->addWidget(m_BrowseButton, 1);
    g_GlobalTool->addShadowEffect(m_BrowseButton);

    // Row2 Layout
    m_AssSubtitleWidget = new QWidget;

    // Row3 Layout
    QHBoxLayout *row3Layout = new QHBoxLayout;
    m_ConvertButton = new QPushButton(tr("转换"));
    QObject::connect(m_ConvertButton, SIGNAL(clicked()), this, SLOT(onClickedConvertButton()));
    g_GlobalTool->addShadowEffect(m_ConvertButton);
    row3Layout->addStretch();
    row3Layout->addWidget(m_ConvertButton);

    // Row4 Layout
    m_ProgressBar = new QProgressBar;
    QHBoxLayout *row4Layout = new QHBoxLayout;
    row4Layout->addWidget(m_ProgressBar);
    m_ProgressBar->setMinimum(0);
    m_ProgressBar->setMaximum(100);

    mainLayout->addLayout(row1Layout);
    mainLayout->addWidget(m_AssSubtitleWidget);
    mainLayout->addLayout(row3Layout);
    mainLayout->addLayout(row4Layout);
    mainLayout->addStretch();
}

void FFMpegAssGetWidget::onClickedBrowseButton(void)
{
    QString fileName = QFileDialog::getOpenFileName(this, "Open File", "./", tr("Video (*.mkv)"));
    if (fileName.isEmpty())
        return;
    m_SrcFileNamePathLineEdit->setText(fileName);

    QString srcFileName = m_SrcFileNamePathLineEdit->text();
    m_FFMpegAssThread->openVideoFile(srcFileName);
    int count = m_FFMpegAssThread->getSubtitleStreamCount();
    setAssPathCount(count);
}

void FFMpegAssGetWidget::onClickedDestBrowseButton(void)
{
    QString fileName = QFileDialog::getSaveFileName(this, "Open File", "./", tr("Video (*.ass)"));
    if (fileName.isEmpty())
        return;

    int number = sender()->objectName().toInt();
    m_DecodecLineEditList[number]->setText(fileName);
}

void FFMpegAssGetWidget::onClickedConvertButton(void)
{
    int count = m_FFMpegAssThread->getSubtitleStreamCount();

    QStringList fileNameList;
    for (int i=0; i<count; ++i)
    {
        QString assFileName = m_DecodecLineEditList.at(i)->text();
        fileNameList << assFileName;
    }

    m_FFMpegAssThread->openAssSaveFile(fileNameList);
    m_FFMpegAssThread->writeHeader();

    if (!m_FFMpegAssThread->isRunning())
        m_FFMpegAssThread->start();
}

void FFMpegAssGetWidget::onRecvConvertProgress(qreal value)
{
    m_ProgressBar->setValue(value);
    if (value >= 100)
        m_FFMpegAssThread->closeVideoFile();
}

线程-FFMpegAssThread.h

#ifndef FFMPEG_ASS_THREAD_H
#define FFMPEG_ASS_THREAD_H

#include <QThread>
#include <QObject>
#include <QFile>
extern "C"{
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/frame.h>
    #include <libswscale/swscale.h>
    #include <libswresample/swresample.h>
    #include <libavfilter/avfiltergraph.h>
    #include <libavfilter/buffersrc.h>
    #include <libavfilter/buffersink.h>
    #include <libavutil/opt.h>
    #include <libavutil/error.h>
}

class FFMpegAssThread : public QThread
{
    Q_OBJECT

public:
    FFMpegAssThread(QObject *parent = nullptr);
    ~FFMpegAssThread();

    void run(void) override;

    // 打开文件
    void openVideoFile(QString fileName);
    // 获取字幕流数目
    int getSubtitleStreamCount(void);
    // 打开ASS文件
    void openAssSaveFile(QStringList pathList);
    // 关闭文件
    void closeVideoFile(void);
    // 写入头
    void writeHeader(void);

private:
    AVFormatContext *m_FormatContext = nullptr;

    int m_SubtitleStream[20];
    AVCodecContext *m_SubtitleCodecContext[20];

    int m_SubtitleCount;
    int m_TotalTime; // ms

    QList<QFile*> m_FileList;

signals:
    void sendCurrentProgress(qreal);
};

#endif

线程-FFMpegAssThread.cpp

#include "FFMpegASSThread.h"

FFMpegAssThread::FFMpegAssThread(QObject *parent)
{
    m_SubtitleCount = 0;
}

FFMpegAssThread::~FFMpegAssThread()
{

}

void FFMpegAssThread::run(void)
{
    while (!this->isInterruptionRequested())
    {
        if (m_FormatContext != nullptr)
        {
            AVPacket pkt;
            av_init_packet(&pkt);

            // 获取一帧数据
            int result = av_read_frame(m_FormatContext, &pkt);
            if (result < 0)
            {
                emit sendCurrentProgress(100);
                av_packet_unref(&pkt);
                break;
            }

            bool needDecodec = false;
            AVCodecContext *codecContext = nullptr;
            int index = -1;
            for (int i=0; i<m_SubtitleCount; ++i)
            {
                if (m_SubtitleStream[i] == pkt.stream_index)
                {
                    needDecodec = true;
                    index = i;
                    codecContext = m_SubtitleCodecContext[i];
                    break;
                }
            }

            if (!needDecodec)
            {
                av_packet_unref(&pkt);
                continue;
            }

            int streamIndex = pkt.stream_index;
            AVRational rational = m_FormatContext->streams[streamIndex]->time_base;
            qreal value = pkt.pts * 1.0 / rational.den * rational.num * 1000 / m_TotalTime * 100;
            emit sendCurrentProgress(value);

            // 解码
            AVSubtitle subtitle;
            int gotSub = 0;
            result = avcodec_decode_subtitle2(codecContext, &subtitle, &gotSub, &pkt);
            if (result < 0)
            {
                av_packet_unref(&pkt);
                continue;
            }

            if (gotSub > 0)
            {
                int number = subtitle.num_rects;
                for (int i=0; i<number; ++i)
                {
                    QFile *file = m_FileList.at(index);
                    file->write(subtitle.rects[i]->ass, strlen(subtitle.rects[0]->ass));
                }
                avsubtitle_free(&subtitle);
            }

            av_packet_unref(&pkt);
        }
        else
            QThread::msleep(10);
    }
}

int FFMpegAssThread::getSubtitleStreamCount(void)
{
    return m_SubtitleCount;
}

void FFMpegAssThread::openVideoFile(QString fileName)
{
    // 打开视频文件
    int result = avformat_open_input(&m_FormatContext, fileName.toLocal8Bit().data(), nullptr, nullptr);
    if (result < 0)
        return;

    // 查找流信息
    result = avformat_find_stream_info(m_FormatContext, nullptr);
    if (result < 0)
        return;

    // 获取字幕流
    int streamCount = m_FormatContext->nb_streams;
    m_SubtitleCount = 0;
    for (int i=0; i<streamCount; ++i)
    {
        if (m_FormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_SUBTITLE)
        {
            m_SubtitleStream[m_SubtitleCount++] = i;
            continue;
        }
    }

    if (m_SubtitleCount == 0)
    {
        avformat_close_input(&m_FormatContext);
        return;
    }

    // 获取视频总时长
    m_TotalTime = m_FormatContext->duration * 1.0 / AV_TIME_BASE * 1000;

    // 获取解码器
    for (int i=0; i<m_SubtitleCount; ++i)
    {
        AVCodecContext *codecContext = m_FormatContext->streams[m_SubtitleStream[i]]->codec;
        if (codecContext->codec_id == AV_CODEC_ID_ASS)
        {
            AVCodec *codec = avcodec_find_decoder(codecContext->codec_id);
            result = avcodec_open2(codecContext, codec, nullptr);
            if (result < 0)
                continue;

            m_SubtitleCodecContext[i] = codecContext;
        }
    }
}

void FFMpegAssThread::openAssSaveFile(QStringList pathList)
{
    for (int i=0; i<pathList.count(); ++i)
    {
        QFile *file = new QFile(pathList.at(i));
        file->open(QFile::WriteOnly);

        m_FileList.push_back(file);
    }
}

void FFMpegAssThread::writeHeader(void)
{
    for (int i=0; i<m_SubtitleCount; ++i)
    {
        QFile *file = m_FileList.at(i);
        file->write((const char*)m_SubtitleCodecContext[i]->subtitle_header, \
                    m_SubtitleCodecContext[i]->subtitle_header_size);
    }
}

void FFMpegAssThread::closeVideoFile(void)
{
    avformat_close_input(&m_FormatContext);
    m_SubtitleCount = 0;

    for (int i=0; i<m_FileList.count(); ++i)
    {
        QFile *file = m_FileList.at(i);
        file->close();
        delete file;
    }

    m_FileList.clear();
}

效果如图所示:

不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:使用Qt录制音频