mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
refactor(voice): remove screen share codec renegotiation (#2417)
This commit is contained in:
1 parent
f5f60c66e7
commit
c97bc53342
80 files changed
+1360
-4726
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user