diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 9674a61f..7722fe45 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -2906,43 +2906,54 @@ class VideoPlayerScreenState extends State with WidgetsBindin ); }, ), - // Watch Together: reconnecting to host overlay - Consumer( - builder: (context, provider, child) { - if (!provider.isWaitingForHostReconnect) return const SizedBox.shrink(); - return Positioned( - bottom: 120, - left: 0, - right: 0, - child: Center( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: const BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + // Watch Together overlays (isolated from video surface repaints) + RepaintBoundary( + child: Stack( + children: [ + // Watch Together: reconnecting to host overlay + Selector( + selector: (_, provider) => provider.isWaitingForHostReconnect, + builder: (context, isWaiting, child) { + if (!isWaiting) return const SizedBox.shrink(); + return Positioned( + bottom: 120, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: const BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (PlatformDetector.isTV()) + const Icon(Symbols.sync_rounded, size: 14, color: Colors.white) + else + const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ), + const SizedBox(width: 8), + Text( + t.watchTogether.reconnectingToHost, + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ], + ), ), - const SizedBox(width: 8), - Text( - t.watchTogether.reconnectingToHost, - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - ], - ), - ), + ), + ); + }, ), - ); - }, + // Watch Together: participant join/leave notifications + const ParticipantNotificationOverlay(), + ], + ), ), - // Watch Together: participant join/leave notifications - const ParticipantNotificationOverlay(), // Black overlay during exit (no spinner - just covers transparency) ValueListenableBuilder( valueListenable: _isExiting, diff --git a/lib/watch_together/providers/watch_together_provider.dart b/lib/watch_together/providers/watch_together_provider.dart index ff15a79b..36be3b7b 100644 --- a/lib/watch_together/providers/watch_together_provider.dart +++ b/lib/watch_together/providers/watch_together_provider.dart @@ -30,6 +30,21 @@ class WatchTogetherProvider with ChangeNotifier { String _displayName = 'User'; String? _lastHandledCurrentPlaybackKey; + // Coalesce rapid-fire notifyListeners() calls into a single rebuild per frame. + // During Watch Together join, 4-5 notifications fire within milliseconds; + // this batches them into one rebuild to avoid overwhelming low-end devices. + bool _notifyScheduled = false; + + @override + void notifyListeners() { + if (_notifyScheduled) return; + _notifyScheduled = true; + scheduleMicrotask(() { + _notifyScheduled = false; + super.notifyListeners(); + }); + } + // Host reconnect grace period Timer? _hostReconnectTimer; bool _isWaitingForHostReconnect = false; diff --git a/lib/watch_together/services/watch_together_sync_manager.dart b/lib/watch_together/services/watch_together_sync_manager.dart index 686ed71a..23d5f4f9 100644 --- a/lib/watch_together/services/watch_together_sync_manager.dart +++ b/lib/watch_together/services/watch_together_sync_manager.dart @@ -316,10 +316,11 @@ class WatchTogetherSyncManager { _clockOffset = 0; _pendingPingTimestamp = null; - // Initial burst of 3 pings for fast convergence + // Initial burst of 2 pings for convergence, with wider spacing to reduce + // main-thread pressure during the join event storm int burstCount = 0; - Timer.periodic(const Duration(milliseconds: 200), (timer) { - if (burstCount >= 3 || _player == null) { + Timer.periodic(const Duration(milliseconds: 500), (timer) { + if (burstCount >= 2 || _player == null) { timer.cancel(); return; } diff --git a/lib/watch_together/widgets/watch_together_overlay.dart b/lib/watch_together/widgets/watch_together_overlay.dart index 986a7cf6..54166ba1 100644 --- a/lib/watch_together/widgets/watch_together_overlay.dart +++ b/lib/watch_together/widgets/watch_together_overlay.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../../i18n/strings.g.dart'; import '../../utils/dialogs.dart'; +import '../../utils/platform_detector.dart'; import '../../utils/snackbar_helper.dart'; import '../../widgets/overlay_sheet.dart'; import '../models/watch_session.dart'; @@ -85,10 +86,12 @@ class _SessionIndicator extends StatelessWidget { children: [ // Sync indicator or group icon if (isSyncing) - const SizedBox( + SizedBox( width: 16, height: 16, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: PlatformDetector.isTV() + ? const Icon(Symbols.sync_rounded, size: 16, color: Colors.white) + : const CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) else Icon(Symbols.group, size: 18, color: isHost ? theme.colorScheme.primary : Colors.white), @@ -230,7 +233,13 @@ class _SessionMenuSheet extends StatelessWidget { title: Text(p.displayName), subtitle: p.isHost ? Text(t.watchTogether.host) : null, trailing: p.isBuffering - ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) + ? SizedBox( + width: 16, + height: 16, + child: PlatformDetector.isTV() + ? const Icon(Symbols.hourglass_empty_rounded, size: 16) + : const CircularProgressIndicator(strokeWidth: 2), + ) : null, dense: true, contentPadding: EdgeInsets.zero, @@ -388,10 +397,12 @@ class SyncingIndicator extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( + SizedBox( width: 14, height: 14, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: PlatformDetector.isTV() + ? const Icon(Symbols.sync_rounded, size: 14, color: Colors.white) + : const CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ), const SizedBox(width: 8), Text(t.watchTogether.syncing, style: const TextStyle(color: Colors.white, fontSize: 12)),