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 Q_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 Q_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 Q_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 Q_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 Q_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 Q_EMIT durationChanged(time);
287 }
288}
289
291{
292 return allOfEncoders(&EncoderThread::isEndOfSourceStream);
293}
294
295void RecordingEngine::handleSourceEndOfStream()
296{
297 // Once finalizing, the EncodingFinalizer thread owns the encoders and deletes
298 // them, so we must not access them. The recording is stopping anyway.
299 // This can be reached via queued end-of-stream signals emitted before finalize().
300 if (m_state == State::Finalizing)
301 return;
302
303 if (m_autoStop && isEndOfSourceStreams())
304 Q_EMIT autoStopped();
305}
306
307void RecordingEngine::handleEncodersResolved(const QList<QFuture<bool>> &resolutions)
308{
309 Q_ASSERT(m_state == State::EncodersInitializing || m_state == State::Finalizing);
310
311 bool allEncodersSucceeded = ranges::all_of(resolutions,
312 [](const QFuture<bool> &future) {
313 return future.result();
314 });
315
316 if (allEncodersSucceeded) {
317 Q_ASSERT(allOfEncoders(&EncoderThread::isInitialized));
318
319 qCDebug(qLcFFmpegEncoder) << "Encoders initialized; writing header";
320
321 avFormatContext()->metadata = QFFmpegMetaData::toAVMetaData(m_metaData);
322
323 AVDictionaryHolder options = muxerOptions(avFormatContext());
324 const int res = avformat_write_header(avFormatContext(), options);
325 if (res < 0) {
326 qWarning() << "could not write header, error:" << res << AVError(res);
327 allEncodersSucceeded = false;
328 Q_EMIT sessionError(QMediaRecorder::ResourceError,
329 QStringLiteral("Cannot start writing the stream"));
330 } else {
331 qCDebug(qLcFFmpegEncoder) << "Stream header is successfully written";
332 // finalize() may already have moved m_state to Finalizing, don't go backwards
333 if (m_state == State::EncodersInitializing)
334 m_state = State::Encoding;
335 m_muxer->start();
336 }
337 }
338
339 // Unblock encoders in init()
340 markEncodingStart(allEncodersSucceeded);
341}
342
343void RecordingEngine::stopAndDeleteThreads()
344{
345 m_audioEncoders.clear();
346 m_videoEncoders.clear();
347 m_muxer.reset();
348}
349
350void RecordingEngine::markEncodingStart(bool startOk)
351{
352 m_startEncodingPromise.addResult(startOk);
353 m_startEncodingPromise.finish();
354}
355
356template <typename F, typename... Args>
357void RecordingEngine::forEachEncoder(F &&f, Args &&...args)
358{
359 for (auto &audioEncoder : m_audioEncoders)
360 std::invoke(f, audioEncoder.get(), args...);
361 for (auto &videoEncoder : m_videoEncoders)
362 std::invoke(f, videoEncoder.get(), args...);
363}
364
365template <typename F>
366bool RecordingEngine::allOfEncoders(F &&f) const
367{
368 auto predicate = [&f](const auto &encoder) { return std::invoke(f, encoder.get()); };
369
370 return ranges::all_of(m_audioEncoders, predicate)
371 && ranges::all_of(m_videoEncoders, predicate);
372}
373
374} // namespace QFFmpeg
375
376QT_END_NAMESPACE
377
378#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,...)