From 6f56150e0b8a5a6ced5224ca4add9ea828559dd9 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Wed, 16 Sep 2020 19:30:28 +0200 Subject: [PATCH] Changes are listed bellow: - Fixed the control bar microphone and speaker buttons - Improved the default identity generation (no web worker required now) - Improved voice connection error handling (especially for firefox) - Adding a max reconnect limit for voice connection - Don't show the newcomer guide when directly connection to a server - Fixed default profile initialisation --- ChangeLog.md | 8 + package-lock.json | 138 +++++++++++++-- package.json | 1 + shared/js/ConnectionHandler.ts | 10 +- shared/js/bookmarks.ts | 6 +- shared/js/connection/HandshakeHandler.ts | 13 +- shared/js/connection/VoiceConnection.ts | 5 +- shared/js/main.tsx | 109 ++---------- shared/js/profiles/ConnectionProfile.ts | 167 ++++++++++-------- shared/js/profiles/Identity.ts | 4 +- .../profiles/identities/TeamSpeakIdentity.ts | 111 +++++++++--- shared/js/ui/modal/ModalBookmarks.ts | 8 +- shared/js/ui/modal/ModalConnect.ts | 10 +- shared/js/ui/modal/ModalIdentity.ts | 4 +- shared/js/ui/modal/ModalSettings.tsx | 71 ++++---- shared/js/ui/modal/echo-test/Controller.tsx | 3 + shared/js/ui/modal/echo-test/Definitions.ts | 6 +- shared/js/ui/modal/echo-test/Renderer.scss | 9 +- shared/js/ui/modal/echo-test/Renderer.tsx | 18 +- web/app/audio-lib/index.ts | 15 +- web/app/index.ts | 2 + web/app/voice/VoiceHandler.ts | 43 ++++- web/app/voice/bridge/VoiceBridge.ts | 2 +- web/app/voice/bridge/WebRTCVoiceBridge.ts | 55 ++++-- 24 files changed, 514 insertions(+), 304 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 550de0b8..e454a905 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,12 @@ # Changelog: +* **16.09.20** + - Fixed the control bar microphone and speaker buttons + - Improved the default identity generation (no web worker required now) + - Improved voice connection error handling (especially for firefox) + - Adding a max reconnect limit for voice connection + - Don't show the newcomer guide when directly connection to a server + - Fixed default profile initialisation + * **07.09.20** - Fixed the web client for safari diff --git a/package-lock.json b/package-lock.json index 500b9044..104db7fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1167,6 +1167,39 @@ "fastq": "^1.6.0" } }, + "@peculiar/asn1-schema": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.17.tgz", + "integrity": "sha512-7rJD8bR1r6NFE4skDxXsLsFEO3zM2TfjX9wdq5SERoBNEuxGkAJ3uIH84sIMxvDgJtb3cMfLsv8iNpGN0nAWdw==", + "requires": { + "@types/asn1js": "^0.0.1", + "asn1js": "^2.0.26", + "pvtsutils": "^1.0.11", + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, + "@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1258,6 +1291,14 @@ "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", "dev": true }, + "@types/asn1js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/asn1js/-/asn1js-0.0.1.tgz", + "integrity": "sha1-74uflwjLFjKhw6nNJ3F8qr55O8I=", + "requires": { + "@types/pvutils": "*" + } + }, "@types/clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz", @@ -1439,6 +1480,11 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", "dev": true }, + "@types/pvutils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@types/pvutils/-/pvutils-0.0.2.tgz", + "integrity": "sha512-CgQAm7pjyeF3Gnv78ty4RBVIfluB+Td+2DR8iPaU0prF18pkzptHHP+DoKPfpsJYknKsVZyVsJEu5AuGgAqQ5w==" + }, "@types/react": { "version": "16.9.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.26.tgz", @@ -2212,6 +2258,11 @@ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true }, + "asmcrypto.js": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-2.3.2.tgz", + "integrity": "sha512-3FgFARf7RupsZETQ1nHnhLUUvpcttcCq1iZCaVAbJZbCZ5VNRrNyvpDyHTOb0KC3llFcsyOT/a99NZcCbeiEsA==" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -2232,6 +2283,14 @@ "minimalistic-assert": "^1.0.0" } }, + "asn1js": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.26.tgz", + "integrity": "sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==", + "requires": { + "pvutils": "^1.0.17" + } + }, "assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", @@ -2577,8 +2636,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "body-parser": { "version": "1.19.0", @@ -2757,8 +2815,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browserify-aes": { "version": "1.2.0", @@ -4007,7 +4064,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, "requires": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" @@ -4208,7 +4264,6 @@ "version": "6.5.3", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", - "dev": true, "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -7046,7 +7101,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -7067,7 +7121,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -8953,14 +9006,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -10288,6 +10339,26 @@ "escape-goat": "^2.0.0" } }, + "pvtsutils": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.0.12.tgz", + "integrity": "sha512-fudCcWFUE7WPHMRVdlEDdeaeLf+8hvZFvfJJ+p8GZlwrrdoiVfv7WZaPt6k7k/NZjMxR8yUbbH51hpwlSmLHiQ==", + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, + "pvutils": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz", + "integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ==" + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -13937,6 +14008,49 @@ } } }, + "webcrypto-core": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.1.8.tgz", + "integrity": "sha512-hKnFXsqh0VloojNeTfrwFoRM4MnaWzH6vtXcaFcGjPEu+8HmBdQZnps3/2ikOFqS8bJN1RYr6mI2P/FJzyZnXg==", + "requires": { + "@peculiar/asn1-schema": "^2.0.12", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^2.0.26", + "pvtsutils": "^1.0.11", + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, + "webcrypto-liner": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/webcrypto-liner/-/webcrypto-liner-1.2.3.tgz", + "integrity": "sha512-e2P3hMkBVlMo1gMQnkEym0uJh+WVskQbvC3lT278ux3/OnxX7KAVaqROi1nlV9FRQBBvdadjuaWiFSLyESh4mA==", + "requires": { + "@peculiar/asn1-schema": "^2.0.3", + "@peculiar/json-schema": "^1.1.10", + "asmcrypto.js": "^2.3.2", + "asn1js": "^2.0.26", + "core-js": "^3.6.5", + "des.js": "^1.0.1", + "elliptic": "^6.5.2", + "pvtsutils": "^1.0.10", + "tslib": "^1.13.0", + "webcrypto-core": "^1.1.2" + }, + "dependencies": { + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, "webpack": { "version": "4.42.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.1.tgz", diff --git a/package.json b/package.json index adab0d98..9dd51de9 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "resize-observer-polyfill": "^1.5.1", "simplebar-react": "^2.2.0", "twemoji": "^13.0.0", + "webcrypto-liner": "^1.2.3", "webrtc-adapter": "^7.5.1" } } diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index e9fab61f..1655e069 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -781,11 +781,15 @@ export class ConnectionHandler { if(currentInput) { if(shouldRecord) { if(this.getInputHardwareState() !== InputHardwareState.START_FAILED) { - this.startVoiceRecorder(Date.now() - this._last_record_error_popup > 10 * 1000).then(() => {}); + this.startVoiceRecorder(Date.now() - this._last_record_error_popup > 10 * 1000).then(() => { + this.event_registry.fire("notify_state_updated", { state: "microphone" }); + }); } } else { currentInput.stop().catch(error => { logWarn(LogCategory.AUDIO, tr("Failed to stop the microphone input recorder: %o"), error); + }).then(() => { + this.event_registry.fire("notify_state_updated", { state: "microphone" }); }); } } @@ -888,7 +892,7 @@ export class ConnectionHandler { reconnect_properties(profile?: ConnectionProfile) : ConnectParameters { const name = (this.getClient() ? this.getClient().clientNickName() : "") || (this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") || - StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) || + StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.defaultUsername : undefined) || "Another TeaSpeak user"; const channel = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().channelId : 0) || (this.serverConnection && this.serverConnection.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).target : ""); @@ -1048,6 +1052,7 @@ export class ConnectionHandler { this.client_status.input_muted = muted; this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED); this.update_voice_status(); + this.event_registry.fire("notify_state_updated", { state: "microphone" }); } toggleMicrophone() { this.setMicrophoneMuted(!this.isMicrophoneMuted()); } @@ -1058,6 +1063,7 @@ export class ConnectionHandler { if(this.client_status.output_muted === muted) return; if(muted) this.sound.play(Sound.SOUND_MUTED); /* play the sound *before* we're setting the muted state */ this.client_status.output_muted = muted; + this.event_registry.fire("notify_state_updated", { state: "speaker" }); if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */ this.update_voice_status(); } diff --git a/shared/js/bookmarks.ts b/shared/js/bookmarks.ts index 72d0dd0f..cb5f2b58 100644 --- a/shared/js/bookmarks.ts +++ b/shared/js/bookmarks.ts @@ -2,7 +2,7 @@ import * as log from "./log"; import {LogCategory} from "./log"; import {guid} from "./crypto/uid"; import {createErrorModal, createInfoModal, createInputModal} from "./ui/elements/Modal"; -import {default_profile, find_profile} from "./profiles/ConnectionProfile"; +import {defaultConnectProfile, findConnectProfile} from "./profiles/ConnectionProfile"; import {server_connections} from "./ui/frames/connection_handlers"; import {spawnConnectModal} from "./ui/modal/ModalConnect"; import * as top_menu from "./ui/frames/MenuBar"; @@ -10,7 +10,7 @@ import {control_bar_instance} from "./ui/frames/control-bar"; import {ConnectionHandler} from "./ConnectionHandler"; export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { - const profile = find_profile(mark.connect_profile) || default_profile(); + const profile = findConnectProfile(mark.connect_profile) || defaultConnectProfile(); if(profile.valid()) { const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection() : server_connections.spawn_server_connection(); server_connections.set_active_connection(connection); @@ -19,7 +19,7 @@ export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { profile, true, { - nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname, + nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connectUsername() : mark.nickname, password: mark.server_properties.server_password_hash ? { password: mark.server_properties.server_password_hash, hashed: true diff --git a/shared/js/connection/HandshakeHandler.ts b/shared/js/connection/HandshakeHandler.ts index 58e07920..9190168c 100644 --- a/shared/js/connection/HandshakeHandler.ts +++ b/shared/js/connection/HandshakeHandler.ts @@ -32,17 +32,18 @@ export class HandshakeHandler { } initialize() { - this.handshake_handler = this.profile.spawn_identity_handshake_handler(this.connection); + this.handshake_handler = this.profile.spawnIdentityHandshakeHandler(this.connection); if(!this.handshake_handler) { this.handshake_failed("failed to create identity handler"); return; } this.handshake_handler.register_callback((flag, message) => { - if(flag) + if(flag) { this.handshake_finished(); - else + } else { this.handshake_failed(message); + } }); } @@ -55,7 +56,7 @@ export class HandshakeHandler { } on_teamspeak() { - const type = this.profile.selected_type(); + const type = this.profile.selectedType(); if(type == IdentitifyType.TEAMSPEAK) this.handshake_finished(); else { @@ -124,8 +125,8 @@ export class HandshakeHandler { data.client_platform = (os_mapping[os.platform()] || os.platform()); } - if(this.profile.selected_type() === IdentitifyType.TEAMSPEAK) - data["client_key_offset"] = (this.profile.selected_identity() as TeaSpeakIdentity).hash_number; + if(this.profile.selectedType() === IdentitifyType.TEAMSPEAK) + data["client_key_offset"] = (this.profile.selectedIdentity() as TeaSpeakIdentity).hash_number; this.connection.send_command("clientinit", data).catch(error => { if(error instanceof CommandResult) { diff --git a/shared/js/connection/VoiceConnection.ts b/shared/js/connection/VoiceConnection.ts index 14b8315c..03127d2b 100644 --- a/shared/js/connection/VoiceConnection.ts +++ b/shared/js/connection/VoiceConnection.ts @@ -11,7 +11,9 @@ export enum VoiceConnectionStatus { Connecting, Connected, Disconnecting, - Disconnected + Disconnected, + + Failed } export interface VoiceConnectionEvents { @@ -55,6 +57,7 @@ export abstract class AbstractVoiceConnection { } abstract getConnectionState() : VoiceConnectionStatus; + abstract getFailedMessage() : string; abstract encodingSupported(codec: number) : boolean; abstract decodingSupported(codec: number) : boolean; diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 9b9cbddd..e40ba731 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -1,13 +1,12 @@ import * as loader from "tc-loader"; import {settings, Settings} from "tc-shared/settings"; -import * as profiles from "tc-shared/profiles/ConnectionProfile"; import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import * as bipc from "./ipc/BrowserIPC"; import * as sound from "./sound/Sounds"; import * as i18n from "./i18n/localize"; import {tra} from "./i18n/localize"; -import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; import {createInfoModal} from "tc-shared/ui/elements/Modal"; import * as stats from "./stats"; import * as fidentity from "./profiles/identities/TeaForumIdentity"; @@ -32,7 +31,6 @@ import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMe import {copy_to_clipboard} from "tc-shared/utils/helpers"; import {checkForUpdatedApp} from "tc-shared/update"; import {setupJSRender} from "tc-shared/ui/jsrender"; -import ContextMenuEvent = JQuery.ContextMenuEvent; import "svg-sprites/client-icons"; /* required import for init */ @@ -44,8 +42,10 @@ import "./connection/CommandHandler"; import "./connection/ConnectionBase"; import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler"; import "./video-viewer/Controller"; - +import "./profiles/ConnectionProfile"; import "./update/UpdaterWeb"; +import ContextMenuEvent = JQuery.ContextMenuEvent; +import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile"; async function initialize() { try { @@ -109,8 +109,6 @@ async function initialize_app() { }); sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100); - await profiles.load(); - try { await ppt.initialize(); } catch(error) { @@ -120,57 +118,15 @@ async function initialize_app() { } } -/* -class TestProxy extends bipc.MethodProxy { - constructor(params: bipc.MethodProxyConnectParameters) { - super(bipc.get_handler(), params.channel_id && params.client_id ? params : undefined); - - if(!this.is_slave()) { - this.register_method(this.add_slave); - } - if(!this.is_master()) { - this.register_method(this.say_hello); - this.register_method(this.add_master); - } - } - - setup() { - super.setup(); - } - - protected on_connected() { - log.info(LogCategory.IPC, "Test proxy connected"); - } - - protected on_disconnected() { - log.info(LogCategory.IPC, "Test proxy disconnected"); - } - - private async say_hello() : Promise { - log.info(LogCategory.IPC, "Hello World"); - } - - private async add_slave(a: number, b: number) : Promise { - return a + b; - } - - private async add_master(a: number, b: number) : Promise { - return a * b; - } -} -interface Window { - proxy_instance: TestProxy & {url: () => string}; -} -*/ - export function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) { - const profile_uuid = properties.profile || (profiles.default_profile() || {id: 'default'}).id; - const profile = profiles.find_profile(profile_uuid) || profiles.default_profile(); - const username = properties.username || profile.connect_username(); + const profile_uuid = properties.profile || (defaultConnectProfile() || { id: 'default' }).id; + const profile = findConnectProfile(profile_uuid) || defaultConnectProfile(); + const username = properties.username || profile.connectUsername(); const password = properties.password ? properties.password.value : ""; const password_hashed = properties.password ? properties.password.hashed : false; + debugger; if(profile && profile.valid()) { connection.startConnection(properties.address, profile, true, { nickname: username, @@ -192,21 +148,6 @@ export function handle_connect_request(properties: ConnectRequestData, connectio } function main() { - /* - window.proxy_instance = new TestProxy({ - client_id: settings.static_global("proxy_client_id", undefined), - channel_id: settings.static_global("proxy_channel_id", undefined) - }) as any; - if(window.proxy_instance.is_master()) { - window.proxy_instance.setup(); - window.proxy_instance.url = () => { - const data = window.proxy_instance.generate_connect_parameters(); - return "proxy_channel_id=" + data.channel_id + "&proxy_client_id=" + data.client_id; - }; - } - */ - //http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1 - /* initialize font */ { const font = settings.static_global(Settings.KEY_FONT_SIZE, 14); //parseInt(getComputedStyle(document.body).fontSize) @@ -268,32 +209,6 @@ function main() { server_connections.set_active_connection(server_connections.all_connections()[0]); checkForUpdatedApp(); - /* - (window as any).test_upload = (message?: string) => { - message = message || "Hello World"; - - const connection = server_connections.active_connection(); - connection.fileManager.upload_file({ - size: message.length, - overwrite: true, - channel: connection.getClient().currentChannel(), - name: '/HelloWorld.txt', - path: '' - }).then(key => { - const upload = new RequestFileUpload(key); - - const buffer = new Uint8Array(message.length); - { - for(let index = 0; index < message.length; index++) - buffer[index] = message.charCodeAt(index); - } - - upload.put_data(buffer).catch(error => { - console.error(error); - }); - }) - }; - */ (window as any).test_download = async () => { const connection = server_connections.active_connection(); const download = connection.fileManager.initializeFileDownload({ @@ -387,9 +302,11 @@ function main() { //setTimeout(() => spawnPermissionEditorModal(server_connections.active_connection()), 3000); //setTimeout(() => spawnGroupCreate(server_connections.active_connection(), "server"), 3000); - if(settings.static_global(Settings.KEY_USER_IS_NEW)) { - const modal = openModalNewcomer(); - modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false)); + if(server_connections.active_connection().getServerConnection().getConnectionState() === ConnectionState.UNCONNECTED) { + if(settings.static_global(Settings.KEY_USER_IS_NEW)) { + const modal = openModalNewcomer(); + modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false)); + } } //spawnVideoPopout(server_connections.active_connection(), "https://www.youtube.com/watch?v=9683D18fyvs"); diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index 203b452a..8d332079 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -6,33 +6,36 @@ import {AbstractServerConnection} from "../connection/ConnectionBase"; import {HandshakeIdentityHandler} from "../connection/HandshakeHandler"; import {createErrorModal} from "../ui/elements/Modal"; import {formatMessage} from "../ui/frames/chat"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {LogCategory, logDebug, logError} from "tc-shared/log"; export class ConnectionProfile { id: string; - profile_name: string; - default_username: string; - default_password: string; + profileName: string; + defaultUsername: string; + defaultPassword: string; - selected_identity_type: string = "unset"; + selectedIdentityType: string = "unset"; identities: { [key: string]: Identity } = {}; constructor(id: string) { this.id = id; } - connect_username(): string { - if (this.default_username && this.default_username !== "Another TeaSpeak user") - return this.default_username; + connectUsername(): string { + if (this.defaultUsername && this.defaultUsername !== "Another TeaSpeak user") + return this.defaultUsername; - let selected = this.selected_identity(); + let selected = this.selectedIdentity(); let name = selected ? selected.fallback_name() : undefined; return name || "Another TeaSpeak user"; } - selected_identity(current_type?: IdentitifyType): Identity { + selectedIdentity(current_type?: IdentitifyType): Identity { if (!current_type) - current_type = this.selected_type(); + current_type = this.selectedType(); if (current_type === undefined) return undefined; @@ -46,55 +49,58 @@ export class ConnectionProfile { return undefined; } - selected_type?(): IdentitifyType { - return this.selected_identity_type ? IdentitifyType[this.selected_identity_type.toUpperCase()] : undefined; + selectedType(): IdentitifyType | undefined { + return this.selectedIdentityType ? IdentitifyType[this.selectedIdentityType.toUpperCase()] : undefined; } - set_identity(type: IdentitifyType, identity: Identity) { + setIdentity(type: IdentitifyType, identity: Identity) { this.identities[IdentitifyType[type].toLowerCase()] = identity; } - spawn_identity_handshake_handler?(connection: AbstractServerConnection): HandshakeIdentityHandler { - const identity = this.selected_identity(); + spawnIdentityHandshakeHandler(connection: AbstractServerConnection): HandshakeIdentityHandler | undefined { + const identity = this.selectedIdentity(); if (!identity) return undefined; return identity.spawn_identity_handshake_handler(connection); } - encode?(): string { + encode(): string { const identity_data = {}; - for (const key in this.identities) - if (this.identities[key]) + for (const key in this.identities) { + if (this.identities[key]) { identity_data[key] = this.identities[key].encode(); + } + } return JSON.stringify({ version: 1, - username: this.default_username, - password: this.default_password, - profile_name: this.profile_name, - identity_type: this.selected_identity_type, + username: this.defaultUsername, + password: this.defaultPassword, + profile_name: this.profileName, + identity_type: this.selectedIdentityType, identity_data: identity_data, id: this.id }); } valid(): boolean { - const identity = this.selected_identity(); + const identity = this.selectedIdentity(); return !!identity && identity.valid(); } } -async function decode_profile(data): Promise { - data = JSON.parse(data); - if (data.version !== 1) +async function decodeProfile(payload: string): Promise { + const data = JSON.parse(payload); + if (data.version !== 1) { return "invalid version"; + } const result: ConnectionProfile = new ConnectionProfile(data.id); - result.default_username = data.username; - result.default_password = data.password; - result.profile_name = data.profile_name; - result.selected_identity_type = (data.identity_type || "").toLowerCase(); + result.defaultUsername = data.username; + result.defaultPassword = data.password; + result.profileName = data.profile_name; + result.selectedIdentityType = (data.identity_type || "").toLowerCase(); if (data.identity_data) { for (const key of Object.keys(data.identity_data)) { @@ -117,20 +123,19 @@ interface ProfilesData { profiles: string[]; } -let available_profiles: ConnectionProfile[] = []; +let availableProfiles_: ConnectionProfile[] = []; -export async function load() { - available_profiles = []; +async function loadConnectProfiles() { + availableProfiles_ = []; const profiles_json = localStorage.getItem("profiles"); let profiles_data: ProfilesData = (() => { try { return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any; } catch (error) { - debugger; console.error(tr("Invalid profile json! Resetting profiles :( (%o)"), profiles_json); createErrorModal(tr("Profile data invalid"), formatMessage(tr("The profile data is invalid.{:br:}This might cause data loss."))).open(); - return {version: 0}; + return { version: 0 }; } })(); @@ -142,56 +147,58 @@ export async function load() { } if (profiles_data.version == 1) { for (const profile_data of profiles_data.profiles) { - const profile = await decode_profile(profile_data); + const profile = await decodeProfile(profile_data); if (typeof profile === "string") { console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data); } else { - available_profiles.push(profile as ConnectionProfile); + availableProfiles_.push(profile as ConnectionProfile); } } } - if (!find_profile("default")) { //Create a default profile and teaforo profile + const defaultProfile = findConnectProfile("default"); + if (!defaultProfile) { //Create a default profile and teaforo profile { - const profile = create_new_profile("default", "default"); - profile.default_password = ""; - profile.default_username = ""; - profile.profile_name = "Default Profile"; + const profile = createConnectProfile(tr("Default Profile"), "default"); + profile.defaultPassword = ""; + profile.defaultUsername = ""; + profile.profileName = "Default Profile"; /* generate default identity */ try { - const identity = await TeaSpeakIdentity.generate_new(); - let active = true; - setTimeout(() => { - active = false; - }, 1000); - await identity.improve_level(8, 1, () => active); - profile.set_identity(IdentitifyType.TEAMSPEAK, identity); - profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAMSPEAK]; + const identity = await TeaSpeakIdentity.generateNew(); + const begin = Date.now(); + + const newLevel = await identity.improveLevelJavascript(8, () => Date.now() - begin < 1000); + /* await identity.improveLevelNative(8, 1, () => doImprove); */ + logDebug(LogCategory.IDENTITIES, tr("Improved the identity level to %d within %s milliseconds"), newLevel, Date.now() - begin); + + profile.setIdentity(IdentitifyType.TEAMSPEAK, identity); + profile.selectedIdentityType = IdentitifyType[IdentitifyType.TEAMSPEAK]; } catch (error) { + logError(LogCategory.GENERAL, tr("Failed to generate the default identity: %o"), error); createErrorModal(tr("Failed to generate default identity"), tr("Failed to generate default identity!
Please manually generate the identity within your settings => profiles")).open(); } } { /* forum identity (works only when connected to the forum) */ - const profile = create_new_profile("TeaSpeak Forum", "teaforo"); - profile.default_password = ""; - profile.default_username = ""; - profile.profile_name = "TeaSpeak Forum profile"; + const profile = createConnectProfile(tr("TeaSpeak Forum Profile"), "teaforo"); + profile.defaultPassword = ""; + profile.defaultUsername = ""; - profile.set_identity(IdentitifyType.TEAFORO, TeaForumIdentity.identity()); - profile.selected_identity_type = IdentitifyType[IdentitifyType.TEAFORO]; + profile.setIdentity(IdentitifyType.TEAFORO, TeaForumIdentity.identity()); + profile.selectedIdentityType = IdentitifyType[IdentitifyType.TEAFORO]; } save(); } } -export function create_new_profile(name: string, id?: string): ConnectionProfile { +export function createConnectProfile(name: string, id?: string): ConnectionProfile { const profile = new ConnectionProfile(id || guid()); - profile.profile_name = name; - profile.default_username = ""; - available_profiles.push(profile); + profile.profileName = name; + profile.defaultUsername = ""; + availableProfiles_.push(profile); return profile; } @@ -199,8 +206,9 @@ let _requires_save = false; export function save() { const profiles: string[] = []; - for (const profile of available_profiles) + for (const profile of availableProfiles_) { profiles.push(profile.encode()); + } const data = JSON.stringify({ version: 1, @@ -217,34 +225,36 @@ export function requires_save(): boolean { return _requires_save; } -export function profiles(): ConnectionProfile[] { - return available_profiles; +export function availableConnectProfiles(): ConnectionProfile[] { + return availableProfiles_; } -export function find_profile(id: string): ConnectionProfile | undefined { - for (const profile of profiles()) - if (profile.id == id) +export function findConnectProfile(id: string): ConnectionProfile | undefined { + for (const profile of availableConnectProfiles()) { + if (profile.id == id) { return profile; + } + } return undefined; } export function find_profile_by_name(name: string): ConnectionProfile | undefined { name = name.toLowerCase(); - for (const profile of profiles()) - if ((profile.profile_name || "").toLowerCase() == name) + for (const profile of availableConnectProfiles()) + if ((profile.profileName || "").toLowerCase() == name) return profile; return undefined; } -export function default_profile(): ConnectionProfile { - return find_profile("default"); +export function defaultConnectProfile(): ConnectionProfile { + return findConnectProfile("default"); } export function set_default_profile(profile: ConnectionProfile) { - const old_default = default_profile(); + const old_default = defaultConnectProfile(); if (old_default && old_default != profile) { old_default.id = guid(); } @@ -253,10 +263,19 @@ export function set_default_profile(profile: ConnectionProfile) { } export function delete_profile(profile: ConnectionProfile) { - available_profiles.remove(profile); + availableProfiles_.remove(profile); } window.addEventListener("beforeunload", event => { - if(requires_save()) + if(requires_save()) { save(); -}); \ No newline at end of file + } +}); + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "Identity setup", + function: async () => { + await loadConnectProfiles(); + }, + priority: 30 +}) \ No newline at end of file diff --git a/shared/js/profiles/Identity.ts b/shared/js/profiles/Identity.ts index f0b6da9b..e4d39134 100644 --- a/shared/js/profiles/Identity.ts +++ b/shared/js/profiles/Identity.ts @@ -81,9 +81,9 @@ export class HandshakeCommandHandler handle_command(command: ServerCommand): boolean { - if($.isFunction(this[command.command])) + if(typeof this[command.command] === "function") { this[command.command](command.arguments); - else if(command.command == "error") { + } else if(command.command == "error") { return false; } else { console.warn(tr("Received unknown command while handshaking (%o)"), command); diff --git a/shared/js/profiles/identities/TeamSpeakIdentity.ts b/shared/js/profiles/identities/TeamSpeakIdentity.ts index b09da25a..ebbdee75 100644 --- a/shared/js/profiles/identities/TeamSpeakIdentity.ts +++ b/shared/js/profiles/identities/TeamSpeakIdentity.ts @@ -1,5 +1,5 @@ import * as log from "../../log"; -import {LogCategory} from "../../log"; +import {LogCategory, logDebug, logTrace} from "../../log"; import * as asn1 from "../../crypto/asn1"; import * as sha from "../../crypto/sha"; @@ -217,6 +217,7 @@ export namespace CryptoHelper { }; } } +import arraybuffer_to_string = CryptoHelper.arraybuffer_to_string; export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { identity: TeaSpeakIdentity; @@ -234,7 +235,7 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { this.connection.send_command("handshakebegin", { intention: 0, authentication_method: this.identity.type(), - publicKey: this.identity.public_key + publicKey: this.identity.publicKey }).catch(error => { log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error); @@ -433,7 +434,7 @@ class IdentityPOWWorker { } export class TeaSpeakIdentity implements Identity { - static async generate_new() : Promise { + static async generateNew() : Promise { let key: CryptoKeyPair; try { key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]); @@ -501,7 +502,7 @@ export class TeaSpeakIdentity implements Identity { private_key: string; /* base64 representation of the private key */ _name: string; - public_key: string; /* only set when initialized */ + publicKey: string; /* only set when initialized */ private _initialized: boolean; private _crypto_key: CryptoKey; @@ -569,21 +570,25 @@ export class TeaSpeakIdentity implements Identity { } async level() : Promise { - if(!this._initialized || !this.public_key) + if(!this._initialized || !this.publicKey) throw "not initialized"; - const hash = new Uint8Array(await sha.sha1(this.public_key + this.hash_number)); + const hash = new Uint8Array(await sha.sha1(this.publicKey + this.hash_number)); + return TeaSpeakIdentity.calculateLevel(hash); + } + + private static calculateLevel(buffer: Uint8Array) : number { let level = 0; - while(level < hash.byteLength && hash[level] == 0) + while(level < buffer.byteLength && buffer[level] === 0) level++; - if(level >= hash.byteLength) { + if(level >= buffer.byteLength) { level = 256; } else { - let byte = hash[level]; + let byte = buffer[level]; level <<= 3; - while((byte & 0x1) == 0) { + while((byte & 0x1) === 0) { level++; byte >>= 1; } @@ -597,7 +602,7 @@ export class TeaSpeakIdentity implements Identity { * @param {string} b * @description b must be smaller (in bytes) then a */ - private string_add(a: string, b: string) { + private static string_add(a: string, b: string) { const char_result: number[] = []; const char_a = [...a].reverse().map(e => e.charCodeAt(0)); const char_b = [...b].reverse().map(e => e.charCodeAt(0)); @@ -628,29 +633,34 @@ export class TeaSpeakIdentity implements Identity { let active = true; setTimeout(() => active = false, time); - return await this.improve_level(-1, threads, () => active); + return await this.improveLevelNative(-1, threads, () => active); } - async improve_level(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise { - if(!this._initialized || !this.public_key) + async improveLevelNative(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise { + if(!this._initialized || !this.publicKey) { throw "not initialized"; - if(target == -1) /* get the highest level possible */ + } + + /* get the highest level possible */ + if(target == -1) { target = 0; - else if(target <= await this.level()) + } else if(target <= await this.level()) { return true; + } const workers: IdentityPOWWorker[] = []; const iterations = 100000; let current_hash; const next_hash = () => { - if(!current_hash) + if(!current_hash) { return (current_hash = this.hash_number); + } if(current_hash.length < iterations.toString().length) { - current_hash = this.string_add(iterations.toString(), current_hash); + current_hash = TeaSpeakIdentity.string_add(iterations.toString(), current_hash); } else { - current_hash = this.string_add(current_hash, iterations.toString()); + current_hash = TeaSpeakIdentity.string_add(current_hash, iterations.toString()); } return current_hash; }; @@ -661,7 +671,7 @@ export class TeaSpeakIdentity implements Identity { for (let index = 0; index < threads; index++) { const worker = new IdentityPOWWorker(); workers.push(worker); - initialize_promise.push(worker.initialize(this.public_key)); + initialize_promise.push(worker.initialize(this.publicKey)); } try { @@ -778,6 +788,63 @@ export class TeaSpeakIdentity implements Identity { throw "this should never be reached"; } + /* Improve the identity within the current thread */ + async improveLevelJavascript(target: number, activeCallback: () => boolean) : Promise { + const publicKey = str2ab8(this.publicKey); + const buffer = new Uint8Array(publicKey.byteLength + 20); /* Max 20 append digest (Appendix is a number in range of [0;2^64]) -> log10(2^64) */ + + buffer.set(new Uint8Array(publicKey)); + buffer.set(new Uint8Array(str2ab8(this.hash_number)), publicKey.byteLength); + + const kChar9 = '9'.charCodeAt(0); + const kChar0 = '0'.charCodeAt(0); + + let numberIndex = publicKey.byteLength + this.hash_number.length; + let bufferView = buffer.subarray(0, numberIndex); + + const incrementCounter = () => { + let currentIndex = numberIndex - 1; + while(currentIndex > publicKey.byteLength && buffer[currentIndex] == kChar9) { + buffer[currentIndex--] = kChar0; + } + + if(currentIndex > publicKey.byteLength) { + buffer[currentIndex]++; + } else { + /* yeah a new diget */ + if(numberIndex >= buffer.byteLength) { + throw "hash number got too big. use another identity"; + } + + buffer[numberIndex] = kChar0; + numberIndex++; + buffer[currentIndex] = '1'.charCodeAt(0); + bufferView = buffer.subarray(0, numberIndex); + } + }; + + let currentLevel = await this.level(); + let iteration = 0; + const timeBegin = Date.now(); + + while(currentLevel < target) { + if((iteration++ % 1000) === 0 && !activeCallback()) { + break; + } + + incrementCounter(); + const newLevel = await TeaSpeakIdentity.calculateLevel(new Uint8Array(await crypto.subtle.digest("SHA-1", bufferView))); + if(newLevel > currentLevel) { + this.hash_number = arraybuffer_to_string(buffer.subarray(publicKey.byteLength, numberIndex)); + logTrace(LogCategory.IDENTITIES, tr("Found a new identity level at %s. Previous level %d now %d (%d hashes/second)"), + this.hash_number, currentLevel, newLevel, iteration * 1000 / (Date.now() - timeBegin)); + currentLevel = newLevel; + } + } + + return currentLevel; + } + private async initialize() { if(!this.private_key) throw "Invalid private key"; @@ -806,8 +873,8 @@ export class TeaSpeakIdentity implements Identity { } try { - this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); - this._unique_id = base64_encode_ab(await sha.sha1(this.public_key)); + this.publicKey = await CryptoHelper.export_ecc_key(this._crypto_key, true); + this._unique_id = base64_encode_ab(await sha.sha1(this.publicKey)); } catch(error) { log.error(LogCategory.IDENTITIES, error); throw "failed to calculate unique id"; diff --git a/shared/js/ui/modal/ModalBookmarks.ts b/shared/js/ui/modal/ModalBookmarks.ts index af54456b..24ddcda1 100644 --- a/shared/js/ui/modal/ModalBookmarks.ts +++ b/shared/js/ui/modal/ModalBookmarks.ts @@ -11,7 +11,7 @@ import { save_bookmark } from "../../bookmarks"; import {connection_log, Regex} from "../../ui/modal/ModalConnect"; -import {profiles} from "../../profiles/ConnectionProfile"; +import {availableConnectProfiles} from "../../profiles/ConnectionProfile"; import {spawnYesNo} from "../../ui/modal/ModalYesNo"; import {Settings, settings} from "../../settings"; import * as log from "../../log"; @@ -203,11 +203,11 @@ export function spawnBookmarkModal() { .text("") .css("display", "none") ); - for (const profile of profiles()) { + for (const profile of availableConnectProfiles()) { input_connect_profile.append( $.spawn("option") .attr("value", profile.id) - .text(profile.profile_name) + .text(profile.profileName) ); } } @@ -345,7 +345,7 @@ export function spawnBookmarkModal() { input_connect_profile.on('change', event => { const id = input_connect_profile.val() as string; - const profile = profiles().find(e => e.id === id); + const profile = availableConnectProfiles().find(e => e.id === id); if (profile) { (selected_bookmark as Bookmark).connect_profile = id; save_bookmark(selected_bookmark); diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts index d1b2a460..75f885cc 100644 --- a/shared/js/ui/modal/ModalConnect.ts +++ b/shared/js/ui/modal/ModalConnect.ts @@ -3,7 +3,7 @@ import * as log from "../../log"; import {LogCategory} from "../../log"; import * as loader from "tc-loader"; import {createModal} from "../../ui/elements/Modal"; -import {ConnectionProfile, default_profile, find_profile, profiles} from "../../profiles/ConnectionProfile"; +import {ConnectionProfile, defaultConnectProfile, findConnectProfile, availableConnectProfiles} from "../../profiles/ConnectionProfile"; import {KeyCode} from "../../PPTListener"; import * as i18nc from "../../i18n/country"; import {spawnSettingsModal} from "../../ui/modal/ModalSettings"; @@ -208,18 +208,18 @@ export function spawnConnectModal(options: { /* Connect Profiles */ { - for (const profile of profiles()) { + for (const profile of availableConnectProfiles()) { input_profile.append( - $.spawn("option").text(profile.profile_name).val(profile.id) + $.spawn("option").text(profile.profileName).val(profile.id) ); } input_profile.on('change', event => { - selected_profile = find_profile(input_profile.val() as string) || default_profile(); + selected_profile = findConnectProfile(input_profile.val() as string) || defaultConnectProfile(); { settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, undefined); input_nickname - .attr('placeholder', selected_profile.connect_username() || "Another TeaSpeak user") + .attr('placeholder', selected_profile.connectUsername() || "Another TeaSpeak user") .val(""); } diff --git a/shared/js/ui/modal/ModalIdentity.ts b/shared/js/ui/modal/ModalIdentity.ts index f847da26..0c2f4f7e 100644 --- a/shared/js/ui/modal/ModalIdentity.ts +++ b/shared/js/ui/modal/ModalIdentity.ts @@ -52,7 +52,7 @@ export function spawnTeamSpeakIdentityImprove(identity: TeaSpeakIdentity, name: const threads = parseInt(input_threads.val() as string); const target_level = parseInt(input_target_level.val() as string); if (target_level == 0) { - identity.improve_level(-1, threads, () => active, current_level => { + identity.improveLevelNative(-1, threads, () => active, current_level => { input_current_level.val(current_level); }, hash_rate => { input_hash_rate.val(hash_rate); @@ -63,7 +63,7 @@ export function spawnTeamSpeakIdentityImprove(identity: TeaSpeakIdentity, name: button_start_stop.trigger('click'); }); } else { - identity.improve_level(target_level, threads, () => active, current_level => { + identity.improveLevelNative(target_level, threads, () => active, current_level => { input_current_level.val(current_level); }, hash_rate => { input_hash_rate.val(hash_rate); diff --git a/shared/js/ui/modal/ModalSettings.tsx b/shared/js/ui/modal/ModalSettings.tsx index 4d7a2889..d856296a 100644 --- a/shared/js/ui/modal/ModalSettings.tsx +++ b/shared/js/ui/modal/ModalSettings.tsx @@ -701,7 +701,7 @@ export namespace modal_settings { error: text }); event_registry.on("create-profile", event => { - const profile = profiles.create_new_profile(event.name); + const profile = profiles.createConnectProfile(event.name); profiles.mark_need_save(); event_registry.fire_async("create-profile-result", { status: "success", @@ -711,7 +711,7 @@ export namespace modal_settings { }); event_registry.on("delete-profile", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("delete-profile-result", event.profile_id, tr("Unknown profile")); @@ -723,15 +723,15 @@ export namespace modal_settings { }); const build_profile_info = (profile: ConnectionProfile) => { - const forum_data = profile.selected_identity(IdentitifyType.TEAFORO) as TeaForumIdentity; - const teamspeak_data = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; - const nickname_data = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity; + const forum_data = profile.selectedIdentity(IdentitifyType.TEAFORO) as TeaForumIdentity; + const teamspeak_data = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + const nickname_data = profile.selectedIdentity(IdentitifyType.NICKNAME) as NameIdentity; return { id: profile.id, - name: profile.profile_name, - nickname: profile.default_username, - identity_type: profile.selected_identity_type as any, + name: profile.profileName, + nickname: profile.defaultUsername, + identity_type: profile.selectedIdentityType as any, identity_forum: !forum_data ? undefined : { valid: forum_data.valid(), fallback_name: forum_data.fallback_name() @@ -749,12 +749,12 @@ export namespace modal_settings { event_registry.on("query-profile-list", event => { event_registry.fire_async("query-profile-list-result", { status: "success", - profiles: profiles.profiles().map(e => build_profile_info(e)) + profiles: profiles.availableConnectProfiles().map(e => build_profile_info(e)) }); }); event_registry.on("query-profile", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("query-profile-result", event.profile_id, tr("Unknown profile")); @@ -769,7 +769,7 @@ export namespace modal_settings { }); event_registry.on("set-default-profile", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("set-default-profile-result", event.profile_id, tr("Unknown profile")); @@ -785,14 +785,14 @@ export namespace modal_settings { }); event_registry.on("set-profile-name", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("set-profile-name-result", event.profile_id, tr("Unknown profile")); return; } - profile.profile_name = event.name; + profile.profileName = event.name; profiles.mark_need_save(); event_registry.fire_async("set-profile-name-result", { name: event.name, @@ -802,14 +802,14 @@ export namespace modal_settings { }); event_registry.on("set-default-name", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("set-default-name-result", event.profile_id, tr("Unknown profile")); return; } - profile.default_username = event.name; + profile.defaultUsername = event.name; profiles.mark_need_save(); event_registry.fire_async("set-default-name-result", { name: event.name, @@ -819,16 +819,16 @@ export namespace modal_settings { }); event_registry.on("set-identity-name-name", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("set-identity-name-name-result", event.profile_id, tr("Unknown profile")); return; } - let identity = profile.selected_identity(IdentitifyType.NICKNAME) as NameIdentity; + let identity = profile.selectedIdentity(IdentitifyType.NICKNAME) as NameIdentity; if (!identity) - profile.set_identity(IdentitifyType.NICKNAME, identity = new NameIdentity()); + profile.setIdentity(IdentitifyType.NICKNAME, identity = new NameIdentity()); identity.set_name(event.name); profiles.mark_need_save(); @@ -840,7 +840,7 @@ export namespace modal_settings { }); event_registry.on("query-profile-validity", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("query-profile-validity-result", event.profile_id, tr("Unknown profile")); @@ -855,14 +855,14 @@ export namespace modal_settings { }); event_registry.on("query-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("query-identity-teamspeak-result", event.profile_id, tr("Unknown profile")); return; } - const ts = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + const ts = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; if (!ts) { event_registry.fire_async("query-identity-teamspeak-result", { status: "error", @@ -884,26 +884,31 @@ export namespace modal_settings { }); event_registry.on("select-identity-type", event => { - const profile = profiles.find_profile(event.profile_id); + if(!event.identity_type) { + /* dummy event for UI init */ + return; + } + + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); return; } - profile.selected_identity_type = event.identity_type; + profile.selectedIdentityType = event.identity_type; profiles.mark_need_save(); }); event_registry.on("generate-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); send_error("generate-identity-teamspeak-result", event.profile_id, tr("Unknown profile")); return; } - TeaSpeakIdentity.generate_new().then(identity => { - profile.set_identity(IdentitifyType.TEAMSPEAK, identity); + TeaSpeakIdentity.generateNew().then(identity => { + profile.setIdentity(IdentitifyType.TEAMSPEAK, identity); profiles.mark_need_save(); identity.level().then(level => { @@ -924,14 +929,14 @@ export namespace modal_settings { }); event_registry.on("import-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); return; } spawnTeamSpeakIdentityImport(identity => { - profile.set_identity(IdentitifyType.TEAMSPEAK, identity); + profile.setIdentity(IdentitifyType.TEAMSPEAK, identity); profiles.mark_need_save(); identity.level().catch(error => { @@ -948,16 +953,16 @@ export namespace modal_settings { }); event_registry.on("improve-identity-teamspeak-level", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); return; } - const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + const identity = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; if (!identity) return; - spawnTeamSpeakIdentityImprove(identity, profile.profile_name).close_listener.push(() => { + spawnTeamSpeakIdentityImprove(identity, profile.profileName).close_listener.push(() => { profiles.mark_need_save(); identity.level().then(level => { @@ -972,13 +977,13 @@ export namespace modal_settings { }); event_registry.on("export-identity-teamspeak", event => { - const profile = profiles.find_profile(event.profile_id); + const profile = profiles.findConnectProfile(event.profile_id); if (!profile) { log.warn(LogCategory.CLIENT, tr("Received profile event with unknown profile id (event: %s, id: %s)"), event.type, event.profile_id); return; } - const identity = profile.selected_identity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; + const identity = profile.selectedIdentity(IdentitifyType.TEAMSPEAK) as TeaSpeakIdentity; if (!identity) return; identity.export_ts(true).then(data => { diff --git a/shared/js/ui/modal/echo-test/Controller.tsx b/shared/js/ui/modal/echo-test/Controller.tsx index c85982a2..2b922a60 100644 --- a/shared/js/ui/modal/echo-test/Controller.tsx +++ b/shared/js/ui/modal/echo-test/Controller.tsx @@ -114,6 +114,9 @@ function initializeController(connection: ConnectionHandler, events: Registry { events.fire("query_voice_connection_state"); return "loading"; }); + const [message, setMessage] = useState(undefined); - events.reactUse("notify_voice_connection_state", event => setState(event.state)); + events.reactUse("notify_voice_connection_state", event => { + setState(event.state); + setMessage(event.message); + }); - let inner, shown = true; + let inner, shown = true, error = false; switch (state) { + case "failed": + error = true; + inner = + Voice connection establishment has been failed:
+ {message} +
; + break; + case "disconnected": inner = Voice connection has been disconnected.; break; @@ -57,7 +69,7 @@ const VoiceStateOverlay = () => { } return ( -
+
{inner}
); diff --git a/web/app/audio-lib/index.ts b/web/app/audio-lib/index.ts index 81ab24f3..ee024781 100644 --- a/web/app/audio-lib/index.ts +++ b/web/app/audio-lib/index.ts @@ -17,14 +17,15 @@ export class AudioLibrary { private registeredClients: {[key: number]: AudioClient} = {}; constructor() { - this.worker = new WorkerOwner(() => { - /* - * Attention don't use () => new Worker(...). - * This confuses the worker plugin and will not emit any modules - */ + this.worker = new WorkerOwner(AudioLibrary.spawnNewWorker); + } - return new Worker("./worker/index.ts", { type: "module" }); - }); + private static spawnNewWorker() { + /* + * Attention don't use () => new Worker(...). + * This confuses the worker plugin and will not emit any modules + */ + return new Worker("./worker/index.ts", { type: "module" }); } async initialize() { diff --git a/web/app/index.ts b/web/app/index.ts index 417ceb78..dbdcaee6 100644 --- a/web/app/index.ts +++ b/web/app/index.ts @@ -1,4 +1,6 @@ import "webrtc-adapter"; +import "webcrypto-liner"; + import "./index.scss"; import "./FileTransfer"; diff --git a/web/app/voice/VoiceHandler.ts b/web/app/voice/VoiceHandler.ts index f1087dce..e5d8bf6b 100644 --- a/web/app/voice/VoiceHandler.ts +++ b/web/app/voice/VoiceHandler.ts @@ -45,6 +45,7 @@ export class VoiceConnection extends AbstractVoiceConnection { private readonly serverConnectionStateListener; private connectionType: VoiceEncodeType = VoiceEncodeType.NATIVE_ENCODE; private connectionState: VoiceConnectionStatus; + private failedConnectionMessage: string; private localAudioStarted = false; private connectionLostModalOpen = false; @@ -65,6 +66,8 @@ export class VoiceConnection extends AbstractVoiceConnection { private encoderCodec: number = 5; + private lastConnectAttempt: number = 0; + constructor(connection: ServerConnection) { super(connection); @@ -83,6 +86,10 @@ export class VoiceConnection extends AbstractVoiceConnection { return this.connectionState; } + getFailedMessage(): string { + return this.failedConnectionMessage; + } + destroy() { this.connection.events.off(this.serverConnectionStateListener); this.dropVoiceBridge(); @@ -98,6 +105,10 @@ export class VoiceConnection extends AbstractVoiceConnection { this.events.destroy(); } + reset() { + this.dropVoiceBridge(); + } + async acquireVoiceRecorder(recorder: RecorderProfile | undefined, enforce?: boolean) { if(this.currentAudioSource === recorder && !enforce) return; @@ -154,12 +165,14 @@ export class VoiceConnection extends AbstractVoiceConnection { return; } - if(this.connection.getConnectionState() !== ConnectionState.CONNECTED) + if(this.connection.getConnectionState() !== ConnectionState.CONNECTED) { return; + } + this.lastConnectAttempt = Date.now(); this.connectAttemptCounter++; if(this.voiceBridge) { - this.voiceBridge.callback_disconnect = undefined; + this.voiceBridge.callbackDisconnect = undefined; this.voiceBridge.disconnect(); } @@ -172,7 +185,7 @@ export class VoiceConnection extends AbstractVoiceConnection { request: request }, payload))) }; - this.voiceBridge.callback_disconnect = () => { + this.voiceBridge.callbackDisconnect = () => { this.connection.client.log.log(EventType.CONNECTION_VOICE_DROPPED, { }); if(!this.connectionLostModalOpen) { this.connectionLostModalOpen = true; @@ -181,13 +194,14 @@ export class VoiceConnection extends AbstractVoiceConnection { modal.open(); } logInfo(LogCategory.WEBRTC, tr("Lost voice connection to target server. Trying to reconnect.")); - this.startVoiceBridge(); + this.executeVoiceBridgeReconnect(); } this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT, { attemptCount: this.connectAttemptCounter }); this.setConnectionState(VoiceConnectionStatus.Connecting); this.voiceBridge.connect().then(result => { if(result.type === "success") { + this.lastConnectAttempt = 0; this.connectAttemptCounter = 0; this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, { }); @@ -204,23 +218,32 @@ export class VoiceConnection extends AbstractVoiceConnection { } else if(result.type === "canceled") { /* we've to do nothing here */ } else if(result.type === "failed") { - logWarn(LogCategory.VOICE, tr("Failed to setup voice bridge: %s. Reconnect: %o"), result.message, result.allowReconnect); + let doReconnect = result.allowReconnect && this.connectAttemptCounter < 5; + logWarn(LogCategory.VOICE, tr("Failed to setup voice bridge: %s. Reconnect: %o"), result.message, doReconnect); this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_FAILED, { reason: result.message, - reconnect_delay: result.allowReconnect ? 1 : 0 + reconnect_delay: doReconnect ? 1 : 0 }); - if(result.allowReconnect) { - this.startVoiceBridge(); + if(doReconnect) { + this.executeVoiceBridgeReconnect(); + } else { + this.failedConnectionMessage = result.message; + this.setConnectionState(VoiceConnectionStatus.Failed); } } }); } + private executeVoiceBridgeReconnect() { + /* TODO: May some kind of incremental timeout? */ + this.startVoiceBridge(); + } + private dropVoiceBridge() { if(this.voiceBridge) { - this.voiceBridge.callback_disconnect = undefined; + this.voiceBridge.callbackDisconnect = undefined; this.voiceBridge.disconnect(); this.voiceBridge = undefined; } @@ -296,6 +319,8 @@ export class VoiceConnection extends AbstractVoiceConnection { if(event.newState === ConnectionState.CONNECTED) { this.startVoiceBridge(); } else { + this.connectAttemptCounter = 0; + this.lastConnectAttempt = 0; this.dropVoiceBridge(); } } diff --git a/web/app/voice/bridge/VoiceBridge.ts b/web/app/voice/bridge/VoiceBridge.ts index 02854eb5..6017847e 100644 --- a/web/app/voice/bridge/VoiceBridge.ts +++ b/web/app/voice/bridge/VoiceBridge.ts @@ -31,7 +31,7 @@ export abstract class VoiceBridge { callback_incoming_voice: (packet: VoicePacket) => void; callback_incoming_whisper: (packet: VoiceWhisperPacket) => void; - callback_disconnect: () => void; + callbackDisconnect: () => void; setMuted(flag: boolean) { this.muted = flag; diff --git a/web/app/voice/bridge/WebRTCVoiceBridge.ts b/web/app/voice/bridge/WebRTCVoiceBridge.ts index 448fc090..33ef6083 100644 --- a/web/app/voice/bridge/WebRTCVoiceBridge.ts +++ b/web/app/voice/bridge/WebRTCVoiceBridge.ts @@ -1,7 +1,7 @@ import * as aplayer from "tc-backend/web/audio/player"; +import * as log from "tc-shared/log"; import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log"; import {tr} from "tc-shared/i18n/localize"; -import * as log from "tc-shared/log"; import {VoiceBridge, VoiceBridgeConnectResult} from "./VoiceBridge"; export abstract class WebRTCVoiceBridge extends VoiceBridge { @@ -14,6 +14,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { private whisperDataChannel: RTCDataChannel; private cachedIceCandidates: RTCIceCandidateInit[]; + private localIceCandidateCount: number; private callbackRtcAnswer: (answer: any) => void; @@ -78,8 +79,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { { let rtcConfig: RTCConfiguration = {}; rtcConfig.iceServers = []; - rtcConfig.iceServers.push({ urls: 'stun:stun.l.google.com:19302' }); - //rtcConfig.iceServers.push({ urls: "stun:stun.teaspeak.de:3478" }); + rtcConfig.iceServers.push({ urls: ['stun:stun.l.google.com:19302'] }); this.rtcConnection = new RTCPeerConnection(rtcConfig); @@ -111,17 +111,23 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { this.whisperDataChannel.binaryType = "arraybuffer"; } + /* setting a dummy connect failed handler in case the rtc peer connection changes it's state to failed */ + const connectFailedPromise = new Promise((resolve, reject) => this.callbackRtcConnectFailed = reject); + const wrapWithError = (promise: Promise) : Promise => Promise.race([ promise, connectFailedPromise ]) as any; + let offer: RTCSessionDescriptionInit; try { - offer = await this.rtcConnection.createOffer(this.generateRtpOfferOptions()); + offer = await wrapWithError(this.rtcConnection.createOffer(this.generateRtpOfferOptions())); if(canceled.value) return; } catch (error) { logError(LogCategory.VOICE, tr("Failed to generate RTC offer: %o"), error); throw tr("failed to generate local offer"); } + this.localIceCandidateCount = 0; + try { - await this.rtcConnection.setLocalDescription(offer); + await wrapWithError(this.rtcConnection.setLocalDescription(offer)); if(canceled.value) return; } catch (error) { logError(LogCategory.VOICE, tr("Failed to apply local description: %o"), error); @@ -140,7 +146,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { sdp: offer.sdp } }); - answer = await new Promise((resolve, reject) => { + answer = await wrapWithError(new Promise((resolve, reject) => { const timeout = setTimeout(() => { if(canceled.value) { resolve(); @@ -156,7 +162,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { clearTimeout(timeout); resolve(answer); }; - }); + })); if(canceled.value) return; } @@ -165,7 +171,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { } try { - await this.rtcConnection.setRemoteDescription(new RTCSessionDescription(answer.msg)); + await wrapWithError(this.rtcConnection.setRemoteDescription(new RTCSessionDescription(answer.msg))); if(canceled.value) return; } catch (error) { const kParseErrorPrefix = "Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': "; @@ -176,9 +182,11 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { throw tr("failed to apply remotes description"); } - while(this.cachedIceCandidates.length > 0) + while(this.cachedIceCandidates.length > 0) { this.registerRemoteIceCandidate(this.cachedIceCandidates.pop_front()); + } + /* ATTENTION: Do not use wrapWithError from now on (this.callbackRtcConnectFailed has been changed) */ await new Promise((resolve, reject) => { if(this.rtcConnection.connectionState === "connected") { resolve(); @@ -215,8 +223,8 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { this.cleanupRtcResources(); this.connectionState = "unconnected"; - if(this.callback_disconnect) - this.callback_disconnect(); + if(this.callbackDisconnect) + this.callbackDisconnect(); } private abortConnectionAttempt() { @@ -253,8 +261,9 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { if(typeof this.voiceDataChannel === "undefined") throw tr("missing main data channel"); - if(this.voiceDataChannel.readyState === "open") + if(this.voiceDataChannel.readyState === "open") { return; + } await new Promise((resolve, reject) => { const id = setTimeout(reject, timeout); @@ -288,7 +297,7 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { } private handleRtcConnectionStateChange() { - log.debug(LogCategory.WEBRTC, tr("Connection state changed to %s"), this.rtcConnection.connectionState); + log.debug(LogCategory.WEBRTC, tr("Connection state changed to %s (Local connection state: %s)"), this.rtcConnection.connectionState, this.connectionState); switch (this.rtcConnection.connectionState) { case "connected": if(this.callbackRtcConnected) @@ -298,14 +307,14 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { case "failed": if(this.callbackRtcConnectFailed) this.callbackRtcConnectFailed(tr("connect attempt failed")); - else if(this.callback_disconnect) - this.callback_disconnect(); + else if(this.callbackDisconnect) + this.callbackDisconnect(); break; case "disconnected": case "closed": - if(this.callback_disconnect) - this.callback_disconnect(); + if(this.callbackDisconnect) + this.callbackDisconnect(); break; } } @@ -319,12 +328,22 @@ export abstract class WebRTCVoiceBridge extends VoiceBridge { } private handleIceCandidate(event: RTCPeerConnectionIceEvent) { - if(event.candidate && event.candidate.protocol !== "tcp") + if(event.candidate && event.candidate.protocol !== "tcp") { return; + } if(event.candidate) { + this.localIceCandidateCount++; log.debug(LogCategory.WEBRTC, tr("Gathered local ice candidate for stream %d: %s"), event.candidate.sdpMLineIndex, event.candidate.candidate); this.callback_send_control_data("ice", { msg: event.candidate.toJSON() }); + } else if(this.localIceCandidateCount === 0) { + logError(LogCategory.WEBRTC, tr("Failed to gather any local ice candidates... This is a fatal error.")); + + /* we don't allow a reconnect here since it's most the times not fixable by just trying again */ + this.allowReconnect = false; + if(this.callbackRtcConnectFailed) { + this.callbackRtcConnectFailed(tr("failed to gather any local ICE candidates")); + } } else { log.debug(LogCategory.WEBRTC, tr("Local ICE candidate gathering finish.")); this.callback_send_control_data("ice_finish", {});