fix: Avoid go2rtc renegotiation on camera selection change (#2483)
This commit is contained in:
committed by
dermotduffy
parent
a68b665f03
commit
9a763db1f3
@@ -137,16 +137,11 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
|||||||
this._player.setControls(this.controls);
|
this._player.setControls(this.controls);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (this._player && changedProps.has('microphoneState')) {
|
||||||
this._player &&
|
// VideoRTC owns the transition: it updates microphoneStream, swaps the
|
||||||
changedProps.has('microphoneState') &&
|
// track on the pre-armed transceiver, and validates against stale async
|
||||||
this._player.microphoneStream !== (this.microphoneState?.stream ?? null)
|
// completions before any reconnect fallback. Fire-and-forget is fine.
|
||||||
) {
|
/* async */ this._player.setMicrophoneStream(this.microphoneState?.stream ?? null);
|
||||||
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
|
||||||
|
|
||||||
// Need to force a reconnect if the microphone stream changes since
|
|
||||||
// WebRTC cannot introduce a new stream after the offer is already made.
|
|
||||||
this._player.reconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,4 +37,5 @@ export class VideoRTC extends HTMLElement {
|
|||||||
reconnect();
|
reconnect();
|
||||||
reset(): void;
|
reset(): void;
|
||||||
setControls(controls: boolean): void;
|
setControls(controls: boolean): void;
|
||||||
|
setMicrophoneStream(stream: MediaStream | null): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,15 @@ export class VideoRTC extends HTMLElement {
|
|||||||
*/
|
*/
|
||||||
this.microphoneStream = null;
|
this.microphoneStream = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The outbound audio transceiver pre-armed during createOffer. Holds a
|
||||||
|
* reference so `setMicrophoneStream` can swap the track via
|
||||||
|
* `replaceTrack` without renegotiating the SDP. Cleared on disconnect
|
||||||
|
* because transceivers belong to the closed peer connection.
|
||||||
|
* @type {RTCRtpTransceiver | null}
|
||||||
|
*/
|
||||||
|
this._microphoneTransceiver = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to a MediaPlayerController for this video
|
* A reference to a MediaPlayerController for this video
|
||||||
* @type {MediaPlayerController | null}
|
* @type {MediaPlayerController | null}
|
||||||
@@ -259,6 +268,50 @@ export class VideoRTC extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns the microphone stream transition end-to-end: updates the property,
|
||||||
|
* extracts the outbound audio track, and swaps it onto the pre-armed audio
|
||||||
|
* transceiver via `replaceTrack` — no SDP renegotiation, no visible reload.
|
||||||
|
*
|
||||||
|
* Falls back to a full reconnect if `replaceTrack` rejects, but only when
|
||||||
|
* the rejection still describes the current desired state. The transceiver
|
||||||
|
* and the requested stream are captured before awaiting so a late rejection
|
||||||
|
* from a stale operation (e.g. after `reset()` cleared the connection, or
|
||||||
|
* after a newer `setMicrophoneStream` superseded this one) cannot bring the
|
||||||
|
* player back online or overwrite a fresher request.
|
||||||
|
*
|
||||||
|
* @param {MediaStream | null} stream
|
||||||
|
*/
|
||||||
|
async setMicrophoneStream(stream) {
|
||||||
|
if (this.microphoneStream === stream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.microphoneStream = stream;
|
||||||
|
|
||||||
|
const transceiver = this._microphoneTransceiver;
|
||||||
|
if (!transceiver) {
|
||||||
|
// No live peer connection yet (or createOffer hasn't run). The next
|
||||||
|
// createOffer will read `this.microphoneStream` and pre-arm the
|
||||||
|
// transceiver with the current track, so no separate fix-up is needed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredTrack = stream?.getAudioTracks()[0] ?? null;
|
||||||
|
try {
|
||||||
|
await transceiver.sender.replaceTrack(desiredTrack);
|
||||||
|
} catch (er) {
|
||||||
|
const stillCurrent =
|
||||||
|
transceiver === this._microphoneTransceiver &&
|
||||||
|
this.microphoneStream === stream &&
|
||||||
|
this.pc !== null;
|
||||||
|
if (!stillCurrent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn(er);
|
||||||
|
this.reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconnect the stream.
|
* Reconnect the stream.
|
||||||
*/
|
*/
|
||||||
@@ -495,6 +548,8 @@ export class VideoRTC extends HTMLElement {
|
|||||||
this.pc.close();
|
this.pc.close();
|
||||||
this.pc = null;
|
this.pc = null;
|
||||||
}
|
}
|
||||||
|
// Transceivers belong to the now-closed peer connection.
|
||||||
|
this._microphoneTransceiver = null;
|
||||||
|
|
||||||
this.video.src = '';
|
this.video.src = '';
|
||||||
this.video.srcObject = null;
|
this.video.srcObject = null;
|
||||||
@@ -775,22 +830,26 @@ export class VideoRTC extends HTMLElement {
|
|||||||
* @return {Promise<RTCSessionDescriptionInit>}
|
* @return {Promise<RTCSessionDescriptionInit>}
|
||||||
*/
|
*/
|
||||||
async createOffer(pc) {
|
async createOffer(pc) {
|
||||||
// Must add microphone tracks prior to making the offer.
|
// Always pre-arm a single outbound audio transceiver so the SDP advertises
|
||||||
this.microphoneStream?.getTracks().forEach((track) => {
|
// the slot from the start. With the slot in place, the mic track can be
|
||||||
pc.addTransceiver(track, { direction: 'sendonly' });
|
// attached/detached later via `setMicrophoneStream` (replaceTrack) without
|
||||||
|
// renegotiating — avoiding a visible reload of this cell each time grid
|
||||||
|
// selection moves the mic between cameras.
|
||||||
|
//
|
||||||
|
// Pure SDP allocation: the kind-only `addTransceiver('audio', ...)` form
|
||||||
|
// never calls `getUserMedia`, so users who don't grant mic access see no
|
||||||
|
// browser permission prompt from this path.
|
||||||
|
//
|
||||||
|
// The upstream `media.includes('microphone')` branch (which would have
|
||||||
|
// performed its own `getUserMedia` and added a second outbound audio
|
||||||
|
// transceiver) is intentionally omitted: the card drives mic acquisition
|
||||||
|
// through `MicrophoneManager`, and a second sender would race the one
|
||||||
|
// owned by `setMicrophoneStream`.
|
||||||
|
const micTrack = this.microphoneStream?.getAudioTracks()[0] ?? null;
|
||||||
|
this._microphoneTransceiver = pc.addTransceiver(micTrack ?? 'audio', {
|
||||||
|
direction: 'sendonly',
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.media.indexOf('microphone') >= 0) {
|
|
||||||
const media = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
media.getTracks().forEach((track) => {
|
|
||||||
pc.addTransceiver(track, { direction: 'sendonly' });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const kind of ['video', 'audio']) {
|
for (const kind of ['video', 'audio']) {
|
||||||
if (this.media.indexOf(kind) >= 0) {
|
if (this.media.indexOf(kind) >= 0) {
|
||||||
pc.addTransceiver(kind, { direction: 'recvonly' });
|
pc.addTransceiver(kind, { direction: 'recvonly' });
|
||||||
|
|||||||
Reference in New Issue
Block a user