Skip to content
File

Blob: worker/src/client/radio/protocol.ts

typescript53 lines
1import { robotMessageSchema } from '@/shared/contracts/robot.ts';
2import type { RobotMessage } from '@/shared/contracts/robot.ts';
3 
4export type SpectrumFrame = {
5 pts: number;
6 positionMs: number;
7 paused: boolean;
8 bands: Uint8Array;
9 revision?: number;
10};
11 
12export function parseRobot(data: unknown): RobotMessage | undefined {
13 if (typeof data !== 'string' || data.length > 4096) return;
14 try {
15 const parsed = robotMessageSchema.safeParse(JSON.parse(data));
16 return parsed.success ? parsed.data : undefined;
17 } catch {
18 return;
19 }
20}
21 
22/** Wrapping transport milliseconds order frames across track changes and song restarts. */
23export function newerTimestamp(next: number, previous: number): boolean {
24 const distance = (next - previous) >>> 0;
25 return distance > 0 && distance < 0x8000_0000;
26}
27export function parseSpectrum(data: unknown, previous?: number): SpectrumFrame | undefined {
28 if (!(data instanceof ArrayBuffer) || (data.byteLength !== 44 && data.byteLength !== 48)) return;
29 const view = new DataView(data);
30 const version = view.getUint8(0);
31 if (
32 !((version === 1 && data.byteLength === 44) || (version === 2 && data.byteLength === 48)) ||
33 view.getUint8(1) > 1
34 )
35 return;
36 const pts = view.getUint32(4, true);
37 if (previous !== undefined && !newerTimestamp(pts, previous)) return;
38 return {
39 pts,
40 positionMs: view.getUint32(8, true),
41 paused: view.getUint8(1) === 1,
42 bands: new Uint8Array(data, 12, 32).slice(),
43 ...(version === 2 ? { revision: view.getUint32(44, true) } : {}),
44 };
45}
46 
47export function commandId(): string {
48 // getRandomValues also works on a phone's HTTP development origin.
49 return Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) =>
50 byte.toString(16).padStart(2, '0'),
51 ).join('');
52}