From d5cd0166884f1a450797e3999a1f60716a2c51c1 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:07:10 +0100 Subject: [PATCH 1/3] Re-enable simulcast variant switching --- .../minimal-react/src/components/App.tsx | 32 ++++++++++++++++++- .../react-client/minimal-react/src/main.tsx | 10 ++++-- .../hooks/internal/useFishjamClientState.ts | 1 + .../src/hooks/useSetTargetTrackEncoding.ts | 27 ++++++++++++++++ packages/react-client/src/index.ts | 1 + packages/ts-client/src/FishjamClient.ts | 4 +++ packages/ts-client/src/types.ts | 1 + packages/webrtc-client/src/webRTCEndpoint.ts | 27 ++++++---------- 8 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 packages/react-client/src/hooks/useSetTargetTrackEncoding.ts diff --git a/examples/react-client/minimal-react/src/components/App.tsx b/examples/react-client/minimal-react/src/components/App.tsx index 2839c706a..c3bc8d6fb 100644 --- a/examples/react-client/minimal-react/src/components/App.tsx +++ b/examples/react-client/minimal-react/src/components/App.tsx @@ -4,6 +4,8 @@ import { useMicrophone, usePeers, useScreenShare, + useSetTargetTrackEncoding, + Variant, } from "@fishjam-cloud/react-client"; import { useStatistics } from "@fishjam-cloud/react-client/debug"; import { Fragment, useState } from "react"; @@ -11,6 +13,15 @@ import { Fragment, useState } from "react"; import AudioPlayer from "./AudioPlayer"; import VideoPlayer from "./VideoPlayer"; +const variantLabel = (variant: Variant | null | undefined): string => { + switch (variant) { + case Variant.VARIANT_LOW: return "Low"; + case Variant.VARIANT_MEDIUM: return "Medium"; + case Variant.VARIANT_HIGH: return "High"; + default: return "N/A"; + } +}; + export const App = () => { const [token, setToken] = useState(""); @@ -20,6 +31,7 @@ export const App = () => { const screenShare = useScreenShare(); const { isCameraOn, toggleCamera } = useCamera(); const { isMicrophoneOn, toggleMicrophone } = useMicrophone(); + const { setTargetTrackEncoding } = useSetTargetTrackEncoding(); const { getStatistics } = useStatistics(); { @@ -95,7 +107,25 @@ export const App = () => { return ( {cameraStream && ( - +
+ +
+ Encoding: {variantLabel(cameraTrack?.encoding)} + {[Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH].map((variant) => ( + + ))} +
+
)} {microphoneStream && } {screenShareStream && ( diff --git a/examples/react-client/minimal-react/src/main.tsx b/examples/react-client/minimal-react/src/main.tsx index 5cdb0b17c..a67f65973 100644 --- a/examples/react-client/minimal-react/src/main.tsx +++ b/examples/react-client/minimal-react/src/main.tsx @@ -1,4 +1,4 @@ -import { FishjamProvider } from "@fishjam-cloud/react-client"; +import { FishjamProvider, Variant } from "@fishjam-cloud/react-client"; import React from "react"; import ReactDOM from "react-dom/client"; @@ -8,7 +8,13 @@ const fishjamId = import.meta.env.VITE_FISHJAM_ID ?? "http://localhost:5555"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + , diff --git a/packages/react-client/src/hooks/internal/useFishjamClientState.ts b/packages/react-client/src/hooks/internal/useFishjamClientState.ts index 6264c7f54..0ee382286 100644 --- a/packages/react-client/src/hooks/internal/useFishjamClientState.ts +++ b/packages/react-client/src/hooks/internal/useFishjamClientState.ts @@ -20,6 +20,7 @@ const eventNames = [ "trackAdded", "trackRemoved", "trackUpdated", + "encodingChanged", "peerJoined", "peerLeft", "peerUpdated", diff --git a/packages/react-client/src/hooks/useSetTargetTrackEncoding.ts b/packages/react-client/src/hooks/useSetTargetTrackEncoding.ts new file mode 100644 index 000000000..9fb07ce8a --- /dev/null +++ b/packages/react-client/src/hooks/useSetTargetTrackEncoding.ts @@ -0,0 +1,27 @@ +import type { Variant } from "@fishjam-cloud/ts-client"; +import { useCallback, useContext } from "react"; + +import { FishjamClientContext } from "../contexts/fishjamClient"; + +/** + * Hook provides a method to set the target encoding (simulcast variant) for a remote track. + * + * The encoding will be sent whenever it is available. If the chosen encoding is temporarily + * unavailable, some other encoding will be sent until the chosen encoding becomes active again. + * + * @category Connection + * @group Hooks + */ +export function useSetTargetTrackEncoding() { + const fishjamClientRef = useContext(FishjamClientContext); + if (!fishjamClientRef) throw Error("useSetTargetTrackEncoding must be used within FishjamProvider"); + + const setTargetTrackEncoding = useCallback( + (trackId: string, encoding: Variant) => { + fishjamClientRef.current.setTargetTrackEncoding(trackId, encoding); + }, + [fishjamClientRef], + ); + + return { setTargetTrackEncoding }; +} diff --git a/packages/react-client/src/index.ts b/packages/react-client/src/index.ts index 53ed49e05..5be412bdf 100644 --- a/packages/react-client/src/index.ts +++ b/packages/react-client/src/index.ts @@ -19,6 +19,7 @@ export { } from "./hooks/useLivestreamViewer"; export { type PeerWithTracks, usePeers } from "./hooks/usePeers"; export { type RoomType, useSandbox, type UseSandboxProps } from "./hooks/useSandbox"; +export { useSetTargetTrackEncoding } from "./hooks/useSetTargetTrackEncoding"; export { useScreenShare } from "./hooks/useScreenShare"; export { useUpdatePeerMetadata } from "./hooks/useUpdatePeerMetadata"; export { useVAD } from "./hooks/useVAD"; diff --git a/packages/ts-client/src/FishjamClient.ts b/packages/ts-client/src/FishjamClient.ts index 992bc862f..9cf2fc729 100644 --- a/packages/ts-client/src/FishjamClient.ts +++ b/packages/ts-client/src/FishjamClient.ts @@ -374,6 +374,10 @@ export class FishjamClient { if (!isPeer(ctx.endpoint)) return; + ctx.on('encodingChanged', (trackCtx) => { + this.emit('encodingChanged', trackCtx as FishjamTrackContext); + }); + this.emit('trackAdded', ctx as FishjamTrackContext); }); this.webrtc?.on('trackRemoved', (ctx: TrackContext) => { diff --git a/packages/ts-client/src/types.ts b/packages/ts-client/src/types.ts index 90c314c34..fae64d934 100644 --- a/packages/ts-client/src/types.ts +++ b/packages/ts-client/src/types.ts @@ -208,6 +208,7 @@ export type MessageEvents = { */ bandwidthEstimationChanged: (estimation: bigint) => void; + encodingChanged: (ctx: FishjamTrackContext) => void; targetTrackEncodingRequested: (event: Parameters[0]) => void; localTrackAdded: (event: Parameters[0]) => void; localTrackRemoved: (event: Parameters[0]) => void; diff --git a/packages/webrtc-client/src/webRTCEndpoint.ts b/packages/webrtc-client/src/webRTCEndpoint.ts index b2e9aeb87..b422c2980 100644 --- a/packages/webrtc-client/src/webRTCEndpoint.ts +++ b/packages/webrtc-client/src/webRTCEndpoint.ts @@ -353,10 +353,8 @@ export class WebRTCEndpoint extends (EventEmitter as new () => TypedEmitter TypedEmitter { const resolutionNotifier = new Deferred(); const trackId = this.getTrackId(uuidv4()); const stream = new MediaStream(); - // TODO: Simulcast is disabled manually, enable it once bandwidth estimation is implemented or we add manual track selection support. - const simulcastConfig: MediaEvent_Track_SimulcastConfig = { - enabled: false, - enabledVariants: [], - disabledVariants: [], - }; - - const maxBandwidth: TrackBandwidthLimit = - typeof _maxBandwidth === 'number' && _maxBandwidth > 0 ? _maxBandwidth : 0; + const resolvedMaxBandwidth: TrackBandwidthLimit = + typeof maxBandwidth === 'number' && maxBandwidth > 0 ? maxBandwidth : 0; try { stream.addTrack(track); this.commandsQueue.pushCommand({ handler: async () => - this.localTrackManager.addTrackHandler(trackId, track, stream, trackMetadata, simulcastConfig, maxBandwidth), - parse: () => this.localTrackManager.parseAddTrack(track, simulcastConfig, maxBandwidth), + this.localTrackManager.addTrackHandler(trackId, track, stream, trackMetadata, simulcastConfig, resolvedMaxBandwidth), + parse: () => this.localTrackManager.parseAddTrack(track, simulcastConfig, resolvedMaxBandwidth), resolve: 'after-renegotiation', resolutionNotifier, }); @@ -436,7 +427,7 @@ export class WebRTCEndpoint extends (EventEmitter as new () => TypedEmitter Date: Mon, 16 Mar 2026 15:04:34 +0100 Subject: [PATCH 2/3] lint --- .../minimal-react/src/components/App.tsx | 32 +++++++++++++++---- .../react-client/minimal-react/src/main.tsx | 6 +++- packages/ts-client/src/types.ts | 2 +- packages/webrtc-client/src/webRTCEndpoint.ts | 9 +++++- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/examples/react-client/minimal-react/src/components/App.tsx b/examples/react-client/minimal-react/src/components/App.tsx index c3bc8d6fb..039eeb4a4 100644 --- a/examples/react-client/minimal-react/src/components/App.tsx +++ b/examples/react-client/minimal-react/src/components/App.tsx @@ -15,10 +15,14 @@ import VideoPlayer from "./VideoPlayer"; const variantLabel = (variant: Variant | null | undefined): string => { switch (variant) { - case Variant.VARIANT_LOW: return "Low"; - case Variant.VARIANT_MEDIUM: return "Medium"; - case Variant.VARIANT_HIGH: return "High"; - default: return "N/A"; + case Variant.VARIANT_LOW: + return "Low"; + case Variant.VARIANT_MEDIUM: + return "Medium"; + case Variant.VARIANT_HIGH: + return "High"; + default: + return "N/A"; } }; @@ -109,15 +113,29 @@ export const App = () => { {cameraStream && (
-
+
Encoding: {variantLabel(cameraTrack?.encoding)} - {[Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH].map((variant) => ( + {[ + Variant.VARIANT_LOW, + Variant.VARIANT_MEDIUM, + Variant.VARIANT_HIGH, + ].map((variant) => (