mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
refactor(voice): drop the native only surface from voice_engine_v2 and narrow the desktop bridge (#1788)
This commit is contained in:
1 parent
9731bac40f
commit
4f21430880
43 files changed
+196
-3682
No files matched your search
@@ -1,29 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {bench, describe} from 'vitest';
|
||||
import type {VoiceEngineV2InboundVideoFrame} from '../protocol/types';
|
||||
import {VoiceEngineV2FrameStatsAccumulator} from './frameStatsBatching';
|
||||
|
||||
const frame: VoiceEngineV2InboundVideoFrame = Object.freeze({
|
||||
participantSid: 'sid-remote',
|
||||
participantIdentity: 'identity-remote',
|
||||
trackSid: 'track-video-1',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
timestampUs: 1_000_000,
|
||||
byteLength: 320_000,
|
||||
});
|
||||
|
||||
const recordAccumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
const flushAccumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
|
||||
describe('voice engine v2 frame stats accumulator hot paths', () => {
|
||||
bench('record per-frame update', () => {
|
||||
recordAccumulator.record(frame);
|
||||
});
|
||||
|
||||
bench('record then flush one dirty track', () => {
|
||||
flushAccumulator.record(frame);
|
||||
flushAccumulator.flushDirty(() => {});
|
||||
});
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import type {VoiceEngineV2InboundVideoFrame} from '../protocol/types';
|
||||
import {
|
||||
VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP,
|
||||
VoiceEngineV2FrameStatsAccumulator,
|
||||
type VoiceEngineV2FrameStatsEvent,
|
||||
} from './frameStatsBatching';
|
||||
|
||||
function makeFrame(overrides: Partial<VoiceEngineV2InboundVideoFrame> = {}): VoiceEngineV2InboundVideoFrame {
|
||||
return {
|
||||
participantSid: 'PA_alice',
|
||||
participantIdentity: 'alice',
|
||||
trackSid: 'TR_screen',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
timestampUs: 1000,
|
||||
byteLength: 1_382_400,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function collectFlush(accumulator: VoiceEngineV2FrameStatsAccumulator): Array<VoiceEngineV2FrameStatsEvent> {
|
||||
const events: Array<VoiceEngineV2FrameStatsEvent> = [];
|
||||
accumulator.flushDirty((event) => events.push(event));
|
||||
return events;
|
||||
}
|
||||
|
||||
describe('voice engine v2 frame stats accumulator', () => {
|
||||
it('accumulates a cumulative frame count and flushes one absolute event per dirty track', () => {
|
||||
const accumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
for (let index = 1; index <= 30; index += 1) {
|
||||
expect(accumulator.record(makeFrame({timestampUs: index * 33_333}))).toBe(true);
|
||||
}
|
||||
expect(accumulator.trackedTracksCount).toBe(1);
|
||||
expect(accumulator.dirtyTracksCount).toBe(1);
|
||||
|
||||
const events = collectFlush(accumulator);
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: 'inboundVideo.frameStats',
|
||||
stats: {
|
||||
participantSid: 'PA_alice',
|
||||
participantIdentity: 'alice',
|
||||
trackSid: 'TR_screen',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameCount: 30,
|
||||
lastFrameTimestampUs: 30 * 33_333,
|
||||
lastFrameByteLength: 1_382_400,
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(accumulator.dirtyTracksCount).toBe(0);
|
||||
expect(collectFlush(accumulator)).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the cumulative count across flushes and reuses the same record without re-allocating slots', () => {
|
||||
const accumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
accumulator.record(makeFrame({timestampUs: 1000}));
|
||||
collectFlush(accumulator);
|
||||
accumulator.record(makeFrame({timestampUs: 2000, width: 640, height: 360, byteLength: undefined}));
|
||||
|
||||
const events = collectFlush(accumulator);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]?.stats).toMatchObject({
|
||||
frameCount: 2,
|
||||
lastFrameTimestampUs: 2000,
|
||||
lastFrameByteLength: null,
|
||||
width: 640,
|
||||
height: 360,
|
||||
});
|
||||
expect(accumulator.trackedTracksCount).toBe(1);
|
||||
});
|
||||
|
||||
it('omits participantIdentity when never observed for the track', () => {
|
||||
const accumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
accumulator.record(makeFrame({participantIdentity: undefined}));
|
||||
|
||||
const events = collectFlush(accumulator);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]?.stats).not.toHaveProperty('participantIdentity');
|
||||
});
|
||||
|
||||
it('drops updates for new tracks at the cap and counts the drops', () => {
|
||||
const accumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
for (let index = 0; index < VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP; index += 1) {
|
||||
expect(accumulator.record(makeFrame({trackSid: `TR_${index}`}))).toBe(true);
|
||||
}
|
||||
expect(accumulator.trackedTracksCount).toBe(VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP);
|
||||
|
||||
expect(accumulator.record(makeFrame({trackSid: 'TR_overflow'}))).toBe(false);
|
||||
expect(accumulator.droppedUpdatesCount).toBe(1);
|
||||
expect(accumulator.record(makeFrame({trackSid: 'TR_0'}))).toBe(true);
|
||||
expect(accumulator.trackedTracksCount).toBe(VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP);
|
||||
|
||||
accumulator.removeTrack('TR_1');
|
||||
expect(accumulator.record(makeFrame({trackSid: 'TR_overflow'}))).toBe(true);
|
||||
expect(accumulator.trackedTracksCount).toBe(VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP);
|
||||
});
|
||||
|
||||
it('removes dirty bookkeeping when a tracked track is removed before a flush', () => {
|
||||
const accumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
accumulator.record(makeFrame({trackSid: 'TR_a'}));
|
||||
accumulator.record(makeFrame({trackSid: 'TR_b'}));
|
||||
expect(accumulator.dirtyTracksCount).toBe(2);
|
||||
|
||||
accumulator.removeTrack('TR_a');
|
||||
expect(accumulator.dirtyTracksCount).toBe(1);
|
||||
const events = collectFlush(accumulator);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]?.stats.trackSid).toBe('TR_b');
|
||||
});
|
||||
|
||||
it('clear resets records, dirty bookkeeping, and keeps the dropped counter', () => {
|
||||
const accumulator = new VoiceEngineV2FrameStatsAccumulator();
|
||||
for (let index = 0; index <= VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP; index += 1) {
|
||||
accumulator.record(makeFrame({trackSid: `TR_${index}`}));
|
||||
}
|
||||
expect(accumulator.droppedUpdatesCount).toBe(1);
|
||||
|
||||
accumulator.clear();
|
||||
expect(accumulator.trackedTracksCount).toBe(0);
|
||||
expect(accumulator.dirtyTracksCount).toBe(0);
|
||||
expect(accumulator.droppedUpdatesCount).toBe(1);
|
||||
expect(collectFlush(accumulator)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import type {VoiceEngineV2Event} from '../protocol/events';
|
||||
import type {VoiceEngineV2InboundVideoFrame} from '../protocol/types';
|
||||
import {VOICE_ENGINE_V2_COALESCED_TRACKS_CAP} from './frameCoalescing';
|
||||
|
||||
export const VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP = VOICE_ENGINE_V2_COALESCED_TRACKS_CAP;
|
||||
export const VOICE_ENGINE_V2_FRAME_STATS_FLUSH_INTERVAL_MS = 1000;
|
||||
|
||||
export type VoiceEngineV2FrameStatsEvent = Extract<VoiceEngineV2Event, {type: 'inboundVideo.frameStats'}>;
|
||||
|
||||
interface VoiceEngineV2FrameStatsRecord {
|
||||
participantSid: string;
|
||||
participantIdentity: string | null;
|
||||
trackSid: string;
|
||||
width: number;
|
||||
height: number;
|
||||
frameCount: number;
|
||||
lastFrameTimestampUs: number;
|
||||
lastFrameByteLength: number | null;
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
export class VoiceEngineV2FrameStatsAccumulator {
|
||||
private readonly recordsByTrack = new Map<string, VoiceEngineV2FrameStatsRecord>();
|
||||
private droppedUpdatesCountValue = 0;
|
||||
private dirtyTracksCountValue = 0;
|
||||
|
||||
record(frame: VoiceEngineV2InboundVideoFrame): boolean {
|
||||
assert.ok(frame != null, 'record requires a non-null frame');
|
||||
assert.equal(typeof frame.trackSid, 'string', 'record requires a string trackSid');
|
||||
let entry = this.recordsByTrack.get(frame.trackSid);
|
||||
if (entry === undefined) {
|
||||
if (this.recordsByTrack.size >= VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP) {
|
||||
this.droppedUpdatesCountValue += 1;
|
||||
assert.ok(this.droppedUpdatesCountValue >= 1, 'dropped updates count must stay positive after a drop');
|
||||
return false;
|
||||
}
|
||||
entry = {
|
||||
participantSid: frame.participantSid,
|
||||
participantIdentity: frame.participantIdentity ?? null,
|
||||
trackSid: frame.trackSid,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
frameCount: 0,
|
||||
lastFrameTimestampUs: frame.timestampUs,
|
||||
lastFrameByteLength: frame.byteLength ?? null,
|
||||
dirty: false,
|
||||
};
|
||||
this.recordsByTrack.set(frame.trackSid, entry);
|
||||
assert.ok(this.recordsByTrack.size <= VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP, 'frame stats records exceeded cap');
|
||||
}
|
||||
entry.participantSid = frame.participantSid;
|
||||
entry.participantIdentity = frame.participantIdentity ?? entry.participantIdentity;
|
||||
entry.width = frame.width;
|
||||
entry.height = frame.height;
|
||||
entry.frameCount += 1;
|
||||
entry.lastFrameTimestampUs = frame.timestampUs;
|
||||
entry.lastFrameByteLength = frame.byteLength ?? null;
|
||||
if (!entry.dirty) {
|
||||
entry.dirty = true;
|
||||
this.dirtyTracksCountValue += 1;
|
||||
}
|
||||
assert.ok(this.dirtyTracksCountValue <= this.recordsByTrack.size, 'dirty count must not exceed tracked records');
|
||||
return true;
|
||||
}
|
||||
|
||||
flushDirty(emit: (event: VoiceEngineV2FrameStatsEvent) => void): number {
|
||||
assert.equal(typeof emit, 'function', 'flushDirty requires a function emitter');
|
||||
assert.ok(this.dirtyTracksCountValue >= 0, 'dirty count must be non-negative before flush');
|
||||
let flushedCount = 0;
|
||||
for (const entry of this.recordsByTrack.values()) {
|
||||
if (!entry.dirty) continue;
|
||||
entry.dirty = false;
|
||||
flushedCount += 1;
|
||||
emit({
|
||||
type: 'inboundVideo.frameStats',
|
||||
stats: {
|
||||
participantSid: entry.participantSid,
|
||||
...(entry.participantIdentity !== null ? {participantIdentity: entry.participantIdentity} : {}),
|
||||
trackSid: entry.trackSid,
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
frameCount: entry.frameCount,
|
||||
lastFrameTimestampUs: entry.lastFrameTimestampUs,
|
||||
lastFrameByteLength: entry.lastFrameByteLength,
|
||||
},
|
||||
});
|
||||
}
|
||||
assert.equal(flushedCount, this.dirtyTracksCountValue, 'flush must visit every dirty record exactly once');
|
||||
this.dirtyTracksCountValue = 0;
|
||||
return flushedCount;
|
||||
}
|
||||
|
||||
removeTrack(trackSid: string): void {
|
||||
assert.equal(typeof trackSid, 'string', 'removeTrack requires a string trackSid');
|
||||
assert.ok(trackSid.length > 0, 'removeTrack requires a non-empty trackSid');
|
||||
const entry = this.recordsByTrack.get(trackSid);
|
||||
if (entry === undefined) return;
|
||||
if (entry.dirty) {
|
||||
this.dirtyTracksCountValue -= 1;
|
||||
assert.ok(this.dirtyTracksCountValue >= 0, 'dirty count must stay non-negative after removal');
|
||||
}
|
||||
this.recordsByTrack.delete(trackSid);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.recordsByTrack.clear();
|
||||
this.dirtyTracksCountValue = 0;
|
||||
assert.equal(this.recordsByTrack.size, 0, 'clear must remove every tracked record');
|
||||
assert.equal(this.dirtyTracksCountValue, 0, 'clear must reset the dirty count');
|
||||
}
|
||||
|
||||
get trackedTracksCount(): number {
|
||||
assert.ok(this.recordsByTrack.size <= VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP, 'frame stats records exceeded cap');
|
||||
return this.recordsByTrack.size;
|
||||
}
|
||||
|
||||
get dirtyTracksCount(): number {
|
||||
assert.ok(this.dirtyTracksCountValue >= 0, 'dirty count must be non-negative');
|
||||
return this.dirtyTracksCountValue;
|
||||
}
|
||||
|
||||
get droppedUpdatesCount(): number {
|
||||
assert.ok(this.droppedUpdatesCountValue >= 0, 'dropped updates count must be non-negative');
|
||||
return this.droppedUpdatesCountValue;
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,6 @@ export {
|
||||
isVoiceEngineV2FrameReceivedEvent,
|
||||
VOICE_ENGINE_V2_COALESCED_TRACKS_CAP,
|
||||
} from './frameCoalescing';
|
||||
export type {VoiceEngineV2FrameStatsEvent} from './frameStatsBatching';
|
||||
export {
|
||||
VOICE_ENGINE_V2_FRAME_STATS_FLUSH_INTERVAL_MS,
|
||||
VOICE_ENGINE_V2_FRAME_STATS_TRACKS_CAP,
|
||||
VoiceEngineV2FrameStatsAccumulator,
|
||||
} from './frameStatsBatching';
|
||||
export type {
|
||||
VoiceEngineV2ClockPort,
|
||||
VoiceEngineV2EntropySource,
|
||||
|
||||
Reference in new issue
Block a user