mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user