Revision control
Copy as Markdown
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qffmpegvideoencoder_p.h"
#include "qffmpegmuxer_p.h"
#include "qffmpegvideobuffer_p.h"
#include "qffmpegrecordingengine_p.h"
#include "qffmpegvideoframeencoder_p.h"
#include "qffmpegrecordingengineutils_p.h"
#include "private/qvideoframe_p.h"
#include "private/qmultimediautils_p.h"
#include <QtCore/qloggingcategory.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
namespace QFFmpeg {
Q_STATIC_LOGGING_CATEGORY(qLcFFmpegVideoEncoder, "qt.multimedia.ffmpeg.videoencoder");
VideoEncoder::VideoEncoder(RecordingEngine &recordingEngine, const QMediaEncoderSettings &settings,
const QVideoFrameFormat &format, std::optional<AVPixelFormat> hwFormat)
: EncoderThread(recordingEngine), m_settings(settings)
{
setObjectName(QLatin1String("VideoEncoder"));
const AVPixelFormat swFormat = QFFmpegVideoBuffer::toAVPixelFormat(format.pixelFormat());
if (format.streamFrameRate() > 0.)
m_fixedSourceFrameRate = format.streamFrameRate();
m_sourceParams.size = format.frameSize();
m_sourceParams.format = hwFormat && *hwFormat != AV_PIX_FMT_NONE ? *hwFormat : swFormat;
// Temporary: check isSwPixelFormat because of android issue (QTBUG-116836)
// TODO: assign swFormat.
m_sourceParams.swFormat =
isSwPixelFormat(m_sourceParams.format) ? m_sourceParams.format : swFormat;
m_sourceParams.transform = qNormalizedSurfaceTransformation(format);
m_sourceParams.frameRate = m_fixedSourceFrameRate.value_or(0.f);
m_sourceParams.colorTransfer = QFFmpeg::toAvColorTransfer(format.colorTransfer());
m_sourceParams.colorSpace = QFFmpeg::toAvColorSpace(format.colorSpace());
m_sourceParams.colorRange = QFFmpeg::toAvColorRange(format.colorRange());
if (!m_settings.videoResolution().isValid())
m_settings.setVideoResolution(m_sourceParams.size);
// Use a provisional target before negotiation with codec. This is for first
// frame, before init() is called
const qreal provisionalTargetRate = m_settings.videoFrameRate() > 0.
? m_settings.videoFrameRate()
: m_sourceParams.frameRate;
m_frameRateAdapter.setRates(m_fixedSourceFrameRate, provisionalTargetRate);
}
VideoEncoder::~VideoEncoder() = default;
void VideoEncoder::addFrame(const QVideoFrame &frame)
{
if (!frame.isValid()) {
setEndOfSourceStream();
dataReady();
return;
}
{
auto guard = lockLoopData();
resetEndOfSourceStream();
if (m_paused)
return; // Frames are dropped while paused
if (!tryPushToInputQueue(frame)) {
if (!isInitialized())
qCWarning(qLcFFmpegVideoEncoder) << "Too many frames received before encoder "
"initialization. Buffer full, dropping "
"frame.";
else
qCDebug(qLcFFmpegVideoEncoder) << "RecordingEngine frame queue full. Frame lost. "
"This might mean we're trying "
"to encode at too high of a frame rate.";
return;
}
}
dataReady();
}
FrameInfo VideoEncoder::dequeueAdaptedFrame()
{
auto guard = lockLoopData();
saturateEncoderQueue();
return dequeueIfPossible(m_encoderQueue);
}
void VideoEncoder::retrievePackets()
{
Q_ASSERT(m_frameEncoder);
while (auto packet = m_frameEncoder->retrievePacket())
m_recordingEngine.getMuxer()->addPacket(std::move(packet));
}
bool VideoEncoder::tryPushToInputQueue(const QVideoFrame &frame)
{
if (m_inputQueue.size() >= m_maxQueueSize)
return false;
m_inputQueue.push({ frame, std::exchange(m_shouldAdjustTimeBaseForNextFrame, false) });
return true;
}
void VideoEncoder::saturateEncoderQueue()
{
// Caller must hold lockLoopData() guard
while (m_encoderQueue.empty() && !m_inputQueue.empty()) {
const InputFrame input = std::move(m_inputQueue.front());
m_inputQueue.pop();
if (input.restartTimeBase) {
flushAdapter();
m_frameRateAdapter.reset();
}
std::vector<FrameInfo> adapted =
m_frameRateAdapter.adapt(input.frame, input.restartTimeBase);
for (FrameInfo &adaptedFrame : adapted)
m_encoderQueue.push(std::move(adaptedFrame));
}
if (m_inputQueue.empty() && isEndOfSourceStream() && m_frameRateAdapter.hasPendingFlush())
flushAdapter();
}
void VideoEncoder::flushAdapter()
{
std::optional<FrameInfo> flushed = m_frameRateAdapter.flush();
if (flushed)
m_encoderQueue.push(std::move(*flushed));
}
void VideoEncoder::handlePausedChanged(bool paused)
{
if (paused)
m_shouldAdjustTimeBaseForNextFrame = true;
}
bool VideoEncoder::init()
{
m_frameEncoder = VideoFrameEncoder::create(m_settings, m_sourceParams,
m_recordingEngine.avFormatContext());
qCDebug(qLcFFmpegVideoEncoder) << "VideoEncoder::init started video device thread.";
if (!m_frameEncoder) {
markResolved(false);
Q_EMIT m_recordingEngine.sessionError(QMediaRecorder::ResourceError,
u"Could not initialize encoder"_s);
return false;
}
{
auto guard = lockLoopData();
m_frameRateAdapter.setRates(m_fixedSourceFrameRate, m_frameEncoder->codecFrameRate());
saturateEncoderQueue();
}
return EncoderThread::init();
}
void VideoEncoder::cleanup()
{
Q_ASSERT(m_frameEncoder);
while (hasData())
processOne();
while (m_frameEncoder->sendFrame(nullptr) == AVERROR(EAGAIN))
retrievePackets();
retrievePackets();
}
bool VideoEncoder::hasData() const
{
return !m_encoderQueue.empty() || !m_inputQueue.empty()
|| (isEndOfSourceStream() && m_frameRateAdapter.hasPendingFlush());
}
struct QVideoFrameHolder
{
QVideoFrame f;
QImage i;
};
static void freeQVideoFrame(void *opaque, uint8_t *)
{
delete reinterpret_cast<QVideoFrameHolder *>(opaque);
}
void VideoEncoder::processOne()
{
Q_ASSERT(m_frameEncoder);
retrievePackets();
FrameInfo frameInfo = dequeueAdaptedFrame();
QVideoFrame &frame = frameInfo.frame;
if (!frame.isValid())
return; // Nothing to encode
// qCDebug(qLcFFmpegEncoder) << "new video buffer" << frame.startTime();
AVFrameUPtr avFrame;
auto *videoBuffer = dynamic_cast<QFFmpegVideoBuffer *>(QVideoFramePrivate::hwBuffer(frame));
if (videoBuffer) {
// ffmpeg video buffer, let's use the native AVFrame stored in there
auto *hwFrame = videoBuffer->getHWFrame();
if (hwFrame && hwFrame->format == m_frameEncoder->sourceFormat())
avFrame.reset(av_frame_clone(hwFrame));
}
if (!avFrame) {
frame.map(QVideoFrame::ReadOnly);
auto size = frame.size();
avFrame = makeAVFrame();
avFrame->format = m_frameEncoder->sourceFormat();
avFrame->width = size.width();
avFrame->height = size.height();
for (int i = 0; i < 4; ++i) {
avFrame->data[i] = const_cast<uint8_t *>(frame.bits(i));
avFrame->linesize[i] = frame.bytesPerLine(i);
}
// TODO: investigate if we need to set color params to AVFrame.
// Setting only codec carameters might be sufficient.
// What happens if frame color params are set and not equal codec prms?
//
// QVideoFrameFormat format = frame.surfaceFormat();
// avFrame->color_trc = QFFmpeg::toAvColorTransfer(format.colorTransfer());
// avFrame->colorspace = QFFmpeg::toAvColorSpace(format.colorSpace());
// avFrame->color_range = QFFmpeg::toAvColorRange(format.colorRange());
QImage img;
if (frame.pixelFormat() == QVideoFrameFormat::Format_Jpeg) {
// the QImage is cached inside the video frame, so we can take the pointer to the image
// data here
img = frame.toImage();
avFrame->data[0] = (uint8_t *)img.bits();
avFrame->linesize[0] = img.bytesPerLine();
}
Q_ASSERT(avFrame->data[0]);
// ensure the video frame and it's data is alive as long as it's being used in the encoder
avFrame->opaque_ref = av_buffer_create(nullptr, 0, freeQVideoFrame,
new QVideoFrameHolder{ frame, img }, 0);
}
auto [startTime, endTime] = frameTimeStamps(frame);
if (frameInfo.overriddenPts) {
startTime = *frameInfo.overriddenPts;
endTime = startTime + m_frameRateAdapter.frameDuration();
}
if (frameInfo.adjustTimeBase) {
m_baseTime += startTime - m_lastFrameTime;
qCDebug(qLcFFmpegVideoEncoder)
<< ">>>> adjusting base time to" << m_baseTime << startTime << m_lastFrameTime;
}
const qint64 time = startTime - m_baseTime;
m_lastFrameTime = endTime;
setAVFrameTime(*avFrame, m_frameEncoder->getPts(time), m_frameEncoder->getTimeBase());
m_recordingEngine.newTimeStamp(time / 1000);
qCDebug(qLcFFmpegVideoEncoder)
<< ">>> sending frame" << avFrame->pts << time << m_lastFrameTime;
int ret = m_frameEncoder->sendFrame(std::move(avFrame));
if (ret < 0) {
qCDebug(qLcFFmpegVideoEncoder) << "error sending frame" << ret << AVError(ret);
Q_EMIT m_recordingEngine.sessionError(QMediaRecorder::ResourceError, err2str(ret));
}
}
bool VideoEncoder::checkIfCanPushFrame() const
{
return isFinished() ? false : m_inputQueue.size() < m_maxQueueSize;
}
std::pair<qint64, qint64> VideoEncoder::frameTimeStamps(const QVideoFrame &frame) const
{
qint64 startTime = frame.startTime();
qint64 endTime = frame.endTime();
if (startTime == -1) {
startTime = m_lastFrameTime;
endTime = -1;
}
if (endTime == -1) {
// No endTime: advance by an assumed duration. If the frame rate
// adapter is enabled, these get overridden later
qreal frameRate = frame.streamFrameRate();
if (frameRate <= 0.)
frameRate = DefaultVideoFrameRate;
Q_ASSERT(frameRate > 0.f);
endTime = startTime + static_cast<qint64>(std::round(VideoFrameTimeBase / frameRate));
}
return { startTime, endTime };
}
} // namespace QFFmpeg
QT_END_NAMESPACE