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
qffmpegaudioencoder.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
12#include <QtCore/qloggingcategory.h>
13
15
16namespace QFFmpeg {
17
18Q_STATIC_LOGGING_CATEGORY(qLcFFmpegAudioEncoder, "qt.multimedia.ffmpeg.audioencoder");
19static constexpr bool audioEncoderExtendedTracing = false;
20
21namespace {
22void setupStreamParameters(AVStream *stream, const Codec &codec,
23 const AVAudioFormat &requestedAudioFormat)
24{
25 const auto channelLayouts = codec.channelLayouts();
26#if QT_FFMPEG_HAS_AV_CHANNEL_LAYOUT
27 stream->codecpar->ch_layout =
28 adjustChannelLayout(channelLayouts, requestedAudioFormat.channelLayout);
29#else
30 stream->codecpar->channel_layout =
31 adjustChannelLayout(channelLayouts, requestedAudioFormat.channelLayoutMask);
32 stream->codecpar->channels = qPopulationCount(stream->codecpar->channel_layout);
33#endif
34 const auto sampleRates = codec.sampleRates();
35 const auto sampleRate = adjustSampleRate(sampleRates, requestedAudioFormat.sampleRate);
36
37 stream->codecpar->sample_rate = sampleRate;
38 // Codecs with a variable frame size (e.g. PCM) must keep frame_size at 0;
39 // FFmpeg 9 rejects any frame whose nb_samples exceeds a non-zero frame_size,
40 // even for such codecs. Codecs with a fixed frame size determine and set
41 // their own frame_size during avcodec_open2, overriding this default.
42 if (!(codec.capabilities() & AV_CODEC_CAP_VARIABLE_FRAME_SIZE))
43 stream->codecpar->frame_size = 1024;
44 const auto sampleFormats = codec.sampleFormats();
45 stream->codecpar->format = adjustSampleFormat(sampleFormats, requestedAudioFormat.sampleFormat);
46
47 stream->time_base = AVRational{ 1, sampleRate };
48
49 qCDebug(qLcFFmpegAudioEncoder)
50 << "set stream time_base" << stream->time_base.num << "/" << stream->time_base.den;
51}
52
53bool openCodecContext(AVCodecContext *codecContext, AVStream *stream,
54 const QMediaEncoderSettings &settings)
55{
56 Q_ASSERT(codecContext);
57 codecContext->time_base = stream->time_base;
58
59 int status = avcodec_parameters_to_context(codecContext, stream->codecpar);
60 if (status < 0) {
61 qCWarning(qLcFFmpegAudioEncoder)
62 << "Cannot set codec parameters; result:" << AVError(status);
63 return false;
64 }
65
66 // if avcodec_open2 fails, it may clean codecContext->codec
67 Codec codec{ codecContext->codec };
68
69 AVDictionaryHolder opts;
70 applyAudioEncoderOptions(settings, codec.name(), codecContext, opts);
71 applyExperimentalCodecOptions(codec, opts);
72
73 status = avcodec_open2(codecContext, codec.get(), opts);
74
75 if (status != 0) {
76 qCWarning(qLcFFmpegAudioEncoder)
77 << "Cannot open audio codec" << codec.name() << "; result:" << AVError(status);
78 return false;
79 }
80
81 qCDebug(qLcFFmpegAudioEncoder) << "audio codec params: fmt=" << codecContext->sample_fmt
82 << "rate=" << codecContext->sample_rate;
83
84 status = avcodec_parameters_from_context(stream->codecpar, codecContext);
85 if (status < 0) {
86 qCWarning(qLcFFmpegAudioEncoder) << "Cannot get parameters from audio codec context"
87 << codec.name() << "; result:" << AVError(status);
88 return false;
89 }
90
91 return true;
92}
93
94} // namespace
95
96AudioEncoder::AudioEncoder(RecordingEngine &recordingEngine, const QAudioFormat &sourceFormat,
97 const QMediaEncoderSettings &settings)
98 : EncoderThread(recordingEngine), m_sourceFormat(sourceFormat), m_settings(settings)
99{
100 setObjectName(QLatin1String("AudioEncoder"));
101 qCDebug(qLcFFmpegAudioEncoder) << "AudioEncoder" << settings.audioCodec();
102
103 const AVCodecID codecID = QFFmpegMediaFormatInfo::codecIdForAudioCodec(settings.audioCodec());
104 Q_ASSERT(avformat_query_codec(recordingEngine.avFormatContext()->oformat, codecID,
105 FF_COMPLIANCE_NORMAL));
106
107 Q_ASSERT(QFFmpeg::findAVEncoder(codecID));
108
109 m_stream = avformat_new_stream(recordingEngine.avFormatContext(), nullptr);
110 m_stream->id = recordingEngine.avFormatContext()->nb_streams - 1;
111 m_stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
112 m_stream->codecpar->codec_id = codecID;
113}
114
115void AudioEncoder::addBuffer(const QAudioBuffer &buffer)
116{
117 if (!buffer.isValid()) {
118 setEndOfSourceStream();
119 return;
120 }
121
122 {
123 const std::chrono::microseconds bufferDuration(buffer.duration());
124 auto guard = lockLoopData();
125
126 resetEndOfSourceStream();
127
128 if (m_paused)
129 return;
130
131 // TODO: apply logic with canPushFrame
132
133 m_audioBufferQueue.push(buffer);
134 m_queueDuration += bufferDuration;
135 }
136
137 dataReady();
138}
139
140QAudioBuffer AudioEncoder::takeBuffer()
141{
142 auto locker = lockLoopData();
143 QAudioBuffer result = dequeueIfPossible(m_audioBufferQueue);
144 m_queueDuration -= std::chrono::microseconds(result.duration());
145 return result;
146}
147
149{
150 const AVAudioFormat requestedAudioFormat(m_sourceFormat);
151 const AVCodecID codecId = m_stream->codecpar->codec_id;
152
153 namespace ranges = QtMultimediaPrivate::ranges;
154
155 const auto encoders = findAndScoreEncoders(codecId, [&](const Codec &codec) {
156 AVScore result = DefaultAVScore;
157
158 // Attempt to find no-conversion format
159 if (auto fmts = codec.sampleFormats(); !fmts.empty())
160 result += ranges::contains(fmts, requestedAudioFormat.sampleFormat) ? 1 : -1;
161
162 if (auto rates = codec.sampleRates(); !rates.empty())
163 result += ranges::contains(rates, requestedAudioFormat.sampleRate) ? 1 : -1;
164
165#if QT_FFMPEG_HAS_AV_CHANNEL_LAYOUT
166 if (auto layouts = codec.channelLayouts(); !layouts.empty())
167 result += ranges::contains(layouts, requestedAudioFormat.channelLayout) ? 1 : -1;
168#else
169 if (auto layouts = codec.channelLayouts(); !layouts.empty())
170 result +=
171 ranges::contains(layouts, requestedAudioFormat.channelLayoutMask) ? 1 : -1;
172#endif
173
174 return result;
175 });
176
177 for (const auto &[codec, score] : encoders) {
178 AVCodecContextUPtr codecContext(avcodec_alloc_context3(codec.get()));
179 if (!codecContext)
180 continue;
181
182 setupStreamParameters(m_stream, Codec{ codecContext->codec }, requestedAudioFormat);
183 if (!openCodecContext(codecContext.get(), m_stream, m_settings))
184 continue;
185
186 m_codecContext = std::move(codecContext);
187 break;
188 }
189
190 if (!m_codecContext) {
191 qCWarning(qLcFFmpegAudioEncoder) << "Unable to open any audio codec";
192 markResolved(false);
193 emit m_recordingEngine.sessionError(QMediaRecorder::FormatError,
194 QStringLiteral("Cannot open any audio codec"));
195 return false;
196 }
197
198 qCDebug(qLcFFmpegAudioEncoder) << "found audio codec" << m_codecContext->codec->name;
199
200 updateResampler(m_sourceFormat);
201
202 // TODO: try to address this dependency here.
203 if (auto input = qobject_cast<QFFmpegAudioInput *>(source()))
204 input->setBufferSize(m_codecContext->frame_size);
205
206 return EncoderThread::init();
207}
208
210{
211 while (!m_audioBufferQueue.empty())
213
214 if (m_avFrameSamplesOffset) {
215 // the size of the last frame can be less than m_codecContext->frame_size
216
217 retrievePackets();
218 sendPendingFrameToAVCodec();
219 }
220
221 while (avcodec_send_frame(m_codecContext.get(), nullptr) == AVERROR(EAGAIN))
222 retrievePackets();
223 retrievePackets();
224}
225
227{
228 return !m_audioBufferQueue.empty();
229}
230
231void AudioEncoder::retrievePackets()
232{
233 while (true) {
234 AVPacketUPtr packet(av_packet_alloc());
235 int ret = avcodec_receive_packet(m_codecContext.get(), packet.get());
236 switch (ret) {
237 case 0:
238 break;
239 case AVERROR(EAGAIN):
240 case AVERROR(EOF):
241 return;
242 default:
243 qCDebug(qLcFFmpegAudioEncoder) << "receive packet" << ret << QFFmpeg::AVError{ ret };
244 return;
245 }
246
247 if constexpr (audioEncoderExtendedTracing)
248 qCDebug(qLcFFmpegAudioEncoder)
249 << "writing audio packet" << packet->size << packet->pts << packet->dts;
250 packet->stream_index = m_stream->id;
251 m_recordingEngine.getMuxer()->addPacket(std::move(packet));
252 }
253}
254
256{
257 QAudioBuffer buffer = takeBuffer();
258 Q_ASSERT(buffer.isValid());
259
260 if constexpr (audioEncoderExtendedTracing)
261 qCDebug(qLcFFmpegAudioEncoder)
262 << "new audio buffer" << buffer.byteCount() << buffer.format()
263 << buffer.frameCount() << m_codecContext->frame_size;
264
265 if (buffer.format() != m_sourceFormat && !updateResampler(buffer.format()))
266 return;
267
268 int samplesOffset = 0;
269 const int bufferSamplesCount = static_cast<int>(buffer.frameCount());
270
271 while (samplesOffset < bufferSamplesCount)
272 handleAudioData(buffer.constData<uint8_t>(), samplesOffset, bufferSamplesCount);
273
274 Q_ASSERT(samplesOffset == bufferSamplesCount);
275}
276
278{
279 if (m_encodingStarted)
280 return m_audioBufferQueue.size() <= 1 || m_queueDuration < m_maxQueueDuration;
281 if (!isFinished())
282 return m_audioBufferQueue.empty();
283
284 return false;
285}
286
287bool AudioEncoder::updateResampler(const QAudioFormat &sourceFormat)
288{
289 m_resampler.reset();
290
291 const AVAudioFormat requestedAudioFormat(sourceFormat);
292 const AVAudioFormat codecAudioFormat(m_codecContext.get());
293
294 if (requestedAudioFormat != codecAudioFormat) {
295 m_resampler = createResampleContext(requestedAudioFormat, codecAudioFormat);
296 if (!swr_is_initialized(m_resampler.get())) {
297 m_sourceFormat = {};
298 qCWarning(qLcFFmpegAudioEncoder) << "Cannot initialize resampler for audio encoder";
299 emit m_recordingEngine.sessionError(
300 QMediaRecorder::FormatError,
301 QStringLiteral("Cannot initialize resampler for audio encoder"));
302 return false;
303 }
304 qCDebug(qLcFFmpegAudioEncoder) << "Created resampler with audio formats conversion\n"
305 << requestedAudioFormat << "->" << codecAudioFormat;
306 } else {
307 qCDebug(qLcFFmpegAudioEncoder) << "Resampler is not needed due to no-conversion format\n"
308 << requestedAudioFormat;
309 }
310
311 m_sourceFormat = sourceFormat;
312
313 return true;
314}
315
316void AudioEncoder::ensurePendingFrame(int availableSamplesCount)
317{
318 Q_ASSERT(availableSamplesCount >= 0);
319
320 if (m_avFrame)
321 return;
322
323 m_avFrame = makeAVFrame();
324
325 m_avFrame->format = m_codecContext->sample_fmt;
326#if QT_FFMPEG_HAS_AV_CHANNEL_LAYOUT
327 m_avFrame->ch_layout = m_codecContext->ch_layout;
328#else
329 m_avFrame->channel_layout = m_codecContext->channel_layout;
330 m_avFrame->channels = m_codecContext->channels;
331#endif
332 m_avFrame->sample_rate = m_codecContext->sample_rate;
333
334 const bool isFixedFrameSize =
335 !(m_codecContext->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)
336 && m_codecContext->frame_size;
337 m_avFrame->nb_samples = isFixedFrameSize ? m_codecContext->frame_size : availableSamplesCount;
338 if (m_avFrame->nb_samples) {
339 const int status = av_frame_get_buffer(m_avFrame.get(), 0);
340 if (status < 0)
341 qCWarning(qLcFFmpegAudioEncoder)
342 << "Failed to allocate audio frame buffer:" << AVError(status);
343 }
344
345 const auto &timeBase = m_stream->time_base;
346 const auto pts = timeBase.den && timeBase.num
347 ? timeBase.den * m_samplesWritten / (m_codecContext->sample_rate * timeBase.num)
348 : m_samplesWritten;
349 setAVFrameTime(*m_avFrame, pts, timeBase);
350}
351
352void AudioEncoder::writeDataToPendingFrame(const uchar *data, int &samplesOffset, int samplesCount)
353{
354 Q_ASSERT(m_avFrame);
355 Q_ASSERT(m_avFrameSamplesOffset <= m_avFrame->nb_samples);
356
357 const int bytesPerSample = av_get_bytes_per_sample(m_codecContext->sample_fmt);
358 const bool isPlanar = av_sample_fmt_is_planar(m_codecContext->sample_fmt);
359
360#if QT_FFMPEG_HAS_AV_CHANNEL_LAYOUT
361 const int channelsCount = m_codecContext->ch_layout.nb_channels;
362#else
363 const int channelsCount = m_codecContext->channels;
364#endif
365
366 const int audioDataOffset = isPlanar ? bytesPerSample * m_avFrameSamplesOffset
367 : bytesPerSample * m_avFrameSamplesOffset * channelsCount;
368
369 const int planesCount = isPlanar ? channelsCount : 1;
370 m_avFramePlanesData.resize(planesCount);
371 for (int plane = 0; plane < planesCount; ++plane)
372 m_avFramePlanesData[plane] = m_avFrame->extended_data[plane] + audioDataOffset;
373
374 const int samplesToWrite = m_avFrame->nb_samples - m_avFrameSamplesOffset;
375 int samplesToRead =
376 (samplesToWrite * m_sourceFormat.sampleRate() + m_codecContext->sample_rate / 2)
377 / m_codecContext->sample_rate;
378 // the lower bound is need to get round infinite loops in corner cases
379 samplesToRead = qBound(1, samplesToRead, samplesCount - samplesOffset);
380
381 data += m_sourceFormat.bytesForFrames(samplesOffset);
382
383 if (m_resampler) {
384 m_avFrameSamplesOffset += swr_convert(m_resampler.get(), m_avFramePlanesData.data(),
385 samplesToWrite, &data, samplesToRead);
386 } else {
387 Q_ASSERT(planesCount == 1);
388 m_avFrameSamplesOffset += samplesToRead;
389 memcpy(m_avFramePlanesData[0], data, m_sourceFormat.bytesForFrames(samplesToRead));
390 }
391
392 samplesOffset += samplesToRead;
393}
394
395void AudioEncoder::sendPendingFrameToAVCodec()
396{
397 Q_ASSERT(m_avFrame);
398 Q_ASSERT(m_avFrameSamplesOffset <= m_avFrame->nb_samples);
399
400 m_avFrame->nb_samples = m_avFrameSamplesOffset;
401
402 m_samplesWritten += m_avFrameSamplesOffset;
403
404 const qint64 time = m_sourceFormat.durationForFrames(
405 m_samplesWritten * m_sourceFormat.sampleRate() / m_codecContext->sample_rate);
406 m_recordingEngine.newTimeStamp(time / 1000);
407
408 if constexpr (audioEncoderExtendedTracing)
409 qCDebug(qLcFFmpegAudioEncoder) << "sendPendingFrameToAVCodec" << m_avFrame->nb_samples
410 << m_codecContext->frame_size << m_avFrame->pts;
411
412 int ret = avcodec_send_frame(m_codecContext.get(), m_avFrame.get());
413 if (ret < 0)
414 qCDebug(qLcFFmpegAudioEncoder) << "error sending frame" << ret << QFFmpeg::AVError(ret);
415
416 m_avFrame = nullptr;
417 m_avFrameSamplesOffset = 0;
418 std::fill(m_avFramePlanesData.begin(), m_avFramePlanesData.end(), nullptr);
419}
420
421void AudioEncoder::handleAudioData(const uchar *data, int &samplesOffset, int samplesCount)
422{
423 ensurePendingFrame(samplesCount - samplesOffset);
424
425 writeDataToPendingFrame(data, samplesOffset, samplesCount);
426
427 // The frame is not ready yet
428 if (m_avFrameSamplesOffset < m_avFrame->nb_samples)
429 return;
430
431 retrievePackets();
432
433 sendPendingFrameToAVCodec();
434}
435
436} // namespace QFFmpeg
437
438QT_END_NAMESPACE
The QAudioFormat class stores audio stream parameter information.
bool init() override
Called on this thread when thread starts.
void cleanup() override
Called on this thread before thread exits.
bool hasData() const override
Must return true when data is available for processing.
void processOne() override
Process one work item.
bool checkIfCanPushFrame() const override
Q_STATIC_LOGGING_CATEGORY(qLCAndroidVideoDevices, "qt.multimedia.ffmpeg.android.videoDevices")
QT_MANGLE_NAMESPACE(QMacScreenCaptureStreamDelegate) QMacScreenCaptureStreamDelegate
static constexpr bool audioEncoderExtendedTracing
Combined button and popup list for selecting options.