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