a6cbcc273e
Made-with: Cursor
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import type { VideoPlaybackEvent, VideoPlaybackState } from '../../shared/types';
|
|
|
|
function nowMs(): number {
|
|
return Date.now();
|
|
}
|
|
|
|
function clamp(v: number, min: number, max: number): number {
|
|
return Math.max(min, Math.min(max, v));
|
|
}
|
|
|
|
export class VideoPlaybackStore {
|
|
private state: VideoPlaybackState = {
|
|
revision: 1,
|
|
serverNowMs: nowMs(),
|
|
targetAssetId: null,
|
|
playing: false,
|
|
playbackRate: 1,
|
|
anchorServerMs: nowMs(),
|
|
anchorVideoTimeSec: 0,
|
|
};
|
|
|
|
getState(): VideoPlaybackState {
|
|
return { ...this.state, serverNowMs: nowMs() };
|
|
}
|
|
|
|
dispatch(event: VideoPlaybackEvent): VideoPlaybackState {
|
|
const s = this.getState();
|
|
const curTime = computeTimeSec(s, s.serverNowMs);
|
|
const bump = (patch: Omit<VideoPlaybackState, 'revision' | 'serverNowMs'>): VideoPlaybackState => ({
|
|
...patch,
|
|
revision: s.revision + 1,
|
|
serverNowMs: nowMs(),
|
|
});
|
|
|
|
switch (event.kind) {
|
|
case 'target.set': {
|
|
const nextTarget = event.assetId ?? null;
|
|
const next: Omit<VideoPlaybackState, 'revision' | 'serverNowMs'> = {
|
|
...s,
|
|
targetAssetId: nextTarget,
|
|
playing: Boolean(event.autostart) && nextTarget !== null,
|
|
playbackRate: s.playbackRate,
|
|
anchorServerMs: s.serverNowMs,
|
|
anchorVideoTimeSec: 0,
|
|
};
|
|
this.state = bump(next);
|
|
return this.state;
|
|
}
|
|
case 'play': {
|
|
this.state = bump({
|
|
...s,
|
|
playing: true,
|
|
anchorServerMs: s.serverNowMs,
|
|
anchorVideoTimeSec: curTime,
|
|
});
|
|
return this.state;
|
|
}
|
|
case 'pause': {
|
|
this.state = bump({
|
|
...s,
|
|
playing: false,
|
|
anchorServerMs: s.serverNowMs,
|
|
anchorVideoTimeSec: curTime,
|
|
});
|
|
return this.state;
|
|
}
|
|
case 'stop': {
|
|
this.state = bump({
|
|
...s,
|
|
playing: false,
|
|
anchorServerMs: s.serverNowMs,
|
|
anchorVideoTimeSec: 0,
|
|
});
|
|
return this.state;
|
|
}
|
|
case 'seek': {
|
|
const t = clamp(event.timeSec, 0, 1_000_000);
|
|
this.state = bump({
|
|
...s,
|
|
anchorServerMs: s.serverNowMs,
|
|
anchorVideoTimeSec: t,
|
|
});
|
|
return this.state;
|
|
}
|
|
case 'rate.set': {
|
|
const rate = clamp(event.rate, 0.25, 3);
|
|
this.state = bump({
|
|
...s,
|
|
playbackRate: rate,
|
|
anchorServerMs: s.serverNowMs,
|
|
anchorVideoTimeSec: curTime,
|
|
});
|
|
return this.state;
|
|
}
|
|
default: {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const _x: never = event;
|
|
return this.state;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function computeTimeSec(state: VideoPlaybackState, atServerNowMs: number): number {
|
|
if (!state.playing) return state.anchorVideoTimeSec;
|
|
const dt = Math.max(0, atServerNowMs - state.anchorServerMs);
|
|
return state.anchorVideoTimeSec + (dt / 1000) * state.playbackRate;
|
|
}
|