Files
plezy/lib/screens/video_player/completion_latch.dart
T

78 lines
3.1 KiB
Dart

/// What a position tick means for the end-of-video prompt flow.
enum CompletionLatchSignal {
/// Nothing to do.
none,
/// Playback just entered the end-of-video window and the latch is clear —
/// the caller should run its completion handling (which latches on
/// success via [CompletionLatch.latch]).
completed,
/// Playback moved back out of the end region and the latch re-armed.
rearmed,
}
/// End-of-video latch with rearm hysteresis for the Play Next / completion
/// prompts.
///
/// The prompt fires when playback enters the last [triggerWindowMs] of the
/// item and must not re-fire on every subsequent position tick — the latch
/// stays set while playback is parked inside the end region. It re-arms only
/// once playback moves back out past [rearmWindowMs] (a larger window, so a
/// position oscillating at the boundary can't flap), and never while a
/// prompt is visible or an auto-play countdown owns the screen.
///
/// Latching is the *caller's* move ([latch]), not [classifyPosition]'s: the
/// completion handler has its own bail-outs (live TV, in-flight media swap)
/// and a tick that bails must stay un-latched so the next tick retries.
class CompletionLatch {
CompletionLatch({required this.triggerWindowMs, required this.rearmWindowMs})
: assert(rearmWindowMs > triggerWindowMs, 'rearm window must exceed trigger window for hysteresis');
/// Fire when within this many ms of the end.
final int triggerWindowMs;
/// Re-arm only after moving back out past this many ms from the end.
final int rearmWindowMs;
bool _triggered = false;
/// Whether the end-of-video handling already ran for this approach to
/// the end region.
bool get triggered => _triggered;
/// Mark the completion handling as done for this approach to the end.
void latch() => _triggered = true;
/// Clear unconditionally — new media was loaded.
void reset() => _triggered = false;
/// Re-arm so the prompt can fire again — but only when no prompt is
/// visible and no auto-play countdown is running, so an active dialog is
/// never clobbered. Callers decide *when* re-arming is safe (media
/// reloaded, or playback moved back out of the end region).
void rearmIfClear({required bool promptVisible, required bool countdownActive}) {
if (_triggered && !promptVisible && !countdownActive) _triggered = false;
}
/// Classify a position tick against the trigger/rearm windows.
CompletionLatchSignal classifyPosition({
required int positionMs,
required int durationMs,
required bool promptVisible,
required bool countdownActive,
}) {
if (durationMs <= 0) return CompletionLatchSignal.none;
if (positionMs >= durationMs - triggerWindowMs) {
if (!promptVisible && !_triggered) return CompletionLatchSignal.completed;
return CompletionLatchSignal.none;
}
if (positionMs < durationMs - rearmWindowMs) {
final wasLatched = _triggered;
rearmIfClear(promptVisible: promptVisible, countdownActive: countdownActive);
if (wasLatched && !_triggered) return CompletionLatchSignal.rearmed;
}
return CompletionLatchSignal.none;
}
}