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