Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/mobile-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export type {
ConnectViewerConfig,
RoomType,
UseSandboxProps,
SandboxOptions,
VideoCodec,
BandwidthLimits,
Brand,
DeviceError,
Expand Down
24 changes: 20 additions & 4 deletions packages/react-client/src/hooks/useSandbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback } from "react";

import type { VideoCodec } from "../types/public";
import { MissingSandboxApiUrlError } from "../utils/errors";

type BasicInfo = { id: string; name: string };
Expand All @@ -26,22 +27,30 @@ export type UseSandboxProps = {

export type RoomType = "conference" | "livestream" | "audio_only";

export type SandboxOptions = {
videoCodec?: VideoCodec;
};

export const useSandbox = (props: UseSandboxProps) => {
const sandboxApiUrl = props?.sandboxApiUrl;

const getSandboxPeerToken = useCallback(
async (roomName: string, peerName: string, roomType: RoomType = "conference") => {
async (roomName: string, peerName: string, roomType: RoomType = "conference", options: SandboxOptions = {}) => {
if (!sandboxApiUrl) throw new MissingSandboxApiUrlError();

const url = new URL(sandboxApiUrl);
url.searchParams.set("roomName", roomName);
url.searchParams.set("peerName", peerName);
url.searchParams.set("roomType", roomType);
if (options.videoCodec) url.searchParams.set("videoCodec", options.videoCodec);

const res = await fetch(url);

if (!res.ok) {
const message = `Failed to retrieve peer token for peer '${peerName}' in ${roomType} room '${roomName}'.`;
let message = `Failed to retrieve peer token for peer '${peerName}' in ${roomType} room '${roomName}'.`;
if (res.status === 409) {
message = `Room '${roomName}' already exists with a different room type or video codec.`;
}
throw new Error(message);
}

Expand Down Expand Up @@ -73,15 +82,22 @@ export const useSandbox = (props: UseSandboxProps) => {
);

const getSandboxLivestream = useCallback(
async (roomName: string, isPublic: boolean = false) => {
async (roomName: string, isPublic: boolean = false, options: SandboxOptions = {}) => {
if (!sandboxApiUrl) throw new MissingSandboxApiUrlError();

const url = new URL(`${sandboxApiUrl}/livestream`);
url.searchParams.set("roomName", roomName);
url.searchParams.set("public", isPublic.toString());
if (options.videoCodec) url.searchParams.set("videoCodec", options.videoCodec);

const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to retrieve streamer token for '${roomName}' livestream room.`);
if (!res.ok) {
let message = `Failed to retrieve streamer token for '${roomName}' livestream room.`;
if (res.status === 409) {
message = `Room '${roomName}' already exists with a different room type or video codec.`;
}
throw new Error(message);
}

const data: { streamerToken: string; room: { id: string; name: string } } = await res.json();
return data;
Expand Down
9 changes: 8 additions & 1 deletion packages/react-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ export {
type UseLivestreamViewerResult,
} from "./hooks/useLivestreamViewer";
export { type PeerWithTracks, usePeers } from "./hooks/usePeers";
export { type MoqAccess, type RoomType, useSandbox, type UseSandboxProps } from "./hooks/useSandbox";
export {
type MoqAccess,
type RoomType,
type SandboxOptions,
useSandbox,
type UseSandboxProps,
} from "./hooks/useSandbox";
export { useScreenShare } from "./hooks/useScreenShare";
export { useUpdatePeerMetadata } from "./hooks/useUpdatePeerMetadata";
export { useVAD } from "./hooks/useVAD";
Expand All @@ -48,6 +54,7 @@ export type {
TracksMiddleware,
TracksMiddlewareResult,
UseDataChannelResult,
VideoCodec,
} from "./types/public";
export type {
AuthErrorReason,
Expand Down
61 changes: 61 additions & 0 deletions packages/react-client/src/tests/sandbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ describe("useSandbox", () => {
expect(calledUrl.searchParams.get("roomType")).toBe("conference");
});

it("getSandboxPeerToken passes the requested video codec", async ({ renderHook }) => {
const fetchSpy = mockFetch({ ok: true, json: async () => ({ peerToken: "pt" }) });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));

await result.current.getSandboxPeerToken("room", "bob", "conference", { videoCodec: "h264" });
const calledUrl = new URL(fetchSpy.mock.calls[0][0]);
expect(calledUrl.searchParams.get("videoCodec")).toBe("h264");
});

it("getSandboxPeerToken omits the video codec by default", async ({ renderHook }) => {
const fetchSpy = mockFetch({ ok: true, json: async () => ({ peerToken: "pt" }) });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));

await result.current.getSandboxPeerToken("room", "bob");
const calledUrl = new URL(fetchSpy.mock.calls[0][0]);
expect(calledUrl.searchParams.has("videoCodec")).toBe(false);
});

it("getSandboxViewerToken gives a friendly error when the room is missing", async ({ renderHook }) => {
mockFetch({ ok: false, status: 404 });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));
Expand All @@ -53,4 +71,47 @@ describe("useSandbox", () => {
const data = await result.current.getSandboxLivestream("room", true);
expect(data.streamerToken).toBe("st");
});

it("getSandboxLivestream passes the visibility", async ({ renderHook }) => {
const fetchSpy = mockFetch({ ok: true, json: async () => ({ streamerToken: "st", room: { id: "r", name: "n" } }) });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));

await result.current.getSandboxLivestream("room", true);
const calledUrl = new URL(fetchSpy.mock.calls[0][0]);
expect(calledUrl.searchParams.get("public")).toBe("true");
});

it("getSandboxLivestream passes the requested video codec", async ({ renderHook }) => {
const fetchSpy = mockFetch({ ok: true, json: async () => ({ streamerToken: "st", room: { id: "r", name: "n" } }) });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));

await result.current.getSandboxLivestream("room", false, { videoCodec: "h264" });
const calledUrl = new URL(fetchSpy.mock.calls[0][0]);
expect(calledUrl.searchParams.get("videoCodec")).toBe("h264");
});

it("getSandboxLivestream omits the video codec by default", async ({ renderHook }) => {
const fetchSpy = mockFetch({ ok: true, json: async () => ({ streamerToken: "st", room: { id: "r", name: "n" } }) });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));

await result.current.getSandboxLivestream("room");
const calledUrl = new URL(fetchSpy.mock.calls[0][0]);
expect(calledUrl.searchParams.has("videoCodec")).toBe(false);
});

it("getSandboxPeerToken explains a conflicting room configuration", async ({ renderHook }) => {
mockFetch({ ok: false, status: 409 });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));
await expect(
result.current.getSandboxPeerToken("room", "bob", "conference", { videoCodec: "h264" }),
).rejects.toThrow(/already exists with a different/);
});

it("getSandboxLivestream explains a conflicting room configuration", async ({ renderHook }) => {
mockFetch({ ok: false, status: 409 });
const { result } = renderHook(() => useSandbox({ sandboxApiUrl: "https://sandbox.test/api" }));
await expect(result.current.getSandboxLivestream("room", false, { videoCodec: "h264" })).rejects.toThrow(
/already exists with a different/,
);
});
});
2 changes: 2 additions & 0 deletions packages/react-client/src/types/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type InitializeDevicesResult = {
errors: { audio: DeviceError | null; video: DeviceError | null } | null;
};

export type VideoCodec = "vp8" | "h264";

export type TrackId = Brand<string, "TrackId">;
export type PeerId = Brand<string, "PeerId">;

Expand Down
Loading