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
qffmpegrecordingengine.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 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
8
9#include "private/qmultimediautils_p.h"
10#include "private/qplatformaudiobufferinput_p.h"
11#include "private/qplatformvideosource_p.h"
12#include "private/qplatformvideoframeinput_p.h"
13
14#include "qdebug.h"
17#include "qffmpegmuxer_p.h"
19
20extern "C" {
21#include "libavutil/opt.h"
22}
23
24QT_BEGIN_NAMESPACE
25namespace ranges = QtMultimediaPrivate::ranges;
26
27Q_STATIC_LOGGING_CATEGORY(qLcFFmpegEncoder, "qt.multimedia.ffmpeg.encoder");
28
29namespace QFFmpeg
30{
31
32namespace {
33
34bool supportsNegativeCtsOffsets(const AVOutputFormat *format)
35{
36 if (!format || !format->priv_class)
37 return false;
38
39 // Check if muxer can store negative composition offsets in a version 1
40 // ctts box, exposed as the negative_cts_offsets movflag
41 const AVClass *avClass = format->priv_class;
42 return av_opt_find(static_cast<void *>(&avClass), "negative_cts_offsets", "movflags", 0, 0)
43 != nullptr;
44}
45
46AVDictionaryHolder muxerOptions(const AVFormatContext *formatContext)
47{
48 AVDictionaryHolder options;
49
50 if (supportsNegativeCtsOffsets(formatContext->oformat)) {
51 // Codecs that reorder frames (H264, H265) emit packets with dts starting negative (frame
52 // reorder delay). Muxer can store that delay as negative composition offsets
53 av_dict_set(options, "movflags", "+negative_cts_offsets", 0);
54 // Disable avformat shifting to keep packets unmodified for muxer
55 av_dict_set(options, "avoid_negative_ts", "disabled", 0);
56 }
57
58 return options;
59}
60
61} // namespace
62
63RecordingEngine::RecordingEngine(const QMediaEncoderSettings &settings,
64 std::unique_ptr<EncodingFormatContext> context)
65 : m_settings(settings), m_formatContext(std::move(context)), m_muxer(new Muxer(this))
66{
67 Q_ASSERT(m_formatContext);
68 Q_ASSERT(m_formatContext->isAVIOOpen());
69 m_startEncodingPromise.start();
70}
71
73{
74 Q_ASSERT(m_state == State::Finalizing);
75}
76
77void RecordingEngine::addAudioInput(QFFmpegAudioInput *input)
78{
79 Q_ASSERT(input);
80 Q_ASSERT(m_state == State::FormatsInitializing);
81
82 if (input->device.isNull()) {
83 emit streamInitializationError(QMediaRecorder::ResourceError,
84 QLatin1StringView("Audio device is null"));
85 return;
86 }
87
88 const QAudioFormat format = input->device.preferredFormat();
89
90 if (!format.isValid()) {
91 emit streamInitializationError(
92 QMediaRecorder::FormatError,
93 QLatin1StringView("Audio device has invalid preferred format"));
94 return;
95 }
96
97 AudioEncoder *audioEncoder = createAudioEncoder(format);
98 connectEncoderToSource(audioEncoder, input);
99}
100
101void RecordingEngine::addAudioBufferInput(QPlatformAudioBufferInput *input,
102 const QAudioBuffer &firstBuffer)
103{
104 Q_ASSERT(input);
105 Q_ASSERT(m_state == State::FormatsInitializing);
106
107 const QAudioFormat format = firstBuffer.isValid() ? firstBuffer.format() : input->audioFormat();
108
109 AudioEncoder *audioEncoder = createAudioEncoder(format);
110
111 // set the buffer before connecting to avoid potential races
112 if (firstBuffer.isValid())
113 audioEncoder->addBuffer(firstBuffer);
114
115 connectEncoderToSource(audioEncoder, input);
116}
117
118AudioEncoder *RecordingEngine::createAudioEncoder(const QAudioFormat &format)
119{
120 Q_ASSERT(format.isValid());
121
122 auto audioEncoder = new AudioEncoder(*this, format, m_settings);
123
124 m_audioEncoders.emplace_back(audioEncoder);
125 connect(audioEncoder, &EncoderThread::endOfSourceStream, this,
126 &RecordingEngine::handleSourceEndOfStream);
127 if (m_autoStop)
128 audioEncoder->setAutoStop(true);
129
130 return audioEncoder;
131}
132
133void RecordingEngine::addVideoSource(QPlatformVideoSource *source, const QVideoFrame &firstFrame)
134{
135 Q_ASSERT(m_state == State::FormatsInitializing);
136
137 QVideoFrameFormat frameFormat =
138 firstFrame.isValid() ? firstFrame.surfaceFormat() : source->frameFormat();
139
140 Q_ASSERT(frameFormat.isValid());
141
142 std::optional<AVPixelFormat> hwPixelFormat = source->ffmpegHWPixelFormat()
143 ? AVPixelFormat(*source->ffmpegHWPixelFormat())
144 : std::optional<AVPixelFormat>{};
145
146 qCDebug(qLcFFmpegEncoder) << "adding video source" << source->metaObject()->className() << ":"
147 << "pixelFormat=" << frameFormat.pixelFormat()
148 << "frameSize=" << frameFormat.frameSize()
149 << "frameRate=" << frameFormat.streamFrameRate()
150 << "ffmpegHWPixelFormat=" << (hwPixelFormat ? *hwPixelFormat : AV_PIX_FMT_NONE);
151
152 auto videoEncoder = new VideoEncoder(*this, m_settings, frameFormat, hwPixelFormat);
153 m_videoEncoders.emplace_back(videoEncoder);
154 if (m_autoStop)
155 videoEncoder->setAutoStop(true);
156
157 connect(videoEncoder, &EncoderThread::endOfSourceStream, this,
158 &RecordingEngine::handleSourceEndOfStream);
159
160 // set the frame before connecting to avoid potential races
161 if (firstFrame.isValid())
162 videoEncoder->addFrame(firstFrame);
163
164 connectEncoderToSource(videoEncoder, source);
165}
166
167bool RecordingEngine::startEncoders()
168{
169 Q_ASSERT(m_state == State::FormatsInitializing);
170 Q_ASSERT(m_formatsInitializer);
171 m_formatsInitializer.reset();
172
173 if (m_audioEncoders.empty() && m_videoEncoders.empty()) {
174 emit sessionError(QMediaRecorder::ResourceError,
175 QLatin1StringView("No valid stream found for encoding"));
176 return false;
177 }
178
179 m_state = State::EncodersInitializing;
180
181 std::vector<QFuture<bool>> resolutions;
182 forEachEncoder([&resolutions](EncoderThread *encoder) { //
183 encoder->start();
184 resolutions.push_back(encoder->resolvedFuture());
185 });
186
187 QtFuture::whenAll(resolutions.begin(), resolutions.end())
188 .then(this, [this](const QList<QFuture<bool>> &resolutions) {
189 handleEncodersResolved(resolutions);
190 });
191
192 return true;
193}
194
195bool RecordingEngine::initialize(const std::vector<QAudioBufferSource *> &audioSources,
196 const std::vector<QPlatformVideoSource *> &videoSources)
197{
198 Q_ASSERT(m_state == State::None);
199
200 m_state = State::FormatsInitializing;
201 m_formatsInitializer = std::make_unique<EncodingInitializer>(*this);
202 return m_formatsInitializer->start(audioSources, videoSources);
203}
204
205RecordingEngine::EncodingFinalizer::EncodingFinalizer(RecordingEngine &recordingEngine)
206 : m_recordingEngine(recordingEngine)
207{
208 setObjectName(QStringLiteral("EncodingFinalizer"));
209 Q_ASSERT(m_recordingEngine.m_state == State::Finalizing);
210 connect(this, &QThread::finished, this, &QObject::deleteLater);
211}
212
213void RecordingEngine::EncodingFinalizer::run()
214{
215 Q_ASSERT(m_recordingEngine.m_state == State::Finalizing);
216
217 // Block until handleEncodersResolved() (or finalize(), if the encoders had
218 // already all resolved) has finished with the encoders
219 const bool headerWritten = m_recordingEngine.waitForEncodingStart();
220
221 m_recordingEngine.stopAndDeleteThreads();
222
223 if (headerWritten) {
224 const int res = av_write_trailer(m_recordingEngine.avFormatContext());
225 if (res < 0) {
226 qCWarning(qLcFFmpegEncoder) << "could not write trailer" << res << AVError(res);
227 emit m_recordingEngine.sessionError(QMediaRecorder::FormatError,
228 QLatin1String("Cannot write trailer: ")
229 + err2str(res));
230 }
231 }
232 // else ffmpeg might crash
233
234 // close AVIO before emitting finalizationDone.
235 m_recordingEngine.m_formatContext->closeAVIO();
236
237 qCDebug(qLcFFmpegEncoder) << "Media recording finalized";
238 emit m_recordingEngine.finalizationDone();
239 m_recordingEngine.deleteLater(); // defer destruction to m_recordingEngine's thread
240}
241
243{
244 qCDebug(qLcFFmpegEncoder) << "Media recording finalizing";
245
246 Q_ASSERT(m_state == State::FormatsInitializing || m_state == State::EncodersInitializing
247 || m_state == State::Encoding);
248
249 Q_ASSERT((m_state == State::FormatsInitializing) == !!m_formatsInitializer);
250
251 if (m_state == State::FormatsInitializing)
252 markEncodingStart(false);
253
254 m_formatsInitializer.reset();
255
256 forEachEncoder(&disconnectEncoderFromSource);
257
258 m_state = State::Finalizing;
259
260 m_finalizer = new EncodingFinalizer(*this);
261 m_finalizer->start();
262}
263
264void RecordingEngine::setPaused(bool paused)
265{
266 forEachEncoder(&EncoderThread::setPaused, paused);
267}
268
269void RecordingEngine::setAutoStop(bool autoStop)
270{
271 m_autoStop = autoStop;
272 forEachEncoder(&EncoderThread::setAutoStop, autoStop);
273 handleSourceEndOfStream();
274}
275
276void RecordingEngine::setMetaData(const QMediaMetaData &metaData)
277{
278 m_metaData = metaData;
279}
280
281void RecordingEngine::newTimeStamp(qint64 time)
282{
283 QMutexLocker locker(&m_timeMutex);
284 if (time > m_timeRecorded) {
285 m_timeRecorded = time;
286 emit durationChanged(time);
287 }
288}
289
291{
292 return allOfEncoders(&EncoderThread::isEndOfSourceStream);
293}
294
295void RecordingEngine::handleSourceEndOfStream()
296{
297 if (m_autoStop && isEndOfSourceStreams())
298 emit autoStopped();
299}
300
301void RecordingEngine::handleEncodersResolved(const QList<QFuture<bool>> &resolutions)
302{
303 Q_ASSERT(m_state == State::EncodersInitializing || m_state == State::Finalizing);
304
305 bool allEncodersSucceeded = ranges::all_of(resolutions,
306 [](const QFuture<bool> &future) {
307 return future.result();
308 });
309
310 if (allEncodersSucceeded) {
311 Q_ASSERT(allOfEncoders(&EncoderThread::isInitialized));
312
313 qCDebug(qLcFFmpegEncoder) << "Encoders initialized; writing header";
314
315 avFormatContext()->metadata = QFFmpegMetaData::toAVMetaData(m_metaData);
316
317 AVDictionaryHolder options = muxerOptions(avFormatContext());
318 const int res = avformat_write_header(avFormatContext(), options);
319 if (res < 0) {
320 qWarning() << "could not write header, error:" << res << AVError(res);
321 allEncodersSucceeded = false;
322 emit sessionError(QMediaRecorder::ResourceError,
323 QStringLiteral("Cannot start writing the stream"));
324 } else {
325 qCDebug(qLcFFmpegEncoder) << "Stream header is successfully written";
326 // finalize() may already have moved m_state to Finalizing, don't go backwards
327 if (m_state == State::EncodersInitializing)
328 m_state = State::Encoding;
329 m_muxer->start();
330 }
331 }
332
333 // Unblock encoders in init()
334 markEncodingStart(allEncodersSucceeded);
335}
336
337void RecordingEngine::stopAndDeleteThreads()
338{
339 m_audioEncoders.clear();
340 m_videoEncoders.clear();
341 m_muxer.reset();
342}
343
344void RecordingEngine::markEncodingStart(bool startOk)
345{
346 m_startEncodingPromise.addResult(startOk);
347 m_startEncodingPromise.finish();
348}
349
350template <typename F, typename... Args>
351void RecordingEngine::forEachEncoder(F &&f, Args &&...args)
352{
353 for (auto &audioEncoder : m_audioEncoders)
354 std::invoke(f, audioEncoder.get(), args...);
355 for (auto &videoEncoder : m_videoEncoders)
356 std::invoke(f, videoEncoder.get(), args...);
357}
358
359template <typename F>
360bool RecordingEngine::allOfEncoders(F &&f) const
361{
362 auto predicate = [&f](const auto &encoder) { return std::invoke(f, encoder.get()); };
363
364 return ranges::all_of(m_audioEncoders, predicate)
365 && ranges::all_of(m_videoEncoders, predicate);
366}
367
368} // namespace QFFmpeg
369
370QT_END_NAMESPACE
371
372#include "moc_qffmpegrecordingengine_p.cpp"
The QAudioFormat class stores audio stream parameter information.
bool initialize(const std::vector< QAudioBufferSource * > &audioSources, const std::vector< QPlatformVideoSource * > &videoSources)
void disconnectEncoderFromSource(EncoderThread *encoder)
#define qCWarning(category,...)
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)