Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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 "private/qplatformvideosink_p.h"
9#include "qiodevice.h"
15
16#include <qloggingcategory.h>
17
19
20namespace QFFmpeg {
21
22static Q_LOGGING_CATEGORY(qLcPlaybackEngine, "qt.multimedia.ffmpeg.playbackengine");
23
24// The helper is needed since on some compilers std::unique_ptr
25// doesn't have a default constructor in the case of sizeof(CustomDeleter) > 0
26template<typename Array>
27inline Array defaultObjectsArray()
28{
29 using T = typename Array::value_type;
30 return { T{ {}, {} }, T{ {}, {} }, T{ {}, {} } };
31}
32
33// TODO: investigate what's better: profile and try network case
34// Most likely, shouldPauseStreams = false is better because of:
35// - packet and frame buffers are not big, the saturration of the is pretty fast.
36// - after any pause a user has some preloaded buffers, so the playback is
37// supposed to be more stable in cases with a weak processor or bad internet.
38// - the code is simplier, usage is more convenient.
39//
40static constexpr bool shouldPauseStreams = false;
41
43 : m_demuxer({}, {}),
44 m_streams(defaultObjectsArray<decltype(m_streams)>()),
45 m_renderers(defaultObjectsArray<decltype(m_renderers)>())
46{
47 qCDebug(qLcPlaybackEngine) << "Create PlaybackEngine";
48 qRegisterMetaType<QFFmpeg::Packet>();
49 qRegisterMetaType<QFFmpeg::Frame>();
50}
51
53 qCDebug(qLcPlaybackEngine) << "Delete PlaybackEngine";
54
55 finalizeOutputs();
56 forEachExistingObject([](auto &object) { object.reset(); });
57 deleteFreeThreads();
58}
59
60void PlaybackEngine::onRendererFinished()
61{
62 auto isAtEnd = [this](auto trackType) {
63 return !m_renderers[trackType] || m_renderers[trackType]->isAtEnd();
64 };
65
67 return;
68
70 return;
71
72 if (!isAtEnd(QPlatformMediaPlayer::SubtitleStream) && !hasMediaStream())
73 return;
74
75 if (std::exchange(m_state, QMediaPlayer::StoppedState) == QMediaPlayer::StoppedState)
76 return;
77
78 finilizeTime(duration());
79
80 forceUpdate();
81
82 qCDebug(qLcPlaybackEngine) << "Playback engine end of stream";
83
85}
86
87void PlaybackEngine::onRendererLoopChanged(quint64 id, qint64 offset, int loopIndex)
88{
89 if (!hasRenderer(id))
90 return;
91
92 if (loopIndex > m_currentLoopOffset.index) {
93 m_currentLoopOffset = { offset, loopIndex };
95 } else if (loopIndex == m_currentLoopOffset.index && offset != m_currentLoopOffset.pos) {
96 qWarning() << "Unexpected offset for loop" << loopIndex << ":" << offset << "vs"
97 << m_currentLoopOffset.pos;
98 m_currentLoopOffset.pos = offset;
99 }
100}
101
102void PlaybackEngine::onRendererSynchronized(quint64 id, std::chrono::steady_clock::time_point tp,
103 qint64 pos)
104{
105 if (!hasRenderer(id))
106 return;
107
109 && m_renderers[QPlatformMediaPlayer::AudioStream]->id() == id);
110
111 m_timeController.sync(tp, pos);
112
113 forEachExistingObject<Renderer>([&](auto &renderer) {
114 if (id != renderer->id())
115 renderer->syncSoft(tp, pos);
116 });
117}
118
120 if (!m_media.avContext())
121 return;
122
123 if (state == m_state)
124 return;
125
126 const auto prevState = std::exchange(m_state, state);
127
128 if (m_state == QMediaPlayer::StoppedState) {
129 finalizeOutputs();
130 finilizeTime(0);
131 }
132
133 if (prevState == QMediaPlayer::StoppedState || m_state == QMediaPlayer::StoppedState)
134 recreateObjects();
135
136 if (prevState == QMediaPlayer::StoppedState)
137 triggerStepIfNeeded();
138
139 updateObjectsPausedState();
140}
141
142void PlaybackEngine::updateObjectsPausedState()
143{
144 const auto paused = m_state != QMediaPlayer::PlayingState;
145 m_timeController.setPaused(paused);
146
147 forEachExistingObject([&](auto &object) {
148 bool objectPaused = false;
149
150 if constexpr (std::is_same_v<decltype(*object), Renderer &>)
151 objectPaused = paused;
152 else if constexpr (shouldPauseStreams) {
153 auto streamPaused = [](bool p, auto &r) {
154 const auto needMoreFrames = r && r->stepInProgress();
155 return p && !needMoreFrames;
156 };
157
158 if constexpr (std::is_same_v<decltype(*object), StreamDecoder &>)
159 objectPaused = streamPaused(paused, renderer(object->trackType()));
160 else
161 objectPaused = std::accumulate(m_renderers.begin(), m_renderers.end(), paused,
162 streamPaused);
163 }
164
165 object->setPaused(objectPaused);
166 });
167}
168
170{
172 if (!std::exchange(engine->m_threadsDirty, true))
173 QMetaObject::invokeMethod(engine, &PlaybackEngine::deleteFreeThreads, Qt::QueuedConnection);
174
175 object->kill();
176}
177
178void PlaybackEngine::registerObject(PlaybackEngineObject &object)
179{
181
182 auto threadName = objectThreadName(object);
183 auto &thread = m_threads[threadName];
184 if (!thread) {
185 thread = std::make_unique<QThread>();
186 thread->setObjectName(threadName);
187 thread->start();
188 }
189
190 Q_ASSERT(object.thread() != thread.get());
191 object.moveToThread(thread.get());
192}
193
196{
197 switch (trackType) {
199 return m_videoSink
200 ? createPlaybackEngineObject<VideoRenderer>(m_timeController, m_videoSink, m_media.rotation())
201 : RendererPtr{ {}, {} };
203 return m_audioOutput
204 ? createPlaybackEngineObject<AudioRenderer>(m_timeController, m_audioOutput)
205 : RendererPtr{ {}, {} };
207 return m_videoSink
208 ? createPlaybackEngineObject<SubtitleRenderer>(m_timeController, m_videoSink)
209 : RendererPtr{ {}, {} };
210 default:
211 return { {}, {} };
212 }
213}
214
215template<typename C, typename Action>
216void PlaybackEngine::forEachExistingObject(Action &&action)
217{
218 auto handleNotNullObject = [&](auto &object) {
219 if constexpr (std::is_base_of_v<C, std::remove_reference_t<decltype(*object)>>)
220 if (object)
221 action(object);
222 };
223
224 handleNotNullObject(m_demuxer);
225 std::for_each(m_streams.begin(), m_streams.end(), handleNotNullObject);
226 std::for_each(m_renderers.begin(), m_renderers.end(), handleNotNullObject);
227}
228
229template<typename Action>
230void PlaybackEngine::forEachExistingObject(Action &&action)
231{
232 forEachExistingObject<PlaybackEngineObject>(std::forward<Action>(action));
233}
234
236{
237 pos = boundPosition(pos);
238
239 m_timeController.setPaused(true);
240 m_timeController.sync(m_currentLoopOffset.pos + pos);
241
242 forceUpdate();
243}
244
246{
247 if (!isSeekable()) {
248 qWarning() << "Cannot set loops for non-seekable source";
249 return;
250 }
251
252 if (std::exchange(m_loops, loops) == loops)
253 return;
254
255 qCDebug(qLcPlaybackEngine) << "set playback engine loops:" << loops << "prev loops:" << m_loops
256 << "index:" << m_currentLoopOffset.index;
257
258 if (m_demuxer)
259 m_demuxer->setLoops(loops);
260}
261
262void PlaybackEngine::triggerStepIfNeeded()
263{
264 if (m_state != QMediaPlayer::PausedState)
265 return;
266
267 if (m_renderers[QPlatformMediaPlayer::VideoStream])
268 m_renderers[QPlatformMediaPlayer::VideoStream]->doForceStep();
269
270 // TODO: maybe trigger SubtitleStream.
271 // If trigger it, we have to make seeking for the current subtitle frame more stable.
272 // Or set some timeout for seeking.
273}
274
275QString PlaybackEngine::objectThreadName(const PlaybackEngineObject &object)
276{
277 QString result = object.metaObject()->className();
278 if (auto stream = qobject_cast<const StreamDecoder *>(&object))
279 result += QString::number(stream->trackType());
280
281 return result;
282}
283
285 if (rate == playbackRate())
286 return;
287
288 m_timeController.setPlaybackRate(rate);
289 forEachExistingObject<Renderer>([rate](auto &renderer) { renderer->setPlaybackRate(rate); });
290}
291
293 return m_timeController.playbackRate();
294}
295
296void PlaybackEngine::recreateObjects()
297{
298 m_timeController.setPaused(true);
299
300 forEachExistingObject([](auto &object) { object.reset(); });
301
302 createObjectsIfNeeded();
303}
304
305void PlaybackEngine::createObjectsIfNeeded()
306{
307 if (m_state == QMediaPlayer::StoppedState || !m_media.avContext())
308 return;
309
310 for (int i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i)
311 createStreamAndRenderer(static_cast<QPlatformMediaPlayer::TrackType>(i));
312
313 createDemuxer();
314}
315
316void PlaybackEngine::forceUpdate()
317{
318 recreateObjects();
319 triggerStepIfNeeded();
320 updateObjectsPausedState();
321}
322
323void PlaybackEngine::createStreamAndRenderer(QPlatformMediaPlayer::TrackType trackType)
324{
325 auto codec = codecForTrack(trackType);
326
327 auto &renderer = m_renderers[trackType];
328
329 if (!codec)
330 return;
331
332 if (!renderer) {
333 renderer = createRenderer(trackType);
334
335 if (!renderer)
336 return;
337
339 &PlaybackEngine::onRendererSynchronized);
340
342 &PlaybackEngine::onRendererLoopChanged);
343
344 if constexpr (shouldPauseStreams)
346 &PlaybackEngine::updateObjectsPausedState);
347
349 &PlaybackEngine::onRendererFinished);
350 }
351
352 auto &stream = m_streams[trackType] =
353 createPlaybackEngineObject<StreamDecoder>(*codec, renderer->seekPosition());
354
355 Q_ASSERT(trackType == stream->trackType());
356
362}
363
364std::optional<Codec> PlaybackEngine::codecForTrack(QPlatformMediaPlayer::TrackType trackType)
365{
366 const auto streamIndex = m_media.currentStreamIndex(trackType);
367 if (streamIndex < 0)
368 return {};
369
370 auto &result = m_codecs[trackType];
371
372 if (!result) {
373 qCDebug(qLcPlaybackEngine)
374 << "Create codec for stream:" << streamIndex << "trackType:" << trackType;
375 auto maybeCodec =
376 Codec::create(m_media.avContext()->streams[streamIndex], m_media.avContext());
377
378 if (!maybeCodec) {
380 "Cannot create codec," + maybeCodec.error());
381 return {};
382 }
383
384 result = maybeCodec.value();
385 }
386
387 return result;
388}
389
390bool PlaybackEngine::hasMediaStream() const
391{
392 return m_renderers[QPlatformMediaPlayer::AudioStream]
393 || m_renderers[QPlatformMediaPlayer::VideoStream];
394}
395
396void PlaybackEngine::createDemuxer()
397{
398 std::array<int, QPlatformMediaPlayer::NTrackTypes> streamIndexes = { -1, -1, -1 };
399
400 bool hasStreams = false;
401 forEachExistingObject<StreamDecoder>([&](auto &stream) {
402 hasStreams = true;
403 const auto trackType = stream->trackType();
404 streamIndexes[trackType] = m_media.currentStreamIndex(trackType);
405 });
406
407 if (!hasStreams)
408 return;
409
410 const PositionWithOffset positionWithOffset{ currentPosition(false), m_currentLoopOffset };
411
412 m_demuxer = createPlaybackEngineObject<Demuxer>(m_media.avContext(), positionWithOffset,
413 streamIndexes, m_loops);
414
416
417 forEachExistingObject<StreamDecoder>([&](auto &stream) {
418 connect(m_demuxer.get(), Demuxer::signalByTrackType(stream->trackType()), stream.get(),
420 connect(m_demuxer.get(), &PlaybackEngineObject::atEnd, stream.get(),
422 connect(stream.get(), &StreamDecoder::packetProcessed, m_demuxer.get(),
424 });
425
426 if (!isSeekable() || duration() <= 0) {
427 // We need initial synchronization for such streams
428 forEachExistingObject([&](auto &object) {
429 using Type = std::remove_reference_t<decltype(*object)>;
430 if constexpr (!std::is_same_v<Type, Demuxer>)
431 connect(m_demuxer.get(), &Demuxer::firstPacketFound, object.get(),
432 &Type::setInitialPosition);
433 });
434
435 auto updateTimeController = [this](TimeController::TimePoint tp, qint64 pos) {
436 m_timeController.sync(tp, pos);
437 };
438
439 connect(m_demuxer.get(), &Demuxer::firstPacketFound, this, updateTimeController);
440 }
441}
442
443void PlaybackEngine::deleteFreeThreads() {
444 m_threadsDirty = false;
445 auto freeThreads = std::move(m_threads);
446
447 forEachExistingObject([&](auto &object) {
448 m_threads.insert(freeThreads.extract(objectThreadName(*object)));
449 });
450
451 for (auto &[name, thr] : freeThreads)
452 thr->quit();
453
454 for (auto &[name, thr] : freeThreads)
455 thr->wait();
456}
457
459{
460 Q_ASSERT(!m_media.avContext()); // Playback engine does not support reloading media
462 Q_ASSERT(m_threads.empty());
463
464 m_media = std::move(media);
465 updateVideoSinkSize();
466}
467
469{
470 auto prev = std::exchange(m_videoSink, sink);
471 if (prev == sink)
472 return;
473
474 updateVideoSinkSize(prev);
476
477 if (!sink || !prev) {
478 // might need some improvements
479 forceUpdate();
480 }
481}
482
486
488{
489 auto prev = std::exchange(m_audioOutput, output);
490 if (prev == output)
491 return;
492
494
495 if (!output || !prev) {
496 // might need some improvements
497 forceUpdate();
498 }
499}
500
502 std::optional<qint64> pos;
503
504 for (size_t i = 0; i < m_renderers.size(); ++i) {
505 const auto &renderer = m_renderers[i];
506 if (!renderer)
507 continue;
508
509 // skip subtitle stream for finding lower rendering position
510 if (!topPos && i == QPlatformMediaPlayer::SubtitleStream && hasMediaStream())
511 continue;
512
513 const auto rendererPos = renderer->lastPosition();
514 pos = !pos ? rendererPos
515 : topPos ? std::max(*pos, rendererPos)
516 : std::min(*pos, rendererPos);
517 }
518
519 if (!pos)
520 pos = m_timeController.currentPosition();
521
522 return boundPosition(*pos - m_currentLoopOffset.pos);
523}
524
526{
527 return m_media.duration();
528}
529
530bool PlaybackEngine::isSeekable() const { return m_media.isSeekable(); }
531
532const QList<MediaDataHolder::StreamInfo> &
534{
535 return m_media.streamInfo(trackType);
536}
537
539{
540 return m_media.metaData();
541}
542
547
549{
550 if (!m_media.setActiveTrack(trackType, streamNumber))
551 return;
552
553 m_codecs[trackType] = {};
554
555 m_renderers[trackType].reset();
556 m_streams = defaultObjectsArray<decltype(m_streams)>();
557 m_demuxer.reset();
558
559 updateVideoSinkSize();
560 createObjectsIfNeeded();
561 updateObjectsPausedState();
562}
563
564void PlaybackEngine::finilizeTime(qint64 pos)
565{
566 Q_ASSERT(pos >= 0 && pos <= duration());
567
568 m_timeController.setPaused(true);
569 m_timeController.sync(pos);
570 m_currentLoopOffset = {};
571}
572
573void PlaybackEngine::finalizeOutputs()
574{
576 updateActiveVideoOutput(nullptr, true);
577}
578
579bool PlaybackEngine::hasRenderer(quint64 id) const
580{
581 return std::any_of(m_renderers.begin(), m_renderers.end(),
582 [id](auto &renderer) { return renderer && renderer->id() == id; });
583}
584
586{
587 if (auto renderer =
588 qobject_cast<AudioRenderer *>(m_renderers[QPlatformMediaPlayer::AudioStream].get()))
589 renderer->setOutput(output);
590}
591
593{
594 if (auto renderer = qobject_cast<SubtitleRenderer *>(
596 renderer->setOutput(sink, cleanOutput);
597 if (auto renderer =
598 qobject_cast<VideoRenderer *>(m_renderers[QPlatformMediaPlayer::VideoStream].get()))
599 renderer->setOutput(sink, cleanOutput);
600}
601
602void PlaybackEngine::updateVideoSinkSize(QVideoSink *prevSink)
603{
604 auto platformVideoSink = m_videoSink ? m_videoSink->platformVideoSink() : nullptr;
605 if (!platformVideoSink)
606 return;
607
608 if (prevSink && prevSink->platformVideoSink())
609 platformVideoSink->setNativeSize(prevSink->platformVideoSink()->nativeSize());
610 else {
611 const auto streamIndex = m_media.currentStreamIndex(QPlatformMediaPlayer::VideoStream);
612 if (streamIndex >= 0) {
613 const auto context = m_media.avContext();
614 const auto stream = context->streams[streamIndex];
615 const AVRational pixelAspectRatio =
616 av_guess_sample_aspect_ratio(context, stream, nullptr);
617 // auto size = metaData().value(QMediaMetaData::Resolution)
618 const QSize size =
619 qCalculateFrameSize({ stream->codecpar->width, stream->codecpar->height },
620 { pixelAspectRatio.num, pixelAspectRatio.den });
621
622 platformVideoSink->setNativeSize(qRotatedFrameSize(size, m_media.rotation()));
623 }
624 }
625}
626
627qint64 PlaybackEngine::boundPosition(qint64 position) const
628{
629 position = qMax(position, 0);
630 return duration() > 0 ? qMin(position, duration()) : position;
631}
632}
633
635
636#include "moc_qffmpegplaybackengine_p.cpp"
\qmltype AudioOutput \instantiates QAudioOutput
static QMaybe< Codec > create(AVStream *stream, AVFormatContext *formatContext)
void packetsBuffered()
void firstPacketFound(TimePoint tp, qint64 trackPos)
void onPacketProcessed(Packet)
static RequestingSignal signalByTrackType(QPlatformMediaPlayer::TrackType trackType)
bool setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
QtVideo::Rotation rotation() const
const QList< StreamInfo > & streamInfo(QPlatformMediaPlayer::TrackType trackType) const
int currentStreamIndex(QPlatformMediaPlayer::TrackType trackType) const
AVFormatContext * avContext()
const QMediaMetaData & metaData() const
int activeTrack(QPlatformMediaPlayer::TrackType type) const
void error(int code, const QString &errorString)
int activeTrack(QPlatformMediaPlayer::TrackType type) const
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
const QList< MediaDataHolder::StreamInfo > & streamInfo(QPlatformMediaPlayer::TrackType trackType) const
void setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
void setMedia(MediaDataHolder media)
const QMediaMetaData & metaData() const
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)
PlaybackRate playbackRate() const
void sync(qint64 trackPos=0)
void setPlaybackRate(PlaybackRate playbackRate)
qint64 currentPosition(const Clock::duration &offset=Clock::duration{ 0 }) const
\inmodule QtMultimedia
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:2960
QThread * thread() const
Returns the thread in which the object lives.
Definition qobject.cpp:1598
Q_WEAK_OVERLOAD void setObjectName(const QString &name)
Sets the object's name to name.
Definition qobject.h:127
\inmodule QtCore
Definition qsize.h:25
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
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:8084
void start(Priority=InheritPriority)
Definition qthread.cpp:996
The QVideoSink class represents a generic sink for video data.
Definition qvideosink.h:22
QPlatformVideoSink * platformVideoSink() const
else opt state
[0]
Array defaultObjectsArray()
static constexpr bool shouldPauseStreams
Combined button and popup list for selecting options.
@ QueuedConnection
static void * context
static QDBusError::ErrorType get(const char *name)
EGLStreamKHR stream
QMediaFormat::AudioCodec codec
#define qWarning
Definition qlogging.h:166
#define Q_LOGGING_CATEGORY(name,...)
#define qCDebug(category,...)
constexpr const T & qMin(const T &a, const T &b)
Definition qminmax.h:40
constexpr const T & qMax(const T &a, const T &b)
Definition qminmax.h:42
QSize qRotatedFrameSize(QSize size, int rotation)
QSize qCalculateFrameSize(QSize resolution, Fraction par)
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLboolean r
[2]
GLuint object
[3]
GLenum type
GLenum GLuint GLintptr offset
GLuint name
GLuint GLenum * rate
GLsizei GLenum GLboolean sink
GLuint64EXT * result
[6]
GLfloat GLfloat p
[1]
static qreal position(const QQuickItem *item, QQuickAnchors::Anchor anchorLine)
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define emit
unsigned long long quint64
Definition qtypes.h:61
long long qint64
Definition qtypes.h:60
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...
Definition moc.h:23