使用FFMpeg 提取MKV檔案中的字幕
阿新 • • 發佈:2019-02-18
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();
}
效果如圖所示: