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++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#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));
}