mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 15:12:48 +00:00
Plugins can declare a subscription with defineSubscription, handle it with
server.handle(contract, (input, { push, signal })), and listen from a surface
with useSubscription or client.subscribe. A top-level Uint8Array in an event,
an RPC output, or an RPC input crosses the wire as a binary frame (opcodes
0x20-0x2f) without JSON or base64; a nested one fails validation on the sender.
Each subscriber runs its own handler, owned by the session that subscribed,
so release, disconnect, plugin stop and reload need no plugin code. The
runtime buffers pushes until the subscribe response has gone out, so a client
never sees an event for a subscription it does not know yet.
Gated on server_info.features.pluginSubscriptions; binary outputs reach only
clients that advertise plugin_binary_payloads.
48 lines
1.8 KiB
TypeScript
48 lines
1.8 KiB
TypeScript
import { defineSubscription } from "@getpaseo/plugin";
|
|
import { z } from "zod";
|
|
|
|
/** The first byte of every frame says which stream it belongs to. */
|
|
export const MediaOpcode = { Video: 0x01, Audio: 0x02, Keyframe: 0x03 } as const;
|
|
|
|
export type MediaFrame =
|
|
| {
|
|
opcode: typeof MediaOpcode.Video | typeof MediaOpcode.Keyframe;
|
|
timestampMs: number;
|
|
payload: Uint8Array;
|
|
}
|
|
| { opcode: typeof MediaOpcode.Audio; timestampMs: number; payload: Uint8Array };
|
|
|
|
const HEADER_BYTES = 1 + 8;
|
|
|
|
/** `[opcode u8][timestampMs f64][payload]` */
|
|
export function encodeMediaFrame(frame: MediaFrame): Uint8Array {
|
|
const bytes = new Uint8Array(HEADER_BYTES + frame.payload.byteLength);
|
|
bytes[0] = frame.opcode;
|
|
new DataView(bytes.buffer).setFloat64(1, frame.timestampMs);
|
|
bytes.set(frame.payload, HEADER_BYTES);
|
|
return bytes;
|
|
}
|
|
|
|
/** Returns a view into `bytes`; the payload is not copied. */
|
|
export function decodeMediaFrame(bytes: Uint8Array): MediaFrame | null {
|
|
if (bytes.byteLength < HEADER_BYTES) return null;
|
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
const opcode = bytes[0];
|
|
const timestampMs = view.getFloat64(1);
|
|
const payload = bytes.subarray(HEADER_BYTES);
|
|
if (opcode === MediaOpcode.Audio) return { opcode, timestampMs, payload };
|
|
if (opcode === MediaOpcode.Video || opcode === MediaOpcode.Keyframe)
|
|
return { opcode, timestampMs, payload };
|
|
return null;
|
|
}
|
|
|
|
/** A video frame is `width * height` RGB pixels; audio is signed 16-bit mono PCM. */
|
|
export const VIDEO = { width: 16, height: 9, framesPerSecond: 15, keyframeInterval: 30 } as const;
|
|
export const AUDIO = { sampleRate: 8000, chunkMs: 20 } as const;
|
|
|
|
export const mediaStream = defineSubscription({
|
|
name: "media.stream",
|
|
input: z.object({ source: z.enum(["test-pattern"]) }),
|
|
event: z.instanceof(Uint8Array),
|
|
});
|