refactor(voice): remove screen share codec renegotiation (#2417)

This commit is contained in:
Hampus
2026-09-03 04:46:11 +02:00
committed by GitHub
parent f5f60c66e7
commit c97bc53342
80 changed files with 1360 additions and 4726 deletions
@@ -766,12 +766,6 @@
}
},
"watchedStreams": {},
"codecNegotiation": {
"overrides": {},
"localSupportedVideoCodecs": [],
"remoteSupportedVideoCodecs": {},
"streams": {}
},
"stats": {
"rttMs": 42,
"outbound": [
@@ -25,7 +25,6 @@ export {
selectVoiceEngineV2StatsPresentationProjection,
selectVoiceEngineV2StatsProjection,
selectVoiceEngineV2StatsSummary,
selectVoiceEngineV2StreamNegotiation,
selectVoiceEngineV2WatchedStreams,
} from './selectors';
export type {
@@ -6,7 +6,6 @@ import type {VoiceEngineV2Snapshot, VoiceEngineV2Transition} from '../state';
import {planDesiredState} from './_plan';
import {transitionCamera} from './camera';
import {transitionCapabilities} from './capabilities';
import {transitionCodecNegotiation} from './codecNegotiation';
import {transitionCommand} from './command';
import {transitionConnection} from './connection';
import {transitionData} from './data';
@@ -137,13 +136,6 @@ export function dispatchSessionEvent(
case 'data.publishSucceeded':
case 'data.publishFailed':
return transitionData(snapshot, event);
case 'codecNegotiation.overrideSetRequested':
case 'codecNegotiation.localCapabilityChanged':
case 'codecNegotiation.remoteCapabilityChanged':
case 'codecNegotiation.streamRegistered':
case 'codecNegotiation.streamUnregistered':
case 'codecNegotiation.viewerChanged':
return transitionCodecNegotiation(snapshot, event);
case 'stats.collectRequested':
case 'stats.collected':
case 'stats.collectFailed':
@@ -105,7 +105,7 @@ function failCamera(
};
}
export function beginCameraEncodingUpdate(
function beginCameraEncodingUpdate(
snapshot: VoiceEngineV2Snapshot,
options: VoiceEngineV2CameraEncodingOptions,
): VoiceEngineV2Transition {
@@ -1,166 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, it} from 'vitest';
import type {VoiceEngineV2Event} from '../../protocol/events';
import {transitionVoiceEngineV2} from '../reducer';
import {
availableVoiceEngineV2Capabilities,
createVoiceEngineV2InitialSnapshot,
type VoiceEngineV2Snapshot,
} from '../state';
function applyEvents(snapshot: VoiceEngineV2Snapshot, events: Array<VoiceEngineV2Event>): VoiceEngineV2Snapshot {
let next = snapshot;
for (const event of events) {
next = transitionVoiceEngineV2(next, event).snapshot;
}
return next;
}
function publishedCameraOn(codec: 'av1' | 'h264' | 'vp9'): VoiceEngineV2Snapshot {
return applyEvents(createVoiceEngineV2InitialSnapshot(availableVoiceEngineV2Capabilities()), [
{type: 'connection.connectRequested', options: {url: 'wss://voice', token: 'token'}},
{type: 'connection.connectSucceeded', operationId: 1},
{type: 'camera.publishRequested', options: {deviceId: 'cam-1', codec}},
{type: 'camera.publishSucceeded', operationId: 2},
{
type: 'codecNegotiation.streamRegistered',
source: 'camera',
streamIdentity: 'cam-stream-1',
preferredCodec: codec,
},
]);
}
describe('codec negotiation reducer', () => {
it('keeps the preferred codec while no one is watching', () => {
const snapshot = publishedCameraOn('av1');
expect(snapshot.codecNegotiation.streams.camera?.negotiatedCodec).toBe('av1');
expect(snapshot.camera.published?.codec).toBe('av1');
});
it('downgrades and republishes when a viewer who cannot decode the codec starts watching', () => {
const snapshot = publishedCameraOn('av1');
const transition = transitionVoiceEngineV2(snapshot, {
type: 'codecNegotiation.viewerChanged',
source: 'camera',
viewerIdentity: 'bob',
watching: true,
supportedVideoCodecs: ['h264', 'vp8'],
});
expect(transition.commands).toEqual([
{type: 'camera.publish', operationId: 3, options: {deviceId: 'cam-1', codec: 'h264'}},
]);
expect(transition.snapshot.codecNegotiation.streams.camera?.negotiatedCodec).toBe('h264');
expect(transition.snapshot.codecNegotiation.streams.camera?.constrainedBy).toBe('bob');
expect(transition.snapshot.camera.status).toBe('publishing');
});
it('preserves the stream identity across the codec republish', () => {
const snapshot = publishedCameraOn('av1');
const downgraded = transitionVoiceEngineV2(snapshot, {
type: 'codecNegotiation.viewerChanged',
source: 'camera',
viewerIdentity: 'bob',
watching: true,
supportedVideoCodecs: ['h264'],
});
expect(downgraded.snapshot.codecNegotiation.streams.camera?.streamIdentity).toBe('cam-stream-1');
});
it('upgrades back toward the preferred codec when the constraining viewer stops watching', () => {
const downgraded = applyEvents(publishedCameraOn('av1'), [
{
type: 'codecNegotiation.viewerChanged',
source: 'camera',
viewerIdentity: 'bob',
watching: true,
supportedVideoCodecs: ['h264'],
},
{type: 'camera.publishSucceeded', operationId: 3},
]);
expect(downgraded.camera.published?.codec).toBe('h264');
const upgraded = transitionVoiceEngineV2(downgraded, {
type: 'codecNegotiation.viewerChanged',
source: 'camera',
viewerIdentity: 'bob',
watching: false,
supportedVideoCodecs: [],
});
expect(upgraded.commands).toEqual([
{type: 'camera.publish', operationId: 4, options: {deviceId: 'cam-1', codec: 'av1'}},
]);
expect(upgraded.snapshot.codecNegotiation.streams.camera?.negotiatedCodec).toBe('av1');
expect(upgraded.snapshot.codecNegotiation.streams.camera?.constrainedBy).toBeNull();
});
it('does not republish when a newly watching viewer can already decode the codec', () => {
const snapshot = publishedCameraOn('h264');
const transition = transitionVoiceEngineV2(snapshot, {
type: 'codecNegotiation.viewerChanged',
source: 'camera',
viewerIdentity: 'bob',
watching: true,
supportedVideoCodecs: ['av1', 'vp9', 'h264', 'vp8'],
});
expect(transition.commands).toEqual([]);
expect(transition.snapshot.codecNegotiation.streams.camera?.negotiatedCodec).toBe('h264');
});
it('clamps the manual override down to a current viewer that cannot decode it', () => {
const watching = transitionVoiceEngineV2(publishedCameraOn('h264'), {
type: 'codecNegotiation.viewerChanged',
source: 'camera',
viewerIdentity: 'bob',
watching: true,
supportedVideoCodecs: ['vp8'],
}).snapshot;
const settled = applyEvents(watching, [{type: 'camera.publishSucceeded', operationId: 3}]);
expect(settled.camera.published?.codec).toBe('vp8');
const overridden = transitionVoiceEngineV2(settled, {
type: 'codecNegotiation.overrideSetRequested',
source: 'camera',
codec: 'av1',
});
expect(overridden.commands).toEqual([]);
expect(overridden.snapshot.codecNegotiation.overrides.camera).toBe('av1');
expect(overridden.snapshot.codecNegotiation.streams.camera?.negotiatedCodec).toBe('vp8');
expect(overridden.snapshot.codecNegotiation.streams.camera?.constrainedBy).toBe('bob');
});
it('applies the manual override as a republish when no viewer constrains it', () => {
const transition = transitionVoiceEngineV2(publishedCameraOn('h264'), {
type: 'codecNegotiation.overrideSetRequested',
source: 'camera',
codec: 'vp9',
});
expect(transition.commands).toEqual([
{type: 'camera.publish', operationId: 3, options: {deviceId: 'cam-1', codec: 'vp9'}},
]);
expect(transition.snapshot.codecNegotiation.streams.camera?.negotiatedCodec).toBe('vp9');
});
it('clears a per-source override back to the published preferred codec', () => {
const overridden = transitionVoiceEngineV2(publishedCameraOn('h264'), {
type: 'codecNegotiation.overrideSetRequested',
source: 'camera',
codec: 'vp9',
});
const settled = applyEvents(overridden.snapshot, [{type: 'camera.publishSucceeded', operationId: 3}]);
expect(settled.camera.published?.codec).toBe('vp9');
const cleared = transitionVoiceEngineV2(settled, {
type: 'codecNegotiation.overrideSetRequested',
source: 'camera',
codec: null,
});
expect(cleared.snapshot.codecNegotiation.overrides.camera).toBeUndefined();
expect(cleared.commands).toEqual([
{type: 'camera.publish', operationId: 4, options: {deviceId: 'cam-1', codec: 'h264'}},
]);
expect(cleared.snapshot.codecNegotiation.streams.camera?.negotiatedCodec).toBe('h264');
});
});
@@ -1,187 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {
maxDecodableVoiceEngineV2VideoCodec,
planVoiceEngineV2NegotiatedVideoCodec,
type VoiceEngineV2CodecViewer,
} from '../../policies/codecNegotiation';
import type {VoiceEngineV2Event} from '../../protocol/events';
import type {
VoiceEngineV2CodecStreamNegotiation,
VoiceEngineV2LocalStreamSource,
VoiceEngineV2VideoCodec,
} from '../../protocol/types';
import type {VoiceEngineV2Snapshot, VoiceEngineV2Transition} from '../state';
import {beginCameraEncodingUpdate} from './camera';
import {beginScreenEncodingUpdate} from './screen';
type VoiceEngineV2CodecNegotiationEvent = Extract<VoiceEngineV2Event, {type: `codecNegotiation.${string}`}>;
function publishedCodecForSource(
snapshot: VoiceEngineV2Snapshot,
source: VoiceEngineV2LocalStreamSource,
): VoiceEngineV2VideoCodec | null {
const media = source === 'camera' ? snapshot.camera : snapshot.screen;
if (media.status !== 'published') return null;
return media.published?.codec ?? null;
}
function viewersForPolicy(stream: VoiceEngineV2CodecStreamNegotiation): Array<VoiceEngineV2CodecViewer> {
const viewers: Array<VoiceEngineV2CodecViewer> = [];
for (const identity of Object.keys(stream.viewers).sort()) {
viewers.push({identity, maxVideoCodec: stream.viewers[identity] ?? null});
}
return viewers;
}
function setStream(
snapshot: VoiceEngineV2Snapshot,
source: VoiceEngineV2LocalStreamSource,
stream: VoiceEngineV2CodecStreamNegotiation,
): VoiceEngineV2Snapshot {
return {
...snapshot,
codecNegotiation: {
...snapshot.codecNegotiation,
streams: {...snapshot.codecNegotiation.streams, [source]: stream},
},
};
}
function recomputeStream(
snapshot: VoiceEngineV2Snapshot,
source: VoiceEngineV2LocalStreamSource,
): VoiceEngineV2Transition {
assert.ok(snapshot != null, 'recomputeStream snapshot must not be null');
const existing = snapshot.codecNegotiation.streams[source];
if (!existing) return {snapshot, commands: []};
const preferred = snapshot.codecNegotiation.overrides[source] ?? existing.preferredCodec;
const plan = planVoiceEngineV2NegotiatedVideoCodec({preferred, viewers: viewersForPolicy(existing)});
const updatedStream: VoiceEngineV2CodecStreamNegotiation = {
...existing,
negotiatedCodec: plan.codec,
constrainedBy: plan.constrainedBy,
};
const withStream = setStream(snapshot, source, updatedStream);
const publishedCodec = publishedCodecForSource(withStream, source);
if (publishedCodec === null) return {snapshot: withStream, commands: []};
if (publishedCodec === plan.codec) return {snapshot: withStream, commands: []};
if (source === 'camera') {
return beginCameraEncodingUpdate(withStream, {codec: plan.codec});
}
const published = withStream.screen.published;
assert.ok(published != null, 'recomputeStream screen republish requires a published screen');
return beginScreenEncodingUpdate(withStream, {
captureId: published.captureId,
width: published.width,
height: published.height,
codec: plan.codec,
});
}
function applyViewerChanged(
snapshot: VoiceEngineV2Snapshot,
source: VoiceEngineV2LocalStreamSource,
viewerIdentity: string,
watching: boolean,
supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>,
): VoiceEngineV2Transition {
assert.equal(typeof viewerIdentity, 'string', 'applyViewerChanged viewerIdentity must be a string');
assert.ok(viewerIdentity.length > 0, 'applyViewerChanged viewerIdentity must not be empty');
const existing = snapshot.codecNegotiation.streams[source];
if (!existing) return {snapshot, commands: []};
const viewers = {...existing.viewers};
if (watching) {
viewers[viewerIdentity] = maxDecodableVoiceEngineV2VideoCodec(supportedVideoCodecs);
} else {
delete viewers[viewerIdentity];
}
const withViewers = setStream(snapshot, source, {...existing, viewers});
return recomputeStream(withViewers, source);
}
function registerStream(
snapshot: VoiceEngineV2Snapshot,
source: VoiceEngineV2LocalStreamSource,
streamIdentity: string,
preferredCodec: VoiceEngineV2VideoCodec,
): VoiceEngineV2Transition {
assert.equal(typeof streamIdentity, 'string', 'registerStream streamIdentity must be a string');
assert.ok(streamIdentity.length > 0, 'registerStream streamIdentity must not be empty');
const existing = snapshot.codecNegotiation.streams[source];
const stream: VoiceEngineV2CodecStreamNegotiation = {
source,
streamIdentity,
preferredCodec,
negotiatedCodec: existing?.negotiatedCodec ?? preferredCodec,
constrainedBy: existing?.constrainedBy ?? null,
viewers: existing?.viewers ?? {},
};
return recomputeStream(setStream(snapshot, source, stream), source);
}
function unregisterStream(
snapshot: VoiceEngineV2Snapshot,
source: VoiceEngineV2LocalStreamSource,
): VoiceEngineV2Transition {
if (!snapshot.codecNegotiation.streams[source]) return {snapshot, commands: []};
const streams = {...snapshot.codecNegotiation.streams};
delete streams[source];
return {snapshot: {...snapshot, codecNegotiation: {...snapshot.codecNegotiation, streams}}, commands: []};
}
export function transitionCodecNegotiation(
snapshot: VoiceEngineV2Snapshot,
event: VoiceEngineV2CodecNegotiationEvent,
): VoiceEngineV2Transition {
assert.ok(snapshot != null, 'transitionCodecNegotiation snapshot must not be null');
assert.ok(event != null, 'transitionCodecNegotiation event must not be null');
assert.ok(event.type.startsWith('codecNegotiation.'), 'codecNegotiation reducer received unrelated event');
switch (event.type) {
case 'codecNegotiation.overrideSetRequested': {
const overrides = {...snapshot.codecNegotiation.overrides};
if (event.codec === null || event.codec === '') {
delete overrides[event.source];
} else {
overrides[event.source] = event.codec;
}
const withOverride = {...snapshot, codecNegotiation: {...snapshot.codecNegotiation, overrides}};
return recomputeStream(withOverride, event.source);
}
case 'codecNegotiation.localCapabilityChanged':
return {
snapshot: {
...snapshot,
codecNegotiation: {...snapshot.codecNegotiation, localSupportedVideoCodecs: [...event.supportedVideoCodecs]},
},
commands: [],
};
case 'codecNegotiation.remoteCapabilityChanged':
return {
snapshot: {
...snapshot,
codecNegotiation: {
...snapshot.codecNegotiation,
remoteSupportedVideoCodecs: {
...snapshot.codecNegotiation.remoteSupportedVideoCodecs,
[event.identity]: [...event.supportedVideoCodecs],
},
},
},
commands: [],
};
case 'codecNegotiation.streamRegistered':
return registerStream(snapshot, event.source, event.streamIdentity, event.preferredCodec);
case 'codecNegotiation.streamUnregistered':
return unregisterStream(snapshot, event.source);
case 'codecNegotiation.viewerChanged':
return applyViewerChanged(
snapshot,
event.source,
event.viewerIdentity,
event.watching,
event.supportedVideoCodecs,
);
}
}
@@ -130,7 +130,7 @@ function failScreen(
};
}
export function beginScreenEncodingUpdate(
function beginScreenEncodingUpdate(
snapshot: VoiceEngineV2Snapshot,
options: VoiceEngineV2ScreenEncodingOptions,
): VoiceEngineV2Transition {
@@ -19,13 +19,11 @@ import type {
VoiceEngineV2Error,
VoiceEngineV2HardwareEncoderCapabilities,
VoiceEngineV2InboundVideoTrack,
VoiceEngineV2LocalStreamSource,
VoiceEngineV2MediaModel,
VoiceEngineV2Model,
VoiceEngineV2Participant,
VoiceEngineV2PermissionResult,
VoiceEngineV2Stats,
VoiceEngineV2StreamNegotiationProjection,
VoiceEngineV2Track,
VoiceEngineV2WatchedStream,
} from '../protocol/types';
@@ -131,27 +129,6 @@ export function selectVoiceEngineV2WatchedStreams(snapshot: VoiceEngineV2Snapsho
return Object.values(snapshot.watchedStreams);
}
export function selectVoiceEngineV2StreamNegotiation(
snapshot: VoiceEngineV2Snapshot,
source: VoiceEngineV2LocalStreamSource,
): VoiceEngineV2StreamNegotiationProjection | null {
const stream = snapshot.codecNegotiation.streams[source];
if (!stream) return null;
const media = source === 'camera' ? snapshot.camera : snapshot.screen;
const publishedCodec = media.status === 'published' ? (media.published?.codec ?? null) : null;
const renegotiating =
media.status === 'publishing' || (publishedCodec !== null && publishedCodec !== stream.negotiatedCodec);
return {
source,
streamIdentity: stream.streamIdentity,
negotiatedCodec: stream.negotiatedCodec,
preferredCodec: stream.preferredCodec,
constrainedBy: stream.constrainedBy,
renegotiating,
viewerCount: Object.keys(stream.viewers).length,
};
}
export function selectVoiceEngineV2DeviceProjection(snapshot: VoiceEngineV2Snapshot): VoiceEngineV2DeviceProjection {
const {inventory} = snapshot.devices;
return {
@@ -9,7 +9,6 @@ import type {
VoiceEngineV2AudioControls,
VoiceEngineV2CameraOptions,
VoiceEngineV2Capabilities,
VoiceEngineV2CodecNegotiationState,
VoiceEngineV2ConnectionStatus,
VoiceEngineV2ConnectOptions,
VoiceEngineV2DeviceInventory,
@@ -170,7 +169,6 @@ export interface VoiceEngineV2Snapshot {
participantVolumes: Record<string, number>;
remoteTrackSubscriptions: Record<string, VoiceEngineV2RemoteTrackSubscriptionOptions>;
watchedStreams: Record<string, VoiceEngineV2WatchedStream>;
codecNegotiation: VoiceEngineV2CodecNegotiationState;
stats: VoiceEngineV2Stats | null;
statsOperationId: VoiceEngineV2OperationId | null;
statsFailure: VoiceEngineV2Error | null;
@@ -338,12 +336,6 @@ export function createVoiceEngineV2InitialSnapshot(
participantVolumes: {},
remoteTrackSubscriptions: {},
watchedStreams: {},
codecNegotiation: {
overrides: {},
localSupportedVideoCodecs: [],
remoteSupportedVideoCodecs: {},
streams: {},
},
stats: null,
statsOperationId: null,
statsFailure: null,
-15
View File
@@ -46,7 +46,6 @@ export {
selectVoiceEngineV2StatsPresentationProjection,
selectVoiceEngineV2StatsProjection,
selectVoiceEngineV2StatsSummary,
selectVoiceEngineV2StreamNegotiation,
selectVoiceEngineV2WatchedStreams,
shouldApplyGatewayVoiceStateEcho,
transitionVoiceEngineV2,
@@ -105,19 +104,12 @@ export {
hasVoiceEngineV2NativeHardwareEncoder,
hasVoiceEngineV2NativeNvencEncoder,
hasVoiceEngineV2ZeroCopyNativeInput,
isMoreEfficientVoiceEngineV2VideoCodec,
maxDecodableVoiceEngineV2VideoCodec,
normalizeVoiceEngineV2HardwareEncoderCapabilities,
planVoiceEngineV2CameraEncodingChange,
planVoiceEngineV2NegotiatedVideoCodec,
planVoiceEngineV2ScreenEncodingChange,
summarizeVoiceEngineV2Stats,
unavailableVoiceEngineV2HardwareEncoderCapabilities,
VOICE_ENGINE_V2_VIDEO_CODEC_FLOOR,
VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE,
VoiceEngineV2StatsTrackSource,
voiceEngineV2VideoCodecRank,
worseVoiceEngineV2VideoCodec,
} from './policies';
export type {
CapabilitiesPort,
@@ -149,9 +141,6 @@ export type {
VoiceEngineV2CameraEncodingOptions,
VoiceEngineV2CameraOptions,
VoiceEngineV2Capabilities,
VoiceEngineV2CodecGossipMessage,
VoiceEngineV2CodecNegotiationState,
VoiceEngineV2CodecStreamNegotiation,
VoiceEngineV2Command,
VoiceEngineV2CommandType,
VoiceEngineV2ConnectionModel,
@@ -217,7 +206,6 @@ export type {
VoiceEngineV2SourceLifecycleTransitionKind,
VoiceEngineV2Stats,
VoiceEngineV2StatsSample,
VoiceEngineV2StreamNegotiationProjection,
VoiceEngineV2TimerOptions,
VoiceEngineV2Track,
VoiceEngineV2TrackKind,
@@ -230,11 +218,8 @@ export type {
VoiceEngineV2WatchedStreamKey,
} from './protocol';
export {
decodeVoiceEngineV2CodecGossip,
encodeVoiceEngineV2CodecGossip,
getVoiceEngineV2CommandResourceKey,
getVoiceEngineV2CommandTypeResourceKey,
VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC,
} from './protocol';
export type {
VoiceEngineV2EventLogEntry,
@@ -1,104 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, it} from 'vitest';
import {
isMoreEfficientVoiceEngineV2VideoCodec,
maxDecodableVoiceEngineV2VideoCodec,
planVoiceEngineV2NegotiatedVideoCodec,
VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE,
voiceEngineV2VideoCodecRank,
worseVoiceEngineV2VideoCodec,
} from './codecNegotiation';
describe('voice engine v2 codec negotiation policy', () => {
it('orders codecs most-efficient to most-compatible', () => {
expect(VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE).toEqual(['av1', 'h265', 'vp9', 'h264', 'vp8']);
expect(voiceEngineV2VideoCodecRank('av1')).toBeLessThan(voiceEngineV2VideoCodecRank('h265'));
expect(voiceEngineV2VideoCodecRank('h265')).toBeLessThan(voiceEngineV2VideoCodecRank('vp9'));
expect(voiceEngineV2VideoCodecRank('vp9')).toBeLessThan(voiceEngineV2VideoCodecRank('h264'));
expect(voiceEngineV2VideoCodecRank('h264')).toBeLessThan(voiceEngineV2VideoCodecRank('vp8'));
});
it('ranks unknown and empty codecs past the floor', () => {
expect(voiceEngineV2VideoCodecRank('')).toBe(VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE.length);
expect(voiceEngineV2VideoCodecRank(undefined)).toBe(VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE.length);
});
it('compares efficiency and compatibility', () => {
expect(isMoreEfficientVoiceEngineV2VideoCodec('av1', 'h264')).toBe(true);
expect(isMoreEfficientVoiceEngineV2VideoCodec('vp8', 'vp9')).toBe(false);
expect(worseVoiceEngineV2VideoCodec('av1', 'h264')).toBe('h264');
expect(worseVoiceEngineV2VideoCodec('vp8', 'vp9')).toBe('vp8');
});
it('computes a viewer max-decodable codec from its supported set', () => {
expect(maxDecodableVoiceEngineV2VideoCodec(['h264', 'vp8', 'vp9'])).toBe('vp9');
expect(maxDecodableVoiceEngineV2VideoCodec(['vp8', 'av1', 'h264'])).toBe('av1');
expect(maxDecodableVoiceEngineV2VideoCodec([])).toBeNull();
expect(maxDecodableVoiceEngineV2VideoCodec(['', 'totally-unknown' as never])).toBeNull();
});
it('uses the preferred codec when no viewer constrains it', () => {
const plan = planVoiceEngineV2NegotiatedVideoCodec({
preferred: 'av1',
viewers: [
{identity: 'alice', maxVideoCodec: 'av1'},
{identity: 'bob', maxVideoCodec: 'av1'},
],
});
expect(plan.codec).toBe('av1');
expect(plan.reason).toBe('preferred');
expect(plan.constrainedBy).toBeNull();
});
it('clamps down to the worst current viewer when one cannot decode the preferred codec', () => {
const plan = planVoiceEngineV2NegotiatedVideoCodec({
preferred: 'av1',
viewers: [
{identity: 'alice', maxVideoCodec: 'av1'},
{identity: 'bob', maxVideoCodec: 'h264'},
],
});
expect(plan.codec).toBe('h264');
expect(plan.reason).toBe('clampedToViewer');
expect(plan.constrainedBy).toBe('bob');
});
it('is order-independent and clamps to the single worst viewer', () => {
const viewers = [
{identity: 'alice', maxVideoCodec: 'vp9' as const},
{identity: 'carol', maxVideoCodec: 'h264' as const},
{identity: 'bob', maxVideoCodec: 'h265' as const},
];
const forward = planVoiceEngineV2NegotiatedVideoCodec({preferred: 'av1', viewers});
const reversed = planVoiceEngineV2NegotiatedVideoCodec({preferred: 'av1', viewers: [...viewers].reverse()});
expect(forward.codec).toBe('h264');
expect(forward.constrainedBy).toBe('carol');
expect(reversed.codec).toBe('h264');
expect(reversed.constrainedBy).toBe('carol');
});
it('does not constrain on viewers with unknown capability (optimistic until they report)', () => {
const plan = planVoiceEngineV2NegotiatedVideoCodec({
preferred: 'av1',
viewers: [
{identity: 'alice', maxVideoCodec: null},
{identity: 'bob', maxVideoCodec: 'vp9'},
],
});
expect(plan.codec).toBe('vp9');
expect(plan.constrainedBy).toBe('bob');
});
it('never upgrades beyond the preferred codec even if every viewer supports better', () => {
const plan = planVoiceEngineV2NegotiatedVideoCodec({
preferred: 'h264',
viewers: [
{identity: 'alice', maxVideoCodec: 'av1'},
{identity: 'bob', maxVideoCodec: 'av1'},
],
});
expect(plan.codec).toBe('h264');
expect(plan.reason).toBe('preferred');
});
});
@@ -1,101 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import type {VoiceEngineV2VideoCodec} from '../protocol/types';
export const VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE: ReadonlyArray<VoiceEngineV2VideoCodec> = [
'av1',
'h265',
'vp9',
'h264',
'vp8',
];
export const VOICE_ENGINE_V2_VIDEO_CODEC_FLOOR: VoiceEngineV2VideoCodec = 'vp8';
const VIDEO_CODEC_RANK: ReadonlyMap<VoiceEngineV2VideoCodec, number> = new Map(
VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE.map((codec, index) => [codec, index]),
);
function isKnownVoiceEngineV2VideoCodec(codec: VoiceEngineV2VideoCodec | null | undefined): boolean {
if (codec == null) return false;
if (codec === '') return false;
return VIDEO_CODEC_RANK.has(codec);
}
export function voiceEngineV2VideoCodecRank(codec: VoiceEngineV2VideoCodec | null | undefined): number {
if (!isKnownVoiceEngineV2VideoCodec(codec)) return VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE.length;
const rank = VIDEO_CODEC_RANK.get(codec as VoiceEngineV2VideoCodec);
assert.ok(rank !== undefined, 'voiceEngineV2VideoCodecRank known codec must have a rank');
return rank;
}
export function isMoreEfficientVoiceEngineV2VideoCodec(
candidate: VoiceEngineV2VideoCodec,
reference: VoiceEngineV2VideoCodec,
): boolean {
return voiceEngineV2VideoCodecRank(candidate) < voiceEngineV2VideoCodecRank(reference);
}
export function worseVoiceEngineV2VideoCodec(
a: VoiceEngineV2VideoCodec,
b: VoiceEngineV2VideoCodec,
): VoiceEngineV2VideoCodec {
return voiceEngineV2VideoCodecRank(b) > voiceEngineV2VideoCodecRank(a) ? b : a;
}
export function maxDecodableVoiceEngineV2VideoCodec(
supported: ReadonlyArray<VoiceEngineV2VideoCodec>,
): VoiceEngineV2VideoCodec | null {
assert.ok(Array.isArray(supported), 'maxDecodableVoiceEngineV2VideoCodec supported must be an array');
let best: VoiceEngineV2VideoCodec | null = null;
for (const codec of supported) {
if (!isKnownVoiceEngineV2VideoCodec(codec)) continue;
if (best === null || isMoreEfficientVoiceEngineV2VideoCodec(codec, best)) {
best = codec;
}
}
return best;
}
export interface VoiceEngineV2CodecViewer {
identity: string;
maxVideoCodec: VoiceEngineV2VideoCodec | null;
}
export type VoiceEngineV2NegotiatedCodecReason = 'preferred' | 'clampedToViewer';
export interface VoiceEngineV2NegotiatedCodecPlan {
codec: VoiceEngineV2VideoCodec;
reason: VoiceEngineV2NegotiatedCodecReason;
constrainedBy: string | null;
}
interface VoiceEngineV2NegotiatedCodecInput {
preferred: VoiceEngineV2VideoCodec;
viewers: ReadonlyArray<VoiceEngineV2CodecViewer>;
}
export function planVoiceEngineV2NegotiatedVideoCodec(
input: VoiceEngineV2NegotiatedCodecInput,
): VoiceEngineV2NegotiatedCodecPlan {
assert.ok(input != null, 'planVoiceEngineV2NegotiatedVideoCodec input must not be null');
assert.ok(Array.isArray(input.viewers), 'planVoiceEngineV2NegotiatedVideoCodec viewers must be an array');
const preferred = isKnownVoiceEngineV2VideoCodec(input.preferred)
? input.preferred
: VOICE_ENGINE_V2_VIDEO_CODEC_FLOOR;
let codec = preferred;
let reason: VoiceEngineV2NegotiatedCodecReason = 'preferred';
let constrainedBy: string | null = null;
for (const viewer of input.viewers) {
if (viewer.maxVideoCodec === null) continue;
if (!isKnownVoiceEngineV2VideoCodec(viewer.maxVideoCodec)) continue;
if (!isMoreEfficientVoiceEngineV2VideoCodec(codec, viewer.maxVideoCodec)) {
continue;
}
codec = viewer.maxVideoCodec;
reason = 'clampedToViewer';
constrainedBy = viewer.identity;
}
return {codec, reason, constrainedBy};
}
@@ -7,20 +7,6 @@ export type {
VoiceEngineV2CameraEncodingPlanReason,
} from './cameraShare';
export {applyVoiceEngineV2CameraEncodingOptions, planVoiceEngineV2CameraEncodingChange} from './cameraShare';
export type {
VoiceEngineV2CodecViewer,
VoiceEngineV2NegotiatedCodecPlan,
VoiceEngineV2NegotiatedCodecReason,
} from './codecNegotiation';
export {
isMoreEfficientVoiceEngineV2VideoCodec,
maxDecodableVoiceEngineV2VideoCodec,
planVoiceEngineV2NegotiatedVideoCodec,
VOICE_ENGINE_V2_VIDEO_CODEC_FLOOR,
VOICE_ENGINE_V2_VIDEO_CODEC_PREFERENCE,
voiceEngineV2VideoCodecRank,
worseVoiceEngineV2VideoCodec,
} from './codecNegotiation';
export {
hasVoiceEngineV2NativeHardwareEncoder,
hasVoiceEngineV2NativeNvencEncoder,
@@ -1,70 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, it} from 'vitest';
import {
decodeVoiceEngineV2CodecGossip,
encodeVoiceEngineV2CodecGossip,
VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC,
} from './codecGossip';
import type {VoiceEngineV2CodecGossipMessage} from './types';
describe('voice engine v2 codec gossip wire protocol', () => {
it('exposes a stable topic', () => {
expect(VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC).toBe('fluxer.codec.v1');
});
it('round-trips a capability message', () => {
const message: VoiceEngineV2CodecGossipMessage = {
kind: 'codec.capability',
supportedVideoCodecs: ['av1', 'vp9', 'h264', 'vp8'],
};
const decoded = decodeVoiceEngineV2CodecGossip(encodeVoiceEngineV2CodecGossip(message));
expect(decoded).toEqual(message);
});
it('round-trips a viewing message', () => {
const message: VoiceEngineV2CodecGossipMessage = {
kind: 'codec.viewing',
source: 'screen',
watching: true,
supportedVideoCodecs: ['h264', 'vp8'],
};
const decoded = decodeVoiceEngineV2CodecGossip(encodeVoiceEngineV2CodecGossip(message));
expect(decoded).toEqual(message);
});
it('decodes from a raw ArrayBuffer payload', () => {
const bytes = encodeVoiceEngineV2CodecGossip({kind: 'codec.capability', supportedVideoCodecs: ['av1']});
const buffer = new ArrayBuffer(bytes.byteLength);
new Uint8Array(buffer).set(bytes);
expect(decodeVoiceEngineV2CodecGossip(buffer)).toEqual({kind: 'codec.capability', supportedVideoCodecs: ['av1']});
});
it('rejects malformed and adversarial payloads as null', () => {
expect(decodeVoiceEngineV2CodecGossip('not json')).toBeNull();
expect(decodeVoiceEngineV2CodecGossip('123')).toBeNull();
expect(decodeVoiceEngineV2CodecGossip(JSON.stringify({kind: 'other'}))).toBeNull();
expect(decodeVoiceEngineV2CodecGossip(JSON.stringify({kind: 'codec.capability'}))).toBeNull();
expect(
decodeVoiceEngineV2CodecGossip(JSON.stringify({kind: 'codec.capability', supportedVideoCodecs: ['fake']})),
).toBeNull();
expect(
decodeVoiceEngineV2CodecGossip(
JSON.stringify({kind: 'codec.viewing', source: 'mic', watching: true, supportedVideoCodecs: []}),
),
).toBeNull();
expect(
decodeVoiceEngineV2CodecGossip(
JSON.stringify({kind: 'codec.viewing', source: 'camera', watching: 'yes', supportedVideoCodecs: []}),
),
).toBeNull();
});
it('rejects an oversized codec list', () => {
const tooMany = JSON.stringify({
kind: 'codec.capability',
supportedVideoCodecs: ['av1', 'av1', 'av1', 'av1', 'av1', 'av1', 'av1', 'av1', 'av1'],
});
expect(decodeVoiceEngineV2CodecGossip(tooMany)).toBeNull();
});
});
@@ -1,75 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import type {VoiceEngineV2CodecGossipMessage, VoiceEngineV2VideoCodec} from './types';
export const VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC = 'fluxer.codec.v1';
const KNOWN_VIDEO_CODECS: ReadonlySet<string> = new Set(['', 'vp8', 'vp9', 'h264', 'h265', 'av1']);
const VIDEO_CODECS_CAP = 8;
function isVideoCodec(value: unknown): value is VoiceEngineV2VideoCodec {
return typeof value === 'string' && KNOWN_VIDEO_CODECS.has(value);
}
function normalizeSupportedVideoCodecs(value: unknown): Array<VoiceEngineV2VideoCodec> | null {
if (!Array.isArray(value)) return null;
if (value.length > VIDEO_CODECS_CAP) return null;
const codecs: Array<VoiceEngineV2VideoCodec> = [];
for (const entry of value) {
if (!isVideoCodec(entry)) return null;
codecs.push(entry);
}
return codecs;
}
export function encodeVoiceEngineV2CodecGossip(message: VoiceEngineV2CodecGossipMessage): Uint8Array {
assert.ok(message != null, 'encodeVoiceEngineV2CodecGossip message must not be null');
assert.ok(message.kind === 'codec.capability' || message.kind === 'codec.viewing', 'unknown gossip message kind');
return new TextEncoder().encode(JSON.stringify(message));
}
export function decodeVoiceEngineV2CodecGossip(
payload: ArrayBuffer | ArrayBufferView | string,
): VoiceEngineV2CodecGossipMessage | null {
const text = typeof payload === 'string' ? payload : decodeUtf8(payload);
if (text === null) return null;
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
return null;
}
if (typeof parsed !== 'object' || parsed === null) return null;
const candidate = parsed as Record<string, unknown>;
if (candidate.kind === 'codec.capability') {
const supportedVideoCodecs = normalizeSupportedVideoCodecs(candidate.supportedVideoCodecs);
if (supportedVideoCodecs === null) return null;
return {kind: 'codec.capability', supportedVideoCodecs};
}
if (candidate.kind === 'codec.viewing') {
if (candidate.source !== 'camera' && candidate.source !== 'screen') return null;
if (typeof candidate.watching !== 'boolean') return null;
const supportedVideoCodecs = normalizeSupportedVideoCodecs(candidate.supportedVideoCodecs);
if (supportedVideoCodecs === null) return null;
return {
kind: 'codec.viewing',
source: candidate.source,
watching: candidate.watching,
supportedVideoCodecs,
};
}
return null;
}
function decodeUtf8(payload: ArrayBuffer | ArrayBufferView): string | null {
try {
const view =
payload instanceof ArrayBuffer
? new Uint8Array(payload)
: new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
return new TextDecoder('utf-8', {fatal: false}).decode(view);
} catch {
return null;
}
}
@@ -42,7 +42,6 @@ import type {
VoiceEngineV2InboundVideoTrackSubscription,
VoiceEngineV2LifecycleReason,
VoiceEngineV2LiveKitRoomState,
VoiceEngineV2LocalStreamSource,
VoiceEngineV2MicrophoneOptions,
VoiceEngineV2NativeAudioTapOptions,
VoiceEngineV2NativeCaptureFrame,
@@ -62,7 +61,6 @@ import type {
VoiceEngineV2Stats,
VoiceEngineV2TimerOptions,
VoiceEngineV2Track,
VoiceEngineV2VideoCodec,
VoiceEngineV2WatchedStream,
VoiceEngineV2WatchedStreamKey,
} from './types';
@@ -237,31 +235,6 @@ export type VoiceEngineV2Event =
| {type: 'room.trackUnpublished'; trackSid: string}
| {type: 'room.trackMuted'; trackSid: string}
| {type: 'room.trackUnmuted'; trackSid: string}
| {
type: 'codecNegotiation.overrideSetRequested';
source: VoiceEngineV2LocalStreamSource;
codec: VoiceEngineV2VideoCodec | null;
}
| {type: 'codecNegotiation.localCapabilityChanged'; supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>}
| {
type: 'codecNegotiation.streamRegistered';
source: VoiceEngineV2LocalStreamSource;
streamIdentity: string;
preferredCodec: VoiceEngineV2VideoCodec;
}
| {type: 'codecNegotiation.streamUnregistered'; source: VoiceEngineV2LocalStreamSource}
| {
type: 'codecNegotiation.viewerChanged';
source: VoiceEngineV2LocalStreamSource;
viewerIdentity: string;
watching: boolean;
supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>;
}
| {
type: 'codecNegotiation.remoteCapabilityChanged';
identity: string;
supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>;
}
| {type: 'watchedStream.watchRequested'; stream: VoiceEngineV2WatchedStream}
| {type: 'watchedStream.unwatchRequested'; stream: VoiceEngineV2WatchedStreamKey}
| {type: 'watchedStreams.replaced'; streams: Array<VoiceEngineV2WatchedStream>}
@@ -1,10 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export {
decodeVoiceEngineV2CodecGossip,
encodeVoiceEngineV2CodecGossip,
VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC,
} from './codecGossip';
export type {VoiceEngineV2Command, VoiceEngineV2CommandType} from './commands';
export {getVoiceEngineV2CommandResourceKey, getVoiceEngineV2CommandTypeResourceKey} from './commands';
export type {
@@ -24,9 +19,6 @@ export type {
VoiceEngineV2CameraEncodingOptions,
VoiceEngineV2CameraOptions,
VoiceEngineV2Capabilities,
VoiceEngineV2CodecGossipMessage,
VoiceEngineV2CodecNegotiationState,
VoiceEngineV2CodecStreamNegotiation,
VoiceEngineV2ConnectionModel,
VoiceEngineV2ConnectionQuality,
VoiceEngineV2ConnectionStatus,
@@ -86,7 +78,6 @@ export type {
VoiceEngineV2SendStats,
VoiceEngineV2Stats,
VoiceEngineV2StatsSample,
VoiceEngineV2StreamNegotiationProjection,
VoiceEngineV2TimerOptions,
VoiceEngineV2Track,
VoiceEngineV2TrackKind,
@@ -676,41 +676,6 @@ export interface VoiceEngineV2WatchedStreamKey {
export type VoiceEngineV2LocalStreamSource = 'camera' | 'screen';
export interface VoiceEngineV2CodecStreamNegotiation {
source: VoiceEngineV2LocalStreamSource;
streamIdentity: string;
preferredCodec: VoiceEngineV2VideoCodec;
negotiatedCodec: VoiceEngineV2VideoCodec;
constrainedBy: string | null;
viewers: Record<string, VoiceEngineV2VideoCodec | null>;
}
export interface VoiceEngineV2CodecNegotiationState {
overrides: Partial<Record<VoiceEngineV2LocalStreamSource, VoiceEngineV2VideoCodec>>;
localSupportedVideoCodecs: Array<VoiceEngineV2VideoCodec>;
remoteSupportedVideoCodecs: Record<string, Array<VoiceEngineV2VideoCodec>>;
streams: Record<string, VoiceEngineV2CodecStreamNegotiation>;
}
export interface VoiceEngineV2StreamNegotiationProjection {
source: VoiceEngineV2LocalStreamSource;
streamIdentity: string;
negotiatedCodec: VoiceEngineV2VideoCodec;
preferredCodec: VoiceEngineV2VideoCodec;
constrainedBy: string | null;
renegotiating: boolean;
viewerCount: number;
}
export type VoiceEngineV2CodecGossipMessage =
| {kind: 'codec.capability'; supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>}
| {
kind: 'codec.viewing';
source: VoiceEngineV2LocalStreamSource;
watching: boolean;
supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>;
};
export interface VoiceEngineV2InboundVideoTrackSubscription {
participantSid: string;
participantIdentity?: string;
@@ -12,7 +12,6 @@ import type {
VoiceEngineV2GatewayDesiredVoiceState,
VoiceEngineV2GatewayVoiceStateWrite,
VoiceEngineV2LifecycleReason,
VoiceEngineV2LocalStreamSource,
VoiceEngineV2MicrophoneOptions,
VoiceEngineV2Model,
VoiceEngineV2NativeAudioTapOptions,
@@ -27,7 +26,6 @@ import type {
VoiceEngineV2ScreenEncodingOptions,
VoiceEngineV2ScreenOptions,
VoiceEngineV2TimerOptions,
VoiceEngineV2VideoCodec,
VoiceEngineV2WatchedStream,
VoiceEngineV2WatchedStreamKey,
} from '../protocol';
@@ -113,45 +111,6 @@ export class VoiceEngineV2Controller {
this.runtime.dispatch({type: 'camera.updateEncodingRequested', options});
}
setVideoCodecOverride(source: VoiceEngineV2LocalStreamSource, codec: VoiceEngineV2VideoCodec | null): void {
this.runtime.dispatch({type: 'codecNegotiation.overrideSetRequested', source, codec});
}
setLocalVideoCodecCapability(supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>): void {
this.runtime.dispatch({type: 'codecNegotiation.localCapabilityChanged', supportedVideoCodecs});
}
registerLocalStreamCodec(
source: VoiceEngineV2LocalStreamSource,
streamIdentity: string,
preferredCodec: VoiceEngineV2VideoCodec,
): void {
this.runtime.dispatch({type: 'codecNegotiation.streamRegistered', source, streamIdentity, preferredCodec});
}
unregisterLocalStreamCodec(source: VoiceEngineV2LocalStreamSource): void {
this.runtime.dispatch({type: 'codecNegotiation.streamUnregistered', source});
}
reportStreamViewer(
source: VoiceEngineV2LocalStreamSource,
viewerIdentity: string,
watching: boolean,
supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>,
): void {
this.runtime.dispatch({
type: 'codecNegotiation.viewerChanged',
source,
viewerIdentity,
watching,
supportedVideoCodecs,
});
}
reportRemoteVideoCodecCapability(identity: string, supportedVideoCodecs: Array<VoiceEngineV2VideoCodec>): void {
this.runtime.dispatch({type: 'codecNegotiation.remoteCapabilityChanged', identity, supportedVideoCodecs});
}
unpublishCamera(options?: VoiceEngineV2CameraOptions): void {
this.runtime.dispatch({type: 'camera.unpublishRequested', options});
}
@@ -84,40 +84,6 @@ export function runVoiceEngineV2ConformanceSuite(
});
});
it('renegotiates the publication codec in place when a viewer cannot decode it', async () => {
const {implementation, driver} = createSubject();
const runtime = createConformanceRuntime(implementation);
runtime.dispatch({type: 'connection.connectRequested', options: {url: 'wss://voice', token: 'token'}});
await waitForRuntime();
runtime.dispatch({type: 'camera.publishRequested', options: {deviceId: 'cam-1', codec: 'av1'}});
await waitForRuntime();
runtime.dispatch({
type: 'codecNegotiation.streamRegistered',
source: 'camera',
streamIdentity: 'cam-stream-1',
preferredCodec: 'av1',
});
await waitForRuntime();
runtime.dispatch({
type: 'codecNegotiation.viewerChanged',
source: 'camera',
viewerIdentity: 'bob',
watching: true,
supportedVideoCodecs: ['h264', 'vp8'],
});
await waitForRuntime();
expect(driver.calls).toEqual([
{type: 'connect', options: {url: 'wss://voice', token: 'token'}},
{type: 'publishCamera', options: {deviceId: 'cam-1', codec: 'av1'}},
{type: 'publishCamera', options: {deviceId: 'cam-1', codec: 'h264'}},
]);
expect(runtime.snapshot.camera.published?.codec).toBe('h264');
expect(runtime.snapshot.codecNegotiation.streams.camera?.streamIdentity).toBe('cam-stream-1');
expect(runtime.snapshot.codecNegotiation.streams.camera?.constrainedBy).toBe('bob');
});
it('adds and removes screen-share audio without republishing the screen video', async () => {
const {implementation, driver} = createSubject();
const runtime = createConformanceRuntime(implementation);