Files
DndGamePlayer/app/main/video/videoPlaybackStore.ts
T

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;
}