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 "qffmpegmediaplayer_p.h"
5#include "private/qplatformaudiooutput_p.h"
6#include "qvideosink.h"
7#include "qaudiooutput.h"
8#include "qaudiobufferoutput.h"
9#include "qffmpegplaybackengine_p.h"
10#include <qiodevice.h>
11#include <qvideosink.h>
12#include <QtMultimedia/qplaybackoptions.h>
13#include <qtimer.h>
14#include <QtConcurrent/QtConcurrent>
15
16#include <qloggingcategory.h>
17
18QT_BEGIN_NAMESPACE
19
20namespace QFFmpeg {
21
22class CancelToken : public ICancelToken
23{
24public:
25
26 bool isCancelled() const override { return m_cancelled.load(m: std::memory_order_acquire); }
27
28 void cancel() { m_cancelled.store(i: true, m: std::memory_order_release); }
29
30private:
31 std::atomic_bool m_cancelled = false;
32};
33
34} // namespace QFFmpeg
35
36using namespace QFFmpeg;
37
38QFFmpegMediaPlayer::QFFmpegMediaPlayer(QMediaPlayer *player)
39 : QPlatformMediaPlayer(player)
40{
41 m_positionUpdateTimer.setInterval(50);
42 m_positionUpdateTimer.setTimerType(Qt::PreciseTimer);
43 connect(sender: &m_positionUpdateTimer, signal: &QTimer::timeout, context: this, slot: &QFFmpegMediaPlayer::updatePosition);
44}
45
46QFFmpegMediaPlayer::~QFFmpegMediaPlayer()
47{
48 if (m_cancelToken)
49 m_cancelToken->cancel();
50
51 m_loadMedia.waitForFinished();
52};
53
54qint64 QFFmpegMediaPlayer::duration() const
55{
56 return m_playbackEngine ? toUserDuration(trackDuration: m_playbackEngine->duration()).get() : 0;
57}
58
59void QFFmpegMediaPlayer::setPosition(qint64 position)
60{
61 if (mediaStatus() == QMediaPlayer::LoadingMedia)
62 return;
63
64 if (m_playbackEngine) {
65 m_playbackEngine->seek(pos: toTrackPosition(userTrackPosition: UserTrackPosition(position)));
66 updatePosition();
67 }
68
69 mediaStatusChanged(QMediaPlayer::LoadedMedia);
70}
71
72void QFFmpegMediaPlayer::updatePosition()
73{
74 positionChanged(position: m_playbackEngine ? toUserPosition(trackPosition: m_playbackEngine->currentPosition()).get()
75 : 0);
76}
77
78void QFFmpegMediaPlayer::endOfStream()
79{
80 // stop update timer and report end position anyway
81 m_positionUpdateTimer.stop();
82 QPointer currentPlaybackEngine(m_playbackEngine.get());
83 positionChanged(position: duration());
84
85 // skip changing state and mediaStatus if playbackEngine has been recreated,
86 // e.g. if new media has been loaded as a response to positionChanged signal
87 if (currentPlaybackEngine)
88 stateChanged(newState: QMediaPlayer::StoppedState);
89 if (currentPlaybackEngine)
90 mediaStatusChanged(QMediaPlayer::EndOfMedia);
91}
92
93void QFFmpegMediaPlayer::onLoopChanged()
94{
95 // report about finish and start
96 // reporting both signals is a bit contraversial
97 // but it eshures the idea of notifications about
98 // imporatant position points.
99 // Also, it ensures more predictable flow for testing.
100 positionChanged(position: duration());
101 positionChanged(position: 0);
102 m_positionUpdateTimer.stop();
103 m_positionUpdateTimer.start();
104}
105
106void QFFmpegMediaPlayer::onBuffered()
107{
108 if (mediaStatus() == QMediaPlayer::BufferingMedia)
109 mediaStatusChanged(QMediaPlayer::BufferedMedia);
110}
111
112float QFFmpegMediaPlayer::bufferProgress() const
113{
114 return m_bufferProgress;
115}
116
117void QFFmpegMediaPlayer::mediaStatusChanged(QMediaPlayer::MediaStatus status)
118{
119 if (mediaStatus() == status)
120 return;
121
122 const auto newBufferProgress = status == QMediaPlayer::BufferingMedia ? 0.25f // to be improved
123 : status == QMediaPlayer::BufferedMedia ? 1.f
124 : 0.f;
125
126 if (!qFuzzyCompare(p1: newBufferProgress, p2: m_bufferProgress)) {
127 m_bufferProgress = newBufferProgress;
128 bufferProgressChanged(progress: newBufferProgress);
129 }
130
131 QPlatformMediaPlayer::mediaStatusChanged(status);
132}
133
134QMediaTimeRange QFFmpegMediaPlayer::availablePlaybackRanges() const
135{
136 return {};
137}
138
139qreal QFFmpegMediaPlayer::playbackRate() const
140{
141 return m_playbackRate;
142}
143
144void QFFmpegMediaPlayer::setPlaybackRate(qreal rate)
145{
146 const float effectiveRate = std::max(a: static_cast<float>(rate), b: 0.0f);
147
148 if (qFuzzyCompare(p1: m_playbackRate, p2: effectiveRate))
149 return;
150
151 m_playbackRate = effectiveRate;
152
153 if (m_playbackEngine)
154 m_playbackEngine->setPlaybackRate(effectiveRate);
155
156 playbackRateChanged(rate: effectiveRate);
157}
158
159QUrl QFFmpegMediaPlayer::media() const
160{
161 return m_url;
162}
163
164const QIODevice *QFFmpegMediaPlayer::mediaStream() const
165{
166 return m_device;
167}
168
169void QFFmpegMediaPlayer::handleIncorrectMedia(QMediaPlayer::MediaStatus status)
170{
171 seekableChanged(seekable: false);
172 audioAvailableChanged(audioAvailable: false);
173 videoAvailableChanged(videoAvailable: false);
174 metaDataChanged();
175 mediaStatusChanged(status);
176 m_playbackEngine = nullptr;
177};
178
179void QFFmpegMediaPlayer::setMedia(const QUrl &media, QIODevice *stream)
180{
181 // Wait for previous unfinished load attempts.
182 if (m_cancelToken)
183 m_cancelToken->cancel();
184
185 m_loadMedia.waitForFinished();
186
187 m_url = media;
188 m_device = stream;
189 m_playbackEngine = nullptr;
190
191 if (media.isEmpty() && !stream) {
192 handleIncorrectMedia(status: QMediaPlayer::NoMedia);
193 return;
194 }
195
196 mediaStatusChanged(status: QMediaPlayer::LoadingMedia);
197
198 m_requestedStatus = QMediaPlayer::StoppedState;
199
200 m_cancelToken = std::make_shared<CancelToken>();
201
202 // Load media asynchronously to keep GUI thread responsive while loading media
203 m_loadMedia = QtConcurrent::run(f: [this, media, stream, cancelToken = m_cancelToken] {
204 // On worker thread
205 const MediaDataHolder::Maybe mediaHolder =
206 MediaDataHolder::create(url: media, stream, options: playbackOptions(), cancelToken);
207
208 // Transition back to calling thread using invokeMethod because
209 // QFuture continuations back on calling thread may deadlock (QTBUG-117918)
210 QMetaObject::invokeMethod(object: this, function: [this, mediaHolder, cancelToken] {
211 setMediaAsync(mediaDataHolder: mediaHolder, cancelToken);
212 });
213 });
214}
215
216void QFFmpegMediaPlayer::setMediaAsync(QFFmpeg::MediaDataHolder::Maybe mediaDataHolder,
217 const std::shared_ptr<QFFmpeg::CancelToken> &cancelToken)
218{
219 // If loading was cancelled, we do not emit any signals about failing
220 // to load media (or any other events). The rationale is that cancellation
221 // either happens during destruction, where the signals are no longer
222 // of interest, or it happens as a response to user requesting to load
223 // another media file. In the latter case, we don't want to risk popping
224 // up error dialogs or similar.
225 if (cancelToken->isCancelled()) {
226 return;
227 }
228
229 Q_ASSERT(mediaStatus() == QMediaPlayer::LoadingMedia);
230
231 if (!mediaDataHolder) {
232 const auto [code, description] = mediaDataHolder.error();
233 error(error: code, errorString: description);
234 handleIncorrectMedia(status: QMediaPlayer::MediaStatus::InvalidMedia);
235 return;
236 }
237
238 m_playbackEngine = std::make_unique<PlaybackEngine>(args: playbackOptions());
239
240 connect(sender: m_playbackEngine.get(), signal: &PlaybackEngine::endOfStream, context: this,
241 slot: &QFFmpegMediaPlayer::endOfStream);
242 connect(sender: m_playbackEngine.get(), signal: &PlaybackEngine::errorOccured, context: this,
243 slot: &QFFmpegMediaPlayer::error);
244 connect(sender: m_playbackEngine.get(), signal: &PlaybackEngine::loopChanged, context: this,
245 slot: &QFFmpegMediaPlayer::onLoopChanged);
246 connect(sender: m_playbackEngine.get(), signal: &PlaybackEngine::buffered, context: this,
247 slot: &QFFmpegMediaPlayer::onBuffered);
248
249 m_playbackEngine->setMedia(std::move(*mediaDataHolder.value()));
250
251 m_playbackEngine->setAudioBufferOutput(m_audioBufferOutput);
252 m_playbackEngine->setAudioSink(m_audioOutput);
253 m_playbackEngine->setVideoSink(m_videoSink);
254
255 m_playbackEngine->setLoops(loops());
256 m_playbackEngine->setPlaybackRate(m_playbackRate);
257 m_playbackEngine->setPitchCompensation(m_pitchCompensation);
258
259 durationChanged(duration: duration());
260 tracksChanged();
261 metaDataChanged();
262 seekableChanged(seekable: m_playbackEngine->isSeekable());
263
264 audioAvailableChanged(
265 audioAvailable: !m_playbackEngine->streamInfo(trackType: QPlatformMediaPlayer::AudioStream).isEmpty());
266 videoAvailableChanged(
267 videoAvailable: !m_playbackEngine->streamInfo(trackType: QPlatformMediaPlayer::VideoStream).isEmpty());
268
269 mediaStatusChanged(status: QMediaPlayer::LoadedMedia);
270
271 if (m_requestedStatus != QMediaPlayer::StoppedState) {
272 if (m_requestedStatus == QMediaPlayer::PlayingState)
273 play();
274 else if (m_requestedStatus == QMediaPlayer::PausedState)
275 pause();
276 }
277}
278
279void QFFmpegMediaPlayer::play()
280{
281 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
282 m_requestedStatus = QMediaPlayer::PlayingState;
283 return;
284 }
285
286 if (!m_playbackEngine)
287 return;
288
289 if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
290 m_playbackEngine->seek(pos: TrackPosition(0));
291 positionChanged(position: 0);
292 }
293
294 runPlayback();
295}
296
297void QFFmpegMediaPlayer::runPlayback()
298{
299 m_playbackEngine->play();
300 m_positionUpdateTimer.start();
301 stateChanged(newState: QMediaPlayer::PlayingState);
302
303 if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia)
304 mediaStatusChanged(status: QMediaPlayer::BufferingMedia);
305}
306
307void QFFmpegMediaPlayer::pause()
308{
309 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
310 m_requestedStatus = QMediaPlayer::PausedState;
311 return;
312 }
313
314 if (!m_playbackEngine)
315 return;
316
317 if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
318 m_playbackEngine->seek(pos: TrackPosition(0));
319 positionChanged(position: 0);
320 }
321 m_playbackEngine->pause();
322 m_positionUpdateTimer.stop();
323 stateChanged(newState: QMediaPlayer::PausedState);
324
325 if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia)
326 mediaStatusChanged(status: QMediaPlayer::BufferingMedia);
327}
328
329void QFFmpegMediaPlayer::stop()
330{
331 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
332 m_requestedStatus = QMediaPlayer::StoppedState;
333 return;
334 }
335
336 if (!m_playbackEngine)
337 return;
338
339 m_playbackEngine->stop();
340 m_positionUpdateTimer.stop();
341 m_playbackEngine->seek(pos: TrackPosition(0));
342 positionChanged(position: 0);
343 stateChanged(newState: QMediaPlayer::StoppedState);
344 mediaStatusChanged(status: QMediaPlayer::LoadedMedia);
345}
346
347void QFFmpegMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
348{
349 m_audioOutput = output;
350 if (m_playbackEngine)
351 m_playbackEngine->setAudioSink(output);
352}
353
354void QFFmpegMediaPlayer::setAudioBufferOutput(QAudioBufferOutput *output) {
355 m_audioBufferOutput = output;
356 if (m_playbackEngine)
357 m_playbackEngine->setAudioBufferOutput(output);
358}
359
360QMediaMetaData QFFmpegMediaPlayer::metaData() const
361{
362 return m_playbackEngine ? m_playbackEngine->metaData() : QMediaMetaData{};
363}
364
365void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink)
366{
367 m_videoSink = sink;
368 if (m_playbackEngine)
369 m_playbackEngine->setVideoSink(sink);
370}
371
372QVideoSink *QFFmpegMediaPlayer::videoSink() const
373{
374 return m_videoSink;
375}
376
377int QFFmpegMediaPlayer::trackCount(TrackType type)
378{
379 return m_playbackEngine ? m_playbackEngine->streamInfo(trackType: type).count() : 0;
380}
381
382QMediaMetaData QFFmpegMediaPlayer::trackMetaData(TrackType type, int streamNumber)
383{
384 if (!m_playbackEngine || streamNumber < 0
385 || streamNumber >= m_playbackEngine->streamInfo(trackType: type).count())
386 return {};
387 return m_playbackEngine->streamInfo(trackType: type).at(i: streamNumber).metaData;
388}
389
390int QFFmpegMediaPlayer::activeTrack(TrackType type)
391{
392 return m_playbackEngine ? m_playbackEngine->activeTrack(type) : -1;
393}
394
395void QFFmpegMediaPlayer::setActiveTrack(TrackType type, int streamNumber)
396{
397 if (m_playbackEngine)
398 m_playbackEngine->setActiveTrack(type, streamNumber);
399 else
400 qWarning() << "Cannot set active track without open source";
401}
402
403void QFFmpegMediaPlayer::setLoops(int loops)
404{
405 if (m_playbackEngine)
406 m_playbackEngine->setLoops(loops);
407
408 QPlatformMediaPlayer::setLoops(loops);
409}
410
411void QFFmpegMediaPlayer::setPitchCompensation(bool enabled)
412{
413 if (enabled == m_pitchCompensation)
414 return;
415
416 m_pitchCompensation = enabled;
417 if (m_playbackEngine)
418 m_playbackEngine->setPitchCompensation(enabled);
419 QPlatformMediaPlayer::pitchCompensationChanged(enabled);
420}
421
422bool QFFmpegMediaPlayer::pitchCompensation() const
423{
424 return m_pitchCompensation;
425}
426
427QPlatformMediaPlayer::PitchCompensationAvailability
428QFFmpegMediaPlayer::pitchCompensationAvailability() const
429{
430 return PitchCompensationAvailability::Available;
431}
432
433QT_END_NAMESPACE
434
435#include "moc_qffmpegmediaplayer_p.cpp"
436

source code of qtmultimedia/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp