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