From 86e435cd384c2a83c8b1f664227ce6392b52439e Mon Sep 17 00:00:00 2001 From: Miron Pawlik Date: Thu, 9 May 2024 15:24:38 +0200 Subject: [PATCH] Merge `membrane-webrtc-js` into `ts-client-sdk` (#38) This PR is merging logic from https://github.com/jellyfish-dev/membrane-webrtc-js into this repository. So we can keep only one SDK for web (apart from React SDK - that can still be separate). Next phase would be to generate new documentation for joined library and update/release new version of ts-client-sdk Note Commit history from previous repo is preserved using trick from https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/ --- .eslintrc | 1 + .github/workflows/node.yaml | 22 +- .github/workflows/playwright.yaml | 13 +- .gitignore | 2 + e2e/app/.gitignore | 28 + e2e/app/README.md | 27 + e2e/app/compile_proto.sh | 20 + e2e/app/index.html | 20 + e2e/app/package.json | 33 + e2e/app/src/App.tsx | 282 ++ e2e/app/src/MockComponent.tsx | 123 + e2e/app/src/VideoPlayer.tsx | 17 + e2e/app/src/VideoPlayerWithDetector.tsx | 57 + e2e/app/src/main.tsx | 9 + e2e/app/src/mocks.ts | 88 + .../protos/jellyfish/peer_notifications.ts | 296 ++ e2e/app/src/vite-env.d.ts | 1 + e2e/app/tsconfig.json | 25 + e2e/app/tsconfig.node.json | 10 + e2e/app/vite.config.ts | 7 + e2e/app/yarn.lock | 1874 ++++++++++ {tests => e2e}/docker-compose-test.yaml | 2 +- e2e/scenarios/basic.spec.ts | 138 + e2e/scenarios/metadataParsing.spec.ts | 114 + e2e/scenarios/raceCondition.spec.ts | 283 ++ e2e/scenarios/utils.ts | 229 ++ {tests => e2e/setup}/globalSetupState.ts | 0 {tests => e2e/setup}/setupJellyfish.ts | 2 +- {tests => e2e/setup}/teardownJellyfish.ts | 0 examples/minimal/package-lock.json | 2784 -------------- examples/minimal/yarn.lock | 995 +++++ examples/simple-app/package-lock.json | 3318 ----------------- examples/simple-app/package.json | 2 +- examples/simple-app/yarn.lock | 1503 ++++++++ package-lock.json | 3047 --------------- package.json | 18 +- playwright.config.ts | 23 +- readme.md | 5 +- src/JellyfishClient.ts | 8 +- src/index.ts | 4 +- src/webrtc/commands.ts | 32 + src/webrtc/const.ts | 39 + src/webrtc/deferred.ts | 21 + src/webrtc/index.ts | 19 + src/webrtc/mediaEvent.ts | 27 + src/webrtc/webRTCEndpoint.ts | 1746 +++++++++ tests/events/bandwidthEstimationEvent.test.ts | 22 + tests/events/connectedEvent.test.ts | 85 + tests/events/encodingSwitchedEvent.test.ts | 86 + tests/events/endpointAddedEvent.test.ts | 107 + tests/events/endpointRemovedEvent.test.ts | 95 + tests/events/endpointUpdatedEvent.test.ts | 144 + tests/events/trackAddedEvent.test.ts | 169 + tests/events/trackRemovedEvent.test.ts | 36 + tests/events/trackUpdatedEvent.test.ts | 139 + tests/events/vadNotificationEvent.test.ts | 51 + tests/fixtures.ts | 340 ++ tests/jellyfish.spec.ts | 188 - tests/methods/addTrackMethod.test.ts | 74 + tests/methods/cleanUpMethod.test.ts | 18 + tests/methods/connectMethod.test.ts | 67 + tests/methods/disconnectMethod.test.ts | 38 + tests/mocks.ts | 86 + tests/schema.ts | 178 + tests/utils.ts | 67 + typedoc.json | 7 - yarn.lock | 976 ++++- 67 files changed, 10854 insertions(+), 9433 deletions(-) create mode 100644 e2e/app/.gitignore create mode 100644 e2e/app/README.md create mode 100755 e2e/app/compile_proto.sh create mode 100644 e2e/app/index.html create mode 100644 e2e/app/package.json create mode 100644 e2e/app/src/App.tsx create mode 100644 e2e/app/src/MockComponent.tsx create mode 100644 e2e/app/src/VideoPlayer.tsx create mode 100644 e2e/app/src/VideoPlayerWithDetector.tsx create mode 100644 e2e/app/src/main.tsx create mode 100644 e2e/app/src/mocks.ts create mode 100644 e2e/app/src/protos/jellyfish/peer_notifications.ts create mode 100644 e2e/app/src/vite-env.d.ts create mode 100644 e2e/app/tsconfig.json create mode 100644 e2e/app/tsconfig.node.json create mode 100644 e2e/app/vite.config.ts create mode 100644 e2e/app/yarn.lock rename {tests => e2e}/docker-compose-test.yaml (98%) create mode 100644 e2e/scenarios/basic.spec.ts create mode 100644 e2e/scenarios/metadataParsing.spec.ts create mode 100644 e2e/scenarios/raceCondition.spec.ts create mode 100644 e2e/scenarios/utils.ts rename {tests => e2e/setup}/globalSetupState.ts (100%) rename {tests => e2e/setup}/setupJellyfish.ts (88%) rename {tests => e2e/setup}/teardownJellyfish.ts (100%) delete mode 100644 examples/minimal/package-lock.json create mode 100644 examples/minimal/yarn.lock delete mode 100644 examples/simple-app/package-lock.json create mode 100644 examples/simple-app/yarn.lock delete mode 100644 package-lock.json create mode 100644 src/webrtc/commands.ts create mode 100644 src/webrtc/const.ts create mode 100644 src/webrtc/deferred.ts create mode 100644 src/webrtc/index.ts create mode 100644 src/webrtc/mediaEvent.ts create mode 100644 src/webrtc/webRTCEndpoint.ts create mode 100644 tests/events/bandwidthEstimationEvent.test.ts create mode 100644 tests/events/connectedEvent.test.ts create mode 100644 tests/events/encodingSwitchedEvent.test.ts create mode 100644 tests/events/endpointAddedEvent.test.ts create mode 100644 tests/events/endpointRemovedEvent.test.ts create mode 100644 tests/events/endpointUpdatedEvent.test.ts create mode 100644 tests/events/trackAddedEvent.test.ts create mode 100644 tests/events/trackRemovedEvent.test.ts create mode 100644 tests/events/trackUpdatedEvent.test.ts create mode 100644 tests/events/vadNotificationEvent.test.ts create mode 100644 tests/fixtures.ts delete mode 100644 tests/jellyfish.spec.ts create mode 100644 tests/methods/addTrackMethod.test.ts create mode 100644 tests/methods/cleanUpMethod.test.ts create mode 100644 tests/methods/connectMethod.test.ts create mode 100644 tests/methods/disconnectMethod.test.ts create mode 100644 tests/mocks.ts create mode 100644 tests/schema.ts create mode 100644 tests/utils.ts diff --git a/.eslintrc b/.eslintrc index cf3b4ce1..86c64dfd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,7 @@ ], "rules": { "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unused-vars": [ "warn", { diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 4ecd597e..a6937567 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -24,19 +24,19 @@ jobs: cache: "npm" - name: Install dependencies ⬇️ - run: npm ci + run: yarn - name: Check formatting 🎨 - run: npm run format:check + run: yarn format:check - name: Run linter - run: npm run lint:check + run: yarn lint:check - name: Build 📦 - run: npm run build --if-present + run: yarn build - name: Test 🚀 - run: npm test --if-present + run: yarn test build_and_test_examples: name: Build and test examples @@ -54,20 +54,16 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} 🛎️ - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: "npm" - name: Install dependencies lib ⬇️ - run: npm ci + run: yarn working-directory: . - name: Install dependencies ⬇️ - run: npm ci + run: yarn - name: Build 📦 - run: npm run build --if-present - - - name: Test 🚀 - run: npm test --if-present + run: yarn build diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index 53376c93..caff8990 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -10,20 +10,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Install dependencies - run: npm ci + run: yarn - name: Build SDK - run: npm run build + run: yarn build - name: Install example dependencies - run: npm ci + run: yarn working-directory: examples/simple-app + - name: Install e2e dependencies + run: yarn + working-directory: e2e/app - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Run Playwright tests - run: npm run e2e + run: yarn e2e - uses: actions/upload-artifact@v3 if: always() with: diff --git a/.gitignore b/.gitignore index ec15ce37..90c8114c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ docs /playwright-report/ /blob-report/ /playwright/.cache/ + +/coverage/ \ No newline at end of file diff --git a/e2e/app/.gitignore b/e2e/app/.gitignore new file mode 100644 index 00000000..b88c8135 --- /dev/null +++ b/e2e/app/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/app/README.md b/e2e/app/README.md new file mode 100644 index 00000000..1ebe379f --- /dev/null +++ b/e2e/app/README.md @@ -0,0 +1,27 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/e2e/app/compile_proto.sh b/e2e/app/compile_proto.sh new file mode 100755 index 00000000..374f6b4a --- /dev/null +++ b/e2e/app/compile_proto.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Terminate on errors +set -e + + +printf "Synchronising submodules... " +git submodule sync --recursive >> /dev/null +git submodule update --recursive --remote --init >> /dev/null +printf "DONE\n\n" + +file="./protos/jellyfish/peer_notifications.proto" + +printf "Compiling: file %s\n" "$file" +protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/ $file +printf "DONE\n" + +cd ../.. +npm run format:fix +npm run lint:fix diff --git a/e2e/app/index.html b/e2e/app/index.html new file mode 100644 index 00000000..4f4239c0 --- /dev/null +++ b/e2e/app/index.html @@ -0,0 +1,20 @@ + + + + + + + Vite + React + TS + + + +
+ + + diff --git a/e2e/app/package.json b/e2e/app/package.json new file mode 100644 index 00000000..88e1a922 --- /dev/null +++ b/e2e/app/package.json @@ -0,0 +1,33 @@ +{ + "name": "example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@jellyfish-dev/ts-client-sdk": "file:../..", + "protobufjs": "^7.2.6", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-proto": "^1.167.3" + }, + "devDependencies": { + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.18", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@typescript-eslint/eslint-plugin": "^7.0.1", + "@typescript-eslint/parser": "^7.0.1", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "typescript": "5.3.3", + "vite": "^5.1.2" + } +} diff --git a/e2e/app/src/App.tsx b/e2e/app/src/App.tsx new file mode 100644 index 00000000..ffc8bddf --- /dev/null +++ b/e2e/app/src/App.tsx @@ -0,0 +1,282 @@ +import { + Endpoint, + SerializedMediaEvent, + TrackContext, + TrackEncoding, + WebRTCEndpoint, + WebRTCEndpointEvents, + TrackContextEvents, + BandwidthLimit, + SimulcastConfig, +} from "@jellyfish-dev/ts-client-sdk"; +import { PeerMessage } from "./protos/jellyfish/peer_notifications"; +import { useEffect, useState, useSyncExternalStore } from "react"; +import { MockComponent } from "./MockComponent.tsx"; +import { VideoPlayerWithDetector } from "./VideoPlayerWithDetector.tsx"; + +/* eslint-disable no-console */ + +export type EndpointMetadata = { + goodStuff: string; +}; + +export type TrackMetadata = { + goodTrack: string; +}; + +function endpointMetadataParser(a: any): EndpointMetadata { + if (typeof a !== "object" || a === null || !("goodStuff" in a) || typeof a.goodStuff !== "string") + throw "Invalid metadata!!!"; + return { goodStuff: a.goodStuff }; +} + +function trackMetadataParser(a: any): TrackMetadata { + if (typeof a !== "object" || a === null || !("goodTrack" in a) || typeof a.goodTrack !== "string") + throw "Invalid track metadata!!!"; + return { goodTrack: a.goodTrack }; +} + +class RemoteStore { + cache: Record< + string, + [ + Record>, + Record>, + ] + > = {}; + invalidateCache: boolean = false; + + constructor(private webrtc: WebRTCEndpoint) {} + + subscribe(callback: () => void) { + const cb = () => { + this.invalidateCache = true; + callback(); + }; + + const trackCb: TrackContextEvents["encodingChanged"] = () => cb(); + + const trackAddedCb: WebRTCEndpointEvents["trackAdded"] = (context) => { + context.on("encodingChanged", () => trackCb); + context.on("voiceActivityChanged", () => trackCb); + + callback(); + }; + + const removeCb: WebRTCEndpointEvents["trackRemoved"] = (context) => { + context.removeListener("encodingChanged", () => trackCb); + context.removeListener("voiceActivityChanged", () => trackCb); + + callback(); + }; + + this.webrtc.on("trackAdded", trackAddedCb); + this.webrtc.on("trackReady", cb); + this.webrtc.on("trackUpdated", cb); + this.webrtc.on("trackRemoved", removeCb); + this.webrtc.on("endpointAdded", cb); + this.webrtc.on("endpointRemoved", cb); + this.webrtc.on("endpointUpdated", cb); + + return () => { + this.webrtc.removeListener("trackAdded", trackAddedCb); + this.webrtc.removeListener("trackReady", cb); + this.webrtc.removeListener("trackUpdated", cb); + this.webrtc.removeListener("trackRemoved", removeCb); + this.webrtc.removeListener("endpointAdded", cb); + this.webrtc.removeListener("endpointRemoved", cb); + this.webrtc.removeListener("endpointUpdated", cb); + }; + } + + snapshot() { + const newTracks = webrtc.getRemoteTracks(); + const newEndpoints = webrtc.getRemoteEndpoints(); + const ids = Object.keys(newTracks).sort().join(":") + Object.keys(newEndpoints).sort().join(":"); + if (!(ids in this.cache) || this.invalidateCache) { + this.cache[ids] = [newEndpoints, newTracks]; + this.invalidateCache = false; + } + return this.cache[ids]; + } +} + +// Assign a random client ID to make it easier to distinguish their messages +const clientId = Math.floor(Math.random() * 100); + +const webrtc = new WebRTCEndpoint({ endpointMetadataParser, trackMetadataParser }); +(window as typeof window & { webrtc: WebRTCEndpoint }).webrtc = webrtc; +const remoteTracksStore = new RemoteStore(webrtc); + +function connect(token: string, metadata: EndpointMetadata) { + const websocketUrl = "ws://localhost:5002/socket/peer/websocket"; + const websocket = new WebSocket(websocketUrl); + websocket.binaryType = "arraybuffer"; + + function socketOpenHandler(_event: Event) { + const message = PeerMessage.encode({ authRequest: { token } }).finish(); + websocket.send(message); + } + + websocket.addEventListener("open", socketOpenHandler); + + webrtc.on("sendMediaEvent", (mediaEvent: SerializedMediaEvent) => { + console.log(`%c(${clientId}) - Send: ${mediaEvent}`, "color:blue"); + const message = PeerMessage.encode({ mediaEvent: { data: mediaEvent } }).finish(); + websocket.send(message); + }); + + const messageHandler = (event: MessageEvent) => { + const uint8Array = new Uint8Array(event.data); + try { + const data = PeerMessage.decode(uint8Array); + if (data?.mediaEvent) { + // @ts-ignore + const mediaEvent = JSON.parse(data?.mediaEvent?.data); + console.log(`%c(${clientId}) - Received: ${JSON.stringify(mediaEvent)}`, "color:green"); + } else { + console.log(`%c(${clientId}) - Received: ${JSON.stringify(data)}`, "color:green"); + } + + if (data.authenticated !== undefined) { + webrtc.connect(metadata); + } else if (data.authRequest !== undefined) { + console.warn("Received unexpected control message: authRequest"); + } else if (data.mediaEvent !== undefined) { + webrtc.receiveMediaEvent(data.mediaEvent.data); + } + } catch (e) { + console.warn(`Received invalid control message, error: ${e}`); + } + }; + + websocket.addEventListener("message", messageHandler); + + const closeHandler = (event: any) => { + console.log({ name: "Close handler!", event }); + }; + + websocket.addEventListener("close", closeHandler); + + const errorHandler = (event: any) => { + console.log({ name: "Error handler!", event }); + }; + + websocket.addEventListener("error", errorHandler); + + const trackReady = (event: any) => { + console.log({ name: "trackReady", event }); + }; + + websocket.addEventListener("trackReady", trackReady); +} + +async function addScreenshareTrack(): Promise { + const stream = await window.navigator.mediaDevices.getDisplayMedia(); + const track = stream.getVideoTracks()[0]; + + const trackMetadata: TrackMetadata = { goodTrack: "screenshare" }; + const simulcastConfig: SimulcastConfig = { enabled: false, activeEncodings: [], disabledEncodings: [] }; + const maxBandwidth: BandwidthLimit = 0; + + return webrtc.addTrack(track, stream, trackMetadata, simulcastConfig, maxBandwidth); +} + +export function App() { + const [tokenInput, setTokenInput] = useState(localStorage.getItem("token") ?? ""); + const [endpointMetadataInput, setEndpointMetadataInput] = useState(JSON.stringify({ goodStuff: "ye" })); + const [connected, setConnected] = useState(false); + + useEffect(() => { + localStorage.setItem("token", tokenInput); + }, [tokenInput]); + + const handleConnect = () => + connect(tokenInput, endpointMetadataInput !== "" ? JSON.parse(endpointMetadataInput) : undefined); + const handleStartScreenshare = () => addScreenshareTrack(); + const handleUpdateEndpointMetadata = () => webrtc.updateEndpointMetadata(JSON.parse(endpointMetadataInput)); + + const [remoteEndpoints, remoteTracks] = useSyncExternalStore( + (callback) => remoteTracksStore.subscribe(callback), + () => remoteTracksStore.snapshot(), + ); + + const setEncoding = (trackId: string, encoding: TrackEncoding) => { + webrtc.setTargetTrackEncoding(trackId, encoding); + }; + + useEffect(() => { + const callback = () => setConnected(true); + + webrtc.on("connected", callback); + + return () => { + webrtc.removeListener("connected", callback); + }; + }, []); + + return ( +
+
+
+ setTokenInput(e.target.value)} placeholder="token" /> + setEndpointMetadataInput(e.target.value)} + placeholder="endpoint metadata" + /> + + + +
+
{connected ? "true" : "false"}
+
+ +
+ {Object.values(remoteTracks).map( + ({ stream, trackId, endpoint, metadata, rawMetadata, metadataParsingError }) => ( +
+
Endpoint id: {endpoint.id}
+ Metadata: {JSON.stringify(metadata)} +
+ Raw: {JSON.stringify(rawMetadata)} +
+ Error: {metadataParsingError} +
+ +
+
{stream?.id}
+
+ + + +
+
+ ), + )} +
+
+
+ Our metadata: + setEndpointMetadataInput(e.target.value)}> +
+
+ Endpoints: + {Object.values(remoteEndpoints).map(({ id, metadata, rawMetadata, metadataParsingError }) => ( +
+ {id} + metadata: {JSON.stringify(metadata)} +
+ raw metadata: {JSON.stringify(rawMetadata)} +
+ metadata parsing error:{" "} + + {metadataParsingError?.toString?.() ?? metadataParsingError} + +
+ ))} +
+
+
+ ); +} diff --git a/e2e/app/src/MockComponent.tsx b/e2e/app/src/MockComponent.tsx new file mode 100644 index 00000000..91ac6ca7 --- /dev/null +++ b/e2e/app/src/MockComponent.tsx @@ -0,0 +1,123 @@ +import { createStream } from "./mocks.ts"; +import { VideoPlayer } from "./VideoPlayer.tsx"; +import { useRef, useState } from "react"; +import { EndpointMetadata, TrackMetadata } from "./App.tsx"; +import { BandwidthLimit, SimulcastConfig, WebRTCEndpoint } from "@jellyfish-dev/ts-client-sdk"; + +const brainMock = createStream("🧠", "white", "low", 24); +const brain2Mock = createStream("🤯", "#00ff00", "low", 24); +const heartMock = createStream("🫀", "white", "low", 24); +const heart2Mock = createStream("💝", "#FF0000", "low", 24); + +type Props = { + webrtc: WebRTCEndpoint; +}; + +export const MockComponent = ({ webrtc }: Props) => { + const heartId = useRef | null>(null); + const brainId = useRef | null>(null); + const [replaceStatus, setReplaceStatus] = useState<"unknown" | "success" | "failure">("unknown"); + const [trackMetadataInput, setTrackMetadataInput] = useState(JSON.stringify({ goodTrack: "ye" })); + + const addHeart = async () => { + const stream = heartMock.stream; + const track = stream.getVideoTracks()[0]; + + heartId.current = webrtc.addTrack(track, stream, JSON.parse(trackMetadataInput)); + }; + + const removeHeart = async () => { + if (!heartId.current) throw Error("Heart id is undefined"); + + webrtc.removeTrack(await heartId.current); + }; + + const removeBrain = async () => { + if (!brainId.current) throw Error("Brain id is undefined"); + + webrtc.removeTrack(await brainId.current); + }; + + const replaceHeart = async () => { + if (!heartId.current) throw Error("Track Id is not set"); + + const stream = heart2Mock.stream; + const track = stream.getVideoTracks()[0]; + + await webrtc.replaceTrack(await heartId.current, track, JSON.parse(trackMetadataInput)); + setReplaceStatus("success"); + }; + + const replaceBrain = async () => { + if (!brainId.current) throw Error("Track Id is not set"); + + const stream = brain2Mock.stream; + const track = stream.getVideoTracks()[0]; + + await webrtc.replaceTrack(await brainId.current, track, JSON.parse(trackMetadataInput)); + }; + + const addBrain = () => { + const stream = brainMock.stream; + const track = stream.getVideoTracks()[0]; + + const simulcastConfig: SimulcastConfig = { enabled: false, activeEncodings: [], disabledEncodings: [] }; + const maxBandwidth: BandwidthLimit = 0; + + brainId.current = webrtc.addTrack(track, stream, JSON.parse(trackMetadataInput), simulcastConfig, maxBandwidth); + }; + + const addBoth = () => { + addHeart(); + addBrain(); + }; + + const addAndReplaceHeart = () => { + addHeart(); + replaceHeart(); + }; + + const addAndRemoveHeart = () => { + addHeart(); + removeHeart(); + }; + + const updateMetadataOnLastTrack = async () => { + const awaitedHeartId = await heartId.current; + if (!awaitedHeartId) return; + webrtc.updateTrackMetadata(awaitedHeartId, JSON.parse(trackMetadataInput)); + }; + + return ( +
+ setTrackMetadataInput(e.target.value)} + placeholder="track metadata" + /> + +
+ + + + + +
+ Replace status: + {replaceStatus} +
+
+
+ + + + + +
+ + + + +
+ ); +}; diff --git a/e2e/app/src/VideoPlayer.tsx b/e2e/app/src/VideoPlayer.tsx new file mode 100644 index 00000000..af7080f0 --- /dev/null +++ b/e2e/app/src/VideoPlayer.tsx @@ -0,0 +1,17 @@ +import { useEffect, useRef } from "react"; + +type Props = { + stream?: MediaStream; + id?: string; +}; + +export const VideoPlayer = ({ stream, id }: Props) => { + const heartRef = useRef(null); + + useEffect(() => { + if (!heartRef.current) return; + heartRef.current.srcObject = stream || null; + }, [stream]); + + return