viewer playback speed
All checks were successful
CI / checks (push) Successful in 1m7s

This commit is contained in:
Robin Appelman 2025-06-22 23:26:32 +02:00
commit 5ed8dea705
4 changed files with 78 additions and 4 deletions

View file

@ -10,6 +10,7 @@ export interface AnalyseMenuProps {
inShared: boolean; inShared: boolean;
open: boolean; open: boolean;
openModal: (ModalState) => void; openModal: (ModalState) => void;
speed: number;
} }
export function AnalyseMenu(props: AnalyseMenuProps) { export function AnalyseMenu(props: AnalyseMenuProps) {
@ -22,6 +23,9 @@ export function AnalyseMenu(props: AnalyseMenuProps) {
} }
return (<div class="analyse-menu"> return (<div class="analyse-menu">
<Show when={props.speed != 1}>
<div class="speed">Playing at <span>{props.speed}x</span></div>
</Show>
<Show when={props.inShared}> <Show when={props.inShared}>
<div class="share shared"> <div class="share shared">
You're spectating a session controlled by someone else You're spectating a session controlled by someone else

View file

@ -42,6 +42,7 @@ export const Analyser = (props: AnalyseProps) => {
const [tick, setTick] = createSignal<number>(0); const [tick, setTick] = createSignal<number>(0);
const [scale, setScale] = createSignal<number>(1); const [scale, setScale] = createSignal<number>(1);
const [speed, setSpeed] = createSignal<number>(1);
const [playing, setPlaying] = createSignal<boolean>(false); const [playing, setPlaying] = createSignal<boolean>(false);
const [sessionName, setSessionName] = createSignal<string>(""); const [sessionName, setSessionName] = createSignal<string>("");
const [clients, setClients] = createSignal<number>(0); const [clients, setClients] = createSignal<number>(0);
@ -79,6 +80,33 @@ export const Analyser = (props: AnalyseProps) => {
togglePlay(); togglePlay();
e.preventDefault(); e.preventDefault();
} }
if (e.key === '=') {
fixPlayOffset();
setSpeed(speed() + 0.25);
e.preventDefault();
if (session) {
session.update({speed: speed()});
}
}
if (e.key === '-') {
fixPlayOffset();
setSpeed(Math.max(speed() - 0.25, 0.25));
e.preventDefault();
if (session) {
session.update({speed: speed()});
}
}
if (e.key === '0') {
fixPlayOffset();
setSpeed(1);
e.preventDefault();
if (session) {
session.update({speed: speed()});
}
}
if (e.key === '?') { if (e.key === '?') {
setModalState(ModalState.Help); setModalState(ModalState.Help);
e.preventDefault(); e.preventDefault();
@ -111,7 +139,7 @@ export const Analyser = (props: AnalyseProps) => {
const onUpdate = (update: StateUpdate) => { const onUpdate = (update: StateUpdate) => {
if (update.hasOwnProperty("tick")) { if (update.hasOwnProperty("tick")) {
setTick(update["tick"]); setTickNow(update["tick"]);
} }
if (update.hasOwnProperty("playing")) { if (update.hasOwnProperty("playing")) {
if (update["playing"]) { if (update["playing"]) {
@ -120,6 +148,10 @@ export const Analyser = (props: AnalyseProps) => {
pause(); pause();
} }
} }
if (update.hasOwnProperty("speed")) {
fixPlayOffset();
setSpeed(update["speed"]);
}
if (update.hasOwnProperty("clients")) { if (update.hasOwnProperty("clients")) {
setClients(update["clients"]); setClients(update["clients"]);
} }
@ -174,9 +206,14 @@ export const Analyser = (props: AnalyseProps) => {
} }
} }
const play = () => { const fixPlayOffset = () => {
lastFrameTime = 0;
playStartTick = tick(); playStartTick = tick();
playStartTime = window.performance.now(); playStartTime = window.performance.now();
}
const play = () => {
fixPlayOffset();
setPlaying(true); setPlaying(true);
requestAnimationFrame(animFrame); requestAnimationFrame(animFrame);
if (session) { if (session) {
@ -197,6 +234,7 @@ export const Analyser = (props: AnalyseProps) => {
session.update({ session.update({
playing: playing(), playing: playing(),
tick: tick(), tick: tick(),
speed: speed(),
}); });
} }
}); });
@ -209,7 +247,7 @@ export const Analyser = (props: AnalyseProps) => {
const animFrame = (timestamp: number) => { const animFrame = (timestamp: number) => {
const timePassed = (timestamp - playStartTime) / 1000; const timePassed = (timestamp - playStartTime) / 1000;
const targetTick = playStartTick + (Math.round(timePassed / intervalPerTick)); const targetTick = playStartTick + (Math.round(timePassed / intervalPerTick * speed()));
lastFrameTime = timestamp; lastFrameTime = timestamp;
if (targetTick >= (lastTick)) { if (targetTick >= (lastTick)) {
pause(); pause();
@ -255,6 +293,7 @@ export const Analyser = (props: AnalyseProps) => {
tick: tick(), tick: tick(),
playing: playing(), playing: playing(),
clients: 0, clients: 0,
speed: speed(),
}, onUpdate); }, onUpdate);
setSessionName(session.sessionName); setSessionName(session.sessionName);
}} }}
@ -263,6 +302,7 @@ export const Analyser = (props: AnalyseProps) => {
isShared={isShared()} isShared={isShared()}
clients={clients()} clients={clients()}
inShared={inShared} inShared={inShared}
speed={speed()}
/> />
<SpecHUD parser={parser} tick={tick()} <SpecHUD parser={parser} tick={tick()}
players={players()} events={events} players={players()} events={events}

View file

@ -47,6 +47,11 @@ export class Session {
session: this.sessionName, session: this.sessionName,
play: this.initialState.playing play: this.initialState.playing
})); }));
this.socket.send(JSON.stringify({
type: 'speed',
session: this.sessionName,
speed: this.initialState.speed
}));
this.initialState = null; this.initialState = null;
} }
this.socket.onmessage = (event) => { this.socket.onmessage = (event) => {
@ -69,6 +74,11 @@ export class Session {
tick: packet.tick tick: packet.tick
}); });
} }
if (packet.type === 'speed') {
this.onState({
speed: packet.speed
});
}
if (packet.type === 'play') { if (packet.type === 'play') {
if (packet.play) { if (packet.play) {
this.onState({ this.onState({
@ -114,6 +124,13 @@ export class Session {
play: update["playing"] play: update["playing"]
})); }));
} }
if (update.hasOwnProperty("speed")) {
this.socket.send(JSON.stringify({
type: 'speed',
session: this.sessionName,
speed: update["speed"]
}));
}
} }
} }
} }
@ -132,6 +149,7 @@ export interface PlaybackState {
tick: number, tick: number,
playing: boolean, playing: boolean,
clients: number, clients: number,
speed: number,
} }
export type StateUpdate = Partial<PlaybackState>; export type StateUpdate = Partial<PlaybackState>;
@ -152,6 +170,12 @@ export interface TickPacket {
tick: number; tick: number;
} }
export interface SpeedPacket {
type: 'speed';
session: string;
speed: number;
}
export interface PlayPacket { export interface PlayPacket {
type: 'play'; type: 'play';
session: string; session: string;
@ -164,4 +188,4 @@ export interface ClientsPacket {
count: number; count: number;
} }
export type Packet = JoinPacket | CreatePacket | TickPacket | PlayPacket | ClientsPacket; export type Packet = JoinPacket | CreatePacket | TickPacket | PlayPacket | ClientsPacket | SpeedPacket;

View file

@ -5,6 +5,12 @@
opacity: 0.5; opacity: 0.5;
transition: opacity 0.3s; transition: opacity 0.3s;
color: white; color: white;
width: 100%;
.speed {
float: right;
padding: 0.5em;
}
& .share { & .share {
display: flex; display: flex;