Music Hub ..
A session-wide music playback service
Loading...
Searching...
No Matches
player_implementation.cpp
Go to the documentation of this file.
1/*
2 * Copyright © 2013-2015 Canonical Ltd.
3 * Copyright © 2022 UBports Foundation.
4 *
5 * Contact: Alberto Mardegan <mardy@users.sourceforge.net>
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU Lesser General Public License version 3,
9 * as published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 * Authored by: Thomas Voß <thomas.voss@canonical.com>
20 * Jim Hodapp <jim.hodapp@canonical.com>
21 */
22
24
25#include <unistd.h>
26#include <ctime>
27
29#include "engine.h"
30#include "logging.h"
32#include "xesam.h"
33
34#include <QDateTime>
35#include <QTimer>
36#include <QVector>
37
38#include "gstreamer/engine.h"
39
40#include <memory>
41#include <exception>
42#include <mutex>
43
45
46using namespace std;
47using namespace media;
48
49namespace lomiri {
50namespace MediaHubService {
51
53{
54 Q_DECLARE_PUBLIC(PlayerImplementation)
55
56public:
64
68
70 {
71 /*
72 * Wakelock state logic:
73 * PLAYING->READY or PLAYING->PAUSED or PLAYING->STOPPED: delay 4 seconds and try to clear current wakelock type
74 * ANY STATE->PLAYING: request a new wakelock (system or display)
75 */
76 MH_DEBUG() << "Setting state:" << state;
77 switch(state)
78 {
80 {
82 {
83 m_wakeLockTimer.start();
84 }
85 break;
86 }
88 {
89 // We update the track metadata prior to updating the playback status.
90 // Some MPRIS clients expect this order of events.
91 const auto trackMetadata = m_engine->trackMetadata();
92 media::Track::MetaData metadata = trackMetadata.second;
93 // Setting this with second resolution makes sure that the track_meta_data property changes
94 // and thus the track_meta_data().changed() signal gets sent out over dbus. Otherwise the
95 // Property caching mechanism would prevent this.
96 metadata.setLastUsed(QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
97 update_mpris_metadata(trackMetadata.first, metadata);
98
99 MH_INFO("Requesting power state");
101 /* If this is part of a playlist, the wakelock timer was started
102 * when the previous track completed. So, we must stop it now or it
103 * will release the power state while we are playing. */
104 m_wakeLockTimer.stop();
105 break;
106 }
108 {
110 {
111 m_wakeLockTimer.start();
112 }
113 break;
114 }
116 {
118 {
119 m_wakeLockTimer.start();
120 }
121 break;
122 }
123 default:
124 break;
125 };
126
127 // Keep track of the previous Engine playback state:
128 previous_state = state;
129 }
130
132 {
134 MH_TRACE("");
135 if (q->isVideoSource())
136 {
137 MH_INFO("Request display on.");
138 if (!m_holdsDisplayOn) {
139 power_state_controller->requestDisplayOn();
140 m_holdsDisplayOn = true;
141 }
142 }
143 else
144 {
145 MH_INFO("Request system state active.");
146 if (!m_holdsSystemActive) {
148 m_holdsSystemActive = true;
149 }
150 }
151 }
152
153 void clear_wakelock(const wakelock_clear_t &wakelock)
154 {
155 MH_TRACE("");
156 switch (wakelock)
157 {
159 break;
161 MH_INFO("Release system state active.");
164 m_holdsSystemActive = false;
165 }
166 break;
168 MH_INFO("Release display on.");
169 if (m_holdsDisplayOn) {
170 power_state_controller->releaseDisplayOn();
171 m_holdsDisplayOn = false;
172 }
173 break;
175 default:
176 MH_WARNING("Can't clear invalid wakelock type");
177 }
178 }
179
181 {
182 // Clear both types of wakelocks (display and system)
185 }
186
188 {
190 m_engine->reset();
191
192 // If the client disconnects, make sure both wakelock types
193 // are cleared
195 m_trackList->reset();
196
197 // And tell the outside world that the client has gone away
198 Q_EMIT q->clientDisconnected();
199 }
200
202 {
203 const QUrl uri = m_trackList->query_uri_for_track(id);
204 if (!uri.isEmpty())
205 {
206 // Using a TrackList for playback, added tracks via add_track(), but open_uri hasn't
207 // been called yet to load a media resource
208 MH_INFO("Calling d->m_engine->open_resource_for_uri() for first track added only: %s",
209 qUtf8Printable(uri.toString()));
210 MH_INFO("\twith a Track::Id: %s", qUtf8Printable(id));
211 static const bool do_pipeline_reset = true;
212 m_engine->open_resource_for_uri(uri, do_pipeline_reset);
213 }
214 }
215
217 {
219 const bool has_previous = m_trackList->hasPrevious()
220 or m_trackList->loopStatus() != Player::LoopStatus::none;
221 const bool has_next = m_trackList->hasNext()
222 or m_trackList->loopStatus() != Player::LoopStatus::none;
223 const auto n_tracks = m_trackList->tracks().count();
224 const bool has_tracks = (n_tracks > 0) ? true : false;
225
226 MH_INFO("Updating MPRIS TrackList properties:");
227 MH_INFO("\tTracks: %d", n_tracks);
228 MH_INFO("\thas_previous: %d", has_previous);
229 MH_INFO("\thas_next: %d", has_next);
230
231 // Update properties
232 m_canPlay = has_tracks;
233 m_canPause = has_tracks;
234 m_canGoPrevious = has_previous;
235 m_canGoNext = has_next;
236 Q_EMIT q->mprisPropertiesChanged();
237 }
238
239 QUrl get_uri_for_album_artwork(const QUrl &uri,
240 const media::Track::MetaData& metadata)
241 {
242 QUrl art_uri;
243 bool is_local_file = false;
244 if (not uri.isEmpty())
245 is_local_file = uri.isLocalFile();
246
247 // If the track has a full image or preview image or is a video and it is a local file,
248 // then use the thumbnailer cache
249 if ( (metadata.value(tags::PreviewImage::name).toBool()
250 or (metadata.value(tags::Image::name).toBool())
251 or m_engine->isVideoSource())
252 and is_local_file)
253 {
254 art_uri = "image://thumbnailer/" + uri.path();
255 }
256 // If all else fails, display a placeholder icon
257 else
258 {
259 art_uri = "file:///usr/share/icons/suru/apps/scalable/music-app-symbolic.svg";
260 }
261
262 return art_uri;
263 }
264
265 // Makes sure all relevant metadata fields are set to current data and
266 // will trigger the track_meta_data().changed() signal to go out over dbus
267 void update_mpris_metadata(const QUrl &uri, const media::Track::MetaData &md)
268 {
270 media::Track::MetaData metadata{md};
272 {
273 const QString current_track = m_trackList->current();
274 if (not current_track.isEmpty())
275 {
276 const int last_slash = current_track.lastIndexOf('/');
277 const QStringRef track_id = current_track.midRef(last_slash + 1);
278 if (not track_id.isEmpty())
279 metadata.setTrackId("/org/mpris/MediaPlayer2/Track/" + track_id);
280 else
281 MH_WARNING("Failed to set MPRIS track id since the id value is NULL");
282 }
283 else
284 MH_WARNING("Failed to set MPRIS track id since the id value is NULL");
285 }
286
287 if (not metadata.isSet(xesam::Title::name) or metadata.title().isEmpty())
288 metadata.setTitle(uri.fileName());
289
291 metadata.setArtUrl(get_uri_for_album_artwork(uri, metadata));
292
294 {
295 // Duration is in nanoseconds, MPRIS spec requires microseconds
296 metadata.setTrackLength(m_engine->duration() / 1000);
297 }
298
299 // not needed, and change frequently:
300 metadata.remove(QStringLiteral("bitrate"));
301 metadata.remove(QStringLiteral("minimum-bitrate"));
302 metadata.remove(QStringLiteral("maximum-bitrate"));
303
304 m_metadataForCurrentTrack = metadata;
305 Q_EMIT q->metadataForCurrentTrackChanged();
306 }
307
309 {
310 return m_engine->audioStreamRole() == media::Player::AudioStreamRole::multimedia;
311 }
312
316
317 QScopedPointer<Engine> m_engine;
318 QSharedPointer<TrackListImplementation> m_trackList;
320 bool m_holdsDisplayOn = false;
322 std::atomic<bool> doing_abandon;
323 // Initialize default values for Player interface properties
324 bool m_canPlay = false;
325 bool m_canPause = false;
326 bool m_canGoPrevious = false;
327 bool m_canGoNext = false;
328 bool m_shuffle = false;
329 double m_playbackRate = 1.f;
331 int64_t m_position = 0;
332 int64_t m_duration = 0;
333 bool m_doingOpenUri = false;
340};
341
342}} // namespace
343
347 m_client(config.client),
348 m_clientDeathObserver(config.client_death_observer),
349 power_state_controller(media::power::StateController::instance()),
350 m_engine(new gstreamer::Engine(config.client.key)),
351 // TODO: set the path on the TrackListSkeleton!
352 // dbus::types::ObjectPath(config.parent.session->path().as_string() + "/TrackList")),
353 m_trackList(QSharedPointer<TrackListImplementation>::create(
354 m_engine->metadataExtractor())),
356 doing_abandon(false),
357 q_ptr(q)
358{
359 // Poor man's logging of release/acquire events.
360 QObject::connect(power_state_controller.data(),
362 q, []() {
363 MH_INFO("Acquired display ON state");
364 });
365
366 QObject::connect(power_state_controller.data(),
368 q, []() {
369 MH_INFO("Released display ON state");
370 });
371
372 QObject::connect(power_state_controller.data(),
374 q, [](media::power::SystemState state)
375 {
376 MH_INFO() << "Acquired new system state:" << state;
377 });
378
379 QObject::connect(power_state_controller.data(),
381 q, [](media::power::SystemState state)
382 {
383 MH_INFO() << "Released system state:" << state;
384 });
385
386 QObject::connect(m_engine.data(), &Engine::stateChanged,
387 q, [this]() {
388 onStateChanged(m_engine->state());
389 });
390
391 QObject::connect(m_engine.data(), &Engine::isVideoSourceChanged,
392 q, [this, q]() {
393 // Video streams on remote media are detected only after the playback
394 // has started; in that case, when the playback started we only
395 // requested the system wakelock, and now (if we are in "playing" state
396 // and the media has a video stream) we need to request the display
397 // lock too.
398 if (m_engine->state() == Engine::State::playing) {
399 MH_INFO("Streams changed, updating power/display locks");
400 request_power_state();
401 }
402 Q_EMIT q->isVideoSourceChanged();
403 });
404 QObject::connect(m_engine.data(), &Engine::isAudioSourceChanged,
406
407 // Initialize default values for Player interface properties
408 m_engine->setAudioStreamRole(Player::AudioStreamRole::multimedia);
409 m_engine->setLifetime(Player::Lifetime::normal);
410
411 // Make sure that the Position property gets updated from the Engine
412 // every time the client requests position
413 QObject::connect(m_engine.data(), &Engine::positionChanged,
414 q, [this, q]() {
415 m_trackList->setCurrentPosition(m_engine->position());
416 Q_EMIT q->positionChanged();
417 });
418
419 // Make sure that the Duration property gets updated from the Engine
420 // every time the client requests duration
421 QObject::connect(m_engine.data(), &Engine::durationChanged,
423
424 // When the value of the orientation Property is changed in the Engine by playbin,
425 // update the Player's cached value
426 QObject::connect(m_engine.data(), &Engine::orientationChanged,
428
429 QObject::connect(m_engine.data(), &Engine::trackMetadataChanged,
430 q, [this]() {
431 const auto md = m_engine->trackMetadata();
432 update_mpris_metadata(md.first, md.second);
433 });
434
435 QObject::connect(m_engine.data(), &Engine::endOfStream,
436 q, [q, this]()
437 {
438 if (doing_abandon)
439 return;
440
441 Q_EMIT q->endOfStream();
442
443 // Make sure that the TrackList keeps advancing. The logic for what gets played next,
444 // if anything at all, occurs in TrackListSkeleton::next()
445 m_trackList->next();
446 });
447
448 QObject::connect(m_engine.data(), &Engine::clientDisconnected,
449 q, [this]()
450 {
451 on_client_died();
452 });
453
454 QObject::connect(m_engine.data(), &Engine::seekedTo,
456 QObject::connect(m_engine.data(), &Engine::bufferingChanged,
458 QObject::connect(m_engine.data(), &Engine::playbackStatusChanged,
460 QObject::connect(m_engine.data(), &Engine::aboutToFinish,
462 QObject::connect(m_engine.data(), &Engine::videoDimensionChanged,
464 QObject::connect(m_engine.data(), &Engine::errorOccurred,
466
467 QObject::connect(m_trackList.data(), &TrackListImplementation::endOfTrackList,
468 q, [this]()
469 {
470 if (m_engine->state() != gstreamer::Engine::State::stopped)
471 {
472 MH_INFO("End of tracklist reached, stopping playback");
473 m_engine->stop();
474 }
475 });
476
477 QObject::connect(m_trackList.data(), &TrackListImplementation::onGoToTrack,
478 q, [this](const media::Track::Id &id)
479 {
480 // Store whether we should restore the current playing state after loading the new uri
481 const bool auto_play = m_engine->playbackStatus() == media::Player::playing;
482
483 const QUrl uri = m_trackList->query_uri_for_track(id);
484 if (!uri.isEmpty())
485 {
486 MH_INFO("Setting next track on playbin (on_go_to_track signal): %s",
487 qUtf8Printable(uri.toString()));
488 MH_INFO("\twith a Track::Id: %s", qUtf8Printable(id));
489 static const bool do_pipeline_reset = true;
490 m_engine->open_resource_for_uri(uri, do_pipeline_reset);
491 }
492
493 if (auto_play)
494 {
495 MH_DEBUG("Restoring playing state");
496 m_engine->play();
497 }
498 });
499
500 QObject::connect(m_trackList.data(), &TrackListImplementation::trackAdded,
501 q, [this](const media::Track::Id &id)
502 {
503 MH_TRACE("** Track was added, handling in PlayerImplementation");
504 if (!m_doingOpenUri && m_trackList->tracks().count() == 1)
505 open_first_track_from_tracklist(id);
506
507 update_mpris_properties();
508 });
509
510 QObject::connect(m_trackList.data(), &TrackListImplementation::tracksAdded,
511 q, [this](const QVector<QUrl> &tracks)
512 {
513 MH_TRACE("** Track was added, handling in PlayerImplementation");
514 // If the two sizes are the same, that means the TrackList was previously empty and we need
515 // to open the first track in the TrackList so that is_audio_source() and is_video_source()
516 // will function correctly.
517 /* FIXME: we are passing a URL to a method expecting a track ID, so
518 * this will not work; on the other hand, the code has always been like
519 * this, so let's fix it later. */
520 if (not tracks.isEmpty() and m_trackList->tracks().count() == tracks.count())
521 open_first_track_from_tracklist(tracks.front().toString());
522
523 update_mpris_properties();
524 });
525
526 QObject::connect(m_trackList.data(),
528 q, [this]() { update_mpris_properties(); });
529
530 QObject::connect(m_trackList.data(),
532 q, [this]() { update_mpris_properties(); });
533
534 QObject::connect(m_trackList.data(),
536 q, [this]() { update_mpris_properties(); });
537
538 QObject::connect(m_trackList.data(),
540 q, [this]() { update_mpris_properties(); });
541
542 // Everything is setup, we now subscribe to death notifications.
543 m_clientDeathObserver->registerForDeathNotifications(m_client);
544 QObject::connect(m_clientDeathObserver.data(),
546 q, [this](const media::Player::Client &died)
547 {
548 if (doing_abandon)
549 return;
550
551 if (died.key != m_client.key)
552 return;
553
554 m_abandonTimer.start();
555 });
556
557 m_abandonTimer.setSingleShot(true);
558 m_abandonTimer.setInterval(1000);
559 m_abandonTimer.callOnTimeout(q, [this]() { on_client_died(); });
560
561 m_wakeLockTimer.setSingleShot(true);
562 int wakelockTimeout =
563 qEnvironmentVariableIsSet("MEDIA_HUB_WAKELOCK_TIMEOUT") ?
564 qEnvironmentVariableIntValue("MEDIA_HUB_WAKELOCK_TIMEOUT") : 4000;
565 m_wakeLockTimer.setInterval(wakelockTimeout);
566 m_wakeLockTimer.setTimerType(Qt::VeryCoarseTimer);
567 m_wakeLockTimer.callOnTimeout(q, [this]() {
568 clear_wakelocks();
569 });
570}
571
573{
574 // Make sure that we don't hold on to the wakelocks if media-hub-server
575 // ever gets restarted manually or automatically
577}
578
580 QObject *parent):
581 QObject(parent),
582 d_ptr(new media::PlayerImplementationPrivate(config, this))
583{
584 QObject::connect(this, &QObject::objectNameChanged,
585 this, [this](const QString &name) {
587 d->m_trackList->setObjectName(name + "/TrackList");
588 });
589}
590
594
599
601{
602 Q_D(const PlayerImplementation);
603 return d->m_client;
604}
605
607{
608 Q_D(const PlayerImplementation);
609 return d->m_canPlay;
610}
611
613{
614 Q_D(const PlayerImplementation);
615 return d->m_canPause;
616}
617
619{
620 Q_D(const PlayerImplementation);
621 return true;
622}
623
625{
626 Q_D(const PlayerImplementation);
627 return d->m_canGoPrevious;
628}
629
631{
632 Q_D(const PlayerImplementation);
633 return d->m_canGoNext;
634}
635
637{
638 Q_UNUSED(rate);
639 MH_WARNING("Setting playback rate not implemented");
640}
641
643{
644 return 1.0;
645}
646
648{
649 return 1.0;
650}
651
653{
654 return 1.0;
655}
656
658{
660 MH_INFO() << "LoopStatus:" << status;
661 d->m_trackList->setLoopStatus(status);
662}
663
665{
666 Q_D(const PlayerImplementation);
667 return d->m_trackList->loopStatus();
668}
669
671{
673 d->m_trackList->setShuffle(shuffle);
674}
675
677{
678 Q_D(const PlayerImplementation);
679 return d->m_trackList->shuffle();
680}
681
683{
685 d->m_engine->setVolume(volume);
686 Q_EMIT volumeChanged();
687}
688
690{
691 Q_D(const PlayerImplementation);
692 return d->m_engine->volume();
693}
694
696{
697 Q_D(const PlayerImplementation);
698 return d->m_engine->playbackStatus();
699}
700
702{
703 Q_D(const PlayerImplementation);
704 return d->m_engine->isVideoSource();
705}
706
708{
709 Q_D(const PlayerImplementation);
710 return d->m_engine->isAudioSource();
711}
712
714{
715 Q_D(const PlayerImplementation);
716 return d->m_engine->videoDimension();
717}
718
720{
721 Q_D(const PlayerImplementation);
722 return d->m_engine->orientation();
723}
724
726{
727 Q_D(const PlayerImplementation);
728 return d->m_metadataForCurrentTrack;
729}
730
732{
733 Q_D(const PlayerImplementation);
734 return d->m_engine->position();
735}
736
738{
739 Q_D(const PlayerImplementation);
740 return d->m_engine->duration();
741}
742
744{
746 d->m_engine->setAudioStreamRole(role);
747}
748
750{
751 Q_D(const PlayerImplementation);
752 return d->m_engine->audioStreamRole();
753}
754
756{
758 d->m_engine->setLifetime(lifetime);
759}
760
762{
763 Q_D(const PlayerImplementation);
764 return d->m_engine->lifetime();
765}
766
768{
770 d->m_clientDeathObserver->registerForDeathNotifications(d->m_client);
771}
772
774{
776 // Signal client disconnection due to abandonment of player
777 d->doing_abandon = true;
778 d->on_client_died();
779}
780
781QSharedPointer<TrackListImplementation> media::PlayerImplementation::trackList()
782{
784 return d->m_trackList;
785}
786
788{
789 Q_D(const PlayerImplementation);
790 return d->m_client.key;
791}
792
794{
796 d->m_engine->create_video_sink(texture_id);
797}
798
800{
801 return open_uri(uri, Headers());
802}
803
804bool PlayerImplementation::open_uri(const QUrl &uri, const Headers &headers)
805{
807 d->m_doingOpenUri = true;
808 d->m_trackList->reset();
809
810 // If empty uri, give the same meaning as QMediaPlayer::setMedia("")
811 if (uri.isEmpty())
812 {
813 MH_DEBUG("Resetting current media");
814 return true;
815 }
816
817 const bool ret = d->m_engine->open_resource_for_uri(uri, headers);
818 // Don't set new track as the current track to play since we're calling open_resource_for_uri above
819 static const bool make_current = false;
820 d->m_trackList->add_track_with_uri_at(uri, TrackListImplementation::afterEmptyTrack(), make_current);
821 d->m_doingOpenUri = false;
822
823 return ret;
824}
825
827{
829 d->m_trackList->next();
830}
831
833{
835 d->m_trackList->previous();
836}
837
839{
840 MH_TRACE("");
842 if (d->is_multimedia_role())
843 {
844 Q_EMIT playbackRequested();
845 }
846
847 d->m_engine->play();
848}
849
851{
852 MH_TRACE("");
854 d->m_engine->pause();
855}
856
858{
859 MH_TRACE("");
861 d->m_engine->stop();
862}
863
864void PlayerImplementation::seek_to(const std::chrono::microseconds& ms)
865{
867 d->m_engine->seek_to(ms);
868}
QSharedPointer< ClientDeathObserver > Ptr
void clientDied(const Player::Client &client)
void seekedTo(uint64_t offset)
void errorOccurred(Player::Error error)
QUrl get_uri_for_album_artwork(const QUrl &uri, const media::Track::MetaData &metadata)
PlayerImplementationPrivate(const media::PlayerImplementation::Configuration &config, PlayerImplementation *q)
void update_mpris_metadata(const QUrl &uri, const media::Track::MetaData &md)
QSharedPointer< TrackListImplementation > m_trackList
void create_gl_texture_video_sink(std::uint32_t texture_id)
void setAudioStreamRole(Player::AudioStreamRole role)
void seek_to(const std::chrono::microseconds &offset)
QSharedPointer< TrackListImplementation > trackList()
PlayerImplementation(const Configuration &configuration, QObject *parent=nullptr)
void tracksAdded(const QVector< QUrl > &tracks)
void trackListReplaced(const QVector< Track::Id > &tracks, const Track::Id &currentTrack)
bool isSet(const QString &key) const
Definition track.h:51
static constexpr const char * TrackLengthKey
Definition track.h:42
void setLastUsed(const QString &datetime)
static constexpr const char * TrackIdKey
Definition track.h:43
static constexpr const char * TrackArtlUrlKey
Definition track.h:41
#define MH_TRACE(...)
Definition logging.h:37
#define MH_INFO(...)
Definition logging.h:39
#define MH_WARNING(...)
Definition logging.h:40
#define MH_DEBUG(...)
Definition logging.h:38
static Backend get_backend_type()
Returns the type of audio/video decoding/encoding backend being used.
Definition backend.cpp:28