180 lines · 5.6 KB · TypeScript
RawDownload
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();  });});