/** * SPDX-FileCopyrightText: 2017 (c) Matthieu Gallien * SPDX-FileCopyrightText: 2021-2023 Bart De Vries * * SPDX-License-Identifier: LGPL-3.0-or-later */ #include "audiomanager.h" #include #include #include #include #include #include "audiologging.h" #include "datamanager.h" #include "feed.h" #include "fetcher.h" #include "models/errorlogmodel.h" #include "settingsmanager.h" #include "utils/networkconnectionmanager.h" class AudioManagerPrivate { private: KMediaSession m_player = KMediaSession(QStringLiteral("kasts"), QStringLiteral("org.kde.kasts")); Entry *m_entry = nullptr; bool m_readyToPlay = false; bool m_isSeekable = false; bool m_continuePlayback = false; // sort of lock mutex to prevent updating the player position while changing // sources (which will emit lots of playerPositionChanged signals) bool m_lockPositionSaving = false; // m_pendingSeek is used to indicate whether a seek action is still pending // * -1 corresponds to no seek action pending // * any positive value indicates that a seek to position=m_pendingSeek is // still pending qint64 m_pendingSeek = -1; QTimer *m_sleepTimer = nullptr; qint64 m_sleepTime = -1; qint64 m_remainingSleepTime = -1; bool m_isStreaming = false; friend class AudioManager; }; AudioManager::AudioManager(QObject *parent) : QObject(parent) , d(std::make_unique()) { d->m_player.setMpris2PauseInsteadOfStop(true); connect(&d->m_player, &KMediaSession::currentBackendChanged, this, &AudioManager::currentBackendChanged); connect(&d->m_player, &KMediaSession::mutedChanged, this, &AudioManager::playerMutedChanged); connect(&d->m_player, &KMediaSession::volumeChanged, this, &AudioManager::playerVolumeChanged); connect(&d->m_player, &KMediaSession::sourceChanged, this, &AudioManager::sourceChanged); connect(&d->m_player, &KMediaSession::mediaStatusChanged, this, &AudioManager::statusChanged); connect(&d->m_player, &KMediaSession::mediaStatusChanged, this, &AudioManager::mediaStatusChanged); connect(&d->m_player, &KMediaSession::playbackStateChanged, this, &AudioManager::playbackStateChanged); connect(&d->m_player, &KMediaSession::playbackRateChanged, this, &AudioManager::playbackRateChanged); connect(&d->m_player, &KMediaSession::errorChanged, this, &AudioManager::errorChanged); connect(&d->m_player, &KMediaSession::durationChanged, this, &AudioManager::playerDurationChanged); connect(&d->m_player, &KMediaSession::positionChanged, this, &AudioManager::positionChanged); connect(this, &AudioManager::positionChanged, this, &AudioManager::savePlayPosition); // connect signals for MPRIS2 connect(this, &AudioManager::canSkipForwardChanged, this, [this]() { d->m_player.setCanGoNext(canSkipForward()); }); connect(this, &AudioManager::canSkipBackwardChanged, this, [this]() { d->m_player.setCanGoPrevious(canSkipBackward()); }); connect(&d->m_player, &KMediaSession::nextRequested, this, &AudioManager::skipForward); connect(&d->m_player, &KMediaSession::previousRequested, this, &AudioManager::skipBackward); connect(&d->m_player, &KMediaSession::raiseWindowRequested, this, &AudioManager::raiseWindowRequested); connect(&d->m_player, &KMediaSession::quitRequested, this, &AudioManager::quitRequested); connect(this, &AudioManager::playbackRateChanged, &DataManager::instance(), &DataManager::playbackRateChanged); connect(&DataManager::instance(), &DataManager::queueEntryMoved, this, &AudioManager::canGoNextChanged); connect(&DataManager::instance(), &DataManager::queueEntryAdded, this, &AudioManager::canGoNextChanged); connect(&DataManager::instance(), &DataManager::queueEntryRemoved, this, &AudioManager::canGoNextChanged); // we'll send custom seekableChanged signal to work around possible backend glitches connect(this, &AudioManager::logError, &ErrorLogModel::instance(), &ErrorLogModel::monitorErrorMessages); // Check if an entry was playing when the program was shut down and restore it if (DataManager::instance().lastPlayingEntry() != QStringLiteral("none")) { setEntry(DataManager::instance().getEntry(DataManager::instance().lastPlayingEntry())); } } AudioManager::~AudioManager() = default; QString AudioManager::backendName(KMediaSession::MediaBackends backend) const { qCDebug(kastsAudio) << "AudioManager::backendName()"; return d->m_player.backendName(backend); } KMediaSession::MediaBackends AudioManager::currentBackend() const { qCDebug(kastsAudio) << "AudioManager::currentBackend()"; return d->m_player.currentBackend(); } QList AudioManager::availableBackends() const { qCDebug(kastsAudio) << "AudioManager::availableBackends()"; return d->m_player.availableBackends(); } Entry *AudioManager::entry() const { return d->m_entry; } bool AudioManager::muted() const { return d->m_player.muted(); } qreal AudioManager::volume() const { return d->m_player.volume(); } QUrl AudioManager::source() const { return d->m_player.source(); } KMediaSession::Error AudioManager::error() const { if (d->m_player.error() != KMediaSession::NoError) { qCDebug(kastsAudio) << "AudioManager::error" << d->m_player.error(); // Some error occurred: probably best to unset the lastPlayingEntry to // avoid a deadlock when starting up again. DataManager::instance().setLastPlayingEntry(QStringLiteral("none")); } return d->m_player.error(); } qint64 AudioManager::duration() const { // we fake the duration in case the track has not been properly loaded yet if (!d->m_readyToPlay) { if (d->m_entry && d->m_entry->enclosure()) { return d->m_entry->enclosure()->duration() * 1000; } else { return 0; } } else if (d->m_player.duration() > 0) { return d->m_player.duration(); } else if (d->m_entry && d->m_entry->enclosure()) { return d->m_entry->enclosure()->duration() * 1000; } else { return 0; } } qint64 AudioManager::position() const { // we fake the player position in case there is still a pending seek if (!d->m_readyToPlay) { if (d->m_entry && d->m_entry->enclosure()) { return d->m_entry->enclosure()->playPosition(); } else { return 0; } } else if (d->m_pendingSeek != -1) { return d->m_pendingSeek; } else { return d->m_player.position(); } } bool AudioManager::seekable() const { return d->m_isSeekable; } bool AudioManager::canPlay() const { return (d->m_readyToPlay); } bool AudioManager::canPause() const { return (d->m_readyToPlay); } bool AudioManager::canSkipForward() const { return (d->m_readyToPlay); } bool AudioManager::canSkipBackward() const { return (d->m_readyToPlay); } KMediaSession::PlaybackState AudioManager::playbackState() const { return d->m_player.playbackState(); } qreal AudioManager::playbackRate() const { return d->m_player.playbackRate(); } qreal AudioManager::minimumPlaybackRate() const { return d->m_player.minimumPlaybackRate(); } qreal AudioManager::maximumPlaybackRate() const { return d->m_player.maximumPlaybackRate(); } bool AudioManager::isStreaming() const { return d->m_isStreaming; } KMediaSession::MediaStatus AudioManager::status() const { return d->m_player.mediaStatus(); } void AudioManager::setCurrentBackend(KMediaSession::MediaBackends backend) { qCDebug(kastsAudio) << "AudioManager::setCurrentBackend(" << backend << ")"; KMediaSession::PlaybackState currentState = playbackState(); qint64 currentRate = playbackRate(); d->m_player.setCurrentBackend(backend); setEntry(d->m_entry); if (currentState == KMediaSession::PlaybackState::PlayingState) { play(); } // TODO: Fix restoring the current playback rate setPlaybackRate(currentRate); } void AudioManager::setEntry(Entry *entry) { qCDebug(kastsAudio) << "begin AudioManager::setEntry"; // First unset current track and save playing state, such that any signal // that still fires doesn't operate on the wrong track. // disconnect any pending redirectUrl signals bool signalDisconnect = disconnect(&Fetcher::instance(), &Fetcher::foundRedirectedUrl, this, nullptr); qCDebug(kastsAudio) << "disconnected dangling foundRedirectedUrl signal:" << signalDisconnect; // reset any pending seek action and lock position saving d->m_pendingSeek = -1; d->m_lockPositionSaving = true; Entry *oldEntry = d->m_entry; d->m_entry = nullptr; // Check if the previous track needs to be marked as read if (oldEntry && !signalDisconnect) { qCDebug(kastsAudio) << "Checking previous track"; qCDebug(kastsAudio) << "Left time" << (duration() - position()); qCDebug(kastsAudio) << "MediaStatus" << d->m_player.mediaStatus(); if (((duration() > 0) && (position() > 0) && ((duration() - position()) < SettingsManager::self()->markAsPlayedBeforeEnd() * 1000)) || (d->m_player.mediaStatus() == KMediaSession::EndOfMedia)) { qCDebug(kastsAudio) << "Mark as read:" << oldEntry->title(); oldEntry->enclosure()->setPlayPosition(0); oldEntry->setRead(true); stop(); d->m_continuePlayback = SettingsManager::self()->continuePlayingNextEntry(); } else { bool continuePlaying = d->m_continuePlayback; // saving to local bool because it will be overwritten by the stop action stop(); d->m_continuePlayback = continuePlaying; } } // do some checks on the new entry to see whether it's valid and not corrupted if (entry != nullptr && entry->hasEnclosure() && entry->enclosure() && (entry->enclosure()->status() == Enclosure::Downloaded || NetworkConnectionManager::instance().streamingAllowed())) { qCDebug(kastsAudio) << "Going to change source"; setEntryInfo(entry); if (entry->enclosure()->status() == Enclosure::Downloaded) { // i.e. local file if (d->m_isStreaming) { d->m_isStreaming = false; Q_EMIT isStreamingChanged(); } // call method which will try to make sure that the stream will skip // to the previously save position and make sure that the duration // and position are reported correctly prepareAudio(QUrl::fromLocalFile(entry->enclosure()->path())); } else { // i.e. streaming; we first want to resolve the real URL, following // redirects QUrl loadUrl = QUrl(entry->enclosure()->url()); Fetcher::instance().getRedirectedUrl(loadUrl); connect(&Fetcher::instance(), &Fetcher::foundRedirectedUrl, this, [this, entry, loadUrl](const QUrl &oldUrl, const QUrl &newUrl) { qCDebug(kastsAudio) << oldUrl << newUrl; if (loadUrl == oldUrl) { bool signalDisconnect = disconnect(&Fetcher::instance(), &Fetcher::foundRedirectedUrl, this, nullptr); qCDebug(kastsAudio) << "disconnected" << signalDisconnect; if (!d->m_isStreaming) { d->m_isStreaming = true; Q_EMIT isStreamingChanged(); } d->m_entry = entry; Q_EMIT entryChanged(entry); // call method which will try to make sure that the stream will skip // to the previously save position and make sure that the duration // and position are reported correctly prepareAudio(newUrl); } }); } } else { DataManager::instance().setLastPlayingEntry(QStringLiteral("none")); d->m_entry = nullptr; Q_EMIT entryChanged(nullptr); d->m_player.stop(); d->m_player.setSource(QUrl()); d->m_readyToPlay = false; Q_EMIT durationChanged(0); Q_EMIT positionChanged(0); Q_EMIT canPlayChanged(); Q_EMIT canPauseChanged(); Q_EMIT canSkipForwardChanged(); Q_EMIT canSkipBackwardChanged(); Q_EMIT canGoNextChanged(); d->m_isSeekable = false; Q_EMIT seekableChanged(false); } } void AudioManager::setMuted(bool muted) { d->m_player.setMuted(muted); } void AudioManager::setVolume(qreal volume) { qCDebug(kastsAudio) << "AudioManager::setVolume" << volume; d->m_player.setVolume(qRound(volume)); } void AudioManager::setPosition(qint64 position) { qCDebug(kastsAudio) << "AudioManager::setPosition" << position; seek(position); } void AudioManager::setPlaybackRate(const qreal rate) { qCDebug(kastsAudio) << "AudioManager::setPlaybackRate" << rate; d->m_player.setPlaybackRate(rate); } void AudioManager::play() { qCDebug(kastsAudio) << "AudioManager::play"; // if we're streaming, check that we're still connected and check for metered // connection if (isStreaming()) { if (!NetworkConnectionManager::instance().streamingAllowed()) { if (NetworkConnectionManager::instance().networkReachable()) { qCDebug(kastsAudio) << "Refusing to play: streaming on metered connection not allowed"; QString feedUrl, entryId; if (d->m_entry) { feedUrl = d->m_entry->feed()->url(); entryId = d->m_entry->id(); } Q_EMIT logError(Error::Type::MeteredStreamingNotAllowed, feedUrl, entryId, 0, i18n("Streaming on metered connection not allowed"), QString()); return; } else { qCDebug(kastsAudio) << "Refusing to play: no network connection"; QString feedUrl, entryId; if (d->m_entry) { feedUrl = d->m_entry->feed()->url(); entryId = d->m_entry->id(); } Q_EMIT logError(Error::Type::NoNetwork, feedUrl, entryId, 0, i18n("No network connection"), QString()); return; } } } // setting m_continuePlayback will make sure that, if the audio stream is // still being prepared, that the playback will start once it's ready d->m_continuePlayback = true; if (d->m_readyToPlay) { d->m_player.play(); d->m_isSeekable = true; Q_EMIT seekableChanged(d->m_isSeekable); if (d->m_entry && d->m_entry->getNew()) { d->m_entry->setNew(false); } } } void AudioManager::pause() { qCDebug(kastsAudio) << "AudioManager::pause"; // setting m_continuePlayback will make sure that, if the audio stream is // still being prepared, that the playback will pause once it's ready d->m_continuePlayback = false; if (d->m_readyToPlay) { d->m_isSeekable = true; d->m_player.pause(); } } void AudioManager::playPause() { if (playbackState() == KMediaSession::PlaybackState::PlayingState) { pause(); } else { play(); } } void AudioManager::stop() { qCDebug(kastsAudio) << "AudioManager::stop"; d->m_player.stop(); d->m_continuePlayback = false; d->m_isSeekable = false; Q_EMIT seekableChanged(d->m_isSeekable); } void AudioManager::seek(qint64 position) { qCDebug(kastsAudio) << "AudioManager::seek" << position; // if there is still a pending seek, then we simply update that pending // value, and then manually send the positionChanged signal to have the UI // updated // NOTE: this can also happen while the streaming URL is still resolving, so // we also allow seeking even when the track is not yet readyToPlay. if (d->m_pendingSeek != -1 || !d->m_readyToPlay) { d->m_pendingSeek = position; Q_EMIT positionChanged(position); } else if (d->m_pendingSeek == -1 && d->m_readyToPlay) { d->m_player.setPosition(position); } } void AudioManager::skipForward() { qCDebug(kastsAudio) << "AudioManager::skipForward"; seek(std::min((position() + (1000 * SettingsManager::skipForward())), duration())); } void AudioManager::skipBackward() { qCDebug(kastsAudio) << "AudioManager::skipBackward"; seek(std::max((qint64)0, (position() - (1000 * SettingsManager::skipBackward())))); } bool AudioManager::canGoNext() const { if (d->m_entry) { int index = DataManager::instance().queue().indexOf(d->m_entry->id()); if (index >= 0) { // check if there is a next track if (index < DataManager::instance().queue().count() - 1) { Entry *next_entry = DataManager::instance().getEntry(DataManager::instance().queue()[index + 1]); if (next_entry && next_entry->enclosure()) { qCDebug(kastsAudio) << "Enclosure status" << next_entry->enclosure()->path() << next_entry->enclosure()->status(); if (next_entry->enclosure()->status() == Enclosure::Downloaded) { return true; } else { if (NetworkConnectionManager::instance().streamingAllowed()) { return true; } } } } } } return false; } void AudioManager::next() { if (canGoNext()) { int index = DataManager::instance().queue().indexOf(d->m_entry->id()); qCDebug(kastsAudio) << "Skipping to" << DataManager::instance().queue()[index + 1]; setEntry(DataManager::instance().getEntry(DataManager::instance().queue()[index + 1])); } else { qCDebug(kastsAudio) << "Next track cannot be played, changing entry to nullptr"; setEntry(nullptr); } } void AudioManager::mediaStatusChanged() { qCDebug(kastsAudio) << "AudioManager::mediaStatusChanged" << d->m_player.mediaStatus(); // File has reached the end and has stopped if (d->m_player.mediaStatus() == KMediaSession::EndOfMedia) { next(); } // if there is a problem with the current track, make sure that it's not // loaded again when the application is restarted, skip to next track and // delete the enclosure if (d->m_player.mediaStatus() == KMediaSession::InvalidMedia) { // save pointer to this bad entry to allow // us to delete the enclosure after the track has been unloaded Entry *badEntry = d->m_entry; DataManager::instance().setLastPlayingEntry(QStringLiteral("none")); stop(); next(); if (badEntry && badEntry->enclosure()) { badEntry->enclosure()->deleteFile(); Q_EMIT logError(Error::Type::InvalidMedia, badEntry->feed()->url(), badEntry->id(), KMediaSession::InvalidMedia, i18n("Invalid Media"), QString()); } } } void AudioManager::playerDurationChanged(qint64 duration) { qCDebug(kastsAudio) << "AudioManager::playerDurationChanged" << duration; // Check if duration mentioned in enclosure corresponds to real duration if (d->m_entry && d->m_entry->enclosure()) { if (duration > 0 && (duration / 1000) != d->m_entry->enclosure()->duration()) { qCDebug(kastsAudio) << "Correcting duration of" << d->m_entry->id() << "to" << duration / 1000 << "(was" << d->m_entry->enclosure()->duration() << ")"; d->m_entry->enclosure()->setDuration(duration / 1000); } } qint64 correctedDuration = duration; QTimer::singleShot(0, this, [this, correctedDuration]() { Q_EMIT durationChanged(correctedDuration); }); } void AudioManager::playerVolumeChanged() { qCDebug(kastsAudio) << "AudioManager::playerVolumeChanged" << d->m_player.volume(); QTimer::singleShot(0, this, [this]() { Q_EMIT volumeChanged(); }); } void AudioManager::playerMutedChanged() { qCDebug(kastsAudio) << "AudioManager::playerMutedChanged" << muted(); QTimer::singleShot(0, this, [this]() { Q_EMIT mutedChanged(muted()); }); } void AudioManager::savePlayPosition() { qCDebug(kastsAudio) << "AudioManager::savePlayPosition"; // First check if there is still a pending seek checkForPendingSeek(); if (!d->m_lockPositionSaving) { if (d->m_entry && d->m_entry->enclosure()) { if (d->m_entry->enclosure()) { d->m_entry->enclosure()->setPlayPosition(position()); } } } qCDebug(kastsAudio) << d->m_player.mediaStatus(); } void AudioManager::setEntryInfo(Entry *entry) { // Set info for next track in preparation for the actual audio player to be // set up and configured. We set all the info based on what's in the entry // and disable all the controls until the track is ready to be played d->m_player.setSource(QUrl()); d->m_entry = entry; Q_EMIT entryChanged(entry); qint64 newDuration = entry->enclosure()->duration() * 1000; qint64 newPosition = entry->enclosure()->playPosition(); if (newPosition > newDuration && newPosition < 0) { newPosition = 0; } // Emit positionChanged and durationChanged signals to make sure that // the GUI can see the faked values (if needed) Q_EMIT durationChanged(newDuration); Q_EMIT positionChanged(newPosition); d->m_readyToPlay = false; Q_EMIT canPlayChanged(); Q_EMIT canPauseChanged(); Q_EMIT canSkipForwardChanged(); Q_EMIT canSkipBackwardChanged(); Q_EMIT canGoNextChanged(); d->m_isSeekable = false; Q_EMIT seekableChanged(false); } void AudioManager::prepareAudio(const QUrl &loadUrl) { d->m_player.setSource(loadUrl); // save the current playing track in the settingsfile for restoring on startup DataManager::instance().setLastPlayingEntry(d->m_entry->id()); qCDebug(kastsAudio) << "Changed source to" << d->m_entry->title(); d->m_player.pause(); qint64 newDuration = duration(); qint64 startingPosition = d->m_entry->enclosure()->playPosition(); qCDebug(kastsAudio) << "Changing position to" << startingPosition / 1000 << "sec"; // if a seek is still pending then we don't set the position here // this can happen e.g. if a chapter marker was clicked on a non-playing entry if (d->m_pendingSeek == -1) { if (startingPosition <= newDuration) { d->m_pendingSeek = startingPosition; } else { d->m_pendingSeek = -1; } } // Emit positionChanged and durationChanged signals to make sure that // the GUI can see the faked values (if needed) qint64 newPosition = position(); Q_EMIT durationChanged(newDuration); Q_EMIT positionChanged(newPosition); d->m_readyToPlay = true; Q_EMIT canPlayChanged(); Q_EMIT canPauseChanged(); Q_EMIT canSkipForwardChanged(); Q_EMIT canSkipBackwardChanged(); Q_EMIT canGoNextChanged(); d->m_isSeekable = true; Q_EMIT seekableChanged(true); qCDebug(kastsAudio) << "Duration reported by d->m_player" << d->m_player.duration(); qCDebug(kastsAudio) << "Duration reported by enclosure (in ms)" << d->m_entry->enclosure()->duration() * 1000; qCDebug(kastsAudio) << "Duration reported by AudioManager" << newDuration; qCDebug(kastsAudio) << "Position reported by d->m_player" << d->m_player.position(); qCDebug(kastsAudio) << "Saved position stored in enclosure (in ms)" << startingPosition; qCDebug(kastsAudio) << "Position reported by AudioManager" << newPosition; if (d->m_continuePlayback) { // we call play() and not d->m_player.play() because we want to trigger // things like inhibit suspend play(); } else { pause(); } d->m_lockPositionSaving = false; // set metadata for MPRIS2 updateMetaData(); } void AudioManager::checkForPendingSeek() { qint64 position = d->m_player.position(); qCDebug(kastsAudio) << "Seek pending?" << d->m_pendingSeek; qCDebug(kastsAudio) << "Current position" << position; // FIXME: the LoadedMedia+PlayingState test is a workaround for a broken // qtmultimedia backend; LoadedMedia should never be allowed in the // PlayingState according to docs. Remove this when upstream is fixed. // Check if we're supposed to skip to a new position if (d->m_pendingSeek != -1 && (d->m_player.mediaStatus() == KMediaSession::BufferedMedia || (d->m_player.mediaStatus() == KMediaSession::LoadedMedia && d->m_player.playbackState() == KMediaSession::PlayingState)) && d->m_player.duration() > 0) { if (abs(d->m_pendingSeek - position) > 2000) { qCDebug(kastsAudio) << "Position seek still pending to position" << d->m_pendingSeek; qCDebug(kastsAudio) << "Current reported position and duration" << d->m_player.position() << d->m_player.duration(); // be very careful because this setPosition call will trigger // a positionChanged signal, which will be nested, so we call it in // a QTimer::singleShot if (playbackState() == KMediaSession::PlaybackState::PlayingState) { qint64 seekPosition = d->m_pendingSeek; QTimer::singleShot(0, this, [this, seekPosition]() { d->m_player.setPosition(seekPosition); }); } } else { qCDebug(kastsAudio) << "Pending position seek has been executed; to position" << d->m_pendingSeek; d->m_pendingSeek = -1; // d->m_player.setNotifyInterval(1000); } } } void AudioManager::updateMetaData() { // set metadata for MPRIS2 if (!d->m_entry->title().isEmpty()) { d->m_player.metaData()->setTitle(d->m_entry->title()); } // TODO: set URL?? d->m_entry->enclosure()->path(); if (!d->m_entry->feed()->name().isEmpty()) { d->m_player.metaData()->setAlbum(d->m_entry->feed()->name()); } if (d->m_entry->authors().length() > 0) { d->m_player.metaData()->setArtist(d->m_entry->authors()); } if (!d->m_entry->image().isEmpty()) { d->m_player.metaData()->setArtworkUrl(QUrl(d->m_entry->cachedImage())); } } QString AudioManager::formattedDuration() const { return m_kformat.formatDuration(duration()); } QString AudioManager::formattedLeftDuration() const { qreal rate = 1.0; if (SettingsManager::self()->adjustTimeLeft()) { rate = playbackRate(); rate = (rate > 0.0) ? rate : 1.0; } qint64 diff = duration() - position(); return m_kformat.formatDuration(diff / rate); } QString AudioManager::formattedPosition() const { return m_kformat.formatDuration(position()); } qint64 AudioManager::sleepTime() const { if (d->m_sleepTimer) { return d->m_sleepTime; } else { return -1; } } qint64 AudioManager::remainingSleepTime() const { if (d->m_sleepTimer) { return d->m_remainingSleepTime; } else { return -1; } } void AudioManager::setSleepTimer(qint64 duration) { if (duration > 0) { if (d->m_sleepTimer) { stopSleepTimer(); } d->m_sleepTime = duration; d->m_remainingSleepTime = duration; d->m_sleepTimer = new QTimer(this); connect(d->m_sleepTimer, &QTimer::timeout, this, [this]() { (d->m_remainingSleepTime)--; if (d->m_remainingSleepTime > 0) { Q_EMIT remainingSleepTimeChanged(remainingSleepTime()); } else { pause(); stopSleepTimer(); } }); d->m_sleepTimer->start(1000); Q_EMIT sleepTimerChanged(duration); Q_EMIT remainingSleepTimeChanged(remainingSleepTime()); } else { stopSleepTimer(); } } void AudioManager::stopSleepTimer() { if (d->m_sleepTimer) { d->m_sleepTime = -1; d->m_remainingSleepTime = -1; delete d->m_sleepTimer; d->m_sleepTimer = nullptr; Q_EMIT sleepTimerChanged(-1); Q_EMIT remainingSleepTimeChanged(-1); } } QString AudioManager::formattedRemainingSleepTime() const { qint64 timeLeft = remainingSleepTime() * 1000; if (timeLeft < 0) { timeLeft = 0; } return m_kformat.formatDuration(timeLeft); }