Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qffmpegvideoencoder.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
9#include "private/qvideoframe_p.h"
10#include "private/qmultimediautils_p.h"
11#include <QtCore/qloggingcategory.h>
12
14
15using namespace Qt::StringLiterals;
16
17namespace QFFmpeg {
18
19Q_STATIC_LOGGING_CATEGORY(qLcFFmpegVideoEncoder, "qt.multimedia.ffmpeg.videoencoder");
20
21VideoEncoder::VideoEncoder(RecordingEngine &recordingEngine, const QMediaEncoderSettings &settings,
22 const QVideoFrameFormat &format, std::optional<AVPixelFormat> hwFormat)
23 : EncoderThread(recordingEngine), m_settings(settings)
24{
25 setObjectName(QLatin1String("VideoEncoder"));
26
27 const AVPixelFormat swFormat = QFFmpegVideoBuffer::toAVPixelFormat(format.pixelFormat());
28 if (format.streamFrameRate() > 0.)
29 m_fixedSourceFrameRate = format.streamFrameRate();
30
31 m_sourceParams.size = format.frameSize();
32 m_sourceParams.format = hwFormat && *hwFormat != AV_PIX_FMT_NONE ? *hwFormat : swFormat;
33 // Temporary: check isSwPixelFormat because of android issue (QTBUG-116836)
34 // TODO: assign swFormat.
35 m_sourceParams.swFormat =
36 isSwPixelFormat(m_sourceParams.format) ? m_sourceParams.format : swFormat;
37 m_sourceParams.transform = qNormalizedSurfaceTransformation(format);
38 m_sourceParams.frameRate = m_fixedSourceFrameRate.value_or(30.f);
39 m_sourceParams.colorTransfer = QFFmpeg::toAvColorTransfer(format.colorTransfer());
40 m_sourceParams.colorSpace = QFFmpeg::toAvColorSpace(format.colorSpace());
41 m_sourceParams.colorRange = QFFmpeg::toAvColorRange(format.colorRange());
42
43 if (!m_settings.videoResolution().isValid())
44 m_settings.setVideoResolution(m_sourceParams.size);
45
46 const bool explicitTargetRate = m_settings.videoFrameRate() > 0.;
47 if (!explicitTargetRate)
48 m_settings.setVideoFrameRate(m_sourceParams.frameRate);
49
50 if (m_fixedSourceFrameRate || explicitTargetRate)
51 m_frameRateAdapter.setRates(m_fixedSourceFrameRate, m_settings.videoFrameRate());
52}
53
54VideoEncoder::~VideoEncoder() = default;
55
56void VideoEncoder::addFrame(const QVideoFrame &frame)
57{
58 if (!frame.isValid()) {
59 bool hasFlushed = false;
60 {
61 auto guard = lockLoopData();
62 std::optional<FrameInfo> flushed = m_frameRateAdapter.flush();
63 if (flushed && m_videoFrameQueue.size() < m_maxQueueSize) {
64 m_videoFrameQueue.push(*flushed);
65 hasFlushed = true;
66 }
67 }
68 if (hasFlushed)
69 dataReady();
70 setEndOfSourceStream();
71 return;
72 }
73
74 {
75 auto guard = lockLoopData();
76 resetEndOfSourceStream();
77
78 if (m_paused) {
79 m_shouldAdjustTimeBaseForNextFrame = true;
80 m_frameRateAdapter.reset();
81 return;
82 }
83
84 const bool adjustTimeBase = m_shouldAdjustTimeBaseForNextFrame;
85 m_shouldAdjustTimeBaseForNextFrame = false;
86
87 if (m_videoFrameQueue.size() >= m_maxQueueSize) {
88 qCDebug(qLcFFmpegVideoEncoder)
89 << "RecordingEngine frame queue full. Frame lost. This might mean we're trying "
90 "to encode at too high of a frame rate.";
91 return;
92 }
93
94 std::vector<FrameInfo> adapted = m_frameRateAdapter.adapt(frame, adjustTimeBase);
95 if (adapted.empty())
96 return; // Adapter can return no frames if converting to a lower frame rate
97
98 for (FrameInfo &adaptedFrame : adapted)
99 m_videoFrameQueue.push(std::move(adaptedFrame));
100 }
101
102 dataReady();
103}
104
105FrameInfo VideoEncoder::takeFrame()
106{
107 auto guard = lockLoopData();
108 return dequeueIfPossible(m_videoFrameQueue);
109}
110
111void VideoEncoder::retrievePackets()
112{
113 Q_ASSERT(m_frameEncoder);
114 while (auto packet = m_frameEncoder->retrievePacket())
115 m_recordingEngine.getMuxer()->addPacket(std::move(packet));
116}
117
119{
120 m_frameEncoder = VideoFrameEncoder::create(m_settings, m_sourceParams,
121 m_recordingEngine.avFormatContext());
122
123 qCDebug(qLcFFmpegVideoEncoder) << "VideoEncoder::init started video device thread.";
124 if (!m_frameEncoder) {
125 emit m_recordingEngine.sessionError(QMediaRecorder::ResourceError,
126 u"Could not initialize encoder"_s);
127 return false;
128 }
129
130 // TODO: Make adapter use adjusted frame rate
131
132 return EncoderThread::init();
133}
134
136{
137 Q_ASSERT(m_frameEncoder);
138
139 while (!m_videoFrameQueue.empty())
141
142 while (m_frameEncoder->sendFrame(nullptr) == AVERROR(EAGAIN))
143 retrievePackets();
144 retrievePackets();
145}
146
148{
149 return !m_videoFrameQueue.empty();
150}
151
153{
154 QVideoFrame f;
156};
157
158static void freeQVideoFrame(void *opaque, uint8_t *)
159{
160 delete reinterpret_cast<QVideoFrameHolder *>(opaque);
161}
162
164{
165 Q_ASSERT(m_frameEncoder);
166
167 retrievePackets();
168
169 FrameInfo frameInfo = takeFrame();
170 QVideoFrame &frame = frameInfo.frame;
171 Q_ASSERT(frame.isValid());
172
173 // qCDebug(qLcFFmpegEncoder) << "new video buffer" << frame.startTime();
174
175 AVFrameUPtr avFrame;
176
177 auto *videoBuffer = dynamic_cast<QFFmpegVideoBuffer *>(QVideoFramePrivate::hwBuffer(frame));
178 if (videoBuffer) {
179 // ffmpeg video buffer, let's use the native AVFrame stored in there
180 auto *hwFrame = videoBuffer->getHWFrame();
181 if (hwFrame && hwFrame->format == m_frameEncoder->sourceFormat())
182 avFrame.reset(av_frame_clone(hwFrame));
183 }
184
185 if (!avFrame) {
186 frame.map(QVideoFrame::ReadOnly);
187 auto size = frame.size();
188 avFrame = makeAVFrame();
189 avFrame->format = m_frameEncoder->sourceFormat();
190 avFrame->width = size.width();
191 avFrame->height = size.height();
192
193 for (int i = 0; i < 4; ++i) {
194 avFrame->data[i] = const_cast<uint8_t *>(frame.bits(i));
195 avFrame->linesize[i] = frame.bytesPerLine(i);
196 }
197
198 // TODO: investigate if we need to set color params to AVFrame.
199 // Setting only codec carameters might be sufficient.
200 // What happens if frame color params are set and not equal codec prms?
201 //
202 // QVideoFrameFormat format = frame.surfaceFormat();
203 // avFrame->color_trc = QFFmpeg::toAvColorTransfer(format.colorTransfer());
204 // avFrame->colorspace = QFFmpeg::toAvColorSpace(format.colorSpace());
205 // avFrame->color_range = QFFmpeg::toAvColorRange(format.colorRange());
206
207 QImage img;
208 if (frame.pixelFormat() == QVideoFrameFormat::Format_Jpeg) {
209 // the QImage is cached inside the video frame, so we can take the pointer to the image
210 // data here
211 img = frame.toImage();
212 avFrame->data[0] = (uint8_t *)img.bits();
213 avFrame->linesize[0] = img.bytesPerLine();
214 }
215
216 Q_ASSERT(avFrame->data[0]);
217 // ensure the video frame and it's data is alive as long as it's being used in the encoder
218 avFrame->opaque_ref = av_buffer_create(nullptr, 0, freeQVideoFrame,
219 new QVideoFrameHolder{ frame, img }, 0);
220 }
221
222 auto [startTime, endTime] = frameTimeStamps(frame);
223
224 if (frameInfo.overriddenPts) {
225 startTime = *frameInfo.overriddenPts;
226 endTime = startTime + m_frameRateAdapter.frameDuration();
227 }
228
229 if (frameInfo.adjustTimeBase) {
230 m_baseTime += startTime - m_lastFrameTime;
231 qCDebug(qLcFFmpegVideoEncoder)
232 << ">>>> adjusting base time to" << m_baseTime << startTime << m_lastFrameTime;
233 }
234
235 const qint64 time = startTime - m_baseTime;
236 m_lastFrameTime = endTime;
237
238 setAVFrameTime(*avFrame, m_frameEncoder->getPts(time), m_frameEncoder->getTimeBase());
239
240 m_recordingEngine.newTimeStamp(time / 1000);
241
242 qCDebug(qLcFFmpegVideoEncoder)
243 << ">>> sending frame" << avFrame->pts << time << m_lastFrameTime;
244 int ret = m_frameEncoder->sendFrame(std::move(avFrame));
245 if (ret < 0) {
246 qCDebug(qLcFFmpegVideoEncoder) << "error sending frame" << ret << AVError(ret);
247 emit m_recordingEngine.sessionError(QMediaRecorder::ResourceError, err2str(ret));
248 }
249}
250
252{
253 if (m_encodingStarted)
254 return m_videoFrameQueue.size() < m_maxQueueSize;
255 if (!isFinished())
256 return m_videoFrameQueue.empty();
257
258 return false;
259}
260
261std::pair<qint64, qint64> VideoEncoder::frameTimeStamps(const QVideoFrame &frame) const
262{
263 qint64 startTime = frame.startTime();
264 qint64 endTime = frame.endTime();
265
266 if (startTime == -1) {
267 startTime = m_lastFrameTime;
268 endTime = -1;
269 }
270
271 if (endTime == -1) {
272 qreal frameRate = frame.streamFrameRate();
273 if (frameRate <= 0.)
274 frameRate = m_settings.videoFrameRate();
275
276 Q_ASSERT(frameRate > 0.f);
277 endTime = startTime + static_cast<qint64>(std::round(VideoFrameTimeBase / frameRate));
278 }
279
280 return { startTime, endTime };
281}
282
283} // namespace QFFmpeg
284
285QT_END_NAMESPACE
bool init() override
Called on this thread when thread starts.
bool checkIfCanPushFrame() const override
~VideoEncoder() override
void processOne() override
Process one work item.
void cleanup() override
Called on this thread before thread exits.
bool hasData() const override
Must return true when data is available for processing.
Q_STATIC_LOGGING_CATEGORY(qLCAndroidVideoDevices, "qt.multimedia.ffmpeg.android.videoDevices")
static void freeQVideoFrame(void *opaque, uint8_t *)
QT_MANGLE_NAMESPACE(QMacScreenCaptureStreamDelegate) QMacScreenCaptureStreamDelegate
Combined button and popup list for selecting options.