/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/system_media_controls_manager.h" #include "media/audio/media_audio.h" #include "base/platform/base_platform_system_media_controls.h" #include "core/application.h" #include "core/core_settings.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" #include "main/main_account.h" #include "main/main_session.h" #include "media/audio/media_audio.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "ui/text/format_song_document_name.h" #include namespace Media { namespace { [[nodiscard]] auto RepeatModeToLoopStatus(Media::RepeatMode mode) { using Mode = Media::RepeatMode; using Status = base::Platform::SystemMediaControls::LoopStatus; switch (mode) { case Mode::None: return Status::None; case Mode::One: return Status::Track; case Mode::All: return Status::Playlist; } Unexpected("RepeatModeToLoopStatus in SystemMediaControlsManager"); } } // namespace bool SystemMediaControlsManager::Supported() { return base::Platform::SystemMediaControls::Supported(); } SystemMediaControlsManager::SystemMediaControlsManager() : _controls(std::make_unique()) { using PlaybackStatus = base::Platform::SystemMediaControls::PlaybackStatus; using Command = base::Platform::SystemMediaControls::Command; // Flatpak provides default permission to MPRIS, but not snap if (!KSandbox::isFlatpak()) { _controls->setServiceName(u"tdesktop"_q); } _controls->setApplicationName(AppName.utf16()); const auto inited = _controls->init(); if (!inited) { LOG(("SystemMediaControlsManager failed to init.")); return; } const auto type = AudioMsgId::Type::Song; using TrackState = Media::Player::TrackState; const auto mediaPlayer = Media::Player::instance(); auto trackFilter = rpl::filter([=](const TrackState &state) { return (state.id.type() == type); }); mediaPlayer->updatedNotifier( ) | trackFilter | rpl::map([=](const TrackState &state) { using namespace Media::Player; if (_streamed) { const auto &player = _streamed->player(); if (player.buffering() || !player.playing()) { return PlaybackStatus::Paused; } } if (IsStoppedOrStopping(state.state)) { return PlaybackStatus::Stopped; } else if (IsPausedOrPausing(state.state)) { return PlaybackStatus::Paused; } return PlaybackStatus::Playing; }) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](PlaybackStatus status) { _controls->setPlaybackStatus(status); }, _lifetime); rpl::merge( mediaPlayer->stops(type) | rpl::map_to(false), mediaPlayer->startsPlay(type) | rpl::map_to(true) ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool audio) { _controls->setEnabled(audio); if (audio) { _controls->setIsNextEnabled(mediaPlayer->nextAvailable(type)); _controls->setIsPreviousEnabled( mediaPlayer->previousAvailable(type)); _controls->setIsPlayPauseEnabled(true); _controls->setIsStopEnabled(true); _controls->setPlaybackStatus(PlaybackStatus::Playing); _controls->updateDisplay(); } else { _cachedMediaView.clear(); _streamed = nullptr; _controls->clearMetadata(); } _lifetimeDownload.destroy(); }, _lifetime); auto trackChanged = mediaPlayer->trackChanged( ) | rpl::filter([=](AudioMsgId::Type audioType) { return audioType == type; }); auto unlocked = Core::App().passcodeLockChanges( ) | rpl::filter([=](bool locked) { return !locked && (mediaPlayer->current(type)); }) | rpl::map([=] { return type; }) | rpl::before_next([=] { _controls->setEnabled(true); _controls->updateDisplay(); }); rpl::merge( std::move(trackChanged), std::move(unlocked) ) | rpl::start_with_next([=](AudioMsgId::Type audioType) { _lifetimeDownload.destroy(); const auto current = mediaPlayer->current(audioType); if (!current) { return; } if ((_lastAudioMsgId.contextId() == current.contextId()) && (_lastAudioMsgId.audio() == current.audio()) && (_lastAudioMsgId.type() == current.type())) { return; } const auto document = current.audio(); const auto &[title, performer] = Ui::Text::FormatSongNameFor(document) .composedName(); _controls->setArtist(performer); _controls->setTitle(title); if (_controls->seekingSupported()) { const auto state = mediaPlayer->getState(audioType); _controls->setDuration(state.length); // macOS NowPlaying and Linux MPRIS update the track position // according to the rate property // while the playback status is "playing", // so we should change the track position only when // the track is changed // or when the position is changed by the user. _controls->setPosition(state.position); _streamed = std::make_unique( document, current.contextId(), nullptr); } // Setting a thumbnail can take a long time, // so we need to update the display before that. _controls->updateDisplay(); if (document && document->isSongWithCover()) { const auto view = document->createMediaView(); view->thumbnailWanted(current.contextId()); _cachedMediaView.push_back(view); if (const auto imagePtr = view->thumbnail()) { _controls->setThumbnail(imagePtr->original()); } else { document->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { if (const auto imagePtr = view->thumbnail()) { _controls->setThumbnail(imagePtr->original()); _lifetimeDownload.destroy(); } }, _lifetimeDownload); _controls->clearThumbnail(); } } else { _controls->clearThumbnail(); } _lastAudioMsgId = current; }, _lifetime); mediaPlayer->playlistChanges( type ) | rpl::start_with_next([=] { _controls->setIsNextEnabled(mediaPlayer->nextAvailable(type)); _controls->setIsPreviousEnabled(mediaPlayer->previousAvailable(type)); }, _lifetime); using Media::RepeatMode; using Media::OrderMode; Core::App().settings().playerRepeatModeValue( ) | rpl::start_with_next([=](RepeatMode mode) { _controls->setLoopStatus(RepeatModeToLoopStatus(mode)); }, _lifetime); Core::App().settings().playerOrderModeValue( ) | rpl::start_with_next([=](OrderMode mode) { if (mode != OrderMode::Shuffle) { _lastOrderMode = mode; } _controls->setShuffle(mode == OrderMode::Shuffle); }, _lifetime); _controls->commandRequests( ) | rpl::start_with_next([=](Command command) { switch (command) { case Command::PlayPause: mediaPlayer->playPause(type); break; case Command::Play: mediaPlayer->play(type); break; case Command::Pause: mediaPlayer->pause(type); break; case Command::Next: mediaPlayer->next(type); break; case Command::Previous: mediaPlayer->previous(type); break; case Command::Stop: mediaPlayer->stop(type); break; case Command::Raise: Core::App().activate(); break; case Command::LoopNone: { Core::App().settings().setPlayerRepeatMode(RepeatMode::None); Core::App().saveSettingsDelayed(); break; } case Command::LoopTrack: { Core::App().settings().setPlayerRepeatMode(RepeatMode::One); Core::App().saveSettingsDelayed(); break; } case Command::LoopPlaylist: { Core::App().settings().setPlayerRepeatMode(RepeatMode::All); Core::App().saveSettingsDelayed(); break; } case Command::Shuffle: { const auto current = Core::App().settings().playerOrderMode(); Core::App().settings().setPlayerOrderMode((current == OrderMode::Shuffle) ? _lastOrderMode : OrderMode::Shuffle); Core::App().saveSettingsDelayed(); break; } case Command::Quit: { Media::Player::instance()->stopAndClose(); break; } } }, _lifetime); if (_controls->seekingSupported()) { mediaPlayer->seekingChanges( type ) | rpl::filter([](Media::Player::Instance::Seeking seeking) { return (seeking == Media::Player::Instance::Seeking::Finish); }) | rpl::map([=] { return mediaPlayer->getState(type).position; }) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](int position) { _controls->setPosition(position); _controls->updateDisplay(); }, _lifetime); _controls->seekRequests( ) | rpl::start_with_next([=](float64 progress) { mediaPlayer->finishSeeking(type, progress); }, _lifetime); _controls->updatePositionRequests( ) | rpl::start_with_next([=] { _controls->setPosition(mediaPlayer->getState(type).position); }, _lifetime); } Core::App().passcodeLockValue( ) | rpl::filter([=](bool locked) { return locked && Core::App().maybePrimarySession(); }) | rpl::start_with_next([=] { _controls->setEnabled(false); }, _lifetime); if (_controls->volumeSupported()) { rpl::single( Core::App().settings().songVolume() ) | rpl::then( Core::App().settings().songVolumeChanges() ) | rpl::start_with_next([=](float64 volume) { _controls->setVolume(volume); }, _lifetime); _controls->volumeChangeRequests( ) | rpl::start_with_next([](float64 volume) { Player::mixer()->setSongVolume(volume); if (volume > 0) { Core::App().settings().setRememberedSongVolume(volume); } Core::App().settings().setSongVolume(volume); }, _lifetime); } } SystemMediaControlsManager::~SystemMediaControlsManager() = default; } // namespace Media