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
qffmpegstreamdecoder.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
3
5
6#include "playbackengine/qffmpegmediadataholder_p.h"
7#include <QtCore/qloggingcategory.h>
8#include <QtCore/qregularexpression.h>
9#include <QtCore/qspan.h>
10
12
13Q_STATIC_LOGGING_CATEGORY(qLcStreamDecoder, "qt.multimedia.ffmpeg.streamdecoder");
14
15namespace QFFmpeg {
16
17using namespace Qt::Literals;
18
19StreamDecoder::StreamDecoder(const PlaybackEngineObjectID &id, const CodecContext &codecContext,
20 TrackPosition absSeekPos)
25{
26 qCDebug(qLcStreamDecoder) << "Create stream decoder, trackType" << m_trackType
27 << "absSeekPos:" << absSeekPos.get();
28}
29
31{
32 avcodec_flush_buffers(m_codecContext.context());
33}
34
35void StreamDecoder::seek(quint64 sessionID, TrackPosition pos, const LoopOffset &offset)
36{
37 updateSession(sessionID, [this, pos, offset]() {
38 m_sessionCtx = { offset.loopStartTimeUs.asDuration() + pos };
39 avcodec_flush_buffers(m_codecContext.context());
40 });
41}
42
43void StreamDecoder::onFinalPacketReceived(PlaybackEngineObjectID sourceID)
44{
45 if (checkSessionID(sourceID.sessionID))
46 decode({});
47}
48
49void StreamDecoder::decode(Packet packet)
50{
51 if (packet.isValid() && !checkSessionID(packet.sourceID().sessionID)) {
52 qCDebug(qLcStreamDecoder) << "Packet session outdated. Source id:" << packet.sourceID()
53 << "current id" << id();
54 // no need to report packetProcessed: demuxer must be cleaned up
55 return;
56 }
57
58 m_sessionCtx.packets.enqueue(std::move(packet));
59 scheduleNextStep();
60}
61
63{
64 Packet packet = m_sessionCtx.packets.dequeue();
65
66 auto decodePacket = [this](const Packet &packet) {
67 if (trackType() == TrackType::SubtitleStream)
68 decodeSubtitle(packet);
69 else
70 decodeMedia(packet);
71 };
72
73 if (packet.isValid() && packet.loopOffset().loopIndex != m_sessionCtx.offset.loopIndex) {
74 decodePacket({});
75
76 qCDebug(qLcStreamDecoder) << "flush buffers due to new loop:"
77 << packet.loopOffset().loopIndex;
78
79 avcodec_flush_buffers(m_codecContext.context());
80 m_sessionCtx.offset = packet.loopOffset();
81 }
82
83 decodePacket(packet);
84
85 setAtEnd(!packet.isValid());
86
87 if (packet.isValid())
88 emit packetProcessed(std::move(packet));
89
90 scheduleNextStep();
91}
92
94{
95 return m_trackType;
96}
97
98qint32 StreamDecoder::maxQueueSize(TrackType type)
99{
100 switch (type) {
101
102 case TrackType::VideoStream:
103 return 3;
104 case TrackType::AudioStream:
105 return 9;
106 case TrackType::SubtitleStream:
107 return 6; /*main packet and closing packet*/
108 default:
109 Q_UNREACHABLE_RETURN(-1);
110 }
111}
112
113void StreamDecoder::onFrameProcessed(const Frame &frame)
114{
115 if (!checkID(frame.sourceID()))
116 return;
117
118 --m_sessionCtx.pendingFramesCount;
119 Q_ASSERT(m_sessionCtx.pendingFramesCount >= 0);
120
121 scheduleNextStep();
122}
123
125{
126 const qint32 maxCount = maxQueueSize(m_trackType);
127
128 return !m_sessionCtx.packets.empty() && m_sessionCtx.pendingFramesCount < maxCount
129 && PlaybackEngineObject::canDoNextStep();
130}
131
132void StreamDecoder::onFrameFound(const Frame &frame)
133{
134 if (frame.isValid() && frame.absoluteEnd() < m_sessionCtx.absSeekPos)
135 return;
136
137 Q_ASSERT(m_sessionCtx.pendingFramesCount >= 0);
138 ++m_sessionCtx.pendingFramesCount;
139 emit requestHandleFrame(frame);
140}
141
142void StreamDecoder::decodeMedia(const Packet &packet)
143{
144 auto sendPacketResult = sendAVPacket(packet);
145
146 if (sendPacketResult == AVERROR(EAGAIN)) {
147 // Doc says:
148 // AVERROR(EAGAIN): input is not accepted in the current state - user
149 // must read output with avcodec_receive_frame() (once
150 // all output is read, the packet should be resent, and
151 // the call will not fail with EAGAIN).
152 receiveAVFrames();
153 sendPacketResult = sendAVPacket(packet);
154
155 if (sendPacketResult != AVERROR(EAGAIN))
156 qWarning() << "Unexpected FFmpeg behavior";
157 }
158
159 if (sendPacketResult == 0)
160 receiveAVFrames(!packet.isValid());
161}
162
163int StreamDecoder::sendAVPacket(const Packet &packet)
164{
165 return avcodec_send_packet(m_codecContext.context(), packet.isValid() ? packet.avPacket() : nullptr);
166}
167
168void StreamDecoder::receiveAVFrames(bool flushPacket)
169{
170 while (true) {
171 auto avFrame = makeAVFrame();
172
173 const auto receiveFrameResult = avcodec_receive_frame(m_codecContext.context(), avFrame.get());
174
175 if (receiveFrameResult == AVERROR_EOF || receiveFrameResult == AVERROR(EAGAIN)) {
176 if (flushPacket && receiveFrameResult == AVERROR(EAGAIN)) {
177 // The documentation says that in the EAGAIN state output is not available. The new
178 // input must be sent. It does not say that this state can also be returned for
179 // Android MediaCodec when the ff_AMediaCodec_dequeueOutputBuffer call times out.
180 // The flush packet means it is the end of the stream. No more packets are available,
181 // so getting EAGAIN is unexpected here. At this point, the EAGAIN status was probably
182 // caused by a timeout in the ffmpeg implementation, not by too few packets. That is
183 // why there will be another try of calling avcodec_receive_frame
184 qWarning() << "Unexpected FFmpeg behavior: EAGAIN state for avcodec_receive_frame "
185 << "at end of the stream";
186 flushPacket = false;
187 continue;
188 }
189 break;
190 }
191
192 if (receiveFrameResult < 0) {
193 emit error(QMediaPlayer::FormatError, err2str(receiveFrameResult));
194 break;
195 }
196
197
198 // Avoid starvation on FFmpeg decoders with fixed size frame pool
199 if (m_trackType == TrackType::VideoStream)
200 avFrame = copyFromHwPool(std::move(avFrame));
201
202 onFrameFound({ m_sessionCtx.offset, std::move(avFrame), m_codecContext, id() });
203 }
204}
205
206void StreamDecoder::decodeSubtitle(const Packet &packet)
207{
208 if (!packet.isValid())
209 return;
210 // qCDebug(qLcDecoder) << " decoding subtitle" << "has delay:" <<
211 // (codec->codec->capabilities & AV_CODEC_CAP_DELAY);
212 AVSubtitle subtitle;
213 memset(&subtitle, 0, sizeof(subtitle));
214 int gotSubtitle = 0;
215
216 const int res =
217 avcodec_decode_subtitle2(m_codecContext.context(), &subtitle, &gotSubtitle, packet.avPacket());
218 // qCDebug(qLcDecoder) << " subtitle got:" << res << gotSubtitle << subtitle.format <<
219 // Qt::hex << (quint64)subtitle.pts;
220 if (res < 0 || !gotSubtitle)
221 return;
222
223 // apparently the timestamps in the AVSubtitle structure are not always filled in
224 // if they are missing, use the packets pts and duration values instead
225 TrackPosition start = 0, end = 0;
226 if (subtitle.pts == AV_NOPTS_VALUE) {
227 start = m_codecContext.toTrackPosition(AVStreamPosition(packet.avPacket()->pts));
228 end = start + m_codecContext.toTrackDuration(AVStreamDuration(packet.avPacket()->duration));
229 } else {
230 auto pts = timeStampUs(subtitle.pts, AVRational{ 1, AV_TIME_BASE });
231 start = TrackPosition(*pts + qint64(subtitle.start_display_time) * 1000);
232 end = TrackPosition(*pts + qint64(subtitle.end_display_time) * 1000);
233 }
234
235 if (end <= start) {
236 qWarning() << "Invalid subtitle time";
237 return;
238 }
239 // qCDebug(qLcDecoder) << " got subtitle (" << start << "--" << end << "):";
240 QString text = subtitleTextFromAVSubtitle(subtitle);
241
242 onFrameFound({ m_sessionCtx.offset, text, start, end - start, id() });
243
244 // TODO: maybe optimize
245 onFrameFound({ m_sessionCtx.offset, QString(), end, TrackDuration(0), id() });
246}
247
248static QString extractSubtitleText(const AVSubtitleRect &r)
249{
250 if (r.text)
251 return QString::fromUtf8(r.text);
252
253 const char *ass = r.ass;
254 int nCommas = 0;
255 while (*ass) {
256 if (nCommas == 8)
257 break;
258 if (*ass == ',')
259 ++nCommas;
260 ++ass;
261 }
262 return QString::fromUtf8(ass);
263}
264
265QString subtitleTextFromAVSubtitle(const AVSubtitle &subtitle)
266{
267 QString text;
268
269 for (const AVSubtitleRect *r : QSpan(subtitle.rects, subtitle.num_rects)) {
270 if (r != subtitle.rects[0])
271 text += u'\n';
272
273 text += extractSubtitleText(*r);
274 }
275 static const QRegularExpression lineBreakRe(u"\\\\N|\\\\n|\\r\\n"_s);
276 text.replace(lineBreakRe, u"\n"_s);
277 if (text.endsWith(u'\n'))
278 text.chop(1);
279 return text;
280}
281
282} // namespace QFFmpeg
283
284QT_END_NAMESPACE
285
286#include "moc_qffmpegstreamdecoder_p.cpp"
bool canDoNextStep() const override
StreamDecoder(const PlaybackEngineObjectID &id, const CodecContext &codecContext, TrackPosition absSeekPos)
void onFrameProcessed(const Frame &frame)
void onFinalPacketReceived(PlaybackEngineObjectID sourceID)
void seek(quint64 sessionID, TrackPosition pos, const LoopOffset &offset)
QT_MANGLE_NAMESPACE(QMacScreenCaptureStreamDelegate) QMacScreenCaptureStreamDelegate
QString subtitleTextFromAVSubtitle(const AVSubtitle &subtitle)
static QString extractSubtitleText(const AVSubtitleRect &r)
Combined button and popup list for selecting options.