diff --git a/examples/react-client/minimal-react/src/components/App.tsx b/examples/react-client/minimal-react/src/components/App.tsx index 2839c706a..039eeb4a4 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,19 @@ 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 +35,7 @@ export const App = () => { const screenShare = useScreenShare(); const { isCameraOn, toggleCamera } = useCamera(); const { isMicrophoneOn, toggleMicrophone } = useMicrophone(); + const { setTargetTrackEncoding } = useSetTargetTrackEncoding(); const { getStatistics } = useStatistics(); { @@ -95,7 +111,39 @@ 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..feb2a1bd9 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,17 @@ 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..9aa85dca9 100644 --- a/packages/react-client/src/index.ts +++ b/packages/react-client/src/index.ts @@ -20,6 +20,7 @@ export { export { type PeerWithTracks, usePeers } from "./hooks/usePeers"; export { type RoomType, useSandbox, type UseSandboxProps } from "./hooks/useSandbox"; export { useScreenShare } from "./hooks/useScreenShare"; +export { useSetTargetTrackEncoding } from "./hooks/useSetTargetTrackEncoding"; export { useUpdatePeerMetadata } from "./hooks/useUpdatePeerMetadata"; export { useVAD } from "./hooks/useVAD"; export type { 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..9cf80b7f4 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: TrackContextEvents['encodingChanged']; 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..a0058113a 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 +434,7 @@ export class WebRTCEndpoint extends (EventEmitter as new () => TypedEmitter