diff --git a/.eslintignore b/.eslintignore
index 6e3e1f3..af0fb01 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,4 @@
dist
docs
-.vscode
\ No newline at end of file
+.vscode
+playwright-report
\ No newline at end of file
diff --git a/examples/minimal-react/src/components/App.tsx b/examples/minimal-react/src/components/App.tsx
index d939544..21b2eb4 100644
--- a/examples/minimal-react/src/components/App.tsx
+++ b/examples/minimal-react/src/components/App.tsx
@@ -67,8 +67,8 @@ export const App = () => {
Status: {status}
{/* Render the remote tracks from other peers*/}
- {Object.values(tracks).map(({ stream, trackId }) => (
- // Simple component to render a video element
+ {Object.values(tracks).map(({ stream, trackId, origin }) => (
+ // Simple component to render a video element
))}
);
diff --git a/examples/minimal-react/src/components/VideoPlayer.tsx b/examples/minimal-react/src/components/VideoPlayer.tsx
index 028e469..eb0d7bc 100644
--- a/examples/minimal-react/src/components/VideoPlayer.tsx
+++ b/examples/minimal-react/src/components/VideoPlayer.tsx
@@ -2,9 +2,10 @@ import { RefObject, useEffect, useRef } from "react";
type Props = {
stream: MediaStream | null | undefined;
+ peerId: string;
};
-const VideoPlayer = ({ stream }: Props) => {
+const VideoPlayer = ({ stream, peerId }: Props) => {
const videoRef: RefObject = useRef(null);
useEffect(() => {
@@ -12,7 +13,7 @@ const VideoPlayer = ({ stream }: Props) => {
videoRef.current.srcObject = stream || null;
}, [stream]);
- return ;
+ return ;
};
export default VideoPlayer;
diff --git a/tests/jellyfish.spec.ts b/tests/jellyfish.spec.ts
index 379fa69..01d849c 100644
--- a/tests/jellyfish.spec.ts
+++ b/tests/jellyfish.spec.ts
@@ -1,5 +1,10 @@
-import { JellyfishClient } from "@jellyfish-dev/ts-client-sdk";
-import { test, expect, type Page } from "@playwright/test";
+import { test, expect } from "@playwright/test";
+import {
+ assertThatOtherVideoIsPlaying,
+ assertThatRemoteTracksAreVisible,
+ createRoom,
+ joinRoomAndAddScreenShare,
+} from "./utils";
test("displays basic UI", async ({ page }) => {
await page.goto("/");
@@ -9,62 +14,107 @@ test("displays basic UI", async ({ page }) => {
await expect(page.getByRole("button", { name: "Connect", exact: true })).toBeVisible();
});
-test("connects to Jellyfish server", async ({ page: firstPage, context }) => {
+test("Connect 2 peers to 1 room", async ({ page: firstPage, context }) => {
const secondPage = await context.newPage();
await firstPage.goto("/");
await secondPage.goto("/");
- const roomRequest = await firstPage.request.post("http://localhost:5002/room");
- const roomId = (await roomRequest.json()).data.room.id as string;
+ const roomId = await createRoom(firstPage);
- await joinRoomAndAddTrack(firstPage, roomId);
- await joinRoomAndAddTrack(secondPage, roomId);
-
- await expect(firstPage.locator("video")).toBeVisible();
- await expect(secondPage.locator("video")).toBeVisible();
+ const firstPageId = await joinRoomAndAddScreenShare(firstPage, roomId);
+ const secondPageId = await joinRoomAndAddScreenShare(secondPage, roomId);
+ await Promise.all([
+ assertThatRemoteTracksAreVisible(firstPage, [secondPageId]),
+ assertThatRemoteTracksAreVisible(secondPage, [firstPageId]),
+ ]);
await Promise.all([assertThatOtherVideoIsPlaying(firstPage), assertThatOtherVideoIsPlaying(secondPage)]);
});
-async function joinRoomAndAddTrack(page: Page, roomId: string): Promise {
- const peerRequest = await page.request.post("http://localhost:5002/room/" + roomId + "/peer", {
- data: {
- type: "webrtc",
- options: {
- enableSimulcast: true,
- },
+test("Client properly sees 3 other peers", async ({ page, context }) => {
+ const pages = [page, ...(await Promise.all([...Array(3)].map(() => context.newPage())))];
+
+ const roomId = await createRoom(page);
+
+ const peerIds = await Promise.all(
+ pages.map(async (page) => {
+ await page.goto("/");
+ return await joinRoomAndAddScreenShare(page, roomId);
+ }),
+ );
+
+ await Promise.all(
+ pages.map(async (page, idx) => {
+ await assertThatRemoteTracksAreVisible(
+ page,
+ peerIds.filter((id) => id !== peerIds[idx]),
+ );
+ await assertThatOtherVideoIsPlaying(page);
+ }),
+ );
+});
+
+test("Peer see peers just in the same room", async ({ page, context }) => {
+ const [p1r1, p2r1, p1r2, p2r2] = [page, ...(await Promise.all([...Array(3)].map(() => context.newPage())))];
+ const [firstRoomPages, secondRoomPages] = [
+ [p1r1, p2r1],
+ [p1r2, p2r2],
+ ];
+
+ const firstRoomId = await createRoom(page);
+ const secondRoomId = await createRoom(page);
+
+ const firstRoomPeerIds = await Promise.all(
+ firstRoomPages.map(async (page) => {
+ await page.goto("/");
+ return await joinRoomAndAddScreenShare(page, firstRoomId);
+ }),
+ );
+
+ const secondRoomPeerIds = await Promise.all(
+ secondRoomPages.map(async (page) => {
+ await page.goto("/");
+ return await joinRoomAndAddScreenShare(page, secondRoomId);
+ }),
+ );
+
+ await Promise.all([
+ ...firstRoomPages.map(async (page, idx) => {
+ await assertThatRemoteTracksAreVisible(
+ page,
+ firstRoomPeerIds.filter((id) => id !== firstRoomPeerIds[idx]),
+ );
+ await expect(assertThatRemoteTracksAreVisible(page, secondRoomPeerIds)).rejects.toThrow();
+ await assertThatOtherVideoIsPlaying(page);
+ }),
+ ...secondRoomPages.map(async (page, idx) => {
+ await assertThatRemoteTracksAreVisible(
+ page,
+ secondRoomPeerIds.filter((id) => id !== secondRoomPeerIds[idx]),
+ );
+ await expect(assertThatRemoteTracksAreVisible(page, firstRoomPeerIds)).rejects.toThrow();
+ await assertThatOtherVideoIsPlaying(page);
+ }),
+ ]);
+});
+
+test("Client throws an error if joining room at max capacity", async ({ page, context }) => {
+ const [page1, page2, overflowingPage] = [page, ...(await Promise.all([...Array(2)].map(() => context.newPage())))];
+
+ const roomId = await createRoom(page, 2);
+
+ await Promise.all(
+ [page1, page2].map(async (page) => {
+ await page.goto("/");
+ return await joinRoomAndAddScreenShare(page, roomId);
+ }),
+ );
+
+ await overflowingPage.goto("/");
+ await expect(joinRoomAndAddScreenShare(overflowingPage, roomId)).rejects.toEqual({
+ status: 503,
+ response: {
+ errors: `Reached peer limit in room ${roomId}`,
},
});
- const {
- peer: { id: peerId },
- token: peerToken,
- } = (await peerRequest.json()).data;
-
- await page.getByPlaceholder("token").fill(peerToken);
- await page.getByRole("button", { name: "Connect", exact: true }).click();
- await page.getByRole("button", { name: "Start screen share" }).click();
-
- return peerId;
-}
-
-async function assertThatOtherVideoIsPlaying(page: Page) {
- const playing = await page.evaluate(async () => {
- const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
- const getDecodedFrames = async () => {
- const stats = await peerConnection.getStats();
- for (const stat of stats.values()) {
- if (stat.type === "inbound-rtp") {
- return stat.framesDecoded;
- }
- }
- };
-
- const client = (window as unknown as { client: JellyfishClient }).client;
- const peerConnection = (client as unknown as { webrtc: { connection: RTCPeerConnection } }).webrtc.connection;
- const firstMeasure = await getDecodedFrames();
- await sleep(400);
- const secondMeasure = await getDecodedFrames();
- return secondMeasure > firstMeasure;
- });
- expect(playing).toBe(true);
-}
+});
diff --git a/tests/utils.ts b/tests/utils.ts
new file mode 100644
index 0000000..dd9592d
--- /dev/null
+++ b/tests/utils.ts
@@ -0,0 +1,76 @@
+import { expect, Page, test } from "@playwright/test";
+
+export const joinRoomAndAddScreenShare = async (page: Page, roomId: string): Promise =>
+ test.step("Join room and add track", async () => {
+ const peerRequest = await createPeer(page, roomId);
+ try {
+ const {
+ peer: { id: peerId },
+ token: peerToken,
+ } = (await peerRequest.json()).data;
+
+ await test.step("Join room", async () => {
+ await page.getByPlaceholder("token").fill(peerToken);
+ await page.getByRole("button", { name: "Connect", exact: true }).click();
+ await expect(page.getByText("Status: joined")).toBeVisible();
+ });
+
+ await test.step("Add screenshare", async () => {
+ await page.getByRole("button", { name: "Start screen share", exact: true }).click();
+ });
+
+ return peerId;
+ } catch (e) {
+ // todo fix
+ throw { status: peerRequest.status(), response: await peerRequest.json() };
+ }
+ });
+
+export const assertThatRemoteTracksAreVisible = async (page: Page, otherClientIds: string[]) => {
+ await test.step("Assert that remote tracks are visible", () =>
+ Promise.all(
+ otherClientIds.map((peerId) => expect(page.locator(`css=video[data-peer-id="${peerId}"]`)).toBeVisible()),
+ ));
+};
+
+export const assertThatOtherVideoIsPlaying = async (page: Page) => {
+ await test.step("Assert that media is working", async () => {
+ const getDecodedFrames: () => Promise = () =>
+ page.evaluate(async () => {
+ const peerConnection = (
+ window as typeof window & { client: { webrtc: { connection: RTCPeerConnection | undefined } } }
+ ).client.webrtc.connection;
+ const stats = await peerConnection?.getStats();
+ for (const stat of stats?.values() ?? []) {
+ if (stat.type === "inbound-rtp") {
+ return stat.framesDecoded;
+ }
+ }
+ return 0;
+ });
+ const firstMeasure = await getDecodedFrames();
+ await expect(async () => expect((await getDecodedFrames()) > firstMeasure).toBe(true)).toPass();
+ });
+};
+
+export const createRoom = async (page: Page, maxPeers?: number) =>
+ await test.step("Create room", async () => {
+ const data = {
+ ...(maxPeers ? { maxPeers } : {}),
+ };
+
+ const roomRequest = await page.request.post("http://localhost:5002/room", { data });
+ return (await roomRequest.json()).data.room.id as string;
+ });
+
+export const createPeer = async (page: Page, roomId: string, enableSimulcast: boolean = true) =>
+ await test.step("Create room", async () => {
+ return await page.request.post("http://localhost:5002/room/" + roomId + "/peer", {
+ data: {
+ type: "webrtc",
+ options: {
+ enableSimulcast,
+ },
+ },
+ });
+ });