FFMpegを使用してMKVファイルから字幕を抽出します



Use Ffmpeg Extract Subtitles From Mkv Files



MKVパッケージ形式は、ほぼすべてのビデオおよびオーディオエンコーディング形式をカプセル化できるユニバーサルパッケージ形式です。複数のビデオストリーム、オーディオストリーム、字幕ストリームを含めることができます。この記事では、FFMpegを使用してビデオファイルをデコードし、字幕コンテンツを削除して保存する方法を紹介します。ここでは、ASS形式の字幕ファイルのみが抽出されます。

FFMpegを使用してMKVパッケージをデコードし、字幕ストリーム情報を取得します



void FFMpegAssThread::openVideoFile(QString fileName) { // Open the video file int result = avformat_open_input(&m_FormatContext, fileName.toLocal8Bit().data(), nullptr, nullptr) if (result <0) return // Find stream information result = avformat_find_stream_info(m_FormatContext, nullptr) if (result <0) return // Get subtitle stream int streamCount = m_FormatContext->nb_streams m_SubtitleCount = 0 for (int i=0 iif (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 } // Get the total video duration m_TotalTime = m_FormatContext->duration * 1.0 / AV_TIME_BASE * 1000 // Get decoder for (int i=0 istreams[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 字幕をデコードする パケットAVSubtitle 字幕の具体的なコンテンツ情報が保存されています。スレッド内のデコード機能は以下のとおりです。

void FFMpegAssThread::run(void) { while (!this->isInterruptionRequested()) { if (m_FormatContext != nullptr) { AVPacket pkt av_init_packet(&pkt) // Get a frame of data 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 iif (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) // decode 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 i0) 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 #include #include #include class FFMpegAssGetWidget : public UIBaseWindow { Q_OBJECT public: FFMpegAssGetWidget(QWidget *parent = nullptr) ~FFMpegAssGetWidget() private: void initUi(void) // Set the ASS path void setAssPathCount(int count) QLineEdit *m_SrcFileNamePathLineEdit = nullptr QPushButton *m_BrowseButton = nullptr QList m_DecodecLineEditList QList 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 #include #include #include #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('Subtitle file directory:')) QLineEdit *destFileNamePathLineEdit = new QLineEdit QPushButton *destBrowseButton = new QPushButton(tr('Browse')) 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('Browse')) QObject::connect(m_BrowseButton, SIGNAL(clicked()), this, SLOT(onClickedBrowseButton())) QLabel *srcVideoTag = new QLabel(tr('Video file directory:')) 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('Convert')) 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 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 #include #include extern 'C'{ #include #include #include #include #include #include #include #include #include #include } class FFMpegAssThread : public QThread { Q_OBJECT public: FFMpegAssThread(QObject *parent = nullptr) ~FFMpegAssThread() void run(void) override // open a file void openVideoFile(QString fileName) // Get the number of subtitle streams int getSubtitleStreamCount(void) // Open ASS file void openAssSaveFile(QStringList pathList) // close the file void closeVideoFile(void) // write header void writeHeader(void) private: AVFormatContext *m_FormatContext = nullptr int m_SubtitleStream[20] AVCodecContext *m_SubtitleCodecContext[20] int m_SubtitleCount int m_TotalTime // ms QList 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) // Get a frame of data 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 iif (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) // decode 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 iwrite(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) { // Open the video file int result = avformat_open_input(&m_FormatContext, fileName.toLocal8Bit().data(), nullptr, nullptr) if (result <0) return // Find stream information result = avformat_find_stream_info(m_FormatContext, nullptr) if (result <0) return // Get subtitle stream int streamCount = m_FormatContext->nb_streams m_SubtitleCount = 0 for (int i=0 iif (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 } // Get the total video duration m_TotalTime = m_FormatContext->duration * 1.0 / AV_TIME_BASE * 1000 // Get decoder for (int i=0 istreams[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 inew QFile(pathList.at(i)) file->open(QFile::WriteOnly) m_FileList.push_back(file) } } void FFMpegAssThread::writeHeader(void) { for (int i=0 iwrite((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 iclose() delete file } m_FileList.clear() }

効果を図に示します。
画像

ASS