Qt 6.x
The Qt SDK
Loading...
Searching...
No Matches
qffmpegplaybackengine.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
4
5#include "qvideosink.h"
6#include "qaudiooutput.h"
7#include "private/qplatformaudiooutput_p.h"
8#include "qiodevice.h"
14
15#include <qloggingcategory.h>
16
18
19namespace QFFmpeg {
20
21static Q_LOGGING_CATEGORY(qLcPlaybackEngine, "qt.multimedia.ffmpeg.playbackengine");
22
23// The helper is needed since on some compilers std::unique_ptr
24// doesn't have a default constructor in the case of sizeof(CustomDeleter) > 0
25template<typename Array>
26inline Array defaultObjectsArray()
27{
28 using T = typename Array::value_type;
29 return { T{ {}, {} }, T{ {}, {} }, T{ {}, {} } };
30}
31
32// TODO: investigate what's better: profile and try network case
33// Most likely, shouldPauseStreams = false is better because of:
34// - packet and frame buffers are not big, the saturration of the is pretty fast.
35// - after any pause a user has some preloaded buffers, so the playback is
36// supposed to be more stable in cases with a weak processor or bad internet.
37// - the code is simplier, usage is more convenient.
38//
39static constexpr bool shouldPauseStreams = false;
40
42 : m_demuxer({}, {}),
43 m_streams(defaultObjectsArray<decltype(m_streams)>()),
44 m_renderers(defaultObjectsArray<decltype(m_renderers)>())
45{
46 qCDebug(qLcPlaybackEngine) << "Create PlaybackEngine";
47 qRegisterMetaType<QFFmpeg::Packet>();
48 qRegisterMetaType<QFFmpeg::Frame>();
49}
50
52 qCDebug(qLcPlaybackEngine) << "Delete PlaybackEngine";
53
54 finalizeOutputs();
55 forEachExistingObject([](auto &object) { object.reset(); });
56 deleteFreeThreads();
57}
58
59void PlaybackEngine::onRendererFinished()
60{
61 auto isAtEnd = [this](auto trackType) {
62 return !m_renderers[trackType] || m_renderers[trackType]->isAtEnd();
63 };
64
66 return;
67
69 return;
70
71 if (!isAtEnd(QPlatformMediaPlayer::SubtitleStream) && !hasMediaStream())
72 return;
73
74 if (std::exchange(m_state, QMediaPlayer::StoppedState) == QMediaPlayer::StoppedState)
75 return;
76
77 finilizeTime(duration());
78
79 forceUpdate();
80
81 qCDebug(qLcPlaybackEngine) << "Playback engine end of stream";
82
84}
85
86void PlaybackEngine::onRendererLoopChanged(quint64 id, qint64 offset, int loopIndex)
87{
88 if (!hasRenderer(id))
89 return;
90
91 if (loopIndex > m_currentLoopOffset.index) {
92 m_currentLoopOffset = { offset, loopIndex };
94 } else if (loopIndex == m_currentLoopOffset.index && offset != m_currentLoopOffset.pos) {
95 qWarning() << "Unexpected offset for loop" << loopIndex << ":" << offset << "vs"
96 << m_currentLoopOffset.pos;
97 m_currentLoopOffset.pos = offset;
98 }
99}
100
101void PlaybackEngine::onRendererSynchronized(quint64 id, std::chrono::steady_clock::time_point tp,
102 qint64 pos)
103{
104 if (!hasRenderer(id))
105 return;
106
108 && m_renderers[QPlatformMediaPlayer::AudioStream]->id() == id);
109
110 if (m_timeController.positionFromTime(tp) < pos) {
111 // TODO: maybe check with an asset
112 qWarning() << "Unexpected synchronization " << m_timeController.positionFromTime(tp) - pos;
113 }
114
115 m_timeController.sync(tp, pos);
116
117 forEachExistingObject<Renderer>([&](auto &renderer) {
118 if (id != renderer->id())
119 renderer->syncSoft(tp, pos);
120 });
121}
122
124 if (!m_context)
125 return;
126
127 if (state == m_state)
128 return;
129
130 const auto prevState = std::exchange(m_state, state);
131
132 if (m_state == QMediaPlayer::StoppedState) {
133 finalizeOutputs();
134 finilizeTime(0);
135 }
136
137 if (prevState == QMediaPlayer::StoppedState || m_state == QMediaPlayer::StoppedState)
138 recreateObjects();
139
140 if (prevState == QMediaPlayer::StoppedState)
141 triggerStepIfNeeded();
142
143 updateObjectsPausedState();
144}
145
146void PlaybackEngine::updateObjectsPausedState()
147{
148 const auto paused = m_state != QMediaPlayer::PlayingState;
149 m_timeController.setPaused(paused);
150
151 forEachExistingObject([&](auto &object) {
152 bool objectPaused = false;
153
154 if constexpr (std::is_same_v<decltype(*object), Renderer &>)
155 objectPaused = paused;
156 else if constexpr (shouldPauseStreams) {
157 auto streamPaused = [](bool p, auto &r) {
158 const auto needMoreFrames = r && r->stepInProgress();
159 return p && !needMoreFrames;
160 };
161
162 if constexpr (std::is_same_v<decltype(*object), StreamDecoder &>)
163 objectPaused = streamPaused(paused, renderer(object->trackType()));
164 else
165 objectPaused = std::accumulate(m_renderers.begin(), m_renderers.end(), paused,
166 streamPaused);
167 }
168
169 object->setPaused(objectPaused);
170 });
171}
172
174{
176 if (!std::exchange(engine->m_threadsDirty, true))
177 QMetaObject::invokeMethod(engine, &PlaybackEngine::deleteFreeThreads, Qt::QueuedConnection);
178
179 object->kill();
180}
181
182void PlaybackEngine::registerObject(PlaybackEngineObject &object)
183{
185
186 auto threadName = objectThreadName(object);
187 auto &thread = m_threads[threadName];
188 if (!thread) {
189 thread = std::make_unique<QThread>();
190 thread->setObjectName(threadName);
191 thread->start();
192 }
193
194 Q_ASSERT(object.thread() != thread.get());
195 object.moveToThread(thread.get());
196}
197
200{
201 switch (trackType) {
203 return m_videoSink
204 ? createPlaybackEngineObject<VideoRenderer>(m_timeController, m_videoSink)
205 : RendererPtr{ {}, {} };
207 return m_audioOutput
208 ? createPlaybackEngineObject<AudioRenderer>(m_timeController, m_audioOutput)
209 : RendererPtr{ {}, {} };
211 return m_videoSink
212 ? createPlaybackEngineObject<SubtitleRenderer>(m_timeController, m_videoSink)
213 : RendererPtr{ {}, {} };
214 default:
215 return { {}, {} };
216 }
217}
218
219template<typename C, typename Action>
220void PlaybackEngine::forEachExistingObject(Action &&action)
221{
222 auto handleNotNullObject = [&](auto &object) {
223 if constexpr (std::is_base_of_v<C, std::remove_reference_t<decltype(*object)>>)
224 if (object)
225 action(object);
226 };
227
228 handleNotNullObject(m_demuxer);
229 std::for_each(m_streams.begin(), m_streams.end(), handleNotNullObject);
230 std::for_each(m_renderers.begin(), m_renderers.end(), handleNotNullObject);
231}
232
233template<typename Action>
234void PlaybackEngine::forEachExistingObject(Action &&action)
235{
236 forEachExistingObject<PlaybackEngineObject>(std::forward<Action>(action));
237}
238
240{
241 pos = qBound(0, pos, duration());
242
243 m_timeController.setPaused(true);
244 m_timeController.sync(m_currentLoopOffset.pos + pos);
245
246 forceUpdate();
247}
248
250{
251 if (!isSeekable()) {
252 qWarning() << "Cannot set loops for non-seekable source";
253 return;
254 }
255
256 if (std::exchange(m_loops, loops) == loops)
257 return;
258
259 qCDebug(qLcPlaybackEngine) << "set playback engine loops:" << loops << "prev loops:" << m_loops
260 << "index:" << m_currentLoopOffset.index;
261
262 if (m_demuxer)
263 m_demuxer->setLoops(loops);
264}
265
266void PlaybackEngine::triggerStepIfNeeded()
267{
268 if (m_state != QMediaPlayer::PausedState)
269 return;
270
271 if (m_renderers[QPlatformMediaPlayer::VideoStream])
272 m_renderers[QPlatformMediaPlayer::VideoStream]->doForceStep();
273
274 // TODO: maybe trigger SubtitleStream.
275 // If trigger it, we have to make seeking for the current subtitle frame more stable.
276 // Or set some timeout for seeking.
277}
278
279QString PlaybackEngine::objectThreadName(const PlaybackEngineObject &object)
280{
281 QString result = object.metaObject()->className();
282 if (auto stream = qobject_cast<const StreamDecoder *>(&object))
283 result += QString::number(stream->trackType());
284
285 return result;
286}
287
289 if (rate == playbackRate())
290 return;
291
292 m_timeController.setPlaybackRate(rate);
293 forEachExistingObject<Renderer>([rate](auto &renderer) { renderer->setPlaybackRate(rate); });
294}
295
297 return m_timeController.playbackRate();
298}
299
300void PlaybackEngine::recreateObjects()
301{
302 m_timeController.setPaused(true);
303
304 forEachExistingObject([](auto &object) { object.reset(); });
305
306 createObjectsIfNeeded();
307}
308
309void PlaybackEngine::createObjectsIfNeeded()
310{
311 if (m_state == QMediaPlayer::StoppedState || !m_context)
312 return;
313
314 for (int i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i)
315 createStreamAndRenderer(static_cast<QPlatformMediaPlayer::TrackType>(i));
316
317 createDemuxer();
318}
319
320void PlaybackEngine::forceUpdate()
321{
322 recreateObjects();
323 triggerStepIfNeeded();
324 updateObjectsPausedState();
325}
326
327void PlaybackEngine::createStreamAndRenderer(QPlatformMediaPlayer::TrackType trackType)
328{
329 auto codec = codecForTrack(trackType);
330
331 auto &renderer = m_renderers[trackType];
332
333 if (!codec)
334 return;
335
336 if (!renderer) {
337 renderer = createRenderer(trackType);
338
339 if (!renderer)
340 return;
341
343 &PlaybackEngine::onRendererSynchronized);
344
346 &PlaybackEngine::onRendererLoopChanged);
347
348 if constexpr (shouldPauseStreams)
350 &PlaybackEngine::updateObjectsPausedState);
351
353 &PlaybackEngine::onRendererFinished);
354 }
355
356 auto &stream = m_streams[trackType] =
357 createPlaybackEngineObject<StreamDecoder>(*codec, renderer->seekPosition());
358
359 Q_ASSERT(trackType == stream->trackType());
360
366}
367
368std::optional<Codec> PlaybackEngine::codecForTrack(QPlatformMediaPlayer::TrackType trackType)
369{
370 const auto streamIndex = m_currentAVStreamIndex[trackType];
371 if (streamIndex < 0)
372 return {};
373
374 auto &result = m_codecs[trackType];
375
376 if (!result) {
377 qCDebug(qLcPlaybackEngine)
378 << "Create codec for stream:" << streamIndex << "trackType:" << trackType;
379 auto maybeCodec = Codec::create(m_context->streams[streamIndex]);
380
381 if (!maybeCodec) {
383 "Cannot create codec," + maybeCodec.error());
384 return {};
385 }
386
387 result = maybeCodec.value();
388 }
389
390 return result;
391}
392
393bool PlaybackEngine::hasMediaStream() const
394{
395 return m_renderers[QPlatformMediaPlayer::AudioStream]
396 || m_renderers[QPlatformMediaPlayer::VideoStream];
397}
398
399void PlaybackEngine::createDemuxer()
400{
401 decltype(m_currentAVStreamIndex) streamIndexes = { -1, -1, -1 };
402
403 bool hasStreams = false;
404 forEachExistingObject<StreamDecoder>([&](auto &stream) {
405 hasStreams = true;
406 const auto trackType = stream->trackType();
407 streamIndexes[trackType] = m_currentAVStreamIndex[trackType];
408 });
409
410 if (!hasStreams)
411 return;
412
413 const PositionWithOffset positionWithOffset{ currentPosition(false), m_currentLoopOffset };
414
415 m_demuxer = createPlaybackEngineObject<Demuxer>(m_context.get(), positionWithOffset,
416 streamIndexes, m_loops);
417
418 forEachExistingObject<StreamDecoder>([&](auto &stream) {
419 connect(m_demuxer.get(), Demuxer::signalByTrackType(stream->trackType()), stream.get(),
421 connect(m_demuxer.get(), &PlaybackEngineObject::atEnd, stream.get(),
423 connect(stream.get(), &StreamDecoder::packetProcessed, m_demuxer.get(),
425 });
426}
427
428void PlaybackEngine::deleteFreeThreads() {
429 m_threadsDirty = false;
430 auto freeThreads = std::move(m_threads);
431
432 forEachExistingObject([&](auto &object) {
433 m_threads.insert(freeThreads.extract(objectThreadName(*object)));
434 });
435
436 for (auto &[name, thr] : freeThreads)
437 thr->quit();
438
439 for (auto &[name, thr] : freeThreads)
440 thr->wait();
441}
442
444{
445 stop();
446
447 m_codecs = {};
448
449 if (auto error = recreateAVFormatContext(media, stream)) {
450 emit errorOccured(error->code, error->description);
451 return false;
452 }
453
454 return true;
455}
456
458{
459 auto prev = std::exchange(m_videoSink, sink);
460 if (prev == sink)
461 return;
462
464
465 if (!sink || !prev) {
466 // might need some improvements
467 forceUpdate();
468 }
469}
470
472 setAudioSink(output ? output->q : nullptr);
473}
474
476{
477 auto prev = std::exchange(m_audioOutput, output);
478 if (prev == output)
479 return;
480
482
483 if (!output || !prev) {
484 // might need some improvements
485 forceUpdate();
486 }
487}
488
490 std::optional<qint64> pos;
491
492 for (size_t i = 0; i < m_renderers.size(); ++i) {
493 const auto &renderer = m_renderers[i];
494 if (!renderer)
495 continue;
496
497 // skip subtitle stream for finding lower rendering position
498 if (!topPos && i == QPlatformMediaPlayer::SubtitleStream && hasMediaStream())
499 continue;
500
501 const auto rendererPos = renderer->lastPosition();
502 pos = !pos ? rendererPos
503 : topPos ? std::max(*pos, rendererPos)
504 : std::min(*pos, rendererPos);
505 }
506
507 if (!pos)
508 pos = m_timeController.currentPosition();
509
510 return qBound(0, *pos - m_currentLoopOffset.pos, duration());
511}
512
514{
515 if (!MediaDataHolder::setActiveTrack(trackType, streamNumber))
516 return;
517
518 m_codecs[trackType] = {};
519
520 m_renderers[trackType].reset();
521 m_streams = defaultObjectsArray<decltype(m_streams)>();
522 m_demuxer.reset();
523
524 createObjectsIfNeeded();
525 updateObjectsPausedState();
526}
527
528void PlaybackEngine::finilizeTime(qint64 pos)
529{
530 Q_ASSERT(pos >= 0 && pos <= duration());
531
532 m_timeController.setPaused(true);
533 m_timeController.sync(pos);
534 m_currentLoopOffset = {};
535}
536
537void PlaybackEngine::finalizeOutputs()
538{
540 updateActiveVideoOutput(nullptr, true);
541}
542
543bool PlaybackEngine::hasRenderer(quint64 id) const
544{
545 return std::any_of(m_renderers.begin(), m_renderers.end(),
546 [id](auto &renderer) { return renderer && renderer->id() == id; });
547}
548
550{
551 if (auto renderer =
552 qobject_cast<AudioRenderer *>(m_renderers[QPlatformMediaPlayer::AudioStream].get()))
553 renderer->setOutput(output);
554}
555
557{
558 if (auto renderer = qobject_cast<SubtitleRenderer *>(
560 renderer->setOutput(sink, cleanOutput);
561 if (auto renderer =
562 qobject_cast<VideoRenderer *>(m_renderers[QPlatformMediaPlayer::VideoStream].get()))
563 renderer->setOutput(sink, cleanOutput);
564}
565}
566
568
569#include "moc_qffmpegplaybackengine_p.cpp"
\qmltype AudioOutput \instantiates QAudioOutput
static QMaybe< Codec > create(AVStream *)
void onPacketProcessed(Packet)
static RequestingSignal signalByTrackType(QPlatformMediaPlayer::TrackType trackType)
std::unique_ptr< AVFormatContext, AVFormatContextDeleter > m_context
bool setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
std::optional< ContextError > recreateAVFormatContext(const QUrl &media, QIODevice *stream)
void error(int code, const QString &errorString)
void setVideoSink(QVideoSink *sink)
void setState(QMediaPlayer::PlaybackState state)
virtual RendererPtr createRenderer(QPlatformMediaPlayer::TrackType trackType)
void setAudioSink(QAudioOutput *output)
qint64 currentPosition(bool topPos=true) const
void errorOccured(int, const QString &)
ObjectPtr< Renderer > RendererPtr
bool setMedia(const QUrl &media, QIODevice *stream)
void setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
void updateActiveAudioOutput(QAudioOutput *output)
void updateActiveVideoOutput(QVideoSink *sink, bool cleanOutput=false)
void synchronized(Id id, TimePoint tp, qint64 pos)
void frameProcessed(Frame)
void loopChanged(Id id, qint64 offset, int index)
void onFrameProcessed(Frame frame)
void packetProcessed(Packet)
void requestHandleFrame(Frame frame)
void sync(qint64 trackPos=0)
qint64 positionFromTime(TimePoint tp, bool ignorePause=false) const
void setPlaybackRate(PlaybackRate playbackRate)
qint64 currentPosition(const Clock::duration &offset=Clock::duration{ 0 }) const
\inmodule QtCore \reentrant
Definition qiodevice.h:34
PlaybackState
Defines the current state of a media player.
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2823
QThread * thread() const
Returns the thread in which the object lives.
Definition qobject.cpp:1561
Q_WEAK_OVERLOAD void setObjectName(const QString &name)
Sets the object's name to name.
Definition qobject.h:114
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
static QString number(int, int base=10)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:7822
void start(Priority=InheritPriority)
Definition qthread.cpp:923
\inmodule QtCore
Definition qurl.h:94
The QVideoSink class represents a generic sink for video data.
Definition qvideosink.h:22
else opt state
[0]
Array defaultObjectsArray()
static constexpr bool shouldPauseStreams
Combined button and popup list for selecting options.
@ QueuedConnection
DBusConnection const char DBusError * error
static QDBusError::ErrorType get(const char *name)
EGLStreamKHR stream
QMediaFormat::AudioCodec codec
#define qWarning
Definition qlogging.h:162
#define Q_LOGGING_CATEGORY(name,...)
#define qCDebug(category,...)
constexpr const T & qBound(const T &min, const T &val, const T &max)
Definition qminmax.h:44
GLboolean r
[2]
GLuint object
[3]
GLenum GLuint GLintptr offset
GLuint name
GLuint GLenum * rate
GLsizei GLenum GLboolean sink
GLuint64EXT * result
[6]
GLfloat GLfloat p
[1]
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define emit
unsigned long long quint64
Definition qtypes.h:56
long long qint64
Definition qtypes.h:55
QT_BEGIN_NAMESPACE typedef uchar * output
QSvgRenderer * renderer
[0]
void operator()(PlaybackEngineObject *) const
static bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType, QGenericReturnArgument ret, QGenericArgument val0=QGenericArgument(nullptr), QGenericArgument val1=QGenericArgument(), QGenericArgument val2=QGenericArgument(), QGenericArgument val3=QGenericArgument(), QGenericArgument val4=QGenericArgument(), QGenericArgument val5=QGenericArgument(), QGenericArgument val6=QGenericArgument(), QGenericArgument val7=QGenericArgument(), QGenericArgument val8=QGenericArgument(), QGenericArgument val9=QGenericArgument())
\threadsafe This is an overloaded member function, provided for convenience. It differs from the abov...