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