Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ Non-obvious setup caveat:

- `packages/protobufs/protos` and `packages/react-native-webrtc` are git submodules. They must be initialized before `yarn install`, otherwise install fails during resolution with `Workspace not found (@fishjam-cloud/react-native-webrtc@workspace:*)`. Run `git submodule update --init --recursive` first (the startup script does this).
- Validated commands: `yarn build`, `yarn test:unit`, and `yarn lint:check` all pass. `yarn test:e2e` needs Playwright browsers and a running backend.

## Conventions

- Log through the SDK logger (`getLogger` in `packages/webrtc-client/src/logger.ts`, threaded as `logger`), never `console.*` directly. It respects the `debug` flag and adds the `[FISHJAM]` prefix.
4 changes: 3 additions & 1 deletion examples/mobile-client/fishjam-chat/app/room/[roomName].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ export default function RoomScreen() {
<InCallButton
iconName={isBlurOn ? 'blur' : 'blur-off'}
onPress={() =>
setCameraTrackMiddleware(isBlurOn ? null : backgroundBlur).catch(reportBackgroundBlurFailure)
setCameraTrackMiddleware(isBlurOn ? null : backgroundBlur).catch(
reportBackgroundBlurFailure,
)
}
/>
<InCallButton
Expand Down
4 changes: 3 additions & 1 deletion examples/mobile-client/fishjam-chat/app/room/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ export default function PreviewScreen() {
<InCallButton
iconName={isBlurOn ? 'blur' : 'blur-off'}
onPress={() =>
setCameraTrackMiddleware(isBlurOn ? null : backgroundBlur).catch(reportBackgroundBlurFailure)
setCameraTrackMiddleware(isBlurOn ? null : backgroundBlur).catch(
reportBackgroundBlurFailure,
)
}
/>
</View>
Expand Down
1 change: 1 addition & 0 deletions packages/mobile-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export type {
RoomType,
UseSandboxProps,
BandwidthLimits,
BandwidthLimitsInput,
Brand,
DeviceError,
DeviceItem,
Expand Down
11 changes: 6 additions & 5 deletions packages/react-client/src/FishjamProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { useFishjamClientState } from "./hooks/internal/useFishjamClientState";
import { usePeerStatus } from "./hooks/internal/usePeerStatus";
import { useScreenShareManager } from "./hooks/internal/useScreenshareManager";
import { useTrackManager } from "./hooks/internal/useTrackManager";
import type { BandwidthLimits, PersistLastDeviceHandlers, StreamConfig } from "./types/public";
import { mergeWithDefaultBandwitdthLimits } from "./utils/bandwidth";
import type { BandwidthLimitsInput, PersistLastDeviceHandlers, StreamConfig } from "./types/public";
import { mergeWithDefaultBandwidthLimits } from "./utils/bandwidth";
import { getLastDevice, saveLastDevice } from "./utils/localStorage";

/**
Expand All @@ -42,9 +42,10 @@ export interface FishjamProviderProps extends PropsWithChildren {
*/
persistLastDevice?: boolean | PersistLastDeviceHandlers;
/**
* Adjust max bandwidth limit for a single stream and simulcast.
* Adjust max bandwidth limit (in kbps) for a single stream and each simulcast layer.
* Missing values default to `MAX_BANDWIDTH_LIMITS`; values above the cap are clamped to it.
*/
bandwidthLimits?: Partial<BandwidthLimits>;
bandwidthLimits?: BandwidthLimitsInput;
/**
* Configure whether to use video simulcast and which quality layers to send if so.
*/
Expand Down Expand Up @@ -96,7 +97,7 @@ export function FishjamProvider(props: FishjamProviderProps) {
const peerStatus = usePeerStatus(fishjamClientRef.current);

const mergedBandwidthLimits = useMemo(
() => mergeWithDefaultBandwitdthLimits(props.bandwidthLimits),
() => mergeWithDefaultBandwidthLimits(props.bandwidthLimits),
[props.bandwidthLimits],
);

Expand Down
1 change: 1 addition & 0 deletions packages/react-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export { useUpdatePeerMetadata } from "./hooks/useUpdatePeerMetadata";
export { useVAD } from "./hooks/useVAD";
export type {
BandwidthLimits,
BandwidthLimitsInput,
Brand,
CustomSource,
DeviceError,
Expand Down
5 changes: 5 additions & 0 deletions packages/react-client/src/types/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ export type StreamConfig = { sentQualities?: Variant[] | false };

export type BandwidthLimits = { singleStream: number; simulcast: SimulcastBandwidthLimits };

/**
* Bandwidth limits accepted by `FishjamProvider`. Every value is optional; missing ones fall back to `MAX_BANDWIDTH_LIMITS`.
*/
export type BandwidthLimitsInput = { singleStream?: number; simulcast?: Partial<SimulcastBandwidthLimits> };

export type DeviceType = "audio" | "video";

export type DeviceError =
Expand Down
20 changes: 20 additions & 0 deletions packages/react-client/src/utils/bandwidth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MAX_BANDWIDTH_LIMITS, Variant } from "@fishjam-cloud/ts-client";
import { expect, it } from "vitest";

import { mergeWithDefaultBandwidthLimits } from "./bandwidth";

it("defaults every limit to the cap", () => {
expect(mergeWithDefaultBandwidthLimits()).toEqual({
singleStream: MAX_BANDWIDTH_LIMITS.singleStream,
simulcast: MAX_BANDWIDTH_LIMITS.simulcast,
});
});

it("keeps user values and fills the missing simulcast layers with the cap", () => {
const merged = mergeWithDefaultBandwidthLimits({ singleStream: 800, simulcast: { [Variant.VARIANT_HIGH]: 1000 } });

expect(merged.singleStream).toBe(800);
expect(merged.simulcast[Variant.VARIANT_HIGH]).toBe(1000);
expect(merged.simulcast[Variant.VARIANT_MEDIUM]).toBe(MAX_BANDWIDTH_LIMITS.simulcast[Variant.VARIANT_MEDIUM]);
expect(merged.simulcast[Variant.VARIANT_LOW]).toBe(MAX_BANDWIDTH_LIMITS.simulcast[Variant.VARIANT_LOW]);
Comment thread
Karolk99 marked this conversation as resolved.
});
12 changes: 5 additions & 7 deletions packages/react-client/src/utils/bandwidth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Variant } from "@fishjam-cloud/ts-client";
import { MAX_BANDWIDTH_LIMITS } from "@fishjam-cloud/ts-client";

import type { BandwidthLimits } from "../types/public";
import type { BandwidthLimits, BandwidthLimitsInput } from "../types/public";

export const ALL_VARIANTS_SIMULCAST = [Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH] as const;

export const mergeWithDefaultBandwitdthLimits = (limits?: Partial<BandwidthLimits>): BandwidthLimits => ({
singleStream: limits?.singleStream ?? 0,
simulcast: limits?.simulcast ?? { [Variant.VARIANT_LOW]: 0, [Variant.VARIANT_MEDIUM]: 0, [Variant.VARIANT_HIGH]: 0 },
export const mergeWithDefaultBandwidthLimits = (limits?: BandwidthLimitsInput): BandwidthLimits => ({
singleStream: limits?.singleStream ?? MAX_BANDWIDTH_LIMITS.singleStream,
simulcast: { ...MAX_BANDWIDTH_LIMITS.simulcast, ...limits?.simulcast },
});
7 changes: 3 additions & 4 deletions packages/react-client/src/utils/track.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FishjamClient, SimulcastConfig, TrackContext, TrackMetadata } from "@fishjam-cloud/ts-client";
import { Variant } from "@fishjam-cloud/ts-client";
import type { FishjamClient, SimulcastConfig, TrackContext, TrackMetadata, Variant } from "@fishjam-cloud/ts-client";
import { SIMULCAST_VARIANTS } from "@fishjam-cloud/ts-client";

import type { BandwidthLimits, Track, TrackId } from "../types/public";

Expand Down Expand Up @@ -53,8 +53,7 @@ export function setupOnEndedCallback(
}

const getDisabledEncodings = (activeEncodings: Variant[] = []) => {
const allEncodings: Variant[] = [Variant.VARIANT_LOW, Variant.VARIANT_MEDIUM, Variant.VARIANT_HIGH];
return allEncodings.filter((encoding) => !activeEncodings.includes(encoding));
return SIMULCAST_VARIANTS.filter((encoding) => !activeEncodings.includes(encoding));
};

export const getConfigAndBandwidthFromProps = (
Expand Down
15 changes: 9 additions & 6 deletions packages/ts-client/src/FishjamClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ export class FishjamClient<PeerMetadata = GenericMetadata, ServerMetadata = Gene
enabledVariants: [],
disabledVariants: [],
},
maxBandwidth: TrackBandwidthLimit = 0, // unlimited bandwidth
maxBandwidth: TrackBandwidthLimit = 0,
): Promise<string> {
if (!this.webrtc) throw this.handleWebRTCNotInitialized();
if (this.isAudioOnlyConnection && track.kind !== 'audio') throw new TrackTypeError();
Expand Down Expand Up @@ -663,14 +663,16 @@ export class FishjamClient<PeerMetadata = GenericMetadata, ServerMetadata = Gene

/**
* Updates maximum bandwidth for the track identified by trackId. This value directly translates to quality of the
* stream and, in case of video, to the amount of RTP packets being sent. In case trackId points at the simulcast
* track bandwidth is split between all of the variant streams proportionally to their resolution.
* stream and, in case of video, to the amount of RTP packets being sent.
* Same rules as the `maxBandwidth` argument of {@link addTrack}: a number is a budget for the whole track (split
* across simulcast layers proportionally to their resolution), a Map holds per-layer simulcast limits, and every
* video value is clamped to `MAX_BANDWIDTH_LIMITS`; 0 means "use the cap(s)".
*
* @param {string} trackId
* @param {BandwidthLimit} bandwidth In kbps
* @param {TrackBandwidthLimit} bandwidth In kbps
* @returns {Promise<boolean>} Success
*/
public async setTrackBandwidth(trackId: string, bandwidth: BandwidthLimit): Promise<boolean> {
public async setTrackBandwidth(trackId: string, bandwidth: TrackBandwidthLimit): Promise<boolean> {
Comment thread
Karolk99 marked this conversation as resolved.
if (!this.webrtc) throw this.handleWebRTCNotInitialized();
await this.webrtc.setTrackBandwidth(trackId, bandwidth);
return true;
Expand All @@ -681,7 +683,8 @@ export class FishjamClient<PeerMetadata = GenericMetadata, ServerMetadata = Gene
*
* @param {string} trackId - Id of the track
* @param {string} rid - Rid of the encoding
* @param {BandwidthLimit} bandwidth - Desired max bandwidth used by the encoding (in kbps)
* @param {BandwidthLimit} bandwidth - Desired max bandwidth used by the encoding (in kbps), clamped to
* `MAX_BANDWIDTH_LIMITS.simulcast[rid]`; 0 means "use the cap"
* @returns
*/
public async setEncodingBandwidth(
Expand Down
14 changes: 13 additions & 1 deletion packages/tsunami/src/FishjamClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ConnectConfig, FishjamClient as TsClient, type GenericMetadata } from "@fishjam-cloud/ts-client";
import { type ConnectConfig, FishjamClient as TsClient, type GenericMetadata, Variant } from "@fishjam-cloud/ts-client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { ClientResourceScope } from "./ClientResourceScope";
Expand Down Expand Up @@ -200,6 +200,18 @@ describe("FishjamClient lifecycle", () => {
expect(joined).toHaveBeenCalledOnce();
});

it("forwards per-variant simulcast limits to setTrackBandwidth", async () => {
const setTrackBandwidth = vi.spyOn(TsClient.prototype, "setTrackBandwidth").mockResolvedValue(true);
vi.spyOn(TsClient.prototype, "connect").mockResolvedValue();
const client = new FishjamClient();
await client.connect(connectConfig);
const limits = new Map([[Variant.VARIANT_HIGH, 900]]);

await expect(client.setTrackBandwidth("track-id", limits)).resolves.toBe(true);

expect(setTrackBandwidth).toHaveBeenCalledWith("track-id", limits);
});

it("does not take ownership of tracks passed through the signalling seam", async () => {
const publication = deferred<string>();
vi.spyOn(TsClient.prototype, "addTrack").mockReturnValue(publication.promise);
Expand Down
2 changes: 1 addition & 1 deletion packages/tsunami/src/FishjamClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class FishjamClient<PeerMetadata = GenericMetadata, ServerMetadata = Gene
return this.resources.run(() => this.getTsClient().replaceTrack(trackId, newTrack));
}

public async setTrackBandwidth(trackId: string, bandwidth: BandwidthLimit): Promise<boolean> {
public async setTrackBandwidth(trackId: string, bandwidth: TrackBandwidthLimit): Promise<boolean> {
return this.resources.run(() => this.getTsClient().setTrackBandwidth(trackId, bandwidth));
}

Expand Down
Loading
Loading