refactor(watch-together): share core primitives

This commit is contained in:
edde746
2026-07-12 08:42:21 +02:00
parent 777780926c
commit 29c029f04a
8 changed files with 42 additions and 36 deletions
@@ -1,5 +1,4 @@
import 'package:collection/collection.dart';
import '../primitives.dart';
import 'watch_session.dart';
/// Playback lifecycle phase broadcast by the host.
@@ -146,7 +145,7 @@ class PlaybackState {
other.anchorHostTimeMs == anchorHostTimeMs &&
other.rate == rate &&
other.controlMode == controlMode &&
const ListEquality<String>().equals(other.waitingOn, waitingOn) &&
orderedStringListsEqual(other.waitingOn, waitingOn) &&
other.actorPeerId == actorPeerId &&
other.actionHint == actionHint;
+9
View File
@@ -0,0 +1,9 @@
int watchTogetherSystemNowMs() => DateTime.now().millisecondsSinceEpoch;
bool orderedStringListsEqual(List<String> first, List<String> second) {
if (first.length != second.length) return false;
for (var i = 0; i < first.length; i++) {
if (first[i] != second[i]) return false;
}
return true;
}
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import '../../mpv/mpv.dart';
import '../../utils/app_logger.dart';
import '../primitives.dart';
enum _ExpectationKind { playing, rate }
@@ -44,7 +45,7 @@ class _Expectation {
class AttachedPlayer {
AttachedPlayer({required Player player, required this._onLost, this._remoteSeek, int Function()? nowMs})
: _player = player,
_nowMs = nowMs ?? _systemNowMs {
_nowMs = nowMs ?? watchTogetherSystemNowMs {
_lastPlaying = player.state.playing;
_lastBuffering = player.state.buffering;
_lastRate = player.state.rate;
@@ -59,8 +60,6 @@ class AttachedPlayer {
);
}
static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch;
/// How long an issued command may wait for its property event before the
/// expectation is considered dead (covers silently-swallowed commands).
static const int _expectationTtlMs = 3000;
+2 -3
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import '../../utils/app_logger.dart';
import '../primitives.dart';
/// NTP-style clock-offset estimation against the session host (guest side).
///
@@ -12,9 +13,7 @@ import '../../utils/app_logger.dart';
/// All time reads go through the injected [nowMs] so tests can virtualize
/// time alongside `fakeAsync`.
class ClockSync {
ClockSync({required this._sendPing, int Function()? nowMs}) : _nowMs = nowMs ?? _systemNowMs;
static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch;
ClockSync({required this._sendPing, int Function()? nowMs}) : _nowMs = nowMs ?? watchTogetherSystemNowMs;
static const int _windowSize = 8;
static const int _maxAcceptedRttMs = 1000;
@@ -4,6 +4,7 @@ import '../../utils/app_logger.dart';
import '../models/playback_state.dart';
import '../models/sync_message.dart';
import '../models/watch_session.dart';
import '../primitives.dart';
import 'attached_player.dart';
import 'clock_sync.dart';
@@ -52,9 +53,7 @@ class GuestPlaybackReconciler {
this._callbacks = const GuestReconcilerCallbacks(),
int Function()? nowMs,
}) : _clock = clockSync,
_nowMs = nowMs ?? _systemNowMs;
static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch;
_nowMs = nowMs ?? watchTogetherSystemNowMs;
// Tuning constants.
static const int tickMs = 500;
@@ -238,7 +237,7 @@ class GuestPlaybackReconciler {
_reportedPhase = state.phase;
_callbacks.onPhaseChanged?.call(state.phase);
}
if (!_listEquals(state.waitingOn, _reportedWaitingOn)) {
if (!orderedStringListsEqual(state.waitingOn, _reportedWaitingOn)) {
_reportedWaitingOn = state.waitingOn;
_callbacks.onWaitingOnChanged?.call(state.waitingOn);
}
@@ -585,12 +584,4 @@ class GuestPlaybackReconciler {
_lastSentStatus = status;
_sendToHost(SyncMessage.status(status, peerId: myPeerId));
}
static bool _listEquals(List<String> a, List<String> b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
}
@@ -4,6 +4,7 @@ import 'dart:math';
import '../../utils/app_logger.dart';
import '../models/playback_state.dart';
import '../models/watch_session.dart';
import '../primitives.dart';
import 'attached_player.dart';
/// Callbacks the coordinator surfaces to the provider/UI layer.
@@ -45,9 +46,7 @@ class HostPlaybackCoordinator {
required this._sendState,
this._callbacks = const HostCoordinatorCallbacks(),
int Function()? nowMs,
}) : _nowMs = nowMs ?? _systemNowMs;
static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch;
}) : _nowMs = nowMs ?? watchTogetherSystemNowMs;
// Tuning constants.
static const int stallGraceMs = 500;
@@ -785,18 +784,10 @@ class HostPlaybackCoordinator {
if (toPeerId == null) {
final previousWaiting = _lastBroadcast?.waitingOn ?? const [];
_lastBroadcast = state;
if (!_listEquals(previousWaiting, waitingOn)) {
if (!orderedStringListsEqual(previousWaiting, waitingOn)) {
_callbacks.onWaitingOnChanged?.call(waitingOn);
}
}
_sendState(state, toPeerId: toPeerId);
}
static bool _listEquals(List<String> a, List<String> b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
}
@@ -5,6 +5,7 @@ import '../../utils/app_logger.dart';
import '../models/playback_state.dart';
import '../models/sync_message.dart';
import '../models/watch_session.dart';
import '../primitives.dart';
import 'attached_player.dart';
import 'clock_sync.dart';
import 'guest_playback_reconciler.dart';
@@ -28,7 +29,7 @@ class WatchTogetherController {
int Function()? nowMs,
}) : _peerService = peerService,
_session = session,
_nowMs = nowMs ?? _systemNowMs {
_nowMs = nowMs ?? watchTogetherSystemNowMs {
if (session.isHost) {
_coordinator = HostPlaybackCoordinator(
myPeerId: peerService.myPeerId ?? '',
@@ -65,8 +66,6 @@ class WatchTogetherController {
_subscriptions.add(peerService.onPeerDisconnected.listen(_handlePeerDisconnected));
}
static int _systemNowMs() => DateTime.now().millisecondsSinceEpoch;
final WatchTogetherPeerService _peerService;
final int Function() _nowMs;
WatchSession _session;
+19
View File
@@ -0,0 +1,19 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/watch_together/primitives.dart';
void main() {
test('orderedStringListsEqual preserves order and multiplicity', () {
expect(orderedStringListsEqual(const ['a', 'b'], const ['a', 'b']), isTrue);
expect(orderedStringListsEqual(const ['a'], const ['a', 'b']), isFalse);
expect(orderedStringListsEqual(const ['a', 'b'], const ['b', 'a']), isFalse);
expect(orderedStringListsEqual(const ['a', 'a'], const ['a', 'b']), isFalse);
});
test('watchTogetherSystemNowMs returns wall-clock milliseconds', () {
final before = DateTime.now().millisecondsSinceEpoch;
final value = watchTogetherSystemNowMs();
final after = DateTime.now().millisecondsSinceEpoch;
expect(value, inInclusiveRange(before, after));
});
}