File
Blob: worker/src/client/radio/protocol.ts
| 1 | import { robotMessageSchema } from '@/shared/contracts/robot.ts'; |
| 2 | import type { RobotMessage } from '@/shared/contracts/robot.ts'; |
| 3 | |
| 4 | export type SpectrumFrame = { |
| 5 | pts: number; |
| 6 | positionMs: number; |
| 7 | paused: boolean; |
| 8 | bands: Uint8Array; |
| 9 | revision?: number; |
| 10 | }; |
| 11 | |
| 12 | export 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. */ |
| 23 | export function newerTimestamp(next: number, previous: number): boolean { |
| 24 | const distance = (next - previous) >>> 0; |
| 25 | return distance > 0 && distance < 0x8000_0000; |
| 26 | } |
| 27 | export 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 | |
| 47 | export 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 | } |