import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { createDeviceStreamClient, AvccDemuxer, avcCodecString, parseSemuPacket, scanAccessUnit,} from "./deviceStream"; const envelope = (tag: number, payload: number[]) => { const length = 1 + payload.length; return [ (length >>> 24) & 0xff, (length >>> 16) & 0xff, (length >>> 8) & 0xff, length & 0xff, tag, ...payload, ];}; describe("AvccDemuxer", () => { it("reassembles envelopes split across reads", () => { const demuxer = new AvccDemuxer(); const bytes = new Uint8Array([ ...envelope(1, [1, 0x64, 0x00, 0x1f]), ...envelope(2, [9, 9, 9]), ...envelope(0x7f, [0]), ...envelope(3, [4]), ]); const first = demuxer.push(bytes.subarray(0, 7)); const rest = demuxer.push(bytes.subarray(7)); const chunks = [...first, ...rest]; expect(chunks.map((chunk) => chunk.type)).toEqual(["description", "keyframe", "delta"]); expect(Array.from(chunks[0]!.payload)).toEqual([1, 0x64, 0x00, 0x1f]); expect(Array.from(chunks[1]!.payload)).toEqual([9, 9, 9]); }); it("derives the WebCodecs codec string from the avcC record", () => { expect(avcCodecString(new Uint8Array([1, 0x64, 0x00, 0x1f]))).toBe("avc1.64001f"); expect(avcCodecString(new Uint8Array([1]))).toBe("avc1.42E01E"); });}); describe("serve-emu frames", () => { it("strips the SEMU header and reads the keyframe flag and timestamp", () => { const buffer = new ArrayBuffer(16 + 3); const view = new DataView(buffer); view.setUint32(0, 0x53454d55); view.setUint8(4, 1); view.setUint8(5, 1); view.setBigUint64(8, 123456n); new Uint8Array(buffer).set([7, 8, 9], 16); const packet = parseSemuPacket(buffer); expect(packet.isKey).toBe(true); expect(packet.timestamp).toBe(123456); expect(Array.from(packet.data)).toEqual([7, 8, 9]); }); it("treats a frame without the header as raw data", () => { const packet = parseSemuPacket(new Uint8Array([0, 0, 1, 0x65]).buffer); expect(packet.isKey).toBeNull(); expect(packet.data.length).toBe(4); }); it("finds the SPS and IDR NAL units in an Annex-B access unit", () => { const unit = new Uint8Array([0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1f, 0, 0, 1, 0x65, 0xaa]); const scanned = scanAccessUnit(unit); expect(scanned.isKey).toBe(true); expect(scanned.sps && avcCodecString(scanned.sps)).toBe("avc1.64001f"); expect(scanAccessUnit(new Uint8Array([0, 0, 1, 0x41, 0x00])).isKey).toBe(false); });}); describe("iOS input startup", () => { afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); }); const setup = () => { vi.useFakeTimers(); const sockets: FakeSocket[] = []; class FakeSocket { static OPEN = 1; readyState = 1; binaryType = ""; onopen: (() => void) | null = null; onmessage: ((event: { data: ArrayBuffer }) => void) | null = null; onclose: ((event: { code: number; reason: string }) => void) | null = null; send = vi.fn(); close = vi.fn(); constructor() { sockets.push(this); } } vi.stubGlobal("WebSocket", FakeSocket); const signals: AbortSignal[] = []; vi.stubGlobal( "fetch", vi.fn((_url, init: RequestInit) => { const signal = init.signal!; signals.push(signal); return Promise.resolve( new Response( new ReadableStream({ start(controller) { signal.addEventListener("abort", () => controller.error(new Error("aborted"))); }, }), ), ); }), ); const client = createDeviceStreamClient( { platform: "ios", deviceId: "test-device", access: { httpBase: "http://test/api/device-hub", wsBase: "ws://test/api/device-hub", credentials: true, query: {}, }, }, { getContext: () => null } as unknown as HTMLCanvasElement, { onStatus: vi.fn(), onScreen: vi.fn(), onUnauthorized: vi.fn(), onMjpegFallback: vi.fn(), onInputConnected: vi.fn(), }, ); return { client, sockets, signals }; }; it("connects input when the MJPEG prime never produces a frame", async () => { const { client, sockets, signals } = setup(); client.start(); await vi.advanceTimersByTimeAsync(2_000); expect(signals[0]?.aborted).toBe(true); expect(sockets).toHaveLength(1); client.stop(); }); it("aborts priming immediately when hidden without opening a socket later", async () => { const { client, sockets, signals } = setup(); client.start(); await vi.advanceTimersByTimeAsync(0); client.stop(); expect(signals[0]?.aborted).toBe(true); await vi.advanceTimersByTimeAsync(2_000); expect(sockets).toHaveLength(0); }); it.each([ ["landscape_left", 0.7, 1 - 0.2], ["landscape_right", 1 - 0.7, 0.2], ])("maps %s touches back to the raw iOS framebuffer", async (orientation, x, y) => { const { client, sockets } = setup(); client.start(); await vi.advanceTimersByTimeAsync(2_000); const socket = sockets[0]!; const json = new TextEncoder().encode(JSON.stringify({ width: 400, height: 800, orientation })); const packet = new Uint8Array(1 + json.length); packet[0] = 0x82; packet.set(json, 1); socket.onmessage?.({ data: packet.buffer }); client.sendTouch("begin", 0.2, 0.7); const sent = socket.send.mock.calls[0]![0] as Uint8Array; expect(JSON.parse(new TextDecoder().decode(sent.subarray(1)))).toEqual({ type: "begin", x, y, }); client.stop(); });});
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import {
createDeviceStreamClient,
AvccDemuxer,
avcCodecString,
parseSemuPacket,
scanAccessUnit,
} from "./deviceStream";
const envelope = (tag: number, payload: number[]) => {
const length = 1 + payload.length;
return [
(length >>> 24) & 0xff,
(length >>> 16) & 0xff,
(length >>> 8) & 0xff,
length & 0xff,
tag,
...payload,
];
};
describe("AvccDemuxer", () => {
it("reassembles envelopes split across reads", () => {
const demuxer = new AvccDemuxer();
const bytes = new Uint8Array([
...envelope(1, [1, 0x64, 0x00, 0x1f]),
...envelope(2, [9, 9, 9]),
...envelope(0x7f, [0]),
...envelope(3, [4]),
]);
const first = demuxer.push(bytes.subarray(0, 7));
const rest = demuxer.push(bytes.subarray(7));
const chunks = [...first, ...rest];
expect(chunks.map((chunk) => chunk.type)).toEqual(["description", "keyframe", "delta"]);
expect(Array.from(chunks[0]!.payload)).toEqual([1, 0x64, 0x00, 0x1f]);
expect(Array.from(chunks[1]!.payload)).toEqual([9, 9, 9]);
});
it("derives the WebCodecs codec string from the avcC record", () => {
expect(avcCodecString(new Uint8Array([1, 0x64, 0x00, 0x1f]))).toBe("avc1.64001f");
expect(avcCodecString(new Uint8Array([1]))).toBe("avc1.42E01E");
});
});
describe("serve-emu frames", () => {
it("strips the SEMU header and reads the keyframe flag and timestamp", () => {
const buffer = new ArrayBuffer(16 + 3);
const view = new DataView(buffer);
view.setUint32(0, 0x53454d55);
view.setUint8(4, 1);
view.setUint8(5, 1);
view.setBigUint64(8, 123456n);
new Uint8Array(buffer).set([7, 8, 9], 16);
const packet = parseSemuPacket(buffer);
expect(packet.isKey).toBe(true);
expect(packet.timestamp).toBe(123456);
expect(Array.from(packet.data)).toEqual([7, 8, 9]);
});
it("treats a frame without the header as raw data", () => {
const packet = parseSemuPacket(new Uint8Array([0, 0, 1, 0x65]).buffer);
expect(packet.isKey).toBeNull();
expect(packet.data.length).toBe(4);
});
it("finds the SPS and IDR NAL units in an Annex-B access unit", () => {
const unit = new Uint8Array([0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1f, 0, 0, 1, 0x65, 0xaa]);
const scanned = scanAccessUnit(unit);
expect(scanned.isKey).toBe(true);
expect(scanned.sps && avcCodecString(scanned.sps)).toBe("avc1.64001f");
expect(scanAccessUnit(new Uint8Array([0, 0, 1, 0x41, 0x00])).isKey).toBe(false);
});
});
describe("iOS input startup", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
const setup = () => {
vi.useFakeTimers();
const sockets: FakeSocket[] = [];
class FakeSocket {
static OPEN = 1;
readyState = 1;
binaryType = "";
onopen: (() => void) | null = null;
onmessage: ((event: { data: ArrayBuffer }) => void) | null = null;
onclose: ((event: { code: number; reason: string }) => void) | null = null;
send = vi.fn();
close = vi.fn();
constructor() {
sockets.push(this);
}
}
vi.stubGlobal("WebSocket", FakeSocket);
const signals: AbortSignal[] = [];
vi.stubGlobal(
"fetch",
vi.fn((_url, init: RequestInit) => {
const signal = init.signal!;
signals.push(signal);
return Promise.resolve(
new Response(
new ReadableStream({
start(controller) {
signal.addEventListener("abort", () => controller.error(new Error("aborted")));
},
}),
),
);
}),
);
const client = createDeviceStreamClient(
{
platform: "ios",
deviceId: "test-device",
access: {
httpBase: "http://test/api/device-hub",
wsBase: "ws://test/api/device-hub",
credentials: true,
query: {},
},
},
{ getContext: () => null } as unknown as HTMLCanvasElement,
{
onStatus: vi.fn(),
onScreen: vi.fn(),
onUnauthorized: vi.fn(),
onMjpegFallback: vi.fn(),
onInputConnected: vi.fn(),
},
);
return { client, sockets, signals };
};
it("connects input when the MJPEG prime never produces a frame", async () => {
const { client, sockets, signals } = setup();
client.start();
await vi.advanceTimersByTimeAsync(2_000);
expect(signals[0]?.aborted).toBe(true);
expect(sockets).toHaveLength(1);
client.stop();
});
it("aborts priming immediately when hidden without opening a socket later", async () => {
const { client, sockets, signals } = setup();
client.start();
await vi.advanceTimersByTimeAsync(0);
client.stop();
expect(signals[0]?.aborted).toBe(true);
await vi.advanceTimersByTimeAsync(2_000);
expect(sockets).toHaveLength(0);
});
it.each([
["landscape_left", 0.7, 1 - 0.2],
["landscape_right", 1 - 0.7, 0.2],
])("maps %s touches back to the raw iOS framebuffer", async (orientation, x, y) => {
const { client, sockets } = setup();
client.start();
await vi.advanceTimersByTimeAsync(2_000);
const socket = sockets[0]!;
const json = new TextEncoder().encode(JSON.stringify({ width: 400, height: 800, orientation }));
const packet = new Uint8Array(1 + json.length);
packet[0] = 0x82;
packet.set(json, 1);
socket.onmessage?.({ data: packet.buffer });
client.sendTouch("begin", 0.2, 0.7);
const sent = socket.send.mock.calls[0]![0] as Uint8Array;
expect(JSON.parse(new TextDecoder().decode(sent.subarray(1)))).toEqual({
type: "begin",
x,
y,
});
client.stop();
});
});