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

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