Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .changeset/olive-pears-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'livekit-client': patch
---

Apply the resolved degradation preference to the backup codec's sender

Degradation preference is a property of the sender, not of the track, and a backup codec publishes over its own sender. Previously only the primary sender was configured, so the backup encoder resolved a preference implicitly and could adapt along a different axis than the primary.
84 changes: 82 additions & 2 deletions src/room/track/LocalVideoTrack.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest';
import { videoLayersFromEncodings } from './LocalVideoTrack';
import { describe, expect, it, vi } from 'vitest';
import LocalVideoTrack, { videoLayersFromEncodings } from './LocalVideoTrack';
import type { SimulcastTrackInfo } from './LocalVideoTrack';
import { VideoQuality } from './Track';
import type { VideoCodec } from './options';

describe('videoLayersFromEncodings', () => {
it('returns single layer for no encoding', () => {
Expand Down Expand Up @@ -131,3 +133,81 @@ describe('videoLayersFromEncodings', () => {
expect(layers[2].width).toBe(720);
});
});

function makeSender() {
let params: RTCRtpSendParameters = {
encodings: [],
transactionId: '',
codecs: [],
headerExtensions: [],
rtcp: {},
};
return {
getParameters: () => params,
setParameters: vi.fn((next: RTCRtpSendParameters) => {
params = next;
return Promise.resolve();
}),
get degradationPreference() {
return params.degradationPreference;
},
};
}

function makeTrack() {
const track = Object.create(LocalVideoTrack.prototype) as LocalVideoTrack;
Object.assign(track, {
log: { debug: vi.fn(), warn: vi.fn() },
simulcastCodecs: new Map<VideoCodec, SimulcastTrackInfo>(),
subscribedCodecs: undefined,
});
// logContext and mediaStreamTrack are getters we don't set up state for here
Object.defineProperty(track, 'logContext', { get: () => ({}) });
Object.defineProperty(track, 'mediaStreamTrack', { get: () => ({ clone: () => ({}) }) });
return track;
}

describe('setDegradationPreference', () => {
it('applies the preference to the primary sender', async () => {
const track = makeTrack();
const sender = makeSender();
Object.assign(track, { _sender: sender });

await track.setDegradationPreference('maintain-resolution');

expect(sender.degradationPreference).toBe('maintain-resolution');
});

it('applies the resolved preference to a backup codec sender', async () => {
const track = makeTrack();
const primary = makeSender();
Object.assign(track, { _sender: primary });

await track.setDegradationPreference('maintain-resolution');

// the backup codec transceiver is created later, when the server asks for it
const backupInfo = track.addSimulcastTrack('vp8', [])!;
const backup = makeSender();
track.setSimulcastTrackSender('vp8', backup as unknown as RTCRtpSender);

expect(backupInfo.sender).toBe(backup);
expect(backup.degradationPreference).toBe('maintain-resolution');
expect(primary.degradationPreference).toBe('maintain-resolution');
});

it('updates every sender when the preference changes after the backup is published', async () => {
const track = makeTrack();
const primary = makeSender();
Object.assign(track, { _sender: primary });
await track.setDegradationPreference('maintain-framerate');

track.addSimulcastTrack('vp8', []);
const backup = makeSender();
track.setSimulcastTrackSender('vp8', backup as unknown as RTCRtpSender);

await track.setDegradationPreference('balanced');

expect(primary.degradationPreference).toBe('balanced');
expect(backup.degradationPreference).toBe('balanced');
});
});
39 changes: 30 additions & 9 deletions src/room/track/LocalVideoTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,15 +406,32 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {

async setDegradationPreference(preference: RTCDegradationPreference) {
this.degradationPreference = preference;
if (this.sender) {
try {
this.log.debug(`setting degradationPreference to ${preference}`, this.logContext);
const params = this.sender.getParameters();
params.degradationPreference = preference;
this.sender.setParameters(params);
} catch (e: any) {
this.log.warn(`failed to set degradationPreference`, { error: e, ...this.logContext });
}
this.applyDegradationPreference(this.sender);
for (const sc of this.simulcastCodecs.values()) {
this.applyDegradationPreference(sc.sender);
}
}

/**
* Degradation preference is a property of the sender, not of the track, so every sender
* publishing this track needs it applied separately. A backup codec publishes over its
* own sender, which would otherwise let the browser resolve a preference implicitly and
* diverge from the primary encoder.
*/
private applyDegradationPreference(sender?: RTCRtpSender) {
if (!sender) {
return;
}
try {
this.log.debug(
`setting degradationPreference to ${this.degradationPreference}`,
this.logContext,
);
const params = sender.getParameters();
params.degradationPreference = this.degradationPreference;
sender.setParameters(params);
} catch (e: any) {
this.log.warn(`failed to set degradationPreference`, { error: e, ...this.logContext });
}
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

Expand Down Expand Up @@ -443,6 +460,10 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
}
simulcastCodecInfo.sender = sender;

// the backup codec publishes over its own sender, so it needs the same degradation
// preference the primary sender resolved to.
this.applyDegradationPreference(sender);

// browser will reenable disabled codec/layers after new codec has been published,
// so refresh subscribedCodecs after publish a new codec
setTimeout(() => {
Expand Down
Loading