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
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 "private/qaudiobufferoutput_p.h"
10#include "qiodevice.h"
11#include "playbackengine/qffmpegdemuxer_p.h"
12#include "playbackengine/qffmpegstreamdecoder_p.h"
13#include "playbackengine/qffmpegsubtitlerenderer_p.h"
14#include "playbackengine/qffmpegvideorenderer_p.h"
15#include "playbackengine/qffmpegaudiorenderer_p.h"
16
17#include <qloggingcategory.h>
18
20namespace ranges = QtMultimediaPrivate::ranges;
21
22namespace QFFmpeg {
23
24Q_STATIC_LOGGING_CATEGORY(qLcPlaybackEngine, "qt.multimedia.ffmpeg.playbackengine");
25
26// The helper is needed since on some compilers std::unique_ptr
27// doesn't have a default constructor in the case of sizeof(CustomDeleter) > 0
28template <typename Array>
29inline static Array defaultObjectsArray()
30{
31 using T = typename Array::value_type;
32 return { T{ {}, {} }, T{ {}, {} }, T{ {}, {} } };
33}
34
36 : m_demuxer({}, {}),
40{
41 qCDebug(qLcPlaybackEngine) << "Create PlaybackEngine";
42 qRegisterMetaType<QFFmpeg::Packet>();
43 qRegisterMetaType<QFFmpeg::Frame>();
44 qRegisterMetaType<QFFmpeg::TrackPosition>();
45 qRegisterMetaType<QFFmpeg::PlaybackEngineObjectID>();
46}
47
49 qCDebug(qLcPlaybackEngine) << "Delete PlaybackEngine";
50
51 finalizeOutputs();
52 forEachExistingObject([](auto &object) { object.reset(); });
53 deleteFreeThreads();
54}
55
56void PlaybackEngine::onRendererFinished(const PlaybackEngineObjectID &id)
57{
58 if (!hasRenderer(id))
59 return;
60
61 auto isAtEnd = [this](TrackType trackType) {
62 auto &renderer = m_renderers[trackType];
63 return !renderer || renderer->isAtEnd();
64 };
65
66 if (!isAtEnd(TrackType::VideoStream))
67 return;
68
69 if (!isAtEnd(TrackType::AudioStream))
70 return;
71
72 if (!isAtEnd(TrackType::SubtitleStream) && !hasMediaStream())
73 return;
74
75 if (std::exchange(m_state, QMediaPlayer::StoppedState) == QMediaPlayer::StoppedState)
76 return;
77
78 finilizeTime(duration().asTimePoint());
79
80 forceUpdate();
81
82 qCDebug(qLcPlaybackEngine) << "Playback engine end of stream";
83
84 emit endOfStream();
85}
86
87void PlaybackEngine::onRendererLoopChanged(const PlaybackEngineObjectID &id, TrackPosition offset,
88 int loopIndex)
89{
90 if (!hasRenderer(id))
91 return;
92
93 if (loopIndex > m_currentLoopOffset.loopIndex) {
94 m_currentLoopOffset = { offset, loopIndex };
95 emit loopChanged();
96 } else if (loopIndex == m_currentLoopOffset.loopIndex && offset != m_currentLoopOffset.loopStartTimeUs) {
97 qWarning() << "Unexpected offset for loop" << loopIndex << ":" << offset.get() << "vs"
98 << m_currentLoopOffset.loopStartTimeUs.get();
99 m_currentLoopOffset.loopStartTimeUs = offset;
100 }
101}
102
103void PlaybackEngine::onFirstPacketFound(const PlaybackEngineObjectID &id, TrackPosition absSeekPos)
104{
105 if (!checkObjectID(m_demuxer, id))
106 return;
107
108 if (m_timeController.isStarted())
109 return;
110
111 const SteadyClock::time_point now = SteadyClock::now();
112 const SteadyClock::time_point expectedTimePoint = m_timeController.timeFromPosition(absSeekPos);
113 const auto delay =
114 std::chrono::round<std::chrono::microseconds>(now - expectedTimePoint);
115 qCDebug(qLcPlaybackEngine) << "Delay of demuxer initialization:" << delay;
116 m_timeController.sync(now, absSeekPos);
117 m_timeController.start();
118
119 forEachExistingObject<Renderer>(
120 [&](auto &renderer) { renderer->setTimeController(m_timeController); });
121}
122
123void PlaybackEngine::onRendererSynchronized(const PlaybackEngineObjectID &id,
124 SteadyClock::time_point tp, TrackPosition pos)
125{
126 if (!hasRenderer(id))
127 return;
128
129 Q_ASSERT(checkObjectID(
130 m_renderers[TrackType::AudioStream], id));
131
132 forEachExistingObject<Renderer>([&](auto &renderer) {
133 if (id.objectID != renderer->objectID()) {
134 auto tc = m_timeController;
135 tc.syncSoft(tp, pos);
136 renderer->setTimeController(tc);
137 }
138 });
139
140 m_timeController.sync(tp, pos);
141}
142
143void PlaybackEngine::setState(QMediaPlayer::PlaybackState state) {
144 if (!m_media.avContext())
145 return;
146
147 if (state == m_state)
148 return;
149
150 const auto prevState = std::exchange(m_state, state);
151
152 if (m_state == QMediaPlayer::StoppedState) {
153 finalizeOutputs();
154 finilizeTime(TrackPosition(0));
155 }
156
157 if (prevState == QMediaPlayer::StoppedState || m_state == QMediaPlayer::StoppedState)
158 recreateObjects();
159
160 if (prevState == QMediaPlayer::StoppedState)
161 triggerStepIfNeeded();
162
163 updateObjectsPausedState();
164}
165
166void PlaybackEngine::updateObjectsPausedState()
167{
168 const bool paused = m_state != QMediaPlayer::PlayingState;
169 m_timeController.setPaused(paused);
170
171 forEachExistingObject([&](auto &object) {
172 if constexpr (std::is_same_v<decltype(*object), Renderer &>)
173 object->setPaused(paused);
174 else
175 object->setPaused(false);
176 });
177}
178
179void PlaybackEngine::ObjectDeleter::operator()(PlaybackEngineObject *object) const
180{
181 Q_ASSERT(engine);
182 if (!std::exchange(engine->m_threadsDirty, true))
183 QMetaObject::invokeMethod(engine, &PlaybackEngine::deleteFreeThreads, Qt::QueuedConnection);
184
185 object->kill();
186}
187
188void PlaybackEngine::registerObject(PlaybackEngineObject &object)
189{
190 connect(&object, &PlaybackEngineObject::error, this, &PlaybackEngine::errorOccured);
191
192 auto threadName = objectThreadName(object);
193 auto &thread = m_threads[threadName];
194 if (!thread) {
195 thread = std::make_unique<QThread>();
196 thread->setObjectName(threadName);
197 thread->start();
198 }
199
200 Q_ASSERT(object.thread() != thread.get());
201 object.moveToThread(thread.get());
202}
203
205PlaybackEngine::createRenderer(TrackType trackType)
206{
207 switch (trackType) {
208 case TrackType::VideoStream:
209 return m_videoSink ? createPlaybackEngineObject<VideoRenderer>(
210 m_timeController, m_videoSink, m_media.transformation())
211 : RendererPtr{ {}, {} };
212 case TrackType::AudioStream:
213 return m_audioOutput || m_audioBufferOutput
214 ? createPlaybackEngineObject<AudioRenderer>(
215 m_timeController, m_audioOutput, m_audioBufferOutput, m_pitchCompensation)
216 : RendererPtr{ {}, {} };
217 case TrackType::SubtitleStream:
218 return m_videoSink
219 ? createPlaybackEngineObject<SubtitleRenderer>(m_timeController, m_videoSink)
220 : RendererPtr{ {}, {} };
221 default:
222 return { {}, {} };
223 }
224}
225
226template<typename C, typename Action>
227void PlaybackEngine::forEachExistingObject(Action &&action)
228{
229 auto handleNotNullObject = [&](auto &object) {
230 if constexpr (std::is_base_of_v<C, std::remove_reference_t<decltype(*object)>>)
231 if (object)
232 action(object);
233 };
234
235 // The order Renderers => Demuxer is required for seek().
236 // For other cases, it doesn't make any difference.
237 std::for_each(m_renderers.begin(), m_renderers.end(), handleNotNullObject);
238 std::for_each(m_streams.begin(), m_streams.end(), handleNotNullObject);
239 handleNotNullObject(m_demuxer);
240}
241
242template<typename Action>
243void PlaybackEngine::forEachExistingObject(Action &&action)
244{
245 forEachExistingObject<PlaybackEngineObject>(std::forward<Action>(action));
246}
247
248void PlaybackEngine::seek(TrackPosition pos)
249{
250 pos = boundPosition(pos);
251
252 m_timeController.sync(m_currentLoopOffset.loopStartTimeUs.asDuration() + pos);
253 if (!m_demuxer || !m_media.avContext()) {
254 m_seekPending = true;
255 return;
256 }
257
258 m_seekPending = false;
259 ++m_currentID.sessionID;
260
261 m_timeController.deactivate();
262 m_timeController.setPaused(m_state != QMediaPlayer::PlayingState);
263
264 forEachExistingObject([&](auto &object) {
265 if constexpr (std::is_same_v<decltype(*object), Renderer &>)
266 object->seek(m_currentID.sessionID, m_timeController, m_currentLoopOffset);
267 else
268 object->seek(m_currentID.sessionID, pos, m_currentLoopOffset);
269 });
270
271 triggerStepIfNeeded();
272}
273
274void PlaybackEngine::setLoops(int loops)
275{
276 if (!isSeekable()) {
277 qWarning() << "Cannot set loops for non-seekable source";
278 return;
279 }
280
281 if (std::exchange(m_loops, loops) == loops)
282 return;
283
284 qCDebug(qLcPlaybackEngine) << "set playback engine loops:" << loops << "prev loops:" << m_loops
285 << "index:" << m_currentLoopOffset.loopIndex;
286
287 if (m_demuxer)
288 m_demuxer->setLoops(loops);
289}
290
291void PlaybackEngine::triggerStepIfNeeded()
292{
293 if (m_state != QMediaPlayer::PausedState)
294 return;
295
296 auto &videoRenderer = m_renderers[TrackType::VideoStream];
297 if (videoRenderer)
298 videoRenderer->doForceStep();
299
300 // TODO: maybe trigger SubtitleStream.
301 // If trigger it, we have to make seeking for the current subtitle frame more stable.
302 // Or set some timeout for seeking.
303}
304
305QString PlaybackEngine::objectThreadName(const PlaybackEngineObject &object)
306{
307 QString result = QString::fromLatin1(object.metaObject()->className());
308 if (auto stream = qobject_cast<const StreamDecoder *>(&object))
309 result += QString::number(qToUnderlying(stream->trackType()));
310
311 return result;
312}
313
315 if (rate == playbackRate())
316 return;
317
318 m_timeController.setPlaybackRate(rate);
319 forEachExistingObject<Renderer>([rate](auto &renderer) { renderer->setPlaybackRate(rate); });
320}
321
323 return m_timeController.playbackRate();
324}
325
326void PlaybackEngine::recreateObjects()
327{
328 m_timeController.deactivate();
329
330 forEachExistingObject([](auto &object) { object.reset(); });
331
332 createObjectsIfNeeded();
333}
334
335void PlaybackEngine::createObjectsIfNeeded()
336{
337 if (m_state == QMediaPlayer::StoppedState || !m_media.avContext())
338 return;
339
340 for (size_t i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i)
341 createStreamAndRenderer(static_cast<TrackType>(i));
342
343 createDemuxer();
344
345 // temporary, to comply with the test disablingAllTracks_doesNotStopPlayback
346 if (!m_demuxer)
347 m_timeController.start();
348}
349
350void PlaybackEngine::forceUpdate()
351{
352 recreateObjects();
353 triggerStepIfNeeded();
354 updateObjectsPausedState();
355}
356
357void PlaybackEngine::createStreamAndRenderer(TrackType trackType)
358{
359 auto codecContext = codecContextForTrack(trackType);
360
361 auto &renderer = m_renderers[trackType];
362
363 if (!codecContext)
364 return;
365
366 if (!renderer) {
367 renderer = createRenderer(trackType);
368
369 if (!renderer)
370 return;
371
372 connect(renderer.get(), &Renderer::synchronized, this,
373 &PlaybackEngine::onRendererSynchronized);
374
375 connect(renderer.get(), &Renderer::loopChanged, this,
376 &PlaybackEngine::onRendererLoopChanged);
377
378 connect(renderer.get(), &PlaybackEngineObject::atEnd, this,
379 &PlaybackEngine::onRendererFinished);
380 }
381
382 Q_ASSERT(MediaDataHolder::trackTypeFromMediaType(codecContext->context()->codec_type));
383
384 auto &stream = m_streams[trackType] =
385 createPlaybackEngineObject<StreamDecoder>(*codecContext, renderer->seekPosition());
386
387 Q_ASSERT(trackType == stream->trackType());
388
389 connect(stream.get(), &StreamDecoder::requestHandleFrame, renderer.get(), &Renderer::render);
390 connect(stream.get(), &PlaybackEngineObject::atEnd, renderer.get(),
391 &Renderer::onFinalFrameReceived);
392 connect(renderer.get(), &Renderer::frameProcessed, stream.get(),
393 &StreamDecoder::onFrameProcessed);
394}
395
396std::optional<CodecContext> PlaybackEngine::codecContextForTrack(TrackType trackType)
397{
398 const auto streamIndex = m_media.currentStreamIndex(trackType);
399 if (streamIndex < 0)
400 return {};
401
402 auto &codecContext = m_codecContexts[trackType];
403
404 if (!codecContext) {
405 qCDebug(qLcPlaybackEngine)
406 << "Create codec for stream:" << streamIndex << "trackType:" << trackType;
407 auto maybeCodecContext = CodecContext::create(m_media.avContext()->streams[streamIndex],
408 m_media.avContext(), m_options);
409
410 if (!maybeCodecContext) {
411 emit errorOccured(QMediaPlayer::FormatError,
412 u"Cannot create codec," + maybeCodecContext.error());
413 return {};
414 }
415
416 codecContext = maybeCodecContext.value();
417 }
418
419 return codecContext;
420}
421
422bool PlaybackEngine::hasMediaStream() const
423{
424 return m_renderers[TrackType::AudioStream]
425 || m_renderers[TrackType::VideoStream];
426}
427
428void PlaybackEngine::createDemuxer()
429{
430 StreamIndexes streamIndexes = { -1, -1, -1 };
431
432 bool hasStreams = false;
433 forEachExistingObject<StreamDecoder>([&](auto &stream) {
434 hasStreams = true;
435 const auto trackType = stream->trackType();
436 streamIndexes[trackType] = m_media.currentStreamIndex(trackType);
437 });
438
439 if (!hasStreams)
440 return;
441
442 const TrackPosition currentLoopPosUs = currentPosition(false);
443
444 m_demuxer = createPlaybackEngineObject<Demuxer>(m_media.avContext(), currentLoopPosUs,
445 m_seekPending, m_currentLoopOffset,
446 streamIndexes, m_loops);
447
448 m_seekPending = false;
449
450 connect(m_demuxer.get(), &Demuxer::packetsBuffered, this, &PlaybackEngine::buffered);
451
452 forEachExistingObject<StreamDecoder>([&](auto &stream) {
453 connect(m_demuxer.get(), Demuxer::signalByTrackType(stream->trackType()), stream.get(),
454 &StreamDecoder::decode);
455 connect(m_demuxer.get(), &PlaybackEngineObject::atEnd, stream.get(),
456 &StreamDecoder::onFinalPacketReceived);
457 connect(stream.get(), &StreamDecoder::packetProcessed, m_demuxer.get(),
458 &Demuxer::onPacketProcessed);
459 });
460
461 connect(m_demuxer.get(), &Demuxer::firstPacketFound, this, &PlaybackEngine::onFirstPacketFound);
462}
463
464void PlaybackEngine::deleteFreeThreads() {
465 m_threadsDirty = false;
466 auto freeThreads = std::move(m_threads);
467
468 forEachExistingObject([&](auto &object) {
469 m_threads.insert(freeThreads.extract(objectThreadName(*object)));
470 });
471
472 for (auto &[name, thr] : freeThreads)
473 thr->quit();
474
475 for (auto &[name, thr] : freeThreads)
476 thr->wait();
477}
478
479void PlaybackEngine::setMedia(MediaDataHolder media)
480{
481 Q_ASSERT(!m_media.avContext()); // Playback engine does not support reloading media
482 Q_ASSERT(m_state == QMediaPlayer::StoppedState);
483 Q_ASSERT(m_threads.empty());
484
485 m_media = std::move(media);
486 updateVideoSinkSize();
487}
488
489void PlaybackEngine::setVideoSink(QVideoSink *sink)
490{
491 auto prev = std::exchange(m_videoSink, sink);
492 if (prev == sink)
493 return;
494
495 updateVideoSinkSize(prev);
497
498 if (!sink || !prev) {
499 // might need some improvements
500 forceUpdate();
501 }
502}
503
504void PlaybackEngine::setAudioSink(QPlatformAudioOutput *output) {
505 setAudioSink(output ? output->q : nullptr);
506}
507
508void PlaybackEngine::setAudioSink(QAudioOutput *output)
509{
510 QAudioOutput *prev = std::exchange(m_audioOutput, output);
511 if (prev == output)
512 return;
513
514 updateActiveAudioOutput(output);
515
516 if (!output || !prev) {
517 // might need some improvements
518 forceUpdate();
519 }
520}
521
522void PlaybackEngine::setAudioBufferOutput(QAudioBufferOutput *output)
523{
524 QAudioBufferOutput *prev = std::exchange(m_audioBufferOutput, output);
525 if (prev == output)
526 return;
527 updateActiveAudioOutput(output);
528}
529
531{
532 std::optional<TrackPosition> pos;
533
534 if (m_timeController.isStarted()) {
535 for (size_t i = 0; i < m_renderers.size(); ++i) {
536 const auto &renderer = m_renderers[i];
537 if (!renderer)
538 continue;
539
540 // skip subtitle stream for finding lower rendering position
541 if (!topPos && i == qToUnderlying(TrackType::SubtitleStream)
542 && hasMediaStream())
543 continue;
544
545 const auto rendererPos = renderer->lastPosition();
546 pos = !pos ? rendererPos
547 : topPos ? std::max(*pos, rendererPos)
548 : std::min(*pos, rendererPos);
549 }
550 }
551 // else we cannot reliably (without RC) getthe current renderers position after seeking
552
553 if (!pos)
554 pos = m_timeController.currentPosition();
555
556 return boundPosition(*pos - m_currentLoopOffset.loopStartTimeUs.asDuration());
557}
558
560{
561 return m_media.duration();
562}
563
564bool PlaybackEngine::isSeekable() const { return m_media.isSeekable(); }
565
567PlaybackEngine::streamInfo(TrackType trackType) const
568{
569 return m_media.streamInfo(trackType);
570}
571
573{
574 return m_media.metaData();
575}
576
577int PlaybackEngine::activeTrack(TrackType type) const
578{
579 return m_media.activeTrack(type);
580}
581
583{
584 m_pitchCompensation = enabled;
585 if (AudioRenderer *renderer = getAudioRenderer())
586 renderer->setPitchCompensation(enabled);
587}
588
589void PlaybackEngine::setActiveTrack(TrackType trackType, int streamNumber)
590{
591 if (!m_media.setActiveTrack(trackType, streamNumber))
592 return;
593
594 m_codecContexts[trackType] = {};
595
596 m_renderers[trackType].reset();
597 m_streams = defaultObjectsArray<decltype(m_streams)>();
598 m_demuxer.reset();
599
600 // Don't deactivate m_timeController:
601 //
602 // We strive to have a smooth playback if we change the active track. It means that
603 // we don't want to do any time shiftings. Instead, we rely on the fact that
604 // buffers in renderers are not empty to compensate the demuxer's lag.
605
606 updateVideoSinkSize();
607 createObjectsIfNeeded();
608 updateObjectsPausedState();
609}
610
611void PlaybackEngine::finilizeTime(TrackPosition pos)
612{
613 Q_ASSERT(pos >= TrackPosition(0) && pos <= duration().asTimePoint());
614
615 m_timeController.deactivate();
616 m_timeController.sync(pos);
617 m_currentLoopOffset = {};
618}
619
620void PlaybackEngine::finalizeOutputs()
621{
622 if (m_audioBufferOutput)
623 updateActiveAudioOutput(static_cast<QAudioBufferOutput *>(nullptr));
624 if (m_audioOutput)
625 updateActiveAudioOutput(static_cast<QAudioOutput *>(nullptr));
626 updateActiveVideoOutput(nullptr, true);
627}
628
629bool PlaybackEngine::hasRenderer(const PlaybackEngineObjectID &id) const
630{
631 return ranges::any_of(m_renderers, [&](auto &renderer) {
632 return checkObjectID(renderer, id);
633 });
634}
635
636template <typename AudioOutput>
637void PlaybackEngine::updateActiveAudioOutput(AudioOutput *output)
638{
639 if (AudioRenderer *renderer = getAudioRenderer())
640 renderer->setOutput(output);
641}
642
643void PlaybackEngine::updateActiveVideoOutput(QVideoSink *sink, bool cleanOutput)
644{
645 if (auto renderer = qobject_cast<SubtitleRenderer *>(
646 m_renderers[TrackType::SubtitleStream]
647 .get()))
648 renderer->setOutput(sink, cleanOutput);
649 if (auto renderer = qobject_cast<VideoRenderer *>(
650 m_renderers[TrackType::VideoStream].get()))
651 renderer->setOutput(sink, cleanOutput);
652}
653
654void PlaybackEngine::updateVideoSinkSize(QVideoSink *prevSink)
655{
656 auto platformVideoSink = m_videoSink ? m_videoSink->platformVideoSink() : nullptr;
657 if (!platformVideoSink)
658 return;
659
660 if (prevSink && prevSink->platformVideoSink())
661 platformVideoSink->setNativeSize(prevSink->platformVideoSink()->nativeSize());
662 else {
663 const auto streamIndex =
664 m_media.currentStreamIndex(TrackType::VideoStream);
665 if (streamIndex >= 0) {
666 const auto context = m_media.avContext();
667 const auto stream = context->streams[streamIndex];
668 const AVRational pixelAspectRatio =
669 av_guess_sample_aspect_ratio(context, stream, nullptr);
670 // auto size = metaData().value(QMediaMetaData::Resolution)
671 const QSize size =
672 qCalculateFrameSize({ stream->codecpar->width, stream->codecpar->height },
673 { pixelAspectRatio.num, pixelAspectRatio.den });
674
675 platformVideoSink->setNativeSize(
676 qRotatedFrameSize(size, m_media.transformation().rotation));
677 }
678 }
679}
680
681TrackPosition PlaybackEngine::boundPosition(TrackPosition position) const
682{
683 position = qMax(position, TrackPosition(0));
684 return duration() > TrackDuration(0) ? qMin(position, duration().asTimePoint()) : position;
685}
686
687AudioRenderer *PlaybackEngine::getAudioRenderer()
688{
689 return qobject_cast<AudioRenderer *>(
690 m_renderers[TrackType::AudioStream].get());
691}
692
693} // namespace QFFmpeg
694
695QT_END_NAMESPACE
696
697#include "moc_qffmpegplaybackengine_p.cpp"
void setPitchCompensation(bool enabled)
void packetsBuffered()
void seek(TrackPosition pos)
void setVideoSink(QVideoSink *sink)
void setState(QMediaPlayer::PlaybackState state)
void updateActiveAudioOutput(AudioOutput *output)
void setAudioBufferOutput(QAudioBufferOutput *output)
void setAudioSink(QAudioOutput *output)
const QList< MediaDataHolder::StreamInfo > & streamInfo(TrackType trackType) const
void setPitchCompensation(bool enabled)
void setActiveTrack(TrackType type, int streamNumber)
void setMedia(MediaDataHolder media)
TrackPosition currentPosition(bool topPos=true) const
const QMediaMetaData & metaData() const
int activeTrack(TrackType type) const
void updateActiveVideoOutput(QVideoSink *sink, bool cleanOutput=false)
PlaybackEngine(const QPlaybackOptions &options)
virtual RendererPtr createRenderer(TrackType trackType)
The QPlaybackOptions class enables low-level control of media playback options.
static Array defaultObjectsArray()
Combined button and popup list for selecting options.
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)
void operator()(PlaybackEngineObject *) const