Improved RTC connection handling and logging

canary
WolverinDEV 2020-11-17 13:10:24 +01:00
parent 9afced5d98
commit ee35e252cf
17 changed files with 214 additions and 46 deletions

View File

@ -194,12 +194,7 @@ export class ConnectionHandler {
this.setInputHardwareState(this.getVoiceRecorder() ? InputHardwareState.VALID : InputHardwareState.MISSING); this.setInputHardwareState(this.getVoiceRecorder() ? InputHardwareState.VALID : InputHardwareState.MISSING);
this.update_voice_status(); this.update_voice_status();
}); });
this.serverConnection.getVoiceConnection().events.on("notify_connection_status_changed", event => { this.serverConnection.getVoiceConnection().events.on("notify_connection_status_changed", () => this.update_voice_status());
this.update_voice_status();
if(event.newStatus === VoiceConnectionStatus.Failed) {
createErrorModal(tr("Voice connection failed"), tra("Failed to establish a voice connection:\n{}", this.serverConnection.getVoiceConnection().getFailedMessage() || tr("Lookup the console for more detail"))).open();
}
});
this.serverConnection.getVoiceConnection().setWhisperSessionInitializer(this.initializeWhisperSession.bind(this)); this.serverConnection.getVoiceConnection().setWhisperSessionInitializer(this.initializeWhisperSession.bind(this));
this.serverFeatures = new ServerFeatures(this); this.serverFeatures = new ServerFeatures(this);

View File

@ -154,4 +154,8 @@ export class DummyVoiceConnection extends AbstractVoiceConnection {
bytesSend: 0 bytesSend: 0
} }
} }
getRetryTimestamp(): number | 0 {
return 0;
}
} }

View File

@ -45,6 +45,9 @@ export interface VideoConnection {
getEvents() : Registry<VideoConnectionEvent>; getEvents() : Registry<VideoConnectionEvent>;
getStatus() : VideoConnectionStatus; getStatus() : VideoConnectionStatus;
getRetryTimestamp() : number | 0;
getFailedMessage() : string;
getConnectionStats() : Promise<ConnectionStatistics>; getConnectionStats() : Promise<ConnectionStatistics>;
isBroadcasting(type: VideoBroadcastType); isBroadcasting(type: VideoBroadcastType);

View File

@ -62,6 +62,8 @@ export abstract class AbstractVoiceConnection {
abstract getConnectionState() : VoiceConnectionStatus; abstract getConnectionState() : VoiceConnectionStatus;
abstract getFailedMessage() : string; abstract getFailedMessage() : string;
abstract getRetryTimestamp() : number | 0;
abstract getConnectionStats() : Promise<ConnectionStatistics>; abstract getConnectionStats() : Promise<ConnectionStatistics>;
abstract encodingSupported(codec: number) : boolean; abstract encodingSupported(codec: number) : boolean;

View File

@ -110,6 +110,10 @@
font-size: .8em; font-size: .8em;
text-align: left; text-align: left;
line-height: 1.2em; line-height: 1.2em;
&.error {
color: #a63030;
}
} }
&.title { &.title {
@ -120,8 +124,20 @@
} }
} }
&.doubleSize { &.error {
height: 2.8em; justify-content: flex-start;
flex-direction: column;
min-height: 2.8em;
height: min-content;
} }
} }
.errorRow {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
} }

View File

@ -97,8 +97,7 @@ export class StatusController {
const videoConnection = this.currentConnectionHandler.getServerConnection().getVideoConnection(); const videoConnection = this.currentConnectionHandler.getServerConnection().getVideoConnection();
switch (videoConnection.getStatus()) { switch (videoConnection.getStatus()) {
case VideoConnectionStatus.Failed: case VideoConnectionStatus.Failed:
/* FIXME: Reason! */ return { type: "unhealthy", reason: videoConnection.getFailedMessage(), retryTimestamp: videoConnection.getRetryTimestamp() };
return { type: "unhealthy", reason: tr("Unknown") };
case VideoConnectionStatus.Connected: case VideoConnectionStatus.Connected:
if(detailed) { if(detailed) {
const statistics = await videoConnection.getConnectionStats(); const statistics = await videoConnection.getConnectionStats();
@ -130,7 +129,7 @@ export class StatusController {
const voiceConnection = this.currentConnectionHandler.getServerConnection().getVoiceConnection(); const voiceConnection = this.currentConnectionHandler.getServerConnection().getVoiceConnection();
switch (voiceConnection.getConnectionState()) { switch (voiceConnection.getConnectionState()) {
case VoiceConnectionStatus.Failed: case VoiceConnectionStatus.Failed:
return { type: "unhealthy", reason: voiceConnection.getFailedMessage() }; return { type: "unhealthy", reason: voiceConnection.getFailedMessage(), retryTimestamp: voiceConnection.getRetryTimestamp() };
case VoiceConnectionStatus.Connected: case VoiceConnectionStatus.Connected:
if(detailed) { if(detailed) {
@ -205,11 +204,11 @@ export class StatusController {
} else if(componentState.type === "disconnected" && component !== "signaling") { } else if(componentState.type === "disconnected" && component !== "signaling") {
switch (component) { switch (component) {
case "voice": case "voice":
componentState = { type: "unhealthy", reason: tr("No voice connection") }; componentState = { type: "unhealthy", reason: tr("No voice connection"), retryTimestamp: 0 };
break; break;
case "video": case "video":
componentState = { type: "unhealthy", reason: tr("No video connection") }; componentState = { type: "unhealthy", reason: tr("No video connection"), retryTimestamp: 0 };
break; break;
} }
} }

View File

@ -6,7 +6,7 @@ export type ConnectionStatus = {
} | { } | {
type: "unhealthy", type: "unhealthy",
reason: string, reason: string,
/* try reconnect attribute */ retryTimestamp: number
} | { } | {
type: "connecting-signalling", type: "connecting-signalling",
state: "initializing" | "connecting" | "authentication" state: "initializing" | "connecting" | "authentication"

View File

@ -6,8 +6,9 @@ import {
} from "tc-shared/ui/frames/footer/StatusDefinitions"; } from "tc-shared/ui/frames/footer/StatusDefinitions";
import * as React from "react"; import * as React from "react";
import {useContext, useEffect, useRef, useState} from "react"; import {useContext, useEffect, useRef, useState} from "react";
import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {network} from "tc-shared/ui/frames/chat"; import {network} from "tc-shared/ui/frames/chat";
import {date_format} from "tc-shared/utils/DateUtils";
const cssStyle = require("./Renderer.scss"); const cssStyle = require("./Renderer.scss");
export const StatusEvents = React.createContext<Registry<ConnectionStatusEvents>>(undefined); export const StatusEvents = React.createContext<Registry<ConnectionStatusEvents>>(undefined);
@ -162,16 +163,26 @@ const ComponentStatusRenderer = React.memo((props: { component: ConnectionCompon
break; break;
} }
body = ( body = (
<div className={cssStyle.row + " " + cssStyle.doubleSize} key={"description"}> <div className={cssStyle.row + " " + cssStyle.error} key={"description"}>
<div className={cssStyle.text}>{text}</div> <div className={cssStyle.text}>{text}</div>
</div> </div>
); );
break; break;
case "unhealthy": case "unhealthy":
let errorText;
if(status.retryTimestamp) {
let time = Math.ceil((status.retryTimestamp - Date.now()) / 1000);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
errorText = <VariadicTranslatable key={"retry"} text={"Error occurred. Retry in {}."}>{(minutes > 0 ? minutes + "m" : "") + seconds + "s"}</VariadicTranslatable>;
} else {
errorText = <Translatable key={"no-retry"}>Error occurred. No retry.</Translatable>;
}
body = ( body = (
<div className={cssStyle.row + " " + cssStyle.doubleSize} key={"error"}> <div className={cssStyle.row + " " + cssStyle.error} key={"error"}>
<div className={cssStyle.text}><Translatable>Some error occured</Translatable></div> <div className={cssStyle.text + " " + cssStyle.errorRow}>{errorText}</div>
<div className={cssStyle.text + " " + cssStyle.error} title={status.reason}>{status.reason}</div>
</div> </div>
); );
break; break;

View File

@ -66,7 +66,9 @@ export enum EventType {
RECONNECT_SCHEDULED = "reconnect.scheduled", RECONNECT_SCHEDULED = "reconnect.scheduled",
RECONNECT_EXECUTE = "reconnect.execute", RECONNECT_EXECUTE = "reconnect.execute",
RECONNECT_CANCELED = "reconnect.canceled" RECONNECT_CANCELED = "reconnect.canceled",
WEBRTC_FATAL_ERROR = "webrtc.fatal.error"
} }
export type EventClient = { export type EventClient = {
@ -249,6 +251,11 @@ export namespace event {
sender: EventClient, sender: EventClient,
message: string message: string
} }
export type EventWebrtcFatalError = {
message: string,
retryTimeout: number | 0
}
} }
export type LogMessage = { export type LogMessage = {
@ -301,7 +308,7 @@ export interface TypeInfo {
"client.nickname.change.failed": event.EventClientNicknameChangeFailed, "client.nickname.change.failed": event.EventClientNicknameChangeFailed,
"client.nickname.changed": event.EventClientNicknameChanged, "client.nickname.changed": event.EventClientNicknameChanged,
"client.nickname.changed.own": event.EventClientNicknameChanged "client.nickname.changed.own": event.EventClientNicknameChanged,
"channel.create": event.EventChannelCreate; "channel.create": event.EventChannelCreate;
"channel.delete": event.EventChannelDelete; "channel.delete": event.EventChannelDelete;
@ -312,10 +319,11 @@ export interface TypeInfo {
"client.poke.received": event.EventClientPokeReceived, "client.poke.received": event.EventClientPokeReceived,
"client.poke.send": event.EventClientPokeSend, "client.poke.send": event.EventClientPokeSend,
"private.message.received": event.EventPrivateMessageReceived, "private.message.received": event.EventPrivateMessageReceived,
"private.message.send": event.EventPrivateMessageSend, "private.message.send": event.EventPrivateMessageSend,
"webrtc.fatal.error": event.EventWebrtcFatalError
"disconnected": any; "disconnected": any;
} }

View File

@ -629,3 +629,28 @@ registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => {
registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined); registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined);
registerDispatcher(EventType.PRIVATE_MESSAGE_SEND, () => undefined); registerDispatcher(EventType.PRIVATE_MESSAGE_SEND, () => undefined);
registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data) => {
if(data.retryTimeout) {
let time = Math.ceil(data.retryTimeout / 1000);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}."}>
<>{data.message}</>
<>{(minutes > 0 ? minutes + "m" : "") + seconds + "s"}</>
</VariadicTranslatable>
</div>
);
} else {
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled."}>
<>{data.message}</>
</VariadicTranslatable>
</div>
);
}
});

View File

@ -480,6 +480,22 @@ registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, (data, handlerId) => {
}); });
}); });
registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data, handlerId) => {
if(data.retryTimeout) {
let time = Math.ceil(data.retryTimeout / 1000);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
spawnServerNotification(handlerId, {
body: tra("WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}.", data.message, (minutes > 0 ? minutes + "m" : "") + seconds + "s")
});
} else {
spawnServerNotification(handlerId, {
body: tra("WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled.", data.message)
});
}
});
/* snipped PRIVATE_MESSAGE_SEND */ /* snipped PRIVATE_MESSAGE_SEND */
loader.register_task(Stage.LOADED, { loader.register_task(Stage.LOADED, {

View File

@ -47,6 +47,7 @@
.timestamp { .timestamp {
padding-right: 5px; padding-right: 5px;
vertical-align: top;
} }
.errorMessage { .errorMessage {

View File

@ -50,6 +50,7 @@ export class Translatable extends React.Component<{
} }
} }
let renderBrElementIndex = 0;
export type VariadicTranslatableChild = React.ReactElement | string; export type VariadicTranslatableChild = React.ReactElement | string;
export const VariadicTranslatable = (props: { text: string, __cacheKey?: string, children?: VariadicTranslatableChild[] | VariadicTranslatableChild }) => { export const VariadicTranslatable = (props: { text: string, __cacheKey?: string, children?: VariadicTranslatableChild[] | VariadicTranslatableChild }) => {
const args = Array.isArray(props.children) ? props.children : [props.children]; const args = Array.isArray(props.children) ? props.children : [props.children];
@ -60,8 +61,15 @@ export const VariadicTranslatable = (props: { text: string, __cacheKey?: string,
return (<> return (<>
{ {
parseMessageWithArguments(translated, args.length).map(e => { parseMessageWithArguments(translated, args.length).map(e => {
if(typeof e === "string") if(typeof e === "string") {
return e; return e.split("\n").reduce((result, element) => {
if(result.length > 0) {
result.push(<br key={++this.renderBrElementIndex}/>);
}
result.push(element);
return result;
}, []);
}
let element = args[e]; let element = args[e];
if(argsUseCount[e]) { if(argsUseCount[e]) {

View File

@ -29,6 +29,40 @@ declare global {
} }
} }
class RetryTimeCalculator {
private readonly minTime: number;
private readonly maxTime: number;
private readonly increment: number;
private retryCount: number;
private currentTime: number;
constructor(minTime: number, maxTime: number, increment: number) {
this.minTime = minTime;
this.maxTime = maxTime;
this.increment = increment;
this.reset();
}
calculateRetryTime() {
if(this.retryCount >= 5) {
/* no more retries */
return 0;
}
this.retryCount++;
const time = this.currentTime;
this.currentTime = Math.min(this.currentTime + this.increment, this.maxTime);
console.error(time + " - " + this.retryCount);
return time;
}
reset() {
this.currentTime = this.minTime;
this.retryCount = 0;
}
}
let dummyVideoTrack: MediaStreamTrack | undefined; let dummyVideoTrack: MediaStreamTrack | undefined;
let dummyAudioTrack: MediaStreamTrack | undefined; let dummyAudioTrack: MediaStreamTrack | undefined;
@ -86,7 +120,7 @@ class CommandHandler extends AbstractCommandHandler {
sdp = SdpCompressor.decompressSdp(sdp, 1); sdp = SdpCompressor.decompressSdp(sdp, 1);
} catch (error) { } catch (error) {
logError(LogCategory.WEBRTC, tr("Failed to decompress remote SDP: %o"), error); logError(LogCategory.WEBRTC, tr("Failed to decompress remote SDP: %o"), error);
this.handle["handleFatalError"](tr("Failed to decompress remote SDP"), 5000); this.handle["handleFatalError"](tr("Failed to decompress remote SDP"), true);
return; return;
} }
if(RTCConnection.kEnableSdpTrace) { if(RTCConnection.kEnableSdpTrace) {
@ -96,7 +130,7 @@ class CommandHandler extends AbstractCommandHandler {
sdp = this.sdpProcessor.processIncomingSdp(sdp, data.mode); sdp = this.sdpProcessor.processIncomingSdp(sdp, data.mode);
} catch (error) { } catch (error) {
logError(LogCategory.WEBRTC, tr("Failed to reprocess SDP %s: %o"), data.mode, error); logError(LogCategory.WEBRTC, tr("Failed to reprocess SDP %s: %o"), data.mode, error);
this.handle["handleFatalError"](tra("Failed to preprocess SDP {}", data.mode as string), 5000); this.handle["handleFatalError"](tra("Failed to preprocess SDP {}", data.mode as string), true);
return; return;
} }
if(RTCConnection.kEnableSdpTrace) { if(RTCConnection.kEnableSdpTrace) {
@ -108,7 +142,7 @@ class CommandHandler extends AbstractCommandHandler {
type: "answer" type: "answer"
}).catch(error => { }).catch(error => {
logError(LogCategory.WEBRTC, tr("Failed to set the remote description: %o"), error); logError(LogCategory.WEBRTC, tr("Failed to set the remote description: %o"), error);
this.handle["handleFatalError"](tr("Failed to set the remote description (answer)"), 5000); this.handle["handleFatalError"](tr("Failed to set the remote description (answer)"), true);
}) })
} else if(data.mode === "offer") { } else if(data.mode === "offer") {
this.handle["peer"].setRemoteDescription({ this.handle["peer"].setRemoteDescription({
@ -137,7 +171,7 @@ class CommandHandler extends AbstractCommandHandler {
}); });
}).catch(error => { }).catch(error => {
logError(LogCategory.WEBRTC, tr("Failed to set the remote description and execute the renegotiation: %o"), error); logError(LogCategory.WEBRTC, tr("Failed to set the remote description and execute the renegotiation: %o"), error);
this.handle["handleFatalError"](tr("Failed to set the remote description (offer/renegotiation)"), 5000); this.handle["handleFatalError"](tr("Failed to set the remote description (offer/renegotiation)"), true);
}); });
} else { } else {
logWarn(LogCategory.NETWORKING, tr("Received invalid mode for rtc session description (%s)."), data.mode); logWarn(LogCategory.NETWORKING, tr("Received invalid mode for rtc session description (%s)."), data.mode);
@ -366,7 +400,8 @@ export class RTCConnection {
private connectionState: RTPConnectionState; private connectionState: RTPConnectionState;
private failedReason: string; private failedReason: string;
private retryCalculator: RetryTimeCalculator;
private retryTimestamp: number;
private retryTimeout: number; private retryTimeout: number;
private peer: RTCPeerConnection; private peer: RTCPeerConnection;
@ -394,6 +429,7 @@ export class RTCConnection {
this.connection = connection; this.connection = connection;
this.sdpProcessor = new SdpProcessor(); this.sdpProcessor = new SdpProcessor();
this.commandHandler = new CommandHandler(connection, this, this.sdpProcessor); this.commandHandler = new CommandHandler(connection, this, this.sdpProcessor);
this.retryCalculator = new RetryTimeCalculator(5000, 30000, 10000);
this.connection.command_handler_boss().register_handler(this.commandHandler); this.connection.command_handler_boss().register_handler(this.commandHandler);
this.reset(true); this.reset(true);
@ -421,6 +457,10 @@ export class RTCConnection {
return this.failedReason; return this.failedReason;
} }
getRetryTimestamp() : number | 0 {
return this.retryTimestamp;
}
reset(updateConnectionState: boolean) { reset(updateConnectionState: boolean) {
if(this.peer) { if(this.peer) {
if(this.getConnection().connected()) { if(this.getConnection().connected()) {
@ -459,6 +499,12 @@ export class RTCConnection {
clearTimeout(this.retryTimeout); clearTimeout(this.retryTimeout);
this.retryTimeout = 0; this.retryTimeout = 0;
this.retryTimestamp = 0;
/*
* We do not reset the retry timer here since we might get called when a fatal error occurs.
* Instead we're resetting it every time we've changed the server connection state.
*/
/* this.retryCalculator.reset(); */
if(updateConnectionState) { if(updateConnectionState) {
this.updateConnectionState(RTPConnectionState.DISCONNECTED); this.updateConnectionState(RTPConnectionState.DISCONNECTED);
@ -522,18 +568,34 @@ export class RTCConnection {
this.events.fire("notify_state_changed", { oldState: oldState, newState: newState }); this.events.fire("notify_state_changed", { oldState: oldState, newState: newState });
} }
private handleFatalError(error: string, retryThreshold: number) { private handleFatalError(error: string, allowRetry: boolean) {
this.reset(false); this.reset(false);
this.failedReason = error; this.failedReason = error;
this.updateConnectionState(RTPConnectionState.FAILED); this.updateConnectionState(RTPConnectionState.FAILED);
/* FIXME: Generate a log message! */ const log = this.connection.client.log;
if(retryThreshold > 0) { if(allowRetry) {
const time = this.retryCalculator.calculateRetryTime();
if(time > 0) {
this.retryTimestamp = Date.now() + time;
this.retryTimeout = setTimeout(() => { this.retryTimeout = setTimeout(() => {
console.error("XXXX Retry");
this.doInitialSetup(); this.doInitialSetup();
}, 5000); }, time);
/* TODO: Schedule a retry? */
log.log("webrtc.fatal.error", {
message: error,
retryTimeout: time
});
} else {
allowRetry = false;
}
}
if(!allowRetry) {
log.log("webrtc.fatal.error", {
message: error,
retryTimeout: 0
});
} }
} }
@ -555,7 +617,7 @@ export class RTCConnection {
private doInitialSetup() { private doInitialSetup() {
if(!('RTCPeerConnection' in window)) { if(!('RTCPeerConnection' in window)) {
this.handleFatalError(tr("WebRTC has been disabled (RTCPeerConnection is not defined)"), 0); this.handleFatalError(tr("WebRTC has been disabled (RTCPeerConnection is not defined)"), false);
return; return;
} }
@ -598,7 +660,7 @@ export class RTCConnection {
this.updateConnectionState(RTPConnectionState.CONNECTING); this.updateConnectionState(RTPConnectionState.CONNECTING);
this.doInitialSetup0().catch(error => { this.doInitialSetup0().catch(error => {
this.handleFatalError(tr("initial setup failed"), 5000); this.handleFatalError(tr("initial setup failed"), true);
logError(LogCategory.WEBRTC, tr("Connection setup failed: %o"), error); logError(LogCategory.WEBRTC, tr("Connection setup failed: %o"), error);
}); });
} }
@ -639,7 +701,7 @@ export class RTCConnection {
logTrace(LogCategory.WEBRTC, tr("Patched initial local offer:\n%s"), offer.sdp); logTrace(LogCategory.WEBRTC, tr("Patched initial local offer:\n%s"), offer.sdp);
} catch (error) { } catch (error) {
logError(LogCategory.WEBRTC, tr("Failed to preprocess outgoing initial offer: %o"), error); logError(LogCategory.WEBRTC, tr("Failed to preprocess outgoing initial offer: %o"), error);
this.handleFatalError(tr("Failed to preprocess outgoing initial offer"), 10000); this.handleFatalError(tr("Failed to preprocess outgoing initial offer"), true);
return; return;
} }
@ -668,6 +730,7 @@ export class RTCConnection {
private handleConnectionStateChanged(event: ServerConnectionEvents["notify_connection_state_changed"]) { private handleConnectionStateChanged(event: ServerConnectionEvents["notify_connection_state_changed"]) {
if(event.newState === ConnectionState.CONNECTED) { if(event.newState === ConnectionState.CONNECTED) {
/* initialize rtc connection */ /* initialize rtc connection */
this.retryCalculator.reset();
this.doInitialSetup(); this.doInitialSetup();
} else { } else {
this.reset(true); this.reset(true);
@ -680,7 +743,7 @@ export class RTCConnection {
logTrace(LogCategory.WEBRTC, tr("Skipping local fqdn ICE candidate %s"), candidate.toJSON().candidate); logTrace(LogCategory.WEBRTC, tr("Skipping local fqdn ICE candidate %s"), candidate.toJSON().candidate);
return; return;
} }
this.localCandidateCount++; //sthis.localCandidateCount++;
const json = candidate.toJSON(); const json = candidate.toJSON();
logTrace(LogCategory.WEBRTC, tr("Received local ICE candidate %s"), json.candidate); logTrace(LogCategory.WEBRTC, tr("Received local ICE candidate %s"), json.candidate);
@ -692,8 +755,8 @@ export class RTCConnection {
}); });
} else { } else {
if(this.localCandidateCount === 0) { if(this.localCandidateCount === 0) {
logError(LogCategory.WEBRTC, tr("Received local ICE candidate finish, without having any candidates.")); logError(LogCategory.WEBRTC, tr("Received local ICE candidate finish, without having any candidates"));
this.handleFatalError(tr("Failed to gather any ICE candidates"), 0); this.handleFatalError(tr("Failed to gather any local ICE candidates."), false);
return; return;
} else { } else {
logTrace(LogCategory.WEBRTC, tr("Received local ICE candidate finish")); logTrace(LogCategory.WEBRTC, tr("Received local ICE candidate finish"));
@ -729,17 +792,17 @@ export class RTCConnection {
logTrace(LogCategory.WEBRTC, tr("Peer connection state changed to %s"), this.peer.connectionState); logTrace(LogCategory.WEBRTC, tr("Peer connection state changed to %s"), this.peer.connectionState);
switch (this.peer.connectionState) { switch (this.peer.connectionState) {
case "connecting": case "connecting":
//this.updateConnectionState(RTPConnectionState.CONNECTING); this.updateConnectionState(RTPConnectionState.CONNECTING);
this.updateConnectionState(RTPConnectionState.CONNECTED);
break; break;
case "connected": case "connected":
this.retryCalculator.reset();
this.updateConnectionState(RTPConnectionState.CONNECTED); this.updateConnectionState(RTPConnectionState.CONNECTED);
break; break;
case "failed": case "failed":
if(this.connectionState !== RTPConnectionState.FAILED) { if(this.connectionState !== RTPConnectionState.FAILED) {
this.handleFatalError(tr("peer connection failed"), 5000); this.handleFatalError(tr("peer connection failed"), true);
} }
break; break;

View File

@ -157,6 +157,11 @@ export class RemoteRTPAudioTrack extends RemoteRTPTrack {
*/ */
aplayer.on_ready(() => { aplayer.on_ready(() => {
if(!this.mediaStream) {
/* we've already been destroyed */
return;
}
const audioContext = aplayer.context(); const audioContext = aplayer.context();
this.audioNode = audioContext.createMediaStreamSource(this.mediaStream); this.audioNode = audioContext.createMediaStreamSource(this.mediaStream);
this.gainNode = audioContext.createGain(); this.gainNode = audioContext.createGain();

View File

@ -115,6 +115,14 @@ export class RtpVideoConnection implements VideoConnection {
return this.connectionState; return this.connectionState;
} }
getRetryTimestamp(): number | 0 {
return this.rtcConnection.getRetryTimestamp();
}
getFailedMessage(): string {
return this.rtcConnection.getFailReason();
}
getBroadcastingState(type: VideoBroadcastType): VideoBroadcastState { getBroadcastingState(type: VideoBroadcastType): VideoBroadcastState {
return this.broadcasts[type] ? this.broadcasts[type].state : VideoBroadcastState.Stopped; return this.broadcasts[type] ? this.broadcasts[type].state : VideoBroadcastState.Stopped;
} }

View File

@ -392,4 +392,8 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
this.speakerMuted = newState; this.speakerMuted = newState;
this.voiceClients.forEach(client => client.setGloballyMuted(this.speakerMuted)); this.voiceClients.forEach(client => client.setGloballyMuted(this.speakerMuted));
} }
getRetryTimestamp(): number | 0 {
return this.rtcConnection.getRetryTimestamp();
}
} }