The card claimed a camera's ONVIF audio backchannel in two places that had nothing to do with a call: the capability probe at camera init (`µphone`), and a pre-armed `sendonly` audio transceiver on every live WebRTC offer. Merely looking at a dashboard occupied the camera's speaker line. Outbound audio now travels on its own audio-only WebRTC connection, opened when a call is answered and closed when it ends. - The backchannel is claimed only for the duration of a call. Idle viewing claims nothing. - Two-way audio now works in `mse`, `mp4` and `mjpeg` modes (note: the outbound audio still traverses WebRTC). - No renegotiation and no video blink at call start or end. - A call that cannot carry audio now reports it and ends, instead of showing a live microphone that goes nowhere. - `live.microphone.always_connected` is now purely about the browser microphone permission prompt. - Call setup measured at 66ms (LAN) and ~260ms (cellular) for ICE and DTLS, plus ~300ms for `go2rtc` to open an RTSP backchannel. Verified against a live Frigate + `go2rtc` instance, and by unit tests at 100% coverage. - Closes #2691 - Closes #2039 - Closes #2178 Ref #2299 -- the probe no longer opens a backchannel, but it still runs per camera on every load and reconnect, and still dials the camera on the direct-`go2rtc` path. Caching remains to be done. Ref AlexxIT/go2rtc#1860 -- once a call has opened a backchannel, `go2rtc` keeps that media set up on the camera's RTSP session for the life of the producer. Diagnoses #2678
122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
import { vi } from 'vitest';
|
|
|
|
import type {
|
|
MediaSourceFactory,
|
|
MediaSourceInterface,
|
|
} from '../../../../../src/components-lib/live/providers/go2rtc-experimental/adapters/media-source';
|
|
import type { StreamSourceChannel } from '../../../../../src/components-lib/live/providers/go2rtc-experimental/types';
|
|
import type {
|
|
BinaryCallback,
|
|
Go2RTCMessage,
|
|
MessageCallback,
|
|
} from '../../../../../src/go2rtc/messages';
|
|
import type { UnsubscribeCallback } from '../../../../../src/types';
|
|
|
|
// ===========================================================================
|
|
// User agents.
|
|
// ===========================================================================
|
|
|
|
export const CHROME_USER_AGENT =
|
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
|
|
'(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
|
|
|
|
export const SAFARI_17_USER_AGENT =
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ' +
|
|
'(KHTML, like Gecko) Version/17.4 Safari/605.1.15';
|
|
|
|
// ===========================================================================
|
|
// Fakes for browser APIs jsdom does not provide.
|
|
// ===========================================================================
|
|
|
|
export const createTimeRanges = (ranges: [number, number][]): TimeRanges => ({
|
|
length: ranges.length,
|
|
start: (index: number) => ranges[index][0],
|
|
end: (index: number) => ranges[index][1],
|
|
});
|
|
|
|
class FakeSourceBuffer extends EventTarget {
|
|
public mode = '';
|
|
public updating = false;
|
|
public buffered: TimeRanges = createTimeRanges([]);
|
|
|
|
public appendBuffer = vi.fn();
|
|
public remove = vi.fn();
|
|
|
|
public asSourceBuffer(): SourceBuffer {
|
|
return this as unknown as SourceBuffer;
|
|
}
|
|
|
|
public fireUpdateEnd(): void {
|
|
this.dispatchEvent(new Event('updateend'));
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Fakes for custom interfaces.
|
|
// ===========================================================================
|
|
|
|
export class FakeStreamSourceChannel implements StreamSourceChannel {
|
|
public sent: Go2RTCMessage[] = [];
|
|
public binaryCallback: BinaryCallback | null = null;
|
|
|
|
private _messageCallbacks = new Set<MessageCallback>();
|
|
|
|
public send(message: Go2RTCMessage): void {
|
|
this.sent.push(message);
|
|
}
|
|
|
|
public subscribeToMessages(callback: MessageCallback): UnsubscribeCallback {
|
|
this._messageCallbacks.add(callback);
|
|
|
|
return () => {
|
|
this._messageCallbacks.delete(callback);
|
|
};
|
|
}
|
|
|
|
public setBinaryCallback(callback: BinaryCallback | null): void {
|
|
this.binaryCallback = callback;
|
|
}
|
|
|
|
public receiveMessage(message: Go2RTCMessage): void {
|
|
[...this._messageCallbacks].forEach((callback) => callback(message));
|
|
}
|
|
|
|
public getMessageCallbackCount(): number {
|
|
return this._messageCallbacks.size;
|
|
}
|
|
}
|
|
|
|
export class FakeMediaSourceInstance implements MediaSourceInterface {
|
|
public sourceBuffer = new FakeSourceBuffer();
|
|
|
|
public attach = vi.fn();
|
|
public detach = vi.fn();
|
|
public setLiveSeekableRange = vi.fn();
|
|
public isOpen = vi.fn<() => boolean>(() => true);
|
|
public isTypeSupported = vi.fn<(mimeType: string) => boolean>(() => true);
|
|
public addSourceBuffer = vi.fn<(mimeType: string) => SourceBuffer>(() =>
|
|
this.sourceBuffer.asSourceBuffer(),
|
|
);
|
|
|
|
private _sourceOpenCallbacks = new Set<() => void>();
|
|
|
|
public subscribeToSourceOpen(callback: () => void): UnsubscribeCallback {
|
|
this._sourceOpenCallbacks.add(callback);
|
|
return () => {
|
|
this._sourceOpenCallbacks.delete(callback);
|
|
};
|
|
}
|
|
|
|
public fireSourceOpen(): void {
|
|
[...this._sourceOpenCallbacks].forEach((callback) => callback());
|
|
}
|
|
|
|
public getSourceOpenCallbackCount(): number {
|
|
return this._sourceOpenCallbacks.size;
|
|
}
|
|
}
|
|
|
|
export const createFakeMediaSourceFactory = (
|
|
instance: FakeMediaSourceInstance | null,
|
|
): MediaSourceFactory => vi.fn(() => instance);
|