You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmSubWxs/nmFFmpegVideoWriter.cpp

859 lines
28 KiB
C++

#include "nmFFmpegVideoWriter.h"
#include <QCoreApplication>
#include <QFile>
#include <QFileInfo>
#include <QtGlobal>
#ifdef NM_ENABLE_FFMPEG_VIDEO_EXPORT
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/error.h>
#include <libavutil/mathematics.h>
#include <libavutil/mem.h>
#include <libavutil/pixfmt.h>
#include <libswscale/swscale.h>
}
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#endif
namespace {
QString writerText(const char* pText)
{
return QCoreApplication::translate("nmFFmpegVideoWriter", pText);
}
QString normalizedExtension(const QString& sExtension)
{
QString sNormalized = sExtension.trimmed().toLower();
if(sNormalized.startsWith('.')) {
sNormalized.remove(0, 1);
}
return sNormalized;
}
#ifdef NM_ENABLE_FFMPEG_VIDEO_EXPORT
struct VideoFormatSpec
{
const char* m_pExtension;
const char* m_pMuxerName;
AVCodecID m_eCodecId;
const char* m_pCodecName;
const char* m_pEncoderName;
AVPixelFormat m_ePixelFormat;
bool m_bMjpeg;
};
// 产品对外承诺的格式、容器和编码器是固定组合,能力探测与实际打开均使用此表。
const VideoFormatSpec g_oFormatSpecs[] =
{
{ "mp4", "mp4", AV_CODEC_ID_MPEG4, "mpeg4", nullptr,
AV_PIX_FMT_YUV420P, false },
{ "avi", "avi", AV_CODEC_ID_MJPEG, "mjpeg", nullptr,
AV_PIX_FMT_YUVJ422P, true },
{ "mov", "mov", AV_CODEC_ID_MPEG4, "mpeg4", nullptr,
AV_PIX_FMT_YUV420P, false },
{ "mpg", "mpeg", AV_CODEC_ID_MPEG2VIDEO, "mpeg2video", nullptr,
AV_PIX_FMT_YUV420P, false },
{ "webm", "webm", AV_CODEC_ID_VP8, "vp8", "libvpx",
AV_PIX_FMT_YUV420P, false },
{ "mkv", "matroska", AV_CODEC_ID_MPEG4, "mpeg4", nullptr,
AV_PIX_FMT_YUV420P, false },
{ "wmv", "asf", AV_CODEC_ID_WMV2, "wmv2", nullptr,
AV_PIX_FMT_YUV420P, false },
{ "flv", "flv", AV_CODEC_ID_FLV1, "flv", nullptr,
AV_PIX_FMT_YUV420P, false }
};
const int FORMAT_SPEC_COUNT =
sizeof(g_oFormatSpecs) / sizeof(g_oFormatSpecs[0]);
const VideoFormatSpec* findFormatSpec(const QString& sExtension)
{
const QByteArray aExtension =
normalizedExtension(sExtension).toLatin1();
for(int nIndex = 0; nIndex < FORMAT_SPEC_COUNT; ++nIndex) {
if(aExtension == g_oFormatSpecs[nIndex].m_pExtension) {
return &g_oFormatSpecs[nIndex];
}
}
return nullptr;
}
AVCodec* findEncoder(const VideoFormatSpec& oSpec)
{
return oSpec.m_pEncoderName != nullptr
? avcodec_find_encoder_by_name(oSpec.m_pEncoderName)
: avcodec_find_encoder(oSpec.m_eCodecId);
}
const char* requestedEncoderName(const VideoFormatSpec& oSpec)
{
return oSpec.m_pEncoderName != nullptr
? oSpec.m_pEncoderName : oSpec.m_pCodecName;
}
QString ffmpegErrorText(int nError)
{
char pBuffer[256] = { 0 };
if(av_strerror(nError, pBuffer, sizeof(pBuffer)) == 0) {
return QString::fromLocal8Bit(pBuffer);
}
return writerText("Unknown FFmpeg error.");
}
int ffmpegMajorVersion(unsigned nVersion)
{
return static_cast<int>((nVersion >> 16) & 0xffU);
}
bool inspectBackend(QString& sError)
{
// 读取实际加载的DLL版本,不使用编译期宏代替运行时ABI校验。
const int nAvformatMajor = ffmpegMajorVersion(avformat_version());
const int nAvcodecMajor = ffmpegMajorVersion(avcodec_version());
const int nAvutilMajor = ffmpegMajorVersion(avutil_version());
const int nSwscaleMajor = ffmpegMajorVersion(swscale_version());
if(nAvformatMajor != 54 || nAvcodecMajor != 54 ||
nAvutilMajor != 52 || nSwscaleMajor != 2) {
sError = writerText(
"The FFmpeg runtime is incompatible. Expected "
"avformat/avcodec/avutil/swscale major versions 54/54/52/2, "
"but found %1/%2/%3/%4.")
.arg(nAvformatMajor)
.arg(nAvcodecMajor)
.arg(nAvutilMajor)
.arg(nSwscaleMajor);
return false;
}
av_register_all();
for(int nIndex = 0; nIndex < FORMAT_SPEC_COUNT; ++nIndex) {
const VideoFormatSpec& oSpec = g_oFormatSpecs[nIndex];
AVOutputFormat* pMuxer = av_guess_format(
oSpec.m_pMuxerName, nullptr, nullptr);
if(pMuxer == nullptr) {
sError = writerText(
"Video format .%1 is unavailable because muxer \"%2\" is missing.")
.arg(QString::fromLatin1(oSpec.m_pExtension))
.arg(QString::fromLatin1(oSpec.m_pMuxerName));
return false;
}
AVCodec* pEncoder = findEncoder(oSpec);
if(pEncoder == nullptr || pEncoder->id != oSpec.m_eCodecId) {
sError = writerText(
"Video format .%1 is unavailable because encoder \"%2\" is missing.")
.arg(QString::fromLatin1(oSpec.m_pExtension))
.arg(QString::fromLatin1(requestedEncoderName(oSpec)));
return false;
}
const int nCodecSupport = avformat_query_codec(
pMuxer, oSpec.m_eCodecId, FF_COMPLIANCE_NORMAL);
// FFmpeg 1.1中负值表示无法判断;只有明确返回0时才能认定组合不受支持。
if(nCodecSupport == 0) {
sError = writerText(
"Video format .%1 is unavailable because muxer \"%2\" "
"does not support codec \"%3\".")
.arg(QString::fromLatin1(oSpec.m_pExtension))
.arg(QString::fromLatin1(oSpec.m_pMuxerName))
.arg(QString::fromLatin1(oSpec.m_pCodecName));
return false;
}
}
return true;
}
#endif
}
nmFFmpegVideoWriter::VideoSettings::VideoSettings()
: m_nWidth(0),
m_nHeight(0),
m_nFramesPerSecond(0)
{
}
class nmFFmpegVideoWriter::Private
{
public:
enum State
{
State_Closed = 0,
State_Opened,
State_Finished,
State_Failed,
State_Cancelled
};
Private()
: m_eState(State_Closed),
m_nWidth(0),
m_nHeight(0),
m_nFramesPerSecond(0),
m_nFrameNumber(0)
#ifdef NM_ENABLE_FFMPEG_VIDEO_EXPORT
, m_pFormatContext(nullptr),
m_pStream(nullptr),
m_pCodecContext(nullptr),
m_pCodec(nullptr),
m_pFrame(nullptr),
m_pScaleContext(nullptr),
m_pAvioContext(nullptr),
m_bCodecOpened(false),
m_bPictureAllocated(false),
m_bMjpeg(false)
#endif
{
}
State m_eState;
QString m_sTemporaryFilePath;
QString m_sExtension;
int m_nWidth;
int m_nHeight;
int m_nFramesPerSecond;
qint64 m_nFrameNumber;
#ifdef NM_ENABLE_FFMPEG_VIDEO_EXPORT
AVFormatContext* m_pFormatContext;
AVStream* m_pStream;
AVCodecContext* m_pCodecContext;
AVCodec* m_pCodec;
AVFrame* m_pFrame;
SwsContext* m_pScaleContext;
AVIOContext* m_pAvioContext;
QFile m_oFile;
bool m_bCodecOpened;
bool m_bPictureAllocated;
bool m_bMjpeg;
static int writePacketCallback(void* pOpaque,
uint8_t* pBuffer,
int nBufferSize)
{
Private* pPrivate = static_cast<Private*>(pOpaque);
if(pPrivate == nullptr || pBuffer == nullptr || nBufferSize < 0 ||
!pPrivate->m_oFile.isOpen()) {
return AVERROR(EIO);
}
// QFile可能只写入部分数据,必须循环到整包落盘;0同样视为IO失败。
qint64 nWrittenTotal = 0;
while(nWrittenTotal < nBufferSize) {
const qint64 nWritten = pPrivate->m_oFile.write(
reinterpret_cast<const char*>(pBuffer) +
nWrittenTotal,
nBufferSize - nWrittenTotal);
if(nWritten <= 0) {
return AVERROR(EIO);
}
nWrittenTotal += nWritten;
}
return nBufferSize;
}
static int64_t seekCallback(void* pOpaque,
int64_t nOffset,
int nWhence)
{
Private* pPrivate = static_cast<Private*>(pOpaque);
if(pPrivate == nullptr || !pPrivate->m_oFile.isOpen()) {
return AVERROR(EIO);
}
// MP4/MOV会在Trailer阶段回写文件头,因此自定义IO必须完整支持Seek。
const int nCleanWhence = nWhence & ~AVSEEK_FORCE;
if(nCleanWhence == AVSEEK_SIZE) {
return pPrivate->m_oFile.size();
}
qint64 nBase = 0;
if(nCleanWhence == SEEK_CUR) {
nBase = pPrivate->m_oFile.pos();
} else if(nCleanWhence == SEEK_END) {
nBase = pPrivate->m_oFile.size();
} else if(nCleanWhence != SEEK_SET) {
return AVERROR(EINVAL);
}
if((nOffset > 0 && nBase > LLONG_MAX - nOffset) ||
(nOffset < 0 && nOffset < -nBase)) {
return AVERROR(EINVAL);
}
const qint64 nTarget = nBase + nOffset;
if(nTarget < 0 || !pPrivate->m_oFile.seek(nTarget)) {
return AVERROR(EIO);
}
return pPrivate->m_oFile.pos();
}
bool writeEncodedPacket(AVPacket& oPacket, QString* pError)
{
// 正常编码和刷新编码器产生的Packet统一在此换算到流时间基。
oPacket.stream_index = m_pStream->index;
if(oPacket.pts != AV_NOPTS_VALUE) {
oPacket.pts = av_rescale_q(
oPacket.pts,
m_pCodecContext->time_base,
m_pStream->time_base);
}
if(oPacket.dts != AV_NOPTS_VALUE) {
oPacket.dts = av_rescale_q(
oPacket.dts,
m_pCodecContext->time_base,
m_pStream->time_base);
}
oPacket.duration = av_rescale_q(
1, m_pCodecContext->time_base,
m_pStream->time_base);
const int nResult = av_interleaved_write_frame(
m_pFormatContext, &oPacket);
// FFmpeg n1.1在函数返回时已经接管Packet数据,无论返回值如何均不得释放。
if(nResult < 0) {
if(pError != nullptr) {
*pError = writerText("Unable to write the encoded video packet: %1")
.arg(ffmpegErrorText(nResult));
}
return false;
}
return true;
}
bool flushEncoder(QString* pError)
{
if(m_pCodec == nullptr ||
(m_pCodec->capabilities & CODEC_CAP_DELAY) == 0) {
return true;
}
for(;;) {
AVPacket oPacket;
av_init_packet(&oPacket);
oPacket.data = nullptr;
oPacket.size = 0;
int nGotPacket = 0;
const int nResult = avcodec_encode_video2(
m_pCodecContext, &oPacket, nullptr, &nGotPacket);
if(nResult < 0) {
av_free_packet(&oPacket);
if(pError != nullptr) {
*pError = writerText("Unable to flush the video encoder: %1")
.arg(ffmpegErrorText(nResult));
}
return false;
}
if(nGotPacket == 0) {
av_free_packet(&oPacket);
return true;
}
// 写入函数被调用后Muxer已接管Packet,返回失败时也不能再次释放。
if(!writeEncodedPacket(oPacket, pError)) {
return false;
}
}
}
void releaseAvioContext()
{
// 先断开FormatContext,防止后续释放阶段误用已经销毁的自定义AVIO。
if(m_pFormatContext != nullptr) {
m_pFormatContext->pb = nullptr;
}
if(m_pAvioContext != nullptr) {
av_freep(&m_pAvioContext->buffer);
av_freep(&m_pAvioContext);
}
}
void releaseCodecAndFormat()
{
if(m_pScaleContext != nullptr) {
sws_freeContext(m_pScaleContext);
m_pScaleContext = nullptr;
}
if(m_pFrame != nullptr) {
if(m_bPictureAllocated) {
avpicture_free(reinterpret_cast<AVPicture*>(m_pFrame));
m_bPictureAllocated = false;
}
av_free(m_pFrame);
m_pFrame = nullptr;
}
if(m_bCodecOpened && m_pCodecContext != nullptr) {
avcodec_close(m_pCodecContext);
m_bCodecOpened = false;
}
if(m_pFormatContext != nullptr) {
avformat_free_context(m_pFormatContext);
m_pFormatContext = nullptr;
}
m_pStream = nullptr;
m_pCodecContext = nullptr;
m_pCodec = nullptr;
}
void abortAndRemoveTemporaryFile()
{
// 取消和失败不写Trailer,也绝不进入最终文件发布事务。
releaseAvioContext();
if(m_oFile.isOpen()) {
m_oFile.close();
}
releaseCodecAndFormat();
if(!m_sTemporaryFilePath.isEmpty()) {
QFile::remove(m_sTemporaryFilePath);
}
}
#endif
};
nmFFmpegVideoWriter::nmFFmpegVideoWriter()
: m_pPrivate(new Private())
{
}
nmFFmpegVideoWriter::~nmFFmpegVideoWriter()
{
if(m_pPrivate != nullptr) {
if(m_pPrivate->m_eState == Private::State_Opened) {
cancel();
}
delete m_pPrivate;
m_pPrivate = nullptr;
}
}
bool nmFFmpegVideoWriter::open(
const VideoSettings& oSettings,
QString* pError)
{
if(pError != nullptr) {
pError->clear();
}
if(m_pPrivate == nullptr ||
m_pPrivate->m_eState == Private::State_Opened ||
oSettings.m_sTemporaryFilePath.isEmpty() ||
!isSupportedExtension(oSettings.m_sFileExtension) ||
oSettings.m_nWidth < 2 || oSettings.m_nWidth > 8192 ||
oSettings.m_nHeight < 2 || oSettings.m_nHeight > 8192 ||
(oSettings.m_nWidth % 2) != 0 ||
(oSettings.m_nHeight % 2) != 0 ||
oSettings.m_nFramesPerSecond < 1 ||
oSettings.m_nFramesPerSecond > 5000) {
if(pError != nullptr) {
*pError = writerText("The video encoder settings are invalid.");
}
return false;
}
#ifndef NM_ENABLE_FFMPEG_VIDEO_EXPORT
if(pError != nullptr) {
*pError = writerText("The video encoding components are unavailable.");
}
return false;
#else
QString sBackendError;
if(!isBackendAvailable(&sBackendError)) {
if(pError != nullptr) {
*pError = sBackendError;
}
return false;
}
m_pPrivate->m_sTemporaryFilePath =
QFileInfo(oSettings.m_sTemporaryFilePath).absoluteFilePath();
m_pPrivate->m_sExtension =
normalizedExtension(oSettings.m_sFileExtension);
m_pPrivate->m_nWidth = oSettings.m_nWidth;
m_pPrivate->m_nHeight = oSettings.m_nHeight;
m_pPrivate->m_nFramesPerSecond = oSettings.m_nFramesPerSecond;
m_pPrivate->m_nFrameNumber = 0;
m_pPrivate->m_eState = Private::State_Failed;
const VideoFormatSpec* pSpec =
findFormatSpec(m_pPrivate->m_sExtension);
m_pPrivate->m_pCodec = pSpec == nullptr
? nullptr : findEncoder(*pSpec);
if(pSpec == nullptr || m_pPrivate->m_pCodec == nullptr ||
avformat_alloc_output_context2(
&m_pPrivate->m_pFormatContext, nullptr,
pSpec->m_pMuxerName, nullptr) < 0 ||
m_pPrivate->m_pFormatContext == nullptr) {
if(pError != nullptr) {
*pError = writerText("Unable to initialize the video container.");
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
m_pPrivate->m_pStream = avformat_new_stream(
m_pPrivate->m_pFormatContext, m_pPrivate->m_pCodec);
if(m_pPrivate->m_pStream == nullptr) {
if(pError != nullptr) {
*pError = writerText("Unable to create the video stream.");
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
m_pPrivate->m_pCodecContext = m_pPrivate->m_pStream->codec;
AVCodecContext* pCodecContext = m_pPrivate->m_pCodecContext;
pCodecContext->codec_id = pSpec->m_eCodecId;
pCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
pCodecContext->width = oSettings.m_nWidth;
pCodecContext->height = oSettings.m_nHeight;
pCodecContext->time_base.num = 1;
pCodecContext->time_base.den = oSettings.m_nFramesPerSecond;
m_pPrivate->m_pStream->time_base = pCodecContext->time_base;
pCodecContext->gop_size = qMax(1, oSettings.m_nFramesPerSecond * 2);
pCodecContext->max_b_frames = 0;
pCodecContext->pix_fmt = pSpec->m_ePixelFormat;
// 以1080p/15 FPS的8 Mbps为基准缩放,计算过程保持64位范围。
const double dScaledBitRate = 8000000.0 *
static_cast<double>(oSettings.m_nWidth) *
static_cast<double>(oSettings.m_nHeight) *
static_cast<double>(oSettings.m_nFramesPerSecond) /
(1920.0 * 1080.0 * 15.0);
const qint64 nBitRate = qBound<qint64>(
500000, static_cast<qint64>(dScaledBitRate + 0.5),
50000000);
pCodecContext->bit_rate = static_cast<int>(nBitRate);
m_pPrivate->m_bMjpeg = pSpec->m_bMjpeg;
if(m_pPrivate->m_bMjpeg) {
pCodecContext->flags |= CODEC_FLAG_QSCALE;
pCodecContext->global_quality = FF_QP2LAMBDA * 2;
}
if((m_pPrivate->m_pFormatContext->oformat->flags &
AVFMT_GLOBALHEADER) != 0) {
pCodecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
int nResult = avcodec_open2(
pCodecContext, m_pPrivate->m_pCodec, nullptr);
if(nResult < 0) {
if(pError != nullptr) {
*pError = writerText("Unable to open the video encoder: %1")
.arg(ffmpegErrorText(nResult));
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
m_pPrivate->m_bCodecOpened = true;
m_pPrivate->m_pFrame = avcodec_alloc_frame();
if(m_pPrivate->m_pFrame == nullptr ||
avpicture_alloc(
reinterpret_cast<AVPicture*>(m_pPrivate->m_pFrame),
pSpec->m_ePixelFormat,
oSettings.m_nWidth, oSettings.m_nHeight) < 0) {
if(pError != nullptr) {
*pError = writerText("Unable to allocate the video frame buffer.");
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
m_pPrivate->m_bPictureAllocated = true;
m_pPrivate->m_pFrame->width = oSettings.m_nWidth;
m_pPrivate->m_pFrame->height = oSettings.m_nHeight;
m_pPrivate->m_pFrame->format = pSpec->m_ePixelFormat;
m_pPrivate->m_pScaleContext = sws_getContext(
oSettings.m_nWidth, oSettings.m_nHeight,
AV_PIX_FMT_RGB24,
oSettings.m_nWidth, oSettings.m_nHeight,
pSpec->m_ePixelFormat,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if(m_pPrivate->m_pScaleContext == nullptr) {
if(pError != nullptr) {
*pError = writerText("Unable to initialize the video color conversion.");
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
QFile::remove(m_pPrivate->m_sTemporaryFilePath);
m_pPrivate->m_oFile.setFileName(m_pPrivate->m_sTemporaryFilePath);
if(!m_pPrivate->m_oFile.open(
QIODevice::ReadWrite | QIODevice::Truncate)) {
if(pError != nullptr) {
*pError = writerText("Unable to create the temporary video file.");
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
// QFile负责Unicode路径,FFmpeg只通过自定义AVIO读写,不直接打开文件名。
const int nAvioBufferSize = 32768;
unsigned char* pAvioBuffer = static_cast<unsigned char*>(
av_malloc(nAvioBufferSize));
if(pAvioBuffer != nullptr) {
m_pPrivate->m_pAvioContext = avio_alloc_context(
pAvioBuffer, nAvioBufferSize, 1,
m_pPrivate, nullptr,
&Private::writePacketCallback,
&Private::seekCallback);
}
if(m_pPrivate->m_pAvioContext == nullptr) {
av_free(pAvioBuffer);
if(pError != nullptr) {
*pError = writerText("Unable to initialize the video file output.");
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
m_pPrivate->m_pAvioContext->seekable = AVIO_SEEKABLE_NORMAL;
m_pPrivate->m_pFormatContext->pb = m_pPrivate->m_pAvioContext;
m_pPrivate->m_pFormatContext->flags |= AVFMT_FLAG_CUSTOM_IO;
// filename仅用于让Muxer识别扩展名;真实输出目标由FormatContext::pb提供。
const QByteArray aPlaceholder =
QString::fromLatin1("output.%1")
.arg(m_pPrivate->m_sExtension).toLatin1();
qstrncpy(m_pPrivate->m_pFormatContext->filename,
aPlaceholder.constData(),
sizeof(m_pPrivate->m_pFormatContext->filename));
nResult = avformat_write_header(
m_pPrivate->m_pFormatContext, nullptr);
if(nResult < 0) {
if(pError != nullptr) {
*pError = writerText("Unable to write the video header: %1")
.arg(ffmpegErrorText(nResult));
}
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
m_pPrivate->m_eState = Private::State_Opened;
return true;
#endif
}
bool nmFFmpegVideoWriter::writeRgb24Frame(
const unsigned char* pData,
int nStride,
bool bBottomUp,
QString* pError)
{
if(pError != nullptr) {
pError->clear();
}
if(m_pPrivate == nullptr ||
m_pPrivate->m_eState != Private::State_Opened ||
pData == nullptr || nStride < m_pPrivate->m_nWidth * 3) {
if(pError != nullptr) {
*pError = writerText("The RGB video frame is invalid.");
}
return false;
}
#ifndef NM_ENABLE_FFMPEG_VIDEO_EXPORT
if(pError != nullptr) {
*pError = writerText("The video encoding components are unavailable.");
}
return false;
#else
const uint8_t* pSourceData[4] = { nullptr, nullptr, nullptr, nullptr };
int pSourceStride[4] = { 0, 0, 0, 0 };
pSourceData[0] = bBottomUp
? pData + static_cast<qint64>(m_pPrivate->m_nHeight - 1) *
nStride
: pData;
pSourceStride[0] = bBottomUp ? -nStride : nStride;
const int nScaledRows = sws_scale(
m_pPrivate->m_pScaleContext,
pSourceData, pSourceStride,
0, m_pPrivate->m_nHeight,
m_pPrivate->m_pFrame->data,
m_pPrivate->m_pFrame->linesize);
if(nScaledRows != m_pPrivate->m_nHeight) {
if(pError != nullptr) {
*pError = writerText("Unable to convert the video frame colors.");
}
m_pPrivate->m_eState = Private::State_Failed;
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
// PTS使用连续输出序号,不能使用被采样的原始结果帧下标。
m_pPrivate->m_pFrame->pts = m_pPrivate->m_nFrameNumber;
if(m_pPrivate->m_bMjpeg) {
m_pPrivate->m_pFrame->quality =
m_pPrivate->m_pCodecContext->global_quality;
}
AVPacket oPacket;
av_init_packet(&oPacket);
oPacket.data = nullptr;
oPacket.size = 0;
int nGotPacket = 0;
const int nResult = avcodec_encode_video2(
m_pPrivate->m_pCodecContext,
&oPacket, m_pPrivate->m_pFrame, &nGotPacket);
if(nResult < 0) {
av_free_packet(&oPacket);
if(pError != nullptr) {
*pError = writerText("Unable to encode the video frame: %1")
.arg(ffmpegErrorText(nResult));
}
m_pPrivate->m_eState = Private::State_Failed;
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
++m_pPrivate->m_nFrameNumber;
if(nGotPacket == 0) {
av_free_packet(&oPacket);
return true;
}
if(!m_pPrivate->writeEncodedPacket(oPacket, pError)) {
m_pPrivate->m_eState = Private::State_Failed;
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
return true;
#endif
}
bool nmFFmpegVideoWriter::finish(QString* pError)
{
if(pError != nullptr) {
pError->clear();
}
if(m_pPrivate == nullptr ||
m_pPrivate->m_eState != Private::State_Opened) {
if(pError != nullptr) {
*pError = writerText("The video writer is not open.");
}
return false;
}
#ifndef NM_ENABLE_FFMPEG_VIDEO_EXPORT
if(pError != nullptr) {
*pError = writerText("The video encoding components are unavailable.");
}
return false;
#else
// 先离开Opened状态,确保任何失败路径和析构都不会再次执行finish。
m_pPrivate->m_eState = Private::State_Failed;
if(!m_pPrivate->flushEncoder(pError)) {
m_pPrivate->abortAndRemoveTemporaryFile();
return false;
}
// Trailer必须在关闭自定义IO前完成,MP4/MOV会在这里Seek并回写文件头。
const int nTrailerResult = av_write_trailer(
m_pPrivate->m_pFormatContext);
avio_flush(m_pPrivate->m_pAvioContext);
const int nAvioError = m_pPrivate->m_pAvioContext->error;
const bool bFileFlushed = m_pPrivate->m_oFile.flush();
m_pPrivate->m_oFile.close();
m_pPrivate->releaseAvioContext();
m_pPrivate->releaseCodecAndFormat();
if(nTrailerResult < 0 || nAvioError != 0 || !bFileFlushed) {
if(pError != nullptr) {
if(nTrailerResult < 0) {
*pError = writerText("Unable to finalize the video container: %1")
.arg(ffmpegErrorText(nTrailerResult));
} else if(nAvioError != 0) {
*pError = writerText("Unable to flush the video output: %1")
.arg(ffmpegErrorText(nAvioError));
} else {
*pError = writerText("Unable to flush the temporary video file.");
}
}
QFile::remove(m_pPrivate->m_sTemporaryFilePath);
return false;
}
const QFileInfo oTemporaryFile(m_pPrivate->m_sTemporaryFilePath);
if(!oTemporaryFile.isFile() || oTemporaryFile.size() <= 0) {
if(pError != nullptr) {
*pError = writerText("The temporary video file is empty.");
}
QFile::remove(m_pPrivate->m_sTemporaryFilePath);
return false;
}
m_pPrivate->m_eState = Private::State_Finished;
return true;
#endif
}
void nmFFmpegVideoWriter::cancel()
{
if(m_pPrivate == nullptr ||
m_pPrivate->m_eState != Private::State_Opened) {
return;
}
m_pPrivate->m_eState = Private::State_Cancelled;
#ifdef NM_ENABLE_FFMPEG_VIDEO_EXPORT
m_pPrivate->abortAndRemoveTemporaryFile();
#endif
}
bool nmFFmpegVideoWriter::isBackendAvailable(QString* pError)
{
if(pError != nullptr) {
pError->clear();
}
#ifndef NM_ENABLE_FFMPEG_VIDEO_EXPORT
if(pError != nullptr) {
*pError = writerText("The video encoding components are unavailable.");
}
return false;
#else
static int nCachedAvailability = -1;
static QString sCachedError;
if(nCachedAvailability >= 0) {
if(nCachedAvailability == 0 && pError != nullptr) {
*pError = sCachedError;
}
return nCachedAvailability == 1;
}
// 八种正式格式采用全有或全无策略,避免按钮可用后才暴露缺失组件。
nCachedAvailability = inspectBackend(sCachedError) ? 1 : 0;
if(nCachedAvailability == 0 && pError != nullptr) {
*pError = sCachedError;
}
return nCachedAvailability == 1;
#endif
}
QStringList nmFFmpegVideoWriter::getSupportedExtensions()
{
QStringList listExtensions;
listExtensions << QString::fromLatin1("mp4")
<< QString::fromLatin1("avi")
<< QString::fromLatin1("mov")
<< QString::fromLatin1("mpg")
<< QString::fromLatin1("webm")
<< QString::fromLatin1("mkv")
<< QString::fromLatin1("wmv")
<< QString::fromLatin1("flv");
return listExtensions;
}
bool nmFFmpegVideoWriter::isSupportedExtension(
const QString& sExtension)
{
return getSupportedExtensions().contains(
normalizedExtension(sExtension));
}