diff --git a/lib/services/discord_rpc_service.dart b/lib/services/discord_rpc_service.dart index 83955a69..324fb3db 100644 --- a/lib/services/discord_rpc_service.dart +++ b/lib/services/discord_rpc_service.dart @@ -53,6 +53,7 @@ class DiscordRPCService { Duration? _mediaDuration; Duration? _currentPosition; double _playbackSpeed = 1.0; + int _playbackRevision = 0; Timer? _reconnectTimer; DateTime? _lastPresenceUpdate; StreamSubscription? _readySubscription; @@ -106,6 +107,7 @@ class DiscordRPCService { /// thumbnail upload uses the neutral [MediaServerClient.thumbnailUrl] / /// [MediaServerClient.streamHeaders] surface. Future startPlayback(MediaItem metadata, MediaServerClient client) async { + final revision = ++_playbackRevision; _currentMetadata = metadata; _currentClient = client; _playbackStartTime = DateTime.now(); @@ -116,7 +118,7 @@ class DiscordRPCService { if (_isEnabled && _isConnected) { // Upload thumbnail in background, don't block playback - unawaited(_uploadThumbnailAndUpdatePresence()); + unawaited(_uploadThumbnailAndUpdatePresence(revision, metadata, client)); } } @@ -172,6 +174,7 @@ class DiscordRPCService { } Future stopPlayback() async { + _playbackRevision++; _currentMetadata = null; _currentClient = null; _playbackStartTime = null; @@ -211,8 +214,10 @@ class DiscordRPCService { await Future.delayed(const Duration(milliseconds: 200)); // Update presence if we have active playback - if (_currentMetadata != null) { - await _uploadThumbnailAndUpdatePresence(); + final metadata = _currentMetadata; + final client = _currentClient; + if (metadata != null && client != null) { + await _uploadThumbnailAndUpdatePresence(_playbackRevision, metadata, client); } }); @@ -276,11 +281,12 @@ class DiscordRPCService { }); } - Future _uploadThumbnailAndUpdatePresence() async { - // Try to upload thumbnail, but don't block on failure - if (_cachedThumbnailUrl == null && _currentMetadata != null && _currentClient != null) { - _cachedThumbnailUrl = await _uploadThumbnail(_currentMetadata!, _currentClient!); + Future _uploadThumbnailAndUpdatePresence(int revision, MediaItem metadata, MediaServerClient client) async { + final thumbnailUrl = await _uploadThumbnail(metadata, client); + if (revision != _playbackRevision || !identical(_currentMetadata, metadata) || !identical(_currentClient, client)) { + return; } + _cachedThumbnailUrl = thumbnailUrl; await _updatePresence(); } diff --git a/lib/watch_together/services/watch_together_peer_service.dart b/lib/watch_together/services/watch_together_peer_service.dart index ef72d827..cbf9fa06 100644 --- a/lib/watch_together/services/watch_together_peer_service.dart +++ b/lib/watch_together/services/watch_together_peer_service.dart @@ -300,7 +300,6 @@ class WatchTogetherPeerService with KeepaliveMixin { try { final channel = await _connectToRelay(); _channel = channel; - _reconnectAttempts = 0; final completer = Completer(); _listenToChannel(channel, setupCompleter: completer); @@ -328,8 +327,13 @@ class WatchTogetherPeerService with KeepaliveMixin { } } + _reconnectAttempts = 0; appLogger.d('WatchTogether: Reconnected successfully'); - onReconnected?.call(); + try { + onReconnected?.call(); + } catch (e) { + appLogger.e('WatchTogether: Reconnect callback failed', error: e); + } } catch (e) { appLogger.e('WatchTogether: Reconnect failed', error: e); _handleWebSocketClosed(); diff --git a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart index e4a2beac..7b631f4f 100644 --- a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart +++ b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart @@ -78,14 +78,15 @@ class PerformanceStatsService { _fpsTrackingActive = true; if (!_fpsCallbackRegistered) { _fpsCallbackRegistered = true; - SchedulerBinding.instance.addPersistentFrameCallback(_onFrame); + SchedulerBinding.instance.addTimingsCallback(_onFrameTimings); } } - /// Called every frame to count FPS. - void _onFrame(Duration timestamp) { + /// Called with completed frame timings to count FPS without retaining an + /// app-lifetime persistent callback. + void _onFrameTimings(List timings) { if (!_fpsTrackingActive) return; - _frameCount++; + _frameCount += timings.length; final now = DateTime.now(); final elapsed = now.difference(_lastFpsUpdate); if (elapsed.inMilliseconds >= 1000) { @@ -101,6 +102,10 @@ class PerformanceStatsService { _pollingTimer = null; _fpsTrackingActive = false; _currentUiFps = null; + if (_fpsCallbackRegistered) { + SchedulerBinding.instance.removeTimingsCallback(_onFrameTimings); + _fpsCallbackRegistered = false; + } } /// Fetch all performance stats from the player.