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