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 emit m_recordingEngine.sessionError(QMediaRecorder::FormatError,
193 QStringLiteral("Cannot open any audio codec"));
194 return false;
195 }
196
197 qCDebug(qLcFFmpegAudioEncoder) << "found audio codec" << m_codecContext->codec->name;
198
199 updateResampler(m_sourceFormat);
200
201 // TODO: try to address this dependency here.
202 if (auto input = qobject_cast<QFFmpegAudioInput *>(source()))
203 input->setBufferSize(m_codecContext->frame_size);
204
205 return EncoderThread::init();
206}
207
209{
210 while (!m_audioBufferQueue.empty())
212
213 if (m_avFrameSamplesOffset) {
214 // the size of the last frame can be less than m_codecContext->frame_size
215
216 retrievePackets();
217 sendPendingFrameToAVCodec();
218 }
219
220 while (avcodec_send_frame(m_codecContext.get(), nullptr) == AVERROR(EAGAIN))
221 retrievePackets();
222 retrievePackets();
223}
224
226{
227 return !m_audioBufferQueue.empty();
228}
229
230void AudioEncoder::retrievePackets()
231{
232 while (true) {
233 AVPacketUPtr packet(av_packet_alloc());
234 int ret = avcodec_receive_packet(m_codecContext.get(), packet.get());
235 switch (ret) {
236 case 0:
237 break;
238 case AVERROR(EAGAIN):
239 case AVERROR(EOF):
240 return;
241 default:
242 qCDebug(qLcFFmpegAudioEncoder) << "receive packet" << ret << QFFmpeg::AVError{ ret };
243 return;
244 }
245
246 if constexpr (audioEncoderExtendedTracing)
247 qCDebug(qLcFFmpegAudioEncoder)
248 << "writing audio packet" << packet->size << packet->pts << packet->dts;
249 packet->stream_index = m_stream->id;
250 m_recordingEngine.getMuxer()->addPacket(std::move(packet));
251 }
252}
253
255{
256 QAudioBuffer buffer = takeBuffer();
257 Q_ASSERT(buffer.isValid());
258
259 if constexpr (audioEncoderExtendedTracing)
260 qCDebug(qLcFFmpegAudioEncoder)
261 << "new audio buffer" << buffer.byteCount() << buffer.format()
262 << buffer.frameCount() << m_codecContext->frame_size;
263
264 if (buffer.format() != m_sourceFormat && !updateResampler(buffer.format()))
265 return;
266
267 int samplesOffset = 0;
268 const int bufferSamplesCount = static_cast<int>(buffer.frameCount());
269
270 while (samplesOffset < bufferSamplesCount)
271 handleAudioData(buffer.constData<uint8_t>(), samplesOffset, bufferSamplesCount);
272
273 Q_ASSERT(samplesOffset == bufferSamplesCount);
274}
275
277{
278 if (m_encodingStarted)
279 return m_audioBufferQueue.size() <= 1 || m_queueDuration < m_maxQueueDuration;
280 if (!isFinished())
281 return m_audioBufferQueue.empty();
282
283 return false;
284}
285
286bool AudioEncoder::updateResampler(const QAudioFormat &sourceFormat)
287{
288 m_resampler.reset();
289
290 const AVAudioFormat requestedAudioFormat(sourceFormat);
291 const AVAudioFormat codecAudioFormat(m_codecContext.get());
292
293 if (requestedAudioFormat != codecAudioFormat) {
294 m_resampler = createResampleContext(requestedAudioFormat, codecAudioFormat);
295 if (!swr_is_initialized(m_resampler.get())) {
296 m_sourceFormat = {};
297 qCWarning(qLcFFmpegAudioEncoder) << "Cannot initialize resampler for audio encoder";
298 emit m_recordingEngine.sessionError(
299 QMediaRecorder::FormatError,
300 QStringLiteral("Cannot initialize resampler for audio encoder"));
301 return false;
302 }
303 qCDebug(qLcFFmpegAudioEncoder) << "Created resampler with audio formats conversion\n"
304 << requestedAudioFormat << "->" << codecAudioFormat;
305 } else {
306 qCDebug(qLcFFmpegAudioEncoder) << "Resampler is not needed due to no-conversion format\n"
307 << requestedAudioFormat;
308 }
309
310 m_sourceFormat = sourceFormat;
311
312 return true;
313}
314
315void AudioEncoder::ensurePendingFrame(int availableSamplesCount)
316{
317 Q_ASSERT(availableSamplesCount >= 0);
318
319 if (m_avFrame)
320 return;
321
322 m_avFrame = makeAVFrame();
323
324 m_avFrame->format = m_codecContext->sample_fmt;
325#if QT_FFMPEG_HAS_AV_CHANNEL_LAYOUT
326 m_avFrame->ch_layout = m_codecContext->ch_layout;
327#else
328 m_avFrame->channel_layout = m_codecContext->channel_layout;
329 m_avFrame->channels = m_codecContext->channels;
330#endif
331 m_avFrame->sample_rate = m_codecContext->sample_rate;
332
333 const bool isFixedFrameSize =
334 !(m_codecContext->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)
335 && m_codecContext->frame_size;
336 m_avFrame->nb_samples = isFixedFrameSize ? m_codecContext->frame_size : availableSamplesCount;
337 if (m_avFrame->nb_samples) {
338 const int status = av_frame_get_buffer(m_avFrame.get(), 0);
339 if (status < 0)
340 qCWarning(qLcFFmpegAudioEncoder)
341 << "Failed to allocate audio frame buffer:" << AVError(status);
342 }
343
344 const auto &timeBase = m_stream->time_base;
345 const auto pts = timeBase.den && timeBase.num
346 ? timeBase.den * m_samplesWritten / (m_codecContext->sample_rate * timeBase.num)
347 : m_samplesWritten;
348 setAVFrameTime(*m_avFrame, pts, timeBase);
349}
350
351void AudioEncoder::writeDataToPendingFrame(const uchar *data, int &samplesOffset, int samplesCount)
352{
353 Q_ASSERT(m_avFrame);
354 Q_ASSERT(m_avFrameSamplesOffset <= m_avFrame->nb_samples);
355
356 const int bytesPerSample = av_get_bytes_per_sample(m_codecContext->sample_fmt);
357 const bool isPlanar = av_sample_fmt_is_planar(m_codecContext->sample_fmt);
358
359#if QT_FFMPEG_HAS_AV_CHANNEL_LAYOUT
360 const int channelsCount = m_codecContext->ch_layout.nb_channels;
361#else
362 const int channelsCount = m_codecContext->channels;
363#endif
364
365 const int audioDataOffset = isPlanar ? bytesPerSample * m_avFrameSamplesOffset
366 : bytesPerSample * m_avFrameSamplesOffset * channelsCount;
367
368 const int planesCount = isPlanar ? channelsCount : 1;
369 m_avFramePlanesData.resize(planesCount);
370 for (int plane = 0; plane < planesCount; ++plane)
371 m_avFramePlanesData[plane] = m_avFrame->extended_data[plane] + audioDataOffset;
372
373 const int samplesToWrite = m_avFrame->nb_samples - m_avFrameSamplesOffset;
374 int samplesToRead =
375 (samplesToWrite * m_sourceFormat.sampleRate() + m_codecContext->sample_rate / 2)
376 / m_codecContext->sample_rate;
377 // the lower bound is need to get round infinite loops in corner cases
378 samplesToRead = qBound(1, samplesToRead, samplesCount - samplesOffset);
379
380 data += m_sourceFormat.bytesForFrames(samplesOffset);
381
382 if (m_resampler) {
383 m_avFrameSamplesOffset += swr_convert(m_resampler.get(), m_avFramePlanesData.data(),
384 samplesToWrite, &data, samplesToRead);
385 } else {
386 Q_ASSERT(planesCount == 1);
387 m_avFrameSamplesOffset += samplesToRead;
388 memcpy(m_avFramePlanesData[0], data, m_sourceFormat.bytesForFrames(samplesToRead));
389 }
390
391 samplesOffset += samplesToRead;
392}
393
394void AudioEncoder::sendPendingFrameToAVCodec()
395{
396 Q_ASSERT(m_avFrame);
397 Q_ASSERT(m_avFrameSamplesOffset <= m_avFrame->nb_samples);
398
399 m_avFrame->nb_samples = m_avFrameSamplesOffset;
400
401 m_samplesWritten += m_avFrameSamplesOffset;
402
403 const qint64 time = m_sourceFormat.durationForFrames(
404 m_samplesWritten * m_sourceFormat.sampleRate() / m_codecContext->sample_rate);
405 m_recordingEngine.newTimeStamp(time / 1000);
406
407 if constexpr (audioEncoderExtendedTracing)
408 qCDebug(qLcFFmpegAudioEncoder) << "sendPendingFrameToAVCodec" << m_avFrame->nb_samples
409 << m_codecContext->frame_size << m_avFrame->pts;
410
411 int ret = avcodec_send_frame(m_codecContext.get(), m_avFrame.get());
412 if (ret < 0)
413 qCDebug(qLcFFmpegAudioEncoder) << "error sending frame" << ret << QFFmpeg::AVError(ret);
414
415 m_avFrame = nullptr;
416 m_avFrameSamplesOffset = 0;
417 std::fill(m_avFramePlanesData.begin(), m_avFramePlanesData.end(), nullptr);
418}
419
420void AudioEncoder::handleAudioData(const uchar *data, int &samplesOffset, int samplesCount)
421{
422 ensurePendingFrame(samplesCount - samplesOffset);
423
424 writeDataToPendingFrame(data, samplesOffset, samplesCount);
425
426 // The frame is not ready yet
427 if (m_avFrameSamplesOffset < m_avFrame->nb_samples)
428 return;
429
430 retrievePackets();
431
432 sendPendingFrameToAVCodec();
433}
434
435} // namespace QFFmpeg
436
437QT_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.