diff --git a/packages/client/src/rtc/Publisher.ts b/packages/client/src/rtc/Publisher.ts index a55de6411e..aaf48c037a 100644 --- a/packages/client/src/rtc/Publisher.ts +++ b/packages/client/src/rtc/Publisher.ts @@ -394,7 +394,8 @@ export class Publisher { ? // for SVC, we only have one layer (q) and often rid is omitted enabledLayers[0] : // for non-SVC, we need to find the layer by rid (simulcast) - enabledLayers.find((l) => l.name === encoder.rid); + enabledLayers.find((l) => l.name === encoder.rid) ?? + (params.encodings.length === 1 ? enabledLayers[0] : undefined); // flip 'active' flag only when necessary const shouldActivate = !!layer?.active; diff --git a/packages/client/src/rtc/__tests__/Publisher.test.ts b/packages/client/src/rtc/__tests__/Publisher.test.ts index 9b73af5b95..84be566e29 100644 --- a/packages/client/src/rtc/__tests__/Publisher.test.ts +++ b/packages/client/src/rtc/__tests__/Publisher.test.ts @@ -360,6 +360,45 @@ describe('Publisher', () => { ]); }); + it('can dynamically activate/deactivate simulcast layers when rid is missing', async () => { + const transceiver = new RTCRtpTransceiver(); + const setParametersSpy = vi + .spyOn(transceiver.sender, 'setParameters') + .mockResolvedValue(); + const getParametersSpy = vi + .spyOn(transceiver.sender, 'getParameters') + .mockReturnValue({ + // @ts-expect-error incomplete data + codecs: [{ mimeType: 'video/VP8' }], + encodings: [{ active: false }], + }); + + // inject the transceiver + publisher['transceiverCache'].set(TrackType.VIDEO, transceiver); + + await publisher['changePublishQuality']([ + { + name: 'q', + active: true, + maxBitrate: 100, + scaleResolutionDownBy: 4, + maxFramerate: 30, + scalabilityMode: '', + }, + ]); + + expect(getParametersSpy).toHaveBeenCalled(); + expect(setParametersSpy).toHaveBeenCalled(); + expect(setParametersSpy.mock.calls[0][0].encodings).toEqual([ + { + active: true, + maxBitrate: 100, + scaleResolutionDownBy: 4, + maxFramerate: 30, + }, + ]); + }); + it('can dynamically update scalability mode in SVC', async () => { const transceiver = new RTCRtpTransceiver(); const setParametersSpy = vi diff --git a/packages/client/src/rtc/bitrateLookup.ts b/packages/client/src/rtc/bitrateLookup.ts index 70fcf6f6bd..448b1df922 100644 --- a/packages/client/src/rtc/bitrateLookup.ts +++ b/packages/client/src/rtc/bitrateLookup.ts @@ -6,8 +6,8 @@ const bitrateLookupTable: Record< > = { h264: { 2160: 5_000_000, - 1440: 3_500_000, - 1080: 2_750_000, + 1440: 3_000_000, + 1080: 2_000_000, 720: 1_250_000, 540: 750_000, 360: 400_000, diff --git a/packages/client/src/rtc/videoLayers.ts b/packages/client/src/rtc/videoLayers.ts index 5fede153b1..2f0a4318bc 100644 --- a/packages/client/src/rtc/videoLayers.ts +++ b/packages/client/src/rtc/videoLayers.ts @@ -65,7 +65,11 @@ export const findOptimalVideoLayers = ( const optimalVideoLayers: OptimalVideoLayer[] = []; const settings = videoTrack.getSettings(); const { width = 0, height = 0 } = settings; - const { scalabilityMode, bitrateDownscaleFactor = 2 } = publishOptions || {}; + const { + scalabilityMode, + bitrateDownscaleFactor = 2, + maxSimulcastLayers = 3, + } = publishOptions || {}; const maxBitrate = getComputedMaxBitrate( targetResolution, width, @@ -76,7 +80,7 @@ export const findOptimalVideoLayers = ( let downscaleFactor = 1; let bitrateFactor = 1; const svcCodec = isSvcCodec(codecInUse); - for (const rid of ['f', 'h', 'q']) { + for (const rid of ['f', 'h', 'q'].slice(0, Math.min(3, maxSimulcastLayers))) { const layer: OptimalVideoLayer = { active: true, rid, diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index e32c82694e..3cb8a59858 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -181,6 +181,10 @@ export type PublishOptions = { * in simulcast mode (non-SVC). */ bitrateDownscaleFactor?: number; + /** + * The maximum number of simulcast layers to use when publishing the video stream. + */ + maxSimulcastLayers?: number; /** * Screen share settings. */ diff --git a/sample-apps/react/react-dogfood/components/MeetingUI.tsx b/sample-apps/react/react-dogfood/components/MeetingUI.tsx index 5479dae277..7b74d416d6 100644 --- a/sample-apps/react/react-dogfood/components/MeetingUI.tsx +++ b/sample-apps/react/react-dogfood/components/MeetingUI.tsx @@ -56,6 +56,9 @@ export const MeetingUI = ({ chatClient, mode }: MeetingUIProps) => { const scalabilityMode = router.query['scalability_mode'] as | string | undefined; + const maxSimulcastLayers = router.query['max_simulcast_layers'] as + | string + | undefined; const onJoin = useCallback( async ({ fastJoin = false } = {}) => { @@ -74,6 +77,9 @@ export const MeetingUI = ({ chatClient, mode }: MeetingUIProps) => { bitrateDownscaleFactor: bitrateFactorOverride ? parseInt(bitrateFactorOverride, 10) : 2, // default to 2 + maxSimulcastLayers: maxSimulcastLayers + ? parseInt(maxSimulcastLayers, 10) + : 3, // default to 3 }); await call.join({ create: true });