fix: Add support for synchronously reseting go2rtc sessions (#2478)
This is part 1 of https://docs.google.com/document/d/1zbHYWqulEUaNLeX0fK3rfIiLugMlCQo_SFg3hcgiRRw/edit?tab=t.0 . There is a significant chance this will improve issues people have been experiencing with poorly designed doorbells that can only open a single stream at a time.
This commit is contained in:
committed by
dermotduffy
parent
31d0c9322f
commit
9d088d7bcb
@@ -74,7 +74,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._player = undefined;
|
this._destroyPlayer();
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +86,14 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _destroyPlayer(): void {
|
||||||
|
// Tear down synchronously so backchannel-enabled streams (e.g. doorbell
|
||||||
|
// 2-way audio) release immediately rather than waiting out
|
||||||
|
// DISCONNECT_TIMEOUT.
|
||||||
|
this._player?.reset();
|
||||||
|
this._player = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private _createPlayer(): void {
|
private _createPlayer(): void {
|
||||||
const src = this._signedURLController.getValue();
|
const src = this._signedURLController.getValue();
|
||||||
if (!src) {
|
if (!src) {
|
||||||
@@ -112,7 +120,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
|||||||
if (changedProps.has('camera')) {
|
if (changedProps.has('camera')) {
|
||||||
// Clear old player; the new one is created by the
|
// Clear old player; the new one is created by the
|
||||||
// SignedURLController's valueChangeCallback once the URL resolves.
|
// SignedURLController's valueChangeCallback once the URL resolves.
|
||||||
this._player = undefined;
|
this._destroyPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only treat a missing go2rtc endpoint as an error after the camera's
|
// Only treat a missing go2rtc endpoint as an error after the camera's
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ export class VideoRTC extends HTMLElement {
|
|||||||
microphoneStream: MediaStream | null;
|
microphoneStream: MediaStream | null;
|
||||||
targetID: string | null;
|
targetID: string | null;
|
||||||
reconnect();
|
reconnect();
|
||||||
|
reset(): void;
|
||||||
setControls(controls: boolean): void;
|
setControls(controls: boolean): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,29 @@ export class VideoRTC extends HTMLElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronously tear down the stream: clear pending disconnect/reconnect
|
||||||
|
* timers, abort any in-flight load registration, and close the WebSocket
|
||||||
|
* and peer connection immediately. Used when intentionally destroying the
|
||||||
|
* player (e.g. swapping cameras) to avoid the DISCONNECT_TIMEOUT defer
|
||||||
|
* that would otherwise keep backchannel-enabled streams connected.
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
if (this.disconnectTID) {
|
||||||
|
clearTimeout(this.disconnectTID);
|
||||||
|
this.disconnectTID = 0;
|
||||||
|
}
|
||||||
|
if (this.reconnectTID) {
|
||||||
|
clearTimeout(this.reconnectTID);
|
||||||
|
this.reconnectTID = 0;
|
||||||
|
}
|
||||||
|
this._abortController?.abort();
|
||||||
|
this._abortController = null;
|
||||||
|
if (this.wsState !== WebSocket.CLOSED || this.pcState !== WebSocket.CLOSED) {
|
||||||
|
this.ondisconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconnect the stream.
|
* Reconnect the stream.
|
||||||
*/
|
*/
|
||||||
@@ -714,14 +737,39 @@ export class VideoRTC extends HTMLElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.createOffer(pc).then((offer) => {
|
this.negotiateOffer(pc);
|
||||||
this.send({ type: 'webrtc/offer', value: offer.sdp });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pcState = WebSocket.CONNECTING;
|
this.pcState = WebSocket.CONNECTING;
|
||||||
this.pc = pc;
|
this.pc = pc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and send the WebRTC offer, guarding against the peer connection being
|
||||||
|
* torn down or replaced while `createOffer` is in flight (e.g. by `reset()`
|
||||||
|
* during a stream swap, or by a `webrtc/offer` error message closing `pc`
|
||||||
|
* without resetting `this.pc`/`this.pcState`). Without these guards a stale
|
||||||
|
* offer would be sent and `createOffer` rejections from mid-await
|
||||||
|
* `pc.close()` would surface as unhandled promise rejections.
|
||||||
|
* @param pc {RTCPeerConnection}
|
||||||
|
*/
|
||||||
|
async negotiateOffer(pc) {
|
||||||
|
try {
|
||||||
|
const offer = await this.createOffer(pc);
|
||||||
|
if (
|
||||||
|
this.pc !== pc ||
|
||||||
|
this.pcState === WebSocket.CLOSED ||
|
||||||
|
pc.signalingState === 'closed'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send({ type: 'webrtc/offer', value: offer.sdp });
|
||||||
|
} catch (er) {
|
||||||
|
if (this.pc === pc && this.pcState !== WebSocket.CLOSED) {
|
||||||
|
console.warn(er);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param pc {RTCPeerConnection}
|
* @param pc {RTCPeerConnection}
|
||||||
* @return {Promise<RTCSessionDescriptionInit>}
|
* @return {Promise<RTCSessionDescriptionInit>}
|
||||||
|
|||||||
Reference in New Issue
Block a user