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
qffmpegdemuxer.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
4#include "playbackengine/qffmpegdemuxer_p.h"
5
6#include <QtCore/qloggingcategory.h>
7
8#include <chrono>
9
10QT_BEGIN_NAMESPACE
11
12namespace QFFmpeg {
13
14// 4 sec for buffering. TODO: maybe move to env var customization
15static constexpr TrackDuration MaxBufferedDurationUs{ 4'000'000 };
16
17// around 4 sec of hdr video
18static constexpr qint64 MaxBufferedSize = 32 * 1024 * 1024;
19
20Q_STATIC_LOGGING_CATEGORY(qLcDemuxer, "qt.multimedia.ffmpeg.demuxer");
21
22static TrackPosition packetEndPos(const Packet &packet, const AVStream *stream,
23 const AVFormatContext *context)
24{
25 const AVPacket &avPacket = *packet.avPacket();
26 return packet.loopOffset().loopStartTimeUs.asDuration()
27 + toTrackPosition(AVStreamPosition(avPacket.pts + avPacket.duration), stream, context);
28}
29
30static bool isPacketWithinStreamDuration(const AVFormatContext *context, const Packet &packet)
31{
32 const AVPacket &avPacket = *packet.avPacket();
33 const AVStream &avStream = *context->streams[avPacket.stream_index];
34 const AVStreamDuration streamDuration(avStream.duration);
35 if (streamDuration.get() <= 0
36 || context->duration_estimation_method != AVFMT_DURATION_FROM_STREAM)
37 return true; // Stream duration shouldn't or doesn't need to be compared to pts
38
39 if (avPacket.pts == AV_NOPTS_VALUE) { // Unexpected situation
40 qWarning() << "QFFmpeg::Demuxer received AVPacket with pts == AV_NOPTS_VALUE";
41 return true;
42 }
43
44 if (avStream.start_time != AV_NOPTS_VALUE)
45 return AVStreamDuration(avPacket.pts - avStream.start_time) <= streamDuration;
46
47 const TrackPosition trackPos = toTrackPosition(AVStreamPosition(avPacket.pts), &avStream, context);
48 const TrackPosition trackPosOfStreamEnd = toTrackDuration(streamDuration, &avStream).asTimePoint();
49 return trackPos <= trackPosOfStreamEnd;
50
51 // TODO: If there is a packet that starts before the canonical end of the stream but has a
52 // malformed duration, rework doNextStep to check for eof after that packet.
53}
54
55Demuxer::Demuxer(const PlaybackEngineObjectID &id, AVFormatContext *context,
56 TrackPosition initialPosUs, bool seekPending, const LoopOffset &loopOffset,
57 const StreamIndexes &streamIndexes, int loops)
58 : PlaybackEngineObject(id),
59 m_context(context),
60 m_sessionCtx{ initialPosUs, loopOffset, !seekPending && initialPosUs == TrackPosition{ 0 } },
61 m_loops(loops)
62{
63 qCDebug(qLcDemuxer) << "Create demuxer."
64 << "pos:" << m_sessionCtx.posInLoopUs.get()
65 << "loop offset:" << m_sessionCtx.loopOffset.loopStartTimeUs.get()
66 << "loop index:" << m_sessionCtx.loopOffset.loopIndex << "loops:" << loops;
67
68 Q_ASSERT(m_context);
69
70 for (auto i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i) {
71 if (streamIndexes[i] >= 0) {
72 const auto trackType = static_cast<QPlatformMediaPlayer::TrackType>(i);
73 qCDebug(qLcDemuxer) << "Activate demuxing stream" << i << ", trackType:" << trackType;
74 m_streams[streamIndexes[i]] = { trackType };
75 }
76 }
77}
78
79void Demuxer::seek(quint64 sessionId, TrackPosition initialPosUs, const LoopOffset &loopOffset)
80{
81 updateSession(sessionId, [this, initialPosUs, loopOffset]() {
82 m_sessionCtx = { initialPosUs, loopOffset };
83
84 for (auto &[id, streamData] : m_streams)
85 streamData = StreamData{ streamData.trackType };
86
87 scheduleNextStep();
88 });
89}
90
92{
93 ensureSeeked();
94
95 Packet packet(m_sessionCtx.loopOffset, AVPacketUPtr{ av_packet_alloc() }, id());
96 AVPacket &avPacket = *packet.avPacket();
97
98 const int demuxStatus = av_read_frame(m_context, &avPacket);
99 if (demuxStatus == AVERROR_EXIT)
100 return;
101
102 const int streamIndex = avPacket.stream_index;
103 auto streamIterator = m_streams.find(streamIndex);
104 const bool streamIsRelevant = streamIterator != m_streams.end();
105
106 if (demuxStatus == AVERROR_EOF
107 || (streamIsRelevant && !isPacketWithinStreamDuration(m_context, packet))) {
108 ++m_sessionCtx.loopOffset.loopIndex;
109
110 const auto loops = m_loops.loadAcquire();
111 if (loops >= 0 && m_sessionCtx.loopOffset.loopIndex >= loops) {
112 qCDebug(qLcDemuxer) << "finish demuxing";
113
114 if (!std::exchange(m_sessionCtx.buffered, true))
115 emit packetsBuffered();
116
117 setAtEnd(true);
118 } else {
119 // start next loop
120 m_sessionCtx.seeked = false;
121 m_sessionCtx.posInLoopUs = TrackPosition(0);
122 m_sessionCtx.loopOffset.loopStartTimeUs = m_sessionCtx.maxPacketsEndPos;
123 m_sessionCtx.maxPacketsEndPos = TrackPosition(0);
124
125 ensureSeeked();
126
127 qCDebug(qLcDemuxer) << "Demuxer loops changed. Index:"
128 << m_sessionCtx.loopOffset.loopIndex
129 << "Offset:" << m_sessionCtx.loopOffset.loopStartTimeUs.get();
130
131 scheduleNextStep();
132 }
133
134 return;
135 }
136
137 if (demuxStatus < 0) {
138 qCWarning(qLcDemuxer) << "Demuxing failed" << demuxStatus << AVError(demuxStatus);
139
140 if (demuxStatus == AVERROR(EAGAIN)
141 && m_sessionCtx.demuxerRetryCount != s_maxDemuxerRetries) {
142 // When demuxer reports EAGAIN, we can try to recover by calling av_read_frame again.
143 // The documentation for av_read_frame does not mention this, but FFmpeg command line
144 // tool does this, see input_thread() function in ffmpeg_demux.c. There, the response
145 // is to sleep for 10 ms before trying again. NOTE: We do not have any known way of
146 // reproducing this in our tests.
147 m_sessionCtx.failTimePoint = std::chrono::steady_clock::now();
148 ++m_sessionCtx.demuxerRetryCount;
149
150 qCDebug(qLcDemuxer) << "Retrying";
151 scheduleNextStep();
152 } else {
153 // av_read_frame reports another error. This could for example happen if network is
154 // disconnected while playing a network stream, where av_read_frame may return
155 // ETIMEDOUT.
156 // TODO: Demuxer errors should likely stop playback in media player examples.
157 emit error(QMediaPlayer::ResourceError,
158 QLatin1StringView("Demuxing failed"));
159 }
160
161 return;
162 }
163
164 m_sessionCtx.demuxerRetryCount = 0;
165 m_sessionCtx.failTimePoint.reset();
166
167 if (streamIsRelevant) {
168 auto &streamData = streamIterator->second;
169 const AVStream *stream = m_context->streams[streamIndex];
170
171 const TrackPosition endPos = packetEndPos(packet, stream, m_context);
172 m_sessionCtx.maxPacketsEndPos = qMax(m_sessionCtx.maxPacketsEndPos, endPos);
173
174 // Increase buffered metrics as the packet has been processed.
175
176 streamData.bufferedDuration += toTrackDuration(AVStreamDuration(avPacket.duration), stream);
177 streamData.bufferedSize += avPacket.size;
178 streamData.maxSentPacketsPos = qMax(streamData.maxSentPacketsPos, endPos);
179 updateStreamDataLimitFlag(streamData);
180
181 if (!m_sessionCtx.buffered && streamData.isDataLimitReached) {
182 m_sessionCtx.buffered = true;
183 emit packetsBuffered();
184 }
185
186 if (!m_sessionCtx.firstPacketFound) {
187 m_sessionCtx.firstPacketFound = true;
188 emit firstPacketFound(id(),
189 m_sessionCtx.posInLoopUs
190 + m_sessionCtx.loopOffset.loopStartTimeUs.asDuration());
191 }
192
193 auto signal = signalByTrackType(streamData.trackType);
194 emit (this->*signal)(std::move(packet));
195 }
196
197 scheduleNextStep();
198}
199
200void Demuxer::onPacketProcessed(const Packet &packet)
201{
202 Q_ASSERT(packet.isValid());
203
204 if (!checkID(packet.sourceID()))
205 return;
206
207 auto &avPacket = *packet.avPacket();
208
209 const auto streamIndex = avPacket.stream_index;
210 const auto stream = m_context->streams[streamIndex];
211 auto it = m_streams.find(streamIndex);
212
213 if (it != m_streams.end()) {
214 auto &streamData = it->second;
215
216 // Decrease buffered metrics as new data (the packet) has been received (buffered)
217
218 streamData.bufferedDuration -= toTrackDuration(AVStreamDuration(avPacket.duration), stream);
219 streamData.bufferedSize -= avPacket.size;
220 streamData.maxProcessedPacketPos =
221 qMax(streamData.maxProcessedPacketPos, packetEndPos(packet, stream, m_context));
222
223 Q_ASSERT(it->second.bufferedDuration >= TrackDuration(0));
224 Q_ASSERT(it->second.bufferedSize >= 0);
225
226 updateStreamDataLimitFlag(streamData);
227 }
228
229 scheduleNextStep();
230}
231
233{
234 Q_ASSERT(m_sessionCtx.failTimePoint.has_value() == !!m_sessionCtx.demuxerRetryCount);
235 return m_sessionCtx.failTimePoint ? *m_sessionCtx.failTimePoint + s_demuxerRetryInterval
236 : PlaybackEngineObject::nextTimePoint();
237}
238
240{
241 auto isDataLimitReached = [](const auto &streamIndexToData) {
242 return streamIndexToData.second.isDataLimitReached;
243 };
244
245 // Demuxer waits:
246 // - if it's paused
247 // - if the end has been reached
248 // - if streams are empty (probably, should be handled on the initialization)
249 // - if at least one of the streams has reached the data limit (duration or size)
250
251 return PlaybackEngineObject::canDoNextStep() && !isAtEnd() && !m_streams.empty()
252 && std::none_of(m_streams.begin(), m_streams.end(), isDataLimitReached);
253}
254
255void Demuxer::ensureSeeked()
256{
257 if (std::exchange(m_sessionCtx.seeked, true))
258 return;
259
260 if ((m_context->ctx_flags & AVFMTCTX_UNSEEKABLE) == 0) {
261
262 // m_posInLoopUs is intended to be the number of microseconds since playback start, and is
263 // in the range [0, duration()]. av_seek_frame seeks to a position relative to the start of
264 // the media timeline, which may be non-zero. We adjust for this by adding the
265 // AVFormatContext's start_time.
266 //
267 // NOTE: m_posInLoop is not calculated correctly if the start_time is non-zero, but
268 // this must be fixed separately.
269 const AVContextPosition seekPos = toContextPosition(m_sessionCtx.posInLoopUs, m_context);
270
271 qCDebug(qLcDemuxer).nospace()
272 << "Seeking to offset " << m_sessionCtx.posInLoopUs.get() << "us from media start.";
273
274 auto err = av_seek_frame(m_context, -1, seekPos.get(), AVSEEK_FLAG_BACKWARD);
275
276 if (err < 0) {
277 qCWarning(qLcDemuxer) << "Failed to seek, pos" << seekPos.get();
278
279 // Drop an error of seeking to initial position of streams with undefined duration.
280 // This needs improvements.
281 if (m_sessionCtx.posInLoopUs != TrackPosition{ 0 } || m_context->duration > 0)
282 emit error(QMediaPlayer::ResourceError,
283 QLatin1StringView("Failed to seek: ") + err2str(err));
284 }
285 }
286
287 setAtEnd(false);
288}
289
290Demuxer::RequestingSignal Demuxer::signalByTrackType(QPlatformMediaPlayer::TrackType trackType)
291{
292 switch (trackType) {
293 case QPlatformMediaPlayer::TrackType::VideoStream:
294 return &Demuxer::requestProcessVideoPacket;
295 case QPlatformMediaPlayer::TrackType::AudioStream:
296 return &Demuxer::requestProcessAudioPacket;
297 case QPlatformMediaPlayer::TrackType::SubtitleStream:
298 return &Demuxer::requestProcessSubtitlePacket;
299 default:
300 Q_ASSERT(!"Unknown track type");
301 }
302
303 return nullptr;
304}
305
306void Demuxer::setLoops(int loopsCount)
307{
308 qCDebug(qLcDemuxer) << "setLoops to demuxer" << loopsCount;
309 m_loops.storeRelease(loopsCount);
310}
311
312void Demuxer::updateStreamDataLimitFlag(StreamData &streamData)
313{
314 const TrackDuration packetsPosDiff =
315 streamData.maxSentPacketsPos - streamData.maxProcessedPacketPos;
316 streamData.isDataLimitReached = streamData.bufferedDuration >= MaxBufferedDurationUs
317 || (streamData.bufferedDuration == TrackDuration(0)
318 && packetsPosDiff >= MaxBufferedDurationUs)
319 || streamData.bufferedSize >= MaxBufferedSize;
320}
321
322} // namespace QFFmpeg
323
324QT_END_NAMESPACE
325
326#include "moc_qffmpegdemuxer_p.cpp"
void setLoops(int loopsCount)
void doNextStep() override
void seek(quint64 sessionId, TrackPosition initialPosUs, const LoopOffset &loopOffset)
void(Demuxer::*)(Packet) RequestingSignal
bool canDoNextStep() const override
TimePoint nextTimePoint() const override
static constexpr TrackDuration MaxBufferedDurationUs
Q_STATIC_LOGGING_CATEGORY(qLCAndroidVideoDevices, "qt.multimedia.ffmpeg.android.videoDevices")
static constexpr qint64 MaxBufferedSize
QT_MANGLE_NAMESPACE(QMacScreenCaptureStreamDelegate) QMacScreenCaptureStreamDelegate
static TrackPosition packetEndPos(const Packet &packet, const AVStream *stream, const AVFormatContext *context)
static bool isPacketWithinStreamDuration(const AVFormatContext *context, const Packet &packet)