#include "nmFFmpegVideoWriter.h" #include #include #include #include #ifdef NM_ENABLE_FFMPEG_VIDEO_EXPORT #ifndef __STDC_CONSTANT_MACROS #define __STDC_CONSTANT_MACROS #endif extern "C" { #include #include #include #include #include #include #include #include } #include #include #include #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((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(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(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(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(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(oSettings.m_nWidth) * static_cast(oSettings.m_nHeight) * static_cast(oSettings.m_nFramesPerSecond) / (1920.0 * 1080.0 * 15.0); const qint64 nBitRate = qBound( 500000, static_cast(dScaledBitRate + 0.5), 50000000); pCodecContext->bit_rate = static_cast(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(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( 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(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)); }