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(0.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 // Use a provisional target before negotiation with codec. This is for first
47 // frame, before init() is called
48 const qreal provisionalTargetRate = m_settings.videoFrameRate() > 0.
49 ? m_settings.videoFrameRate()
50 : m_sourceParams.frameRate;
51 m_frameRateAdapter.setRates(m_fixedSourceFrameRate, provisionalTargetRate);
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 if (isInitialized()) {
63 // Don't flush if not initialized, since frame rate used in adapter is only provisional
64 // Will be flushed in init()
65 std::optional<FrameInfo> flushed = m_frameRateAdapter.flush();
66 if (flushed && m_videoFrameQueue.size() < m_maxQueueSize) {
67 m_videoFrameQueue.push(*flushed);
68 hasFlushed = true;
69 }
70 }
71 }
72 if (hasFlushed)
73 dataReady();
74 setEndOfSourceStream();
75 return;
76 }
77
78 {
79 auto guard = lockLoopData();
80 resetEndOfSourceStream();
81
82 if (m_paused) {
83 m_shouldAdjustTimeBaseForNextFrame = true;
84 m_frameRateAdapter.reset();
85 m_framesBeforeInit = {};
86 return;
87 }
88
89 if (!isInitialized()) {
90 // m_frameRateAdapter isn't configured with the real codec frame rate yet, so
91 // buffer the frame and adapt it once init() has set the real rates. Bounded by
92 // m_maxFramesBeforeInit. Sources should use checkIfCanPushFrame.
93 if (m_framesBeforeInit.size() >= m_maxFramesBeforeInit) {
94 qCWarning(qLcFFmpegVideoEncoder) << "Too many frames received before encoder "
95 "initialization. Buffer full, dropping "
96 "frame.";
97 return;
98 }
99 m_framesBeforeInit.push(frame);
100 return;
101 }
102
103 const bool adjustTimeBase = m_shouldAdjustTimeBaseForNextFrame;
104 m_shouldAdjustTimeBaseForNextFrame = false;
105
106 if (m_videoFrameQueue.size() >= m_maxQueueSize) {
107 qCDebug(qLcFFmpegVideoEncoder)
108 << "RecordingEngine frame queue full. Frame lost. This might mean we're trying "
109 "to encode at too high of a frame rate.";
110 return;
111 }
112
113 std::vector<FrameInfo> adapted = m_frameRateAdapter.adapt(frame, adjustTimeBase);
114 if (adapted.empty())
115 return; // Adapter can return no frames if converting to a lower frame rate
116
117 for (FrameInfo &adaptedFrame : adapted)
118 m_videoFrameQueue.push(std::move(adaptedFrame));
119 }
120
121 dataReady();
122}
123
124FrameInfo VideoEncoder::takeFrame()
125{
126 auto guard = lockLoopData();
127 return dequeueIfPossible(m_videoFrameQueue);
128}
129
130void VideoEncoder::retrievePackets()
131{
132 Q_ASSERT(m_frameEncoder);
133 while (auto packet = m_frameEncoder->retrievePacket())
134 m_recordingEngine.getMuxer()->addPacket(std::move(packet));
135}
136
138{
139 m_frameEncoder = VideoFrameEncoder::create(m_settings, m_sourceParams,
140 m_recordingEngine.avFormatContext());
141
142 qCDebug(qLcFFmpegVideoEncoder) << "VideoEncoder::init started video device thread.";
143 if (!m_frameEncoder) {
144 markResolved(false);
145 emit m_recordingEngine.sessionError(QMediaRecorder::ResourceError,
146 u"Could not initialize encoder"_s);
147 return false;
148 }
149
150 {
151 auto guard = lockLoopData();
152 m_frameRateAdapter.setRates(m_fixedSourceFrameRate, m_frameEncoder->codecFrameRate());
153
154 while (!m_framesBeforeInit.empty()) {
155 const QVideoFrame bufferedFrame = std::move(m_framesBeforeInit.front());
156 m_framesBeforeInit.pop();
157
158 std::vector<FrameInfo> adapted = m_frameRateAdapter.adapt(
159 bufferedFrame, std::exchange(m_shouldAdjustTimeBaseForNextFrame, false));
160 for (FrameInfo &adaptedFrame : adapted)
161 m_videoFrameQueue.push(std::move(adaptedFrame));
162 }
163
164 // EOS may have arrived before initialization. Flush now that the buffered
165 // frames above have been adapted.
166 if (isEndOfSourceStream()) {
167 std::optional<FrameInfo> flushed = m_frameRateAdapter.flush();
168 if (flushed && m_videoFrameQueue.size() < m_maxQueueSize)
169 m_videoFrameQueue.push(std::move(*flushed));
170 }
171 }
172
173 return EncoderThread::init();
174}
175
177{
178 Q_ASSERT(m_frameEncoder);
179
180 while (!m_videoFrameQueue.empty())
182
183 while (m_frameEncoder->sendFrame(nullptr) == AVERROR(EAGAIN))
184 retrievePackets();
185 retrievePackets();
186}
187
189{
190 return !m_videoFrameQueue.empty();
191}
192
194{
195 QVideoFrame f;
197};
198
199static void freeQVideoFrame(void *opaque, uint8_t *)
200{
201 delete reinterpret_cast<QVideoFrameHolder *>(opaque);
202}
203
205{
206 Q_ASSERT(m_frameEncoder);
207
208 retrievePackets();
209
210 FrameInfo frameInfo = takeFrame();
211 QVideoFrame &frame = frameInfo.frame;
212 Q_ASSERT(frame.isValid());
213
214 // qCDebug(qLcFFmpegEncoder) << "new video buffer" << frame.startTime();
215
216 AVFrameUPtr avFrame;
217
218 auto *videoBuffer = dynamic_cast<QFFmpegVideoBuffer *>(QVideoFramePrivate::hwBuffer(frame));
219 if (videoBuffer) {
220 // ffmpeg video buffer, let's use the native AVFrame stored in there
221 auto *hwFrame = videoBuffer->getHWFrame();
222 if (hwFrame && hwFrame->format == m_frameEncoder->sourceFormat())
223 avFrame.reset(av_frame_clone(hwFrame));
224 }
225
226 if (!avFrame) {
227 frame.map(QVideoFrame::ReadOnly);
228 auto size = frame.size();
229 avFrame = makeAVFrame();
230 avFrame->format = m_frameEncoder->sourceFormat();
231 avFrame->width = size.width();
232 avFrame->height = size.height();
233
234 for (int i = 0; i < 4; ++i) {
235 avFrame->data[i] = const_cast<uint8_t *>(frame.bits(i));
236 avFrame->linesize[i] = frame.bytesPerLine(i);
237 }
238
239 // TODO: investigate if we need to set color params to AVFrame.
240 // Setting only codec carameters might be sufficient.
241 // What happens if frame color params are set and not equal codec prms?
242 //
243 // QVideoFrameFormat format = frame.surfaceFormat();
244 // avFrame->color_trc = QFFmpeg::toAvColorTransfer(format.colorTransfer());
245 // avFrame->colorspace = QFFmpeg::toAvColorSpace(format.colorSpace());
246 // avFrame->color_range = QFFmpeg::toAvColorRange(format.colorRange());
247
248 QImage img;
249 if (frame.pixelFormat() == QVideoFrameFormat::Format_Jpeg) {
250 // the QImage is cached inside the video frame, so we can take the pointer to the image
251 // data here
252 img = frame.toImage();
253 avFrame->data[0] = (uint8_t *)img.bits();
254 avFrame->linesize[0] = img.bytesPerLine();
255 }
256
257 Q_ASSERT(avFrame->data[0]);
258 // ensure the video frame and it's data is alive as long as it's being used in the encoder
259 avFrame->opaque_ref = av_buffer_create(nullptr, 0, freeQVideoFrame,
260 new QVideoFrameHolder{ frame, img }, 0);
261 }
262
263 auto [startTime, endTime] = frameTimeStamps(frame);
264
265 if (frameInfo.overriddenPts) {
266 startTime = *frameInfo.overriddenPts;
267 endTime = startTime + m_frameRateAdapter.frameDuration();
268 }
269
270 if (frameInfo.adjustTimeBase) {
271 m_baseTime += startTime - m_lastFrameTime;
272 qCDebug(qLcFFmpegVideoEncoder)
273 << ">>>> adjusting base time to" << m_baseTime << startTime << m_lastFrameTime;
274 }
275
276 const qint64 time = startTime - m_baseTime;
277 m_lastFrameTime = endTime;
278
279 setAVFrameTime(*avFrame, m_frameEncoder->getPts(time), m_frameEncoder->getTimeBase());
280
281 m_recordingEngine.newTimeStamp(time / 1000);
282
283 qCDebug(qLcFFmpegVideoEncoder)
284 << ">>> sending frame" << avFrame->pts << time << m_lastFrameTime;
285 int ret = m_frameEncoder->sendFrame(std::move(avFrame));
286 if (ret < 0) {
287 qCDebug(qLcFFmpegVideoEncoder) << "error sending frame" << ret << AVError(ret);
288 emit m_recordingEngine.sessionError(QMediaRecorder::ResourceError, err2str(ret));
289 }
290}
291
293{
294 if (!isInitialized())
295 return m_framesBeforeInit.size() < m_maxFramesBeforeInit;
296 if (m_encodingStarted)
297 return m_videoFrameQueue.size() < m_maxQueueSize;
298 if (!isFinished())
299 return m_videoFrameQueue.empty();
300
301 return false;
302}
303
304std::pair<qint64, qint64> VideoEncoder::frameTimeStamps(const QVideoFrame &frame) const
305{
306 qint64 startTime = frame.startTime();
307 qint64 endTime = frame.endTime();
308
309 if (startTime == -1) {
310 startTime = m_lastFrameTime;
311 endTime = -1;
312 }
313
314 if (endTime == -1) {
315 // No endTime: advance by an assumed duration. If the frame rate
316 // adapter is enabled, these get overridden later
317 qreal frameRate = frame.streamFrameRate();
318 if (frameRate <= 0.)
319 frameRate = DefaultVideoFrameRate;
320
321 Q_ASSERT(frameRate > 0.f);
322 endTime = startTime + static_cast<qint64>(std::round(VideoFrameTimeBase / frameRate));
323 }
324
325 return { startTime, endTime };
326}
327
328} // namespace QFFmpeg
329
330QT_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
constexpr int DefaultVideoFrameRate
Combined button and popup list for selecting options.