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 ? 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, 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>();
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
258 durationChanged(duration: duration());
259 tracksChanged();
260 metaDataChanged();
261 seekableChanged(seekable: m_playbackEngine->isSeekable());
262
263 audioAvailableChanged(
264 audioAvailable: !m_playbackEngine->streamInfo(trackType: QPlatformMediaPlayer::AudioStream).isEmpty());
265 videoAvailableChanged(
266 videoAvailable: !m_playbackEngine->streamInfo(trackType: QPlatformMediaPlayer::VideoStream).isEmpty());
267
268 mediaStatusChanged(status: QMediaPlayer::LoadedMedia);
269
270 if (m_requestedStatus != QMediaPlayer::StoppedState) {
271 if (m_requestedStatus == QMediaPlayer::PlayingState)
272 play();
273 else if (m_requestedStatus == QMediaPlayer::PausedState)
274 pause();
275 }
276}
277
278void QFFmpegMediaPlayer::play()
279{
280 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
281 m_requestedStatus = QMediaPlayer::PlayingState;
282 return;
283 }
284
285 if (!m_playbackEngine)
286 return;
287
288 if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
289 m_playbackEngine->seek(pos: TrackPosition(0));
290 positionChanged(position: 0);
291 }
292
293 runPlayback();
294}
295
296void QFFmpegMediaPlayer::runPlayback()
297{
298 m_playbackEngine->play();
299 m_positionUpdateTimer.start();
300 stateChanged(newState: QMediaPlayer::PlayingState);
301
302 if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia)
303 mediaStatusChanged(status: QMediaPlayer::BufferingMedia);
304}
305
306void QFFmpegMediaPlayer::pause()
307{
308 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
309 m_requestedStatus = QMediaPlayer::PausedState;
310 return;
311 }
312
313 if (!m_playbackEngine)
314 return;
315
316 if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
317 m_playbackEngine->seek(pos: TrackPosition(0));
318 positionChanged(position: 0);
319 }
320 m_playbackEngine->pause();
321 m_positionUpdateTimer.stop();
322 stateChanged(newState: QMediaPlayer::PausedState);
323
324 if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia)
325 mediaStatusChanged(status: QMediaPlayer::BufferingMedia);
326}
327
328void QFFmpegMediaPlayer::stop()
329{
330 if (mediaStatus() == QMediaPlayer::LoadingMedia) {
331 m_requestedStatus = QMediaPlayer::StoppedState;
332 return;
333 }
334
335 if (!m_playbackEngine)
336 return;
337
338 m_playbackEngine->stop();
339 m_positionUpdateTimer.stop();
340 m_playbackEngine->seek(pos: TrackPosition(0));
341 positionChanged(position: 0);
342 stateChanged(newState: QMediaPlayer::StoppedState);
343 mediaStatusChanged(status: QMediaPlayer::LoadedMedia);
344}
345
346void QFFmpegMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
347{
348 m_audioOutput = output;
349 if (m_playbackEngine)
350 m_playbackEngine->setAudioSink(output);
351}
352
353void QFFmpegMediaPlayer::setAudioBufferOutput(QAudioBufferOutput *output) {
354 m_audioBufferOutput = output;
355 if (m_playbackEngine)
356 m_playbackEngine->setAudioBufferOutput(output);
357}
358
359QMediaMetaData QFFmpegMediaPlayer::metaData() const
360{
361 return m_playbackEngine ? m_playbackEngine->metaData() : QMediaMetaData{};
362}
363
364void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink)
365{
366 m_videoSink = sink;
367 if (m_playbackEngine)
368 m_playbackEngine->setVideoSink(sink);
369}
370
371QVideoSink *QFFmpegMediaPlayer::videoSink() const
372{
373 return m_videoSink;
374}
375
376int QFFmpegMediaPlayer::trackCount(TrackType type)
377{
378 return m_playbackEngine ? m_playbackEngine->streamInfo(trackType: type).count() : 0;
379}
380
381QMediaMetaData QFFmpegMediaPlayer::trackMetaData(TrackType type, int streamNumber)
382{
383 if (!m_playbackEngine || streamNumber < 0
384 || streamNumber >= m_playbackEngine->streamInfo(trackType: type).count())
385 return {};
386 return m_playbackEngine->streamInfo(trackType: type).at(i: streamNumber).metaData;
387}
388
389int QFFmpegMediaPlayer::activeTrack(TrackType type)
390{
391 return m_playbackEngine ? m_playbackEngine->activeTrack(type) : -1;
392}
393
394void QFFmpegMediaPlayer::setActiveTrack(TrackType type, int streamNumber)
395{
396 if (m_playbackEngine)
397 m_playbackEngine->setActiveTrack(type, streamNumber);
398 else
399 qWarning() << "Cannot set active track without open source";
400}
401
402void QFFmpegMediaPlayer::setLoops(int loops)
403{
404 if (m_playbackEngine)
405 m_playbackEngine->setLoops(loops);
406
407 QPlatformMediaPlayer::setLoops(loops);
408}
409
410QT_END_NAMESPACE
411
412#include "moc_qffmpegmediaplayer_p.cpp"
413

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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