More mpris async changes

This commit is contained in:
Mathew-D
2026-05-10 16:18:07 -04:00
parent 0508037b8a
commit e6967d1a25
4 changed files with 431 additions and 299 deletions
+385 -271
View File
@@ -54,20 +54,6 @@ namespace {
return loop_status == "None" || loop_status == "Track" || loop_status == "Playlist";
}
template <typename T>
T get_property_or(sdbus::IProxy& proxy, std::string_view interface_name, std::string_view property_name, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(interface_name);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
std::map<std::string, sdbus::Variant> get_metadata_or(sdbus::IProxy& proxy) {
return get_property_or(proxy, k_mpris_player_interface, "Metadata", std::map<std::string, sdbus::Variant>{});
}
std::string get_string_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
@@ -673,6 +659,49 @@ void MprisService::registerIpc(IpcService& ipc) {
"media <next|previous|toggle>", "Control active media playback");
}
auto MprisService::makeAsyncReplyHandler(std::string op, std::string busName) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
return [this, aliveGuard, op = std::move(op), busName = std::move(busName)](std::optional<sdbus::Error> err) {
if (aliveGuard.expired()) {
return;
}
if (err.has_value()) {
kLog.warn("{} failed name={} err={}", op, busName, err->what());
return;
}
DeferredCall::callLater([this, aliveGuard, busName]() {
if (aliveGuard.expired()) {
return;
}
addOrRefreshPlayer(busName);
});
};
}
auto MprisService::makeAsyncReplyHandler(std::string op, std::string busName, std::string_view method) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
return [this, aliveGuard, op = std::move(op), busName = std::move(busName),
method = std::string(method)](std::optional<sdbus::Error> err) {
if (aliveGuard.expired()) {
return;
}
if (err.has_value()) {
kLog.warn("{} failed name={} method={} err={}", op, busName, method, err->what());
return;
}
kLog.debug("{} name={} method={}", op, busName, method);
DeferredCall::callLater([this, aliveGuard, busName]() {
if (aliveGuard.expired()) {
return;
}
addOrRefreshPlayer(busName);
});
};
}
bool MprisService::playPause(const std::string& busName) {
const auto it = m_players.find(busName);
if (it == m_players.end()) {
@@ -1316,23 +1345,56 @@ void MprisService::registerBusSignals() {
}
void MprisService::discoverPlayers() {
std::vector<std::string> names;
try {
m_dbusProxy->callMethod("ListNames").onInterface(k_dbus_interface).storeResultsTo(names);
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
m_dbusProxy->callMethodAsync("ListNames")
.onInterface(k_dbus_interface)
.uponReplyInvoke([this, aliveGuard](std::optional<sdbus::Error> err, std::vector<std::string> names) {
if (aliveGuard.expired()) {
return;
}
if (err.has_value()) {
kLog.warn("discover players failed err={}", err->what());
scheduleRecoveryDiscovery();
return;
}
m_pendingDiscoveryBusNames.clear();
for (const auto& name : names) {
if (is_mpris_bus_name(name)) {
m_pendingDiscoveryBusNames.push_back(name);
}
}
scheduleDiscoveryDrain();
});
} catch (const sdbus::Error& e) {
kLog.warn("discover players failed err={}", e.what());
scheduleRecoveryDiscovery();
return;
}
}
for (const auto& name : names) {
if (is_mpris_bus_name(name)) {
// kLog.debug("discover found mpris bus={}", name);
addOrRefreshPlayer(name);
}
void MprisService::scheduleDiscoveryDrain() {
if (m_discoveryDrainScheduled || m_pendingDiscoveryBusNames.empty()) {
return;
}
// kLog.debug("discover players listed={} cached_after={}", names.size(), m_players.size());
m_discoveryDrainScheduled = true;
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
m_discoveryDrainScheduled = false;
if (aliveGuard.expired() || m_pendingDiscoveryBusNames.empty()) {
return;
}
const std::string busName = std::move(m_pendingDiscoveryBusNames.front());
m_pendingDiscoveryBusNames.pop_front();
addOrRefreshPlayer(busName);
if (!m_pendingDiscoveryBusNames.empty()) {
scheduleDiscoveryDrain();
}
});
}
void MprisService::scheduleStartupRediscovery() {
@@ -1340,7 +1402,11 @@ void MprisService::scheduleStartupRediscovery() {
return;
}
DeferredCall::callLater([this]() {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired()) {
return;
}
discoverPlayers();
--m_startupRediscoveryPassesRemaining;
if (m_startupRediscoveryPassesRemaining > 0) {
@@ -1355,7 +1421,11 @@ void MprisService::scheduleRecoveryDiscovery() {
}
m_recoveryDiscoveryScheduled = true;
DeferredCall::callLater([this]() {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired()) {
return;
}
m_recoveryDiscoveryScheduled = false;
discoverPlayers();
});
@@ -1458,228 +1528,286 @@ void MprisService::addOrRefreshPlayer(const std::string& busName) {
});
}
const bool hadPositionSignal = m_pendingPositionSignalRefresh[busName];
m_pendingPositionSignalRefresh[busName] = false;
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
try {
const bool hadPositionSignal = m_pendingPositionSignalRefresh[busName];
m_pendingPositionSignalRefresh[busName] = false;
const MprisPlayerInfo info = readPlayerInfo(*proxyIt->second, busName);
const auto now = std::chrono::steady_clock::now();
// kLog.debug(
// "queried player name={} identity=\"{}\" status=\"{}\" title=\"{}\" artist=\"{}\" track_id=\"{}\"
// art_url=\"{}\"", info.busName, info.identity, info.playbackStatus, info.title, primary_artist(info.artists),
// info.trackId, info.artUrl);
if (info.artUrl.empty()) {
const auto metadata = get_metadata_or(*proxyIt->second);
// kLog.debug("queried player missing art url name={} metadata_keys=[{}]", info.busName, joinKeys(metadata));
}
if (info.playbackStatus == "Playing") {
m_lastActivePlayer = busName;
m_lastPlayingUpdate[busName] = now;
}
if (hasStrongNowPlayingMetadata(info)) {
m_lastStrongMetadataUpdate[busName] = now;
}
const auto existing = m_players.find(busName);
if (existing == m_players.end()) {
MprisPlayerInfo initial = info;
if (!hadPositionSignal) {
initial.positionUs = 0;
}
m_logicalTrackSignatures[busName] = logicalTrackSignature(initial);
m_positionOffsetsUs[busName] = 0;
m_lastLogicalTrackChangeAt[busName] = now;
m_lastPositionSampleAt[busName] = now;
m_hasAuthoritativePositionSample[busName] = false;
m_previousTrackRawPositionUs.erase(busName);
m_pendingPositionCandidateUs.erase(busName);
m_pendingPositionCandidateMatches.erase(busName);
m_pendingPositionCandidateAt.erase(busName);
// kLog.debug("added player name={} identity=\"{}\" status={} title=\"{}\" artist=\"{}\" art_url=\"{}\"",
// info.busName, info.identity, info.playbackStatus, info.title, primary_artist(info.artists),
// info.artUrl);
m_players.emplace(busName, std::move(initial));
emitPlayersChanged();
syncSignals(previousActive);
if (m_changeCallback) {
m_changeCallback();
}
if (info.playbackStatus != "Stopped" || info.positionUs > 0) {
DeferredCall::callLater([this, busName]() { refreshPlayerPosition(busName, true); });
auto& timerId = m_positionResyncTimers[busName];
timerId = TimerManager::instance().start(timerId, k_position_retry_interval,
[this, busName]() { refreshPlayerPosition(busName, true); });
}
return;
}
if (existing->second != info) {
const MprisPlayerInfo previous_info = existing->second;
MprisPlayerInfo merged = info;
if (merged.artUrl.empty() && !previous_info.artUrl.empty()) {
merged.artUrl = previous_info.artUrl;
}
const bool incomingSnapshotEmpty = merged.playbackStatus.empty() && merged.trackId.empty() &&
merged.title.empty() && merged.artists.empty() && merged.album.empty() &&
merged.sourceUrl.empty() && merged.artUrl.empty() && merged.lengthUs == 0;
if (incomingSnapshotEmpty) {
merged = previous_info;
}
const bool previousStrong = hasStrongNowPlayingMetadata(previous_info) || !previous_info.artUrl.empty();
const bool incomingWeak = !hasStrongNowPlayingMetadata(info);
const auto strongIt = m_lastStrongMetadataUpdate.find(busName);
const bool withinStabilizeWindow =
strongIt != m_lastStrongMetadataUpdate.end() && now - strongIt->second < k_metadata_stabilize_window;
const auto seekCommandIt = m_lastSeekCommandAt.find(busName);
const bool recentLocalSeek =
seekCommandIt != m_lastSeekCommandAt.end() && now - seekCommandIt->second <= k_seek_pause_grace_window;
if (merged.playbackStatus == "Playing" && previousStrong && incomingWeak && withinStabilizeWindow &&
recentLocalSeek) {
const std::string incomingArtUrl = info.artUrl;
const std::string incomingSourceUrl = info.sourceUrl;
merged.trackId = previous_info.trackId;
merged.title = previous_info.title;
merged.artists = previous_info.artists;
merged.album = previous_info.album;
merged.sourceUrl = previous_info.sourceUrl;
merged.artUrl = previous_info.artUrl;
if (!incomingArtUrl.empty()) {
merged.artUrl = incomingArtUrl;
}
if (!incomingSourceUrl.empty()) {
merged.sourceUrl = incomingSourceUrl;
}
}
const std::string newSignature = logicalTrackSignature(merged);
const auto signatureIt = m_logicalTrackSignatures.find(busName);
const std::string previousSignature =
signatureIt != m_logicalTrackSignatures.end() ? signatureIt->second : logicalTrackSignature(previous_info);
auto offsetIt = m_positionOffsetsUs.find(busName);
if (offsetIt == m_positionOffsetsUs.end()) {
offsetIt = m_positionOffsetsUs.emplace(busName, 0).first;
}
const bool previousPositionAuthoritative =
m_hasAuthoritativePositionSample.contains(busName) && m_hasAuthoritativePositionSample.at(busName);
const bool logicalTrackChanged = !newSignature.empty() && previousSignature != newSignature;
const bool playbackStatusChanged = previous_info.playbackStatus != merged.playbackStatus;
if (previousPositionAuthoritative && !logicalTrackChanged && !hadPositionSignal &&
previous_info.playbackStatus == "Playing" && merged.playbackStatus == "Paused" && previous_info.canSeek) {
m_recentNoSignalPauseAt[busName] = now;
} else if (merged.playbackStatus != "Paused") {
m_recentNoSignalPauseAt.erase(busName);
}
bool preservedNormalizedPosition = false;
if (previousPositionAuthoritative && !logicalTrackChanged && !hadPositionSignal &&
previous_info.playbackStatus != "Stopped" && merged.playbackStatus != "Stopped" &&
previous_info.positionUs != merged.positionUs) {
bool preservePreviousPosition = playbackStatusChanged;
if (!preservePreviousPosition) {
const auto sampleIt = m_lastPositionSampleAt.find(busName);
const std::int64_t elapsedSinceSampleUs =
sampleIt != m_lastPositionSampleAt.end()
? std::chrono::duration_cast<std::chrono::microseconds>(now - sampleIt->second).count()
: 0;
const std::int64_t rawDeltaUs = std::llabs(merged.positionUs - previous_info.positionUs);
const std::int64_t maxReasonableDeltaUs = std::max<std::int64_t>(5'000'000, elapsedSinceSampleUs + 2'000'000);
preservePreviousPosition = rawDeltaUs > maxReasonableDeltaUs;
if (!preservePreviousPosition && previous_info.playbackStatus == "Paused" &&
merged.playbackStatus == "Paused") {
preservePreviousPosition = !recentLocalSeek && rawDeltaUs < k_paused_same_track_position_jump_tolerance_us;
proxyIt->second->callMethodAsync("GetAll")
.onInterface(k_properties_interface)
.withArguments(std::string{k_mpris_root_interface})
.uponReplyInvoke([this, aliveGuard, busName, hadPositionSignal, previousActive](
std::optional<sdbus::Error> rootErr, std::map<std::string, sdbus::Variant> rootProps) {
if (aliveGuard.expired()) {
return;
}
}
if (preservePreviousPosition) {
merged.positionUs = previous_info.positionUs;
preservedNormalizedPosition = true;
}
}
if (previousPositionAuthoritative && !logicalTrackChanged && !hadPositionSignal && merged.positionUs == 0 &&
previous_info.positionUs > 0 && previous_info.playbackStatus != "Stopped" &&
merged.playbackStatus != "Stopped") {
merged.positionUs = previous_info.positionUs;
preservedNormalizedPosition = true;
}
if (logicalTrackChanged) {
const std::int64_t previousOffset = offsetIt->second;
const std::int64_t previousNormalized = std::max<std::int64_t>(0, previous_info.positionUs - previousOffset);
const std::int64_t previousRawPositionUs = std::max<std::int64_t>(0, previous_info.positionUs + previousOffset);
m_previousTrackRawPositionUs[busName] = previousRawPositionUs;
offsetIt->second = 0;
const bool looksLikePreviousTrackContinuation =
merged.positionUs > 5'000'000 && previousNormalized > 5'000'000 &&
std::llabs(merged.positionUs - previousRawPositionUs) <= k_previous_track_continuation_slack_us;
if (looksLikePreviousTrackContinuation) {
offsetIt->second = merged.positionUs;
}
auto proxyLookup = m_playerProxies.find(busName);
if (proxyLookup == m_playerProxies.end()) {
return;
}
m_lastLogicalTrackChangeAt[busName] = now;
m_hasAuthoritativePositionSample[busName] = false;
m_recentNoSignalPauseAt.erase(busName);
m_lastPositionSampleAt[busName] = now;
m_pendingPositionCandidateUs.erase(busName);
m_pendingPositionCandidateMatches.erase(busName);
m_pendingPositionCandidateAt.erase(busName);
}
try {
proxyLookup->second->callMethodAsync("GetAll")
.onInterface(k_properties_interface)
.withArguments(std::string{k_mpris_player_interface})
.uponReplyInvoke([this, aliveGuard, busName, hadPositionSignal, previousActive, rootErr,
rootProps = std::move(rootProps)](std::optional<sdbus::Error> playerErr,
std::map<std::string, sdbus::Variant> playerProps) {
if (aliveGuard.expired()) {
return;
}
if (!preservedNormalizedPosition) {
const std::int64_t offsetUs = offsetIt->second;
merged.positionUs = std::max<std::int64_t>(0, merged.positionUs - offsetUs);
}
m_logicalTrackSignatures[busName] = newSignature;
if (hadPositionSignal) {
m_hasAuthoritativePositionSample[busName] = true;
}
if (previous_info.positionUs != merged.positionUs || previous_info.playbackStatus != merged.playbackStatus) {
m_lastPositionSampleAt[busName] = now;
}
std::map<std::string, sdbus::Variant> effectiveRootProps;
if (!rootErr.has_value()) {
effectiveRootProps = rootProps;
}
existing->second = merged;
// kLog.debug("updated player name={} status={} title=\"{}\" artist=\"{}\" art_url=\"{}\"", merged.busName,
// merged.playbackStatus, merged.title, primary_artist(merged.artists), merged.artUrl);
std::map<std::string, sdbus::Variant> effectivePlayerProps;
if (!playerErr.has_value()) {
effectivePlayerProps = playerProps;
}
const bool trackChanged = previous_info.title != merged.title || previous_info.album != merged.album ||
previous_info.artists != merged.artists || previous_info.artUrl != merged.artUrl ||
previous_info.sourceUrl != merged.sourceUrl ||
previous_info.trackId != merged.trackId || previous_info.lengthUs != merged.lengthUs;
const bool significantChanged =
trackChanged || previous_info.identity != merged.identity ||
previous_info.playbackStatus != merged.playbackStatus || previous_info.loopStatus != merged.loopStatus ||
previous_info.shuffle != merged.shuffle || previous_info.canGoPrevious != merged.canGoPrevious ||
previous_info.canGoNext != merged.canGoNext || previous_info.canPlay != merged.canPlay ||
previous_info.canPause != merged.canPause || previous_info.canSeek != merged.canSeek;
if (trackChanged || previous_info.playbackStatus != merged.playbackStatus) {
DeferredCall::callLater([this, busName]() { refreshPlayerPosition(busName, true); });
auto& timerId = m_positionResyncTimers[busName];
timerId = TimerManager::instance().start(timerId, k_position_retry_interval,
[this, busName]() { refreshPlayerPosition(busName, true); });
}
if (trackChanged) {
emitTrackChanged(merged);
}
syncSignals(previousActive);
if (significantChanged && m_changeCallback) {
m_changeCallback();
}
}
const MprisPlayerInfo info =
readPlayerInfoFromProperties(busName, effectiveRootProps, effectivePlayerProps);
applyPlayerSnapshot(busName, info, hadPositionSignal, previousActive);
});
} catch (const sdbus::Error& e) {
kLog.warn("player query failed name={} err={}", busName, e.what());
scheduleRecoveryDiscovery();
}
});
} catch (const sdbus::Error& e) {
kLog.warn("player query failed name={} err={}", busName, e.what());
scheduleRecoveryDiscovery();
}
}
void MprisService::applyPlayerSnapshot(const std::string& busName, const MprisPlayerInfo& info, bool hadPositionSignal,
const std::optional<MprisPlayerInfo>& previousActive) {
const auto now = std::chrono::steady_clock::now();
if (info.playbackStatus == "Playing") {
m_lastActivePlayer = busName;
m_lastPlayingUpdate[busName] = now;
}
if (hasStrongNowPlayingMetadata(info)) {
m_lastStrongMetadataUpdate[busName] = now;
}
const auto existing = m_players.find(busName);
if (existing == m_players.end()) {
MprisPlayerInfo initial = info;
if (!hadPositionSignal) {
initial.positionUs = 0;
}
m_logicalTrackSignatures[busName] = logicalTrackSignature(initial);
m_positionOffsetsUs[busName] = 0;
m_lastLogicalTrackChangeAt[busName] = now;
m_lastPositionSampleAt[busName] = now;
m_hasAuthoritativePositionSample[busName] = false;
m_previousTrackRawPositionUs.erase(busName);
m_pendingPositionCandidateUs.erase(busName);
m_pendingPositionCandidateMatches.erase(busName);
m_pendingPositionCandidateAt.erase(busName);
m_players.emplace(busName, std::move(initial));
emitPlayersChanged();
syncSignals(previousActive);
if (m_changeCallback) {
m_changeCallback();
}
if (info.playbackStatus != "Stopped" || info.positionUs > 0) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard, busName]() {
if (aliveGuard.expired()) {
return;
}
refreshPlayerPosition(busName, true);
});
auto& timerId = m_positionResyncTimers[busName];
const std::weak_ptr<void> timerAliveGuard = m_aliveGuard;
timerId = TimerManager::instance().start(timerId, k_position_retry_interval, [this, timerAliveGuard, busName]() {
if (timerAliveGuard.expired()) {
return;
}
refreshPlayerPosition(busName, true);
});
}
return;
}
if (existing->second != info) {
const MprisPlayerInfo previous_info = existing->second;
MprisPlayerInfo merged = info;
if (merged.artUrl.empty() && !previous_info.artUrl.empty()) {
merged.artUrl = previous_info.artUrl;
}
const bool incomingSnapshotEmpty = merged.playbackStatus.empty() && merged.trackId.empty() &&
merged.title.empty() && merged.artists.empty() && merged.album.empty() &&
merged.sourceUrl.empty() && merged.artUrl.empty() && merged.lengthUs == 0;
if (incomingSnapshotEmpty) {
merged = previous_info;
}
const bool previousStrong = hasStrongNowPlayingMetadata(previous_info) || !previous_info.artUrl.empty();
const bool incomingWeak = !hasStrongNowPlayingMetadata(info);
const auto strongIt = m_lastStrongMetadataUpdate.find(busName);
const bool withinStabilizeWindow =
strongIt != m_lastStrongMetadataUpdate.end() && now - strongIt->second < k_metadata_stabilize_window;
const auto seekCommandIt = m_lastSeekCommandAt.find(busName);
const bool recentLocalSeek =
seekCommandIt != m_lastSeekCommandAt.end() && now - seekCommandIt->second <= k_seek_pause_grace_window;
if (merged.playbackStatus == "Playing" && previousStrong && incomingWeak && withinStabilizeWindow &&
recentLocalSeek) {
const std::string incomingArtUrl = info.artUrl;
const std::string incomingSourceUrl = info.sourceUrl;
merged.trackId = previous_info.trackId;
merged.title = previous_info.title;
merged.artists = previous_info.artists;
merged.album = previous_info.album;
merged.sourceUrl = previous_info.sourceUrl;
merged.artUrl = previous_info.artUrl;
if (!incomingArtUrl.empty()) {
merged.artUrl = incomingArtUrl;
}
if (!incomingSourceUrl.empty()) {
merged.sourceUrl = incomingSourceUrl;
}
}
const std::string newSignature = logicalTrackSignature(merged);
const auto signatureIt = m_logicalTrackSignatures.find(busName);
const std::string previousSignature =
signatureIt != m_logicalTrackSignatures.end() ? signatureIt->second : logicalTrackSignature(previous_info);
auto offsetIt = m_positionOffsetsUs.find(busName);
if (offsetIt == m_positionOffsetsUs.end()) {
offsetIt = m_positionOffsetsUs.emplace(busName, 0).first;
}
const bool previousPositionAuthoritative =
m_hasAuthoritativePositionSample.contains(busName) && m_hasAuthoritativePositionSample.at(busName);
const bool logicalTrackChanged = !newSignature.empty() && previousSignature != newSignature;
const bool playbackStatusChanged = previous_info.playbackStatus != merged.playbackStatus;
if (previousPositionAuthoritative && !logicalTrackChanged && !hadPositionSignal &&
previous_info.playbackStatus == "Playing" && merged.playbackStatus == "Paused" && previous_info.canSeek) {
m_recentNoSignalPauseAt[busName] = now;
} else if (merged.playbackStatus != "Paused") {
m_recentNoSignalPauseAt.erase(busName);
}
bool preservedNormalizedPosition = false;
if (previousPositionAuthoritative && !logicalTrackChanged && !hadPositionSignal &&
previous_info.playbackStatus != "Stopped" && merged.playbackStatus != "Stopped" &&
previous_info.positionUs != merged.positionUs) {
bool preservePreviousPosition = playbackStatusChanged;
if (!preservePreviousPosition) {
const auto sampleIt = m_lastPositionSampleAt.find(busName);
const std::int64_t elapsedSinceSampleUs =
sampleIt != m_lastPositionSampleAt.end()
? std::chrono::duration_cast<std::chrono::microseconds>(now - sampleIt->second).count()
: 0;
const std::int64_t rawDeltaUs = std::llabs(merged.positionUs - previous_info.positionUs);
const std::int64_t maxReasonableDeltaUs = std::max<std::int64_t>(5'000'000, elapsedSinceSampleUs + 2'000'000);
preservePreviousPosition = rawDeltaUs > maxReasonableDeltaUs;
if (!preservePreviousPosition && previous_info.playbackStatus == "Paused" &&
merged.playbackStatus == "Paused") {
preservePreviousPosition = !recentLocalSeek && rawDeltaUs < k_paused_same_track_position_jump_tolerance_us;
}
}
if (preservePreviousPosition) {
merged.positionUs = previous_info.positionUs;
preservedNormalizedPosition = true;
}
}
if (previousPositionAuthoritative && !logicalTrackChanged && !hadPositionSignal && merged.positionUs == 0 &&
previous_info.positionUs > 0 && previous_info.playbackStatus != "Stopped" &&
merged.playbackStatus != "Stopped") {
merged.positionUs = previous_info.positionUs;
preservedNormalizedPosition = true;
}
if (logicalTrackChanged) {
const std::int64_t previousOffset = offsetIt->second;
const std::int64_t previousNormalized = std::max<std::int64_t>(0, previous_info.positionUs - previousOffset);
const std::int64_t previousRawPositionUs = std::max<std::int64_t>(0, previous_info.positionUs + previousOffset);
m_previousTrackRawPositionUs[busName] = previousRawPositionUs;
offsetIt->second = 0;
const bool looksLikePreviousTrackContinuation =
merged.positionUs > 5'000'000 && previousNormalized > 5'000'000 &&
std::llabs(merged.positionUs - previousRawPositionUs) <= k_previous_track_continuation_slack_us;
if (looksLikePreviousTrackContinuation) {
offsetIt->second = merged.positionUs;
}
m_lastLogicalTrackChangeAt[busName] = now;
m_hasAuthoritativePositionSample[busName] = false;
m_recentNoSignalPauseAt.erase(busName);
m_lastPositionSampleAt[busName] = now;
m_pendingPositionCandidateUs.erase(busName);
m_pendingPositionCandidateMatches.erase(busName);
m_pendingPositionCandidateAt.erase(busName);
}
if (!preservedNormalizedPosition) {
const std::int64_t offsetUs = offsetIt->second;
merged.positionUs = std::max<std::int64_t>(0, merged.positionUs - offsetUs);
}
m_logicalTrackSignatures[busName] = newSignature;
if (hadPositionSignal) {
m_hasAuthoritativePositionSample[busName] = true;
}
if (previous_info.positionUs != merged.positionUs || previous_info.playbackStatus != merged.playbackStatus) {
m_lastPositionSampleAt[busName] = now;
}
existing->second = merged;
const bool trackChanged = previous_info.title != merged.title || previous_info.album != merged.album ||
previous_info.artists != merged.artists || previous_info.artUrl != merged.artUrl ||
previous_info.sourceUrl != merged.sourceUrl || previous_info.trackId != merged.trackId ||
previous_info.lengthUs != merged.lengthUs;
const bool significantChanged =
trackChanged || previous_info.identity != merged.identity ||
previous_info.playbackStatus != merged.playbackStatus || previous_info.loopStatus != merged.loopStatus ||
previous_info.shuffle != merged.shuffle || previous_info.canGoPrevious != merged.canGoPrevious ||
previous_info.canGoNext != merged.canGoNext || previous_info.canPlay != merged.canPlay ||
previous_info.canPause != merged.canPause || previous_info.canSeek != merged.canSeek;
if (trackChanged || previous_info.playbackStatus != merged.playbackStatus) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard, busName]() {
if (aliveGuard.expired()) {
return;
}
refreshPlayerPosition(busName, true);
});
auto& timerId = m_positionResyncTimers[busName];
const std::weak_ptr<void> timerAliveGuard = m_aliveGuard;
timerId = TimerManager::instance().start(timerId, k_position_retry_interval, [this, timerAliveGuard, busName]() {
if (timerAliveGuard.expired()) {
return;
}
refreshPlayerPosition(busName, true);
});
}
if (trackChanged) {
emitTrackChanged(merged);
}
syncSignals(previousActive);
if (significantChanged && m_changeCallback) {
m_changeCallback();
}
}
}
void MprisService::removePlayer(const std::string& busName) {
const auto previousActive = activePlayer();
@@ -1806,27 +1934,6 @@ bool MprisService::isBlacklisted(const MprisPlayerInfo& player) const {
return false;
}
std::function<void(std::optional<sdbus::Error> err)>
MprisService::makeAsyncReplyHandler(std::string op, std::string busName, std::optional<std::string> method) {
return [this, op = std::move(op), busName = std::move(busName),
method = std::move(method)](std::optional<sdbus::Error> err) {
if (err.has_value()) {
if (method.has_value()) {
kLog.warn("{} failed name={} method={} err={}", op, busName, *method, err->what());
} else {
kLog.warn("{} failed name={} err={}", op, busName, err->what());
}
return;
}
if (method.has_value()) {
kLog.debug("{} name={} method={}", op, busName, *method);
}
DeferredCall::callLater([this, busName]() { addOrRefreshPlayer(busName); });
};
}
bool MprisService::callPlayerMethod(const std::string& busName, const char* methodName) {
const auto it = m_playerProxies.find(busName);
if (it == m_playerProxies.end()) {
@@ -2170,23 +2277,10 @@ std::tuple<bool, std::string, std::vector<std::string>> MprisService::onGetPlaye
return {true, *m_pinnedPlayerPreference, m_preferredPlayers};
}
MprisPlayerInfo MprisService::readPlayerInfo(sdbus::IProxy& proxy, const std::string& busName) const {
std::map<std::string, sdbus::Variant> rootProps;
std::map<std::string, sdbus::Variant> playerProps;
try {
for (auto& [k, v] : proxy.getAllProperties().onInterface(k_mpris_root_interface)) {
rootProps.emplace(std::string(k), std::move(v));
}
} catch (const sdbus::Error&) {
}
try {
for (auto& [k, v] : proxy.getAllProperties().onInterface(k_mpris_player_interface)) {
playerProps.emplace(std::string(k), std::move(v));
}
} catch (const sdbus::Error&) {
}
MprisPlayerInfo
MprisService::readPlayerInfoFromProperties(const std::string& busName,
const std::map<std::string, sdbus::Variant>& rootProps,
const std::map<std::string, sdbus::Variant>& playerProps) const {
auto metadata = get_variant_map_from_props(playerProps, "Metadata");
return MprisPlayerInfo{
@@ -2212,3 +2306,23 @@ MprisPlayerInfo MprisService::readPlayerInfo(sdbus::IProxy& proxy, const std::st
.canSeek = get_bool_from_props(playerProps, "CanSeek"),
};
}
MprisPlayerInfo MprisService::readPlayerInfo(sdbus::IProxy& proxy, const std::string& busName) const {
std::map<std::string, sdbus::Variant> rootProps;
std::map<std::string, sdbus::Variant> playerProps;
try {
for (auto& [k, v] : proxy.getAllProperties().onInterface(k_mpris_root_interface)) {
rootProps.emplace(std::string(k), std::move(v));
}
} catch (const sdbus::Error&) {
}
try {
for (auto& [k, v] : proxy.getAllProperties().onInterface(k_mpris_player_interface)) {
playerProps.emplace(std::string(k), std::move(v));
}
} catch (const sdbus::Error&) {
}
return readPlayerInfoFromProperties(busName, rootProps, playerProps);
}
+14 -2
View File
@@ -2,7 +2,9 @@
#include <chrono>
#include <cstdint>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
@@ -14,6 +16,7 @@ namespace sdbus {
class Error;
class IObject;
class IProxy;
class Variant;
} // namespace sdbus
class SessionBus;
@@ -99,18 +102,24 @@ private:
void syncSignals(const std::optional<MprisPlayerInfo>& previousActive);
void registerBusSignals();
void discoverPlayers();
void scheduleDiscoveryDrain();
void scheduleStartupRediscovery();
void scheduleRecoveryDiscovery();
void addOrRefreshPlayer(const std::string& busName);
void applyPlayerSnapshot(const std::string& busName, const MprisPlayerInfo& info, bool hadPositionSignal,
const std::optional<MprisPlayerInfo>& previousActive);
void refreshPlayerPosition(const std::string& busName, bool notifyChange);
void removePlayer(const std::string& busName);
[[nodiscard]] MprisPlayerInfo
readPlayerInfoFromProperties(const std::string& busName, const std::map<std::string, sdbus::Variant>& rootProps,
const std::map<std::string, sdbus::Variant>& playerProps) const;
[[nodiscard]] MprisPlayerInfo readPlayerInfo(sdbus::IProxy& proxy, const std::string& busName) const;
[[nodiscard]] MprisPlayerInfo projectedPlayerInfo(const MprisPlayerInfo& player) const;
[[nodiscard]] std::int64_t projectedPositionUs(const MprisPlayerInfo& player) const;
[[nodiscard]] std::optional<std::string> chooseActivePlayer() const;
[[nodiscard]] bool isBlacklisted(const MprisPlayerInfo& player) const;
std::function<void(std::optional<sdbus::Error> err)>
makeAsyncReplyHandler(std::string op, std::string busName, std::optional<std::string> method = std::nullopt);
auto makeAsyncReplyHandler(std::string op, std::string busName);
auto makeAsyncReplyHandler(std::string op, std::string busName, std::string_view method);
[[nodiscard]] bool callPlayerMethod(const std::string& busName, const char* methodName);
[[nodiscard]] bool canInvoke(const MprisPlayerInfo& player, const char* methodName) const;
@@ -144,6 +153,7 @@ private:
[[nodiscard]] std::tuple<bool, std::string, std::vector<std::string>> onGetPlayerPreferences() const;
SessionBus& m_bus;
std::shared_ptr<void> m_aliveGuard = std::make_shared<int>(0);
std::unique_ptr<sdbus::IObject> m_controlObject;
std::unique_ptr<sdbus::IProxy> m_dbusProxy;
std::unordered_map<std::string, std::unique_ptr<sdbus::IProxy>> m_playerProxies;
@@ -164,6 +174,7 @@ private:
std::unordered_map<std::string, std::chrono::steady_clock::time_point> m_lastPropertiesUpdate;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> m_lastPlayingUpdate;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> m_lastStrongMetadataUpdate;
std::deque<std::string> m_pendingDiscoveryBusNames;
std::string m_lastActivePlayer;
std::string m_lastEmittedActivePlayer;
std::optional<std::string> m_pinnedPlayerPreference;
@@ -172,4 +183,5 @@ private:
std::function<void()> m_changeCallback;
int m_startupRediscoveryPassesRemaining = 4;
bool m_recoveryDiscoveryScheduled = false;
bool m_discoveryDrainScheduled = false;
};
+28 -23
View File
@@ -72,11 +72,7 @@ MediaTab::MediaTab(MprisService* mpris, HttpClient* httpClient, PipeWireSpectrum
: m_mpris(mpris), m_httpClient(httpClient), m_spectrum(spectrum), m_wayland(wayland),
m_renderContext(renderContext) {}
MediaTab::~MediaTab() {
// Signal to any pending DeferredCall callbacks that this object is being destroyed.
// This prevents use-after-free if callbacks run after destruction.
m_alive = false;
}
MediaTab::~MediaTab() { m_aliveGuard.reset(); }
void MediaTab::openPlayerMenu() {
if (m_playerMenuPopup == nullptr || m_mpris == nullptr || m_playerMenuButton == nullptr) {
@@ -275,9 +271,10 @@ std::unique_ptr<Flex> MediaTab::create() {
m_pendingSeekBusName = seekBusName;
m_pendingSeekUs = targetUs;
m_pendingSeekUntil = now + std::chrono::milliseconds(3000);
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, seekBusName, targetUs]() {
if (!m_alive || m_mpris == nullptr) {
DeferredCall::callLater([this, aliveGuard, seekBusName, targetUs]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
if (!seekBusName.empty()) {
@@ -311,8 +308,9 @@ std::unique_ptr<Flex> MediaTab::create() {
repeat->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
repeat->setRadius(Style::radiusLg * scale);
repeat->setOnClick([this]() {
DeferredCall::callLater([this]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
const auto current = m_mpris->loopStatusActive().value_or("None");
@@ -332,8 +330,9 @@ std::unique_ptr<Flex> MediaTab::create() {
previous->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
previous->setRadius(Style::radiusLg * scale);
previous->setOnClick([this]() {
DeferredCall::callLater([this]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
(void)m_mpris->previousActive();
@@ -351,8 +350,9 @@ std::unique_ptr<Flex> MediaTab::create() {
playPause->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
playPause->setRadius(Style::radiusLg * scale);
playPause->setOnClick([this]() {
DeferredCall::callLater([this]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
(void)m_mpris->playPauseActive();
@@ -370,8 +370,9 @@ std::unique_ptr<Flex> MediaTab::create() {
next->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
next->setRadius(Style::radiusLg * scale);
next->setOnClick([this]() {
DeferredCall::callLater([this]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
(void)m_mpris->nextActive();
@@ -389,8 +390,9 @@ std::unique_ptr<Flex> MediaTab::create() {
shuffle->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
shuffle->setRadius(Style::radiusLg * scale);
shuffle->setOnClick([this]() {
DeferredCall::callLater([this]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
const bool enabled = m_mpris->shuffleActive().value_or(false);
@@ -439,8 +441,9 @@ std::unique_ptr<Flex> MediaTab::create() {
if (m_wayland != nullptr && m_renderContext != nullptr) {
m_playerMenuPopup = std::make_unique<ContextMenuPopup>(*m_wayland, *m_renderContext);
m_playerMenuPopup->setOnActivate([this](const ContextMenuControlEntry& entry) {
DeferredCall::callLater([this, entry]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard, entry]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
if (entry.id == 0) {
@@ -626,8 +629,9 @@ void MediaTab::setActive(bool active) {
// Pull a fresh snapshot (including Position) when the tab opens so the
// progress slider starts at the current playback position.
m_positionSampleAt = {};
DeferredCall::callLater([this]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
m_mpris->refreshPlayers();
@@ -706,8 +710,9 @@ void MediaTab::refresh(Renderer& renderer) {
if (shouldRetryMpris) {
m_lastMprisRefreshAttempt = now;
kLog.debug("media tab retrying mpris discovery players={} active={}", players.size(), active.has_value());
DeferredCall::callLater([this]() {
if (!m_alive || m_mpris == nullptr) {
const std::weak_ptr<void> aliveGuard = m_aliveGuard;
DeferredCall::callLater([this, aliveGuard]() {
if (aliveGuard.expired() || m_mpris == nullptr) {
return;
}
m_mpris->refreshPlayers();
+4 -3
View File
@@ -43,9 +43,10 @@ private:
void openPlayerMenu();
// Guard flag to detect use-after-free in deferred callbacks.
// Set to false in destructor so pending DeferredCall callbacks can safely check if this object is alive.
bool m_alive = true;
// Guard token for deferred callbacks that run on the next main-loop tick.
// Callbacks capture a weak_ptr so they can detect destruction without
// relying on a raw this pointer staying valid.
std::shared_ptr<void> m_aliveGuard = std::make_shared<int>(0);
MprisService* m_mpris = nullptr;
HttpClient* m_httpClient = nullptr;