perf: coalesce watch together rebuilds, static TV overlays
This commit is contained in:
@@ -2906,43 +2906,54 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Watch Together: reconnecting to host overlay
|
// Watch Together overlays (isolated from video surface repaints)
|
||||||
Consumer<WatchTogetherProvider>(
|
RepaintBoundary(
|
||||||
builder: (context, provider, child) {
|
child: Stack(
|
||||||
if (!provider.isWaitingForHostReconnect) return const SizedBox.shrink();
|
children: [
|
||||||
return Positioned(
|
// Watch Together: reconnecting to host overlay
|
||||||
bottom: 120,
|
Selector<WatchTogetherProvider, bool>(
|
||||||
left: 0,
|
selector: (_, provider) => provider.isWaitingForHostReconnect,
|
||||||
right: 0,
|
builder: (context, isWaiting, child) {
|
||||||
child: Center(
|
if (!isWaiting) return const SizedBox.shrink();
|
||||||
child: Container(
|
return Positioned(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
bottom: 120,
|
||||||
decoration: const BoxDecoration(
|
left: 0,
|
||||||
color: Colors.black54,
|
right: 0,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
child: Center(
|
||||||
),
|
child: Container(
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
mainAxisSize: MainAxisSize.min,
|
decoration: const BoxDecoration(
|
||||||
children: [
|
color: Colors.black54,
|
||||||
const SizedBox(
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||||
width: 14,
|
),
|
||||||
height: 14,
|
child: Row(
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
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)
|
// Black overlay during exit (no spinner - just covers transparency)
|
||||||
ValueListenableBuilder<bool>(
|
ValueListenableBuilder<bool>(
|
||||||
valueListenable: _isExiting,
|
valueListenable: _isExiting,
|
||||||
|
|||||||
@@ -30,6 +30,21 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
String _displayName = 'User';
|
String _displayName = 'User';
|
||||||
String? _lastHandledCurrentPlaybackKey;
|
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
|
// Host reconnect grace period
|
||||||
Timer? _hostReconnectTimer;
|
Timer? _hostReconnectTimer;
|
||||||
bool _isWaitingForHostReconnect = false;
|
bool _isWaitingForHostReconnect = false;
|
||||||
|
|||||||
@@ -316,10 +316,11 @@ class WatchTogetherSyncManager {
|
|||||||
_clockOffset = 0;
|
_clockOffset = 0;
|
||||||
_pendingPingTimestamp = null;
|
_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;
|
int burstCount = 0;
|
||||||
Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
Timer.periodic(const Duration(milliseconds: 500), (timer) {
|
||||||
if (burstCount >= 3 || _player == null) {
|
if (burstCount >= 2 || _player == null) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../../i18n/strings.g.dart';
|
import '../../i18n/strings.g.dart';
|
||||||
import '../../utils/dialogs.dart';
|
import '../../utils/dialogs.dart';
|
||||||
|
import '../../utils/platform_detector.dart';
|
||||||
import '../../utils/snackbar_helper.dart';
|
import '../../utils/snackbar_helper.dart';
|
||||||
import '../../widgets/overlay_sheet.dart';
|
import '../../widgets/overlay_sheet.dart';
|
||||||
import '../models/watch_session.dart';
|
import '../models/watch_session.dart';
|
||||||
@@ -85,10 +86,12 @@ class _SessionIndicator extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Sync indicator or group icon
|
// Sync indicator or group icon
|
||||||
if (isSyncing)
|
if (isSyncing)
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 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
|
else
|
||||||
Icon(Symbols.group, size: 18, color: isHost ? theme.colorScheme.primary : Colors.white),
|
Icon(Symbols.group, size: 18, color: isHost ? theme.colorScheme.primary : Colors.white),
|
||||||
@@ -230,7 +233,13 @@ class _SessionMenuSheet extends StatelessWidget {
|
|||||||
title: Text(p.displayName),
|
title: Text(p.displayName),
|
||||||
subtitle: p.isHost ? Text(t.watchTogether.host) : null,
|
subtitle: p.isHost ? Text(t.watchTogether.host) : null,
|
||||||
trailing: p.isBuffering
|
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,
|
: null,
|
||||||
dense: true,
|
dense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -388,10 +397,12 @@ class SyncingIndicator extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 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),
|
const SizedBox(width: 8),
|
||||||
Text(t.watchTogether.syncing, style: const TextStyle(color: Colors.white, fontSize: 12)),
|
Text(t.watchTogether.syncing, style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||||
|
|||||||
Reference in New Issue
Block a user