From 92e5b72677e10302b54660c9f88a11aa77e67dec Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Mon, 10 Aug 2020 14:41:34 +0200 Subject: [PATCH] Reworked the connection API a bit and the voice unsupported icon now changes with the voice connection state --- loader/app/loader/script_loader.ts | 12 +- shared/backend.d/connection.d.ts | 5 - shared/js/ConnectionHandler.ts | 28 +-- shared/js/connection/CommandHandler.ts | 8 - shared/js/connection/ConnectionBase.ts | 91 ++------ shared/js/connection/ConnectionFactory.ts | 20 ++ shared/js/connection/DummyVoiceConnection.ts | 127 +++++++++++ shared/js/connection/VoiceConnection.ts | 84 ++++++++ shared/js/ui/client.ts | 3 +- shared/js/ui/frames/side/music_info.ts | 5 +- shared/js/ui/react-elements/Icons.tsx | 6 + shared/js/ui/tree/Channel.tsx | 79 +++++-- shared/js/ui/view.tsx | 6 +- web/app/ExternalModalFactory.ts | 15 +- web/app/connection/CommandParser.ts | 2 +- web/app/connection/ServerConnection.ts | 59 +++-- web/app/factories/ExternalModal.ts | 12 ++ web/app/factories/ServerConnection.ts | 25 +++ web/app/index.ts | 4 +- web/app/voice/CodecConverter.ts | 139 ++++++++++++ web/app/voice/VoiceClient.ts | 5 +- web/app/voice/VoiceHandler.ts | 216 ++++--------------- 22 files changed, 604 insertions(+), 347 deletions(-) delete mode 100644 shared/backend.d/connection.d.ts create mode 100644 shared/js/connection/ConnectionFactory.ts create mode 100644 shared/js/connection/DummyVoiceConnection.ts create mode 100644 shared/js/connection/VoiceConnection.ts create mode 100644 shared/js/ui/react-elements/Icons.tsx create mode 100644 web/app/factories/ExternalModal.ts create mode 100644 web/app/factories/ServerConnection.ts create mode 100644 web/app/voice/CodecConverter.ts diff --git a/loader/app/loader/script_loader.ts b/loader/app/loader/script_loader.ts index 2a7c1551..30c60039 100644 --- a/loader/app/loader/script_loader.ts +++ b/loader/app/loader/script_loader.ts @@ -1,5 +1,6 @@ import {config, critical_error, SourcePath} from "./loader"; import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; +import {type} from "os"; let _script_promises: {[key: string]: Promise} = {}; @@ -116,7 +117,16 @@ export async function load_multiple(paths: SourcePath[], options: MultipleOption } } - critical_error("Failed to load script " + script_name(result.failed[0].request, true) + "
" + "View the browser console for more information!"); + { + const error = result.failed[0].error; + console.error(error); + let errorMessage; + if(error instanceof LoadSyntaxError) + errorMessage = error.source.message; + else + errorMessage = "View the browser console for more information!"; + critical_error("Failed to load script " + script_name(result.failed[0].request, true), errorMessage); + } throw "failed to load script " + script_name(result.failed[0].request, false); } } \ No newline at end of file diff --git a/shared/backend.d/connection.d.ts b/shared/backend.d/connection.d.ts deleted file mode 100644 index b362a4eb..00000000 --- a/shared/backend.d/connection.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {ConnectionHandler} from "tc-shared/ConnectionHandler"; -import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; - -export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection; -export function destroy_server_connection(handle: AbstractServerConnection); \ No newline at end of file diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 0c6aec37..2c8f2a5a 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -24,7 +24,6 @@ import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect"; import {formatMessage} from "tc-shared/ui/frames/chat"; import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar"; -import * as connection from "tc-backend/connection"; import * as dns from "tc-backend/dns"; import * as top_menu from "tc-shared/ui/frames/MenuBar"; import {EventHandler, Registry} from "tc-shared/events"; @@ -37,6 +36,8 @@ import {ServerEventLog} from "tc-shared/ui/frames/log/ServerEventLog"; import {EventType} from "tc-shared/ui/frames/log/Definitions"; import {PluginCmdRegistry} from "tc-shared/connection/PluginCmdHandler"; import {W2GPluginCmdHandler} from "tc-shared/video-viewer/W2GPlugin"; +import {VoiceConnectionStatus} from "tc-shared/connection/VoiceConnection"; +import {getServerConnectionFactory} from "tc-shared/connection/ConnectionFactory"; export enum DisconnectReason { HANDLER_DESTROYED, @@ -185,8 +186,11 @@ export class ConnectionHandler { this.settings = new ServerSettings(); - this.serverConnection = connection.spawn_server_connection(this); - this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this); + this.serverConnection = getServerConnectionFactory().create(this); + this.serverConnection.events.on("notify_connection_state_changed", event => this.on_connection_state_changed(event.oldState, event.newState)); + + this.serverConnection.getVoiceConnection().events.on("notify_recorder_changed", () => this.update_voice_status()); + this.serverConnection.getVoiceConnection().events.on("notify_connection_status_changed", () => this.update_voice_status()); this.channelTree = new ChannelTree(this); this.fileManager = new FileManager(this); @@ -729,8 +733,8 @@ export class ConnectionHandler { targetChannel = targetChannel || this.getClient().currentChannel(); - const vconnection = this.serverConnection.voice_connection(); - const basic_voice_support = this.serverConnection.support_voice() && vconnection.connected() && targetChannel; + const vconnection = this.serverConnection.getVoiceConnection(); + const basic_voice_support = vconnection.getConnectionState() === VoiceConnectionStatus.Connected && targetChannel; const support_record = basic_voice_support && (!targetChannel || vconnection.encoding_supported(targetChannel.properties.channel_codec)); const support_playback = basic_voice_support && (!targetChannel || vconnection.decoding_supported(targetChannel.properties.channel_codec)); @@ -742,7 +746,7 @@ export class ConnectionHandler { if(support_record && basic_voice_support) vconnection.set_encoder_codec(targetChannel.properties.channel_codec); - if(!this.serverConnection.support_voice() || !this.serverConnection.connected() || !vconnection.connected()) { + if(!this.serverConnection.connected() || vconnection.getConnectionState() !== VoiceConnectionStatus.Connected) { property_update["client_input_hardware"] = false; property_update["client_output_hardware"] = false; this.client_status.input_hardware = true; /* IDK if we have input hardware or not, but it dosn't matter at all so */ @@ -858,16 +862,13 @@ export class ConnectionHandler { } acquire_recorder(voice_recoder: RecorderProfile, update_control_bar: boolean) { - /* TODO: If the voice connection hasn't been set upped cache the target recorder */ - const vconnection = this.serverConnection.voice_connection(); - (vconnection ? vconnection.acquire_voice_recorder(voice_recoder) : Promise.resolve()).catch(error => { + const vconnection = this.serverConnection.getVoiceConnection(); + vconnection.acquire_voice_recorder(voice_recoder).catch(error => { log.warn(LogCategory.VOICE, tr("Failed to acquire recorder (%o)"), error); - }).then(() => { - this.update_voice_status(undefined); }); } - getVoiceRecorder() :RecorderProfile | undefined { return this.serverConnection?.voice_connection()?.voice_recorder(); } + getVoiceRecorder() : RecorderProfile | undefined { return this.serverConnection.getVoiceConnection().voice_recorder(); } reconnect_properties(profile?: ConnectionProfile) : ConnectParameters { const name = (this.getClient() ? this.getClient().clientNickName() : "") || @@ -998,8 +999,7 @@ export class ConnectionHandler { this.settings = undefined; if(this.serverConnection) { - this.serverConnection.onconnectionstatechanged = undefined; - connection.destroy_server_connection(this.serverConnection); + getServerConnectionFactory().destroy(this.serverConnection); } this.serverConnection = undefined; diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index 10a921ed..f2899715 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -182,14 +182,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { } handleCommandServerInit(json){ - //We could setup the voice channel - if(this.connection.support_voice()) { - log.debug(LogCategory.NETWORKING, tr("Setting up voice")); - } else { - log.debug(LogCategory.NETWORKING, tr("Skipping voice setup (No voice bridge available)")); - } - - json = json[0]; //Only one bulk this.connection.client.initializeLocalClient(parseInt(json["aclid"]), json["acn"]); diff --git a/shared/js/connection/ConnectionBase.ts b/shared/js/connection/ConnectionBase.ts index ab2f429a..804925f6 100644 --- a/shared/js/connection/ConnectionBase.ts +++ b/shared/js/connection/ConnectionBase.ts @@ -2,9 +2,10 @@ import {CommandHelper} from "tc-shared/connection/CommandHelper"; import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {ServerAddress} from "tc-shared/ui/server"; -import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; +import {Registry} from "tc-shared/events"; +import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection"; export interface CommandOptions { flagset?: string[]; /* default: [] */ @@ -18,13 +19,23 @@ export const CommandOptionDefaults: CommandOptions = { timeout: 1000 }; +export interface ServerConnectionEvents { + notify_connection_state_changed: { + oldState: ConnectionState, + newState: ConnectionState + } +} + export type ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any; export abstract class AbstractServerConnection { + readonly events: Registry; + readonly client: ConnectionHandler; readonly command_helper: CommandHelper; - protected connection_state_: ConnectionState = ConnectionState.UNCONNECTED; + protected connectionState: ConnectionState = ConnectionState.UNCONNECTED; protected constructor(client: ConnectionHandler) { + this.events = new Registry(); this.client = client; this.command_helper = new CommandHelper(this); @@ -36,15 +47,11 @@ export abstract class AbstractServerConnection { abstract connected() : boolean; abstract disconnect(reason?: string) : Promise; - abstract support_voice() : boolean; - abstract voice_connection() : voice.AbstractVoiceConnection | undefined; + abstract getVoiceConnection() : AbstractVoiceConnection; abstract command_handler_boss() : AbstractCommandHandlerBoss; abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise; - abstract get onconnectionstatechanged() : ConnectionStateListener; - abstract set onconnectionstatechanged(listener: ConnectionStateListener); - abstract remote_address() : ServerAddress; /* only valid when connected */ connectionProxyAddress() : ServerAddress | undefined { return undefined; }; @@ -52,12 +59,11 @@ export abstract class AbstractServerConnection { //FIXME: Remove this this is currently only some kind of hack updateConnectionState(state: ConnectionState) { - if(state === this.connection_state_) return; + if(state === this.connectionState) return; - const old_state = this.connection_state_; - this.connection_state_ = state; - if(this.onconnectionstatechanged) - this.onconnectionstatechanged(old_state, state); + const oldState = this.connectionState; + this.connectionState = state; + this.events.fire("notify_connection_state_changed", { oldState: oldState, newState: state }); } abstract ping() : { @@ -66,67 +72,6 @@ export abstract class AbstractServerConnection { }; } -export namespace voice { - export enum PlayerState { - PREBUFFERING, - PLAYING, - BUFFERING, - STOPPING, - STOPPED - } - - export type LatencySettings = { - min_buffer: number; /* milliseconds */ - max_buffer: number; /* milliseconds */ - } - - export interface VoiceClient { - client_id: number; - - callback_playback: () => any; - callback_stopped: () => any; - - callback_state_changed: (new_state: PlayerState) => any; - - get_state() : PlayerState; - - get_volume() : number; - set_volume(volume: number) : void; - - abort_replay(); - - support_latency_settings() : boolean; - - reset_latency_settings(); - latency_settings(settings?: LatencySettings) : LatencySettings; - - support_flush() : boolean; - flush(); - } - - export abstract class AbstractVoiceConnection { - readonly connection: AbstractServerConnection; - - protected constructor(connection: AbstractServerConnection) { - this.connection = connection; - } - - abstract connected() : boolean; - abstract encoding_supported(codec: number) : boolean; - abstract decoding_supported(codec: number) : boolean; - - abstract register_client(client_id: number) : VoiceClient; - abstract available_clients() : VoiceClient[]; - abstract unregister_client(client: VoiceClient) : Promise; - - abstract voice_recorder() : RecorderProfile; - abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise; - - abstract get_encoder_codec() : number; - abstract set_encoder_codec(codec: number); - } -} - export class ServerCommand { command: string; arguments: any[]; diff --git a/shared/js/connection/ConnectionFactory.ts b/shared/js/connection/ConnectionFactory.ts new file mode 100644 index 00000000..f1d1aaa1 --- /dev/null +++ b/shared/js/connection/ConnectionFactory.ts @@ -0,0 +1,20 @@ +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; + +export interface ServerConnectionFactory { + create(client: ConnectionHandler) : AbstractServerConnection; + destroy(instance: AbstractServerConnection); +} + +let factoryInstance: ServerConnectionFactory; +export function setServerConnectionFactory(factory: ServerConnectionFactory) { + factoryInstance = factory; +} + +export function getServerConnectionFactory() : ServerConnectionFactory { + if(!factoryInstance) { + throw "server connection factory hasn't been set"; + } + + return factoryInstance; +} \ No newline at end of file diff --git a/shared/js/connection/DummyVoiceConnection.ts b/shared/js/connection/DummyVoiceConnection.ts new file mode 100644 index 00000000..b3276cd8 --- /dev/null +++ b/shared/js/connection/DummyVoiceConnection.ts @@ -0,0 +1,127 @@ +import { + AbstractVoiceConnection, LatencySettings, + PlayerState, + VoiceClient, + VoiceConnectionStatus +} from "tc-shared/connection/VoiceConnection"; +import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; + +class DummyVoiceClient implements VoiceClient { + client_id: number; + + callback_playback: () => any; + callback_stopped: () => any; + + callback_state_changed: (new_state: PlayerState) => any; + + private volume: number; + + constructor(clientId: number) { + this.client_id = clientId; + + this.volume = 1; + this.reset_latency_settings(); + } + + abort_replay() { } + + flush() { + throw "flush isn't supported";} + + get_state(): PlayerState { + return PlayerState.STOPPED; + } + + latency_settings(settings?: LatencySettings): LatencySettings { + throw "latency settings are not supported"; + } + + reset_latency_settings() { + throw "latency settings are not supported"; + } + + set_volume(volume: number): void { + this.volume = volume; + } + + get_volume(): number { + return this.volume; + } + + support_flush(): boolean { + return false; + } + + support_latency_settings(): boolean { + return false; + } +} + +export class DummyVoiceConnection extends AbstractVoiceConnection { + private recorder: RecorderProfile; + private voiceClients: DummyVoiceClient[] = []; + + constructor(connection: AbstractServerConnection) { + super(connection); + } + + async acquire_voice_recorder(recorder: RecorderProfile | undefined): Promise { + if(this.recorder === recorder) + return; + + if(this.recorder) { + this.recorder.callback_unmount = undefined; + await this.recorder.unmount(); + } + + await recorder?.unmount(); + this.recorder = recorder; + + if(this.recorder) { + this.recorder.callback_unmount = () => { + this.recorder = undefined; + this.events.fire("notify_recorder_changed"); + } + } + + this.events.fire("notify_recorder_changed", {}); + } + + available_clients(): VoiceClient[] { + return this.voiceClients; + } + + decoding_supported(codec: number): boolean { + return false; + } + + encoding_supported(codec: number): boolean { + return false; + } + + getConnectionState(): VoiceConnectionStatus { + return VoiceConnectionStatus.ClientUnsupported; + } + + get_encoder_codec(): number { + return 0; + } + + register_client(clientId: number): VoiceClient { + const client = new DummyVoiceClient(clientId); + this.voiceClients.push(client); + return client; + } + + set_encoder_codec(codec: number) {} + + async unregister_client(client: VoiceClient): Promise { + this.voiceClients.remove(client as any); + } + + voice_recorder(): RecorderProfile { + return this.recorder; + } + +} \ No newline at end of file diff --git a/shared/js/connection/VoiceConnection.ts b/shared/js/connection/VoiceConnection.ts new file mode 100644 index 00000000..cab64190 --- /dev/null +++ b/shared/js/connection/VoiceConnection.ts @@ -0,0 +1,84 @@ +import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {Registry} from "tc-shared/events"; + +export enum PlayerState { + PREBUFFERING, + PLAYING, + BUFFERING, + STOPPING, + STOPPED +} + +export type LatencySettings = { + min_buffer: number; /* milliseconds */ + max_buffer: number; /* milliseconds */ +} + +export interface VoiceClient { + client_id: number; + + callback_playback: () => any; + callback_stopped: () => any; + + callback_state_changed: (new_state: PlayerState) => any; + + get_state() : PlayerState; + + get_volume() : number; + set_volume(volume: number) : void; + + abort_replay(); + + support_latency_settings() : boolean; + + reset_latency_settings(); + latency_settings(settings?: LatencySettings) : LatencySettings; + + support_flush() : boolean; + flush(); +} + +export enum VoiceConnectionStatus { + ClientUnsupported, + ServerUnsupported, + + Connecting, + Connected, + Disconnecting, + Disconnected +} + +export interface VoiceConnectionEvents { + "notify_connection_status_changed": { + oldStatus: VoiceConnectionStatus, + newStatus: VoiceConnectionStatus + }, + + "notify_recorder_changed": {} +} + +export abstract class AbstractVoiceConnection { + readonly events: Registry; + readonly connection: AbstractServerConnection; + + protected constructor(connection: AbstractServerConnection) { + this.events = new Registry(); + this.connection = connection; + } + + abstract getConnectionState() : VoiceConnectionStatus; + + abstract encoding_supported(codec: number) : boolean; + abstract decoding_supported(codec: number) : boolean; + + abstract register_client(client_id: number) : VoiceClient; + abstract available_clients() : VoiceClient[]; + abstract unregister_client(client: VoiceClient) : Promise; + + abstract voice_recorder() : RecorderProfile; + abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise; + + abstract get_encoder_codec() : number; + abstract set_encoder_codec(codec: number); +} \ No newline at end of file diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index 6026ed27..0c9d7d1c 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -12,8 +12,6 @@ import * as htmltags from "tc-shared/ui/htmltags"; import {CommandResult, PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; import {ChannelEntry} from "tc-shared/ui/channel"; import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; -import {voice} from "tc-shared/connection/ConnectionBase"; -import VoiceClient = voice.VoiceClient; import {createServerGroupAssignmentModal} from "tc-shared/ui/modal/ModalGroupAssignment"; import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo"; import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient"; @@ -30,6 +28,7 @@ import {EventClient, EventType} from "tc-shared/ui/frames/log/Definitions"; import {W2GPluginCmdHandler} from "tc-shared/video-viewer/W2GPlugin"; import {global_client_actions} from "tc-shared/events/GlobalEvents"; import { ClientIcon } from "svg-sprites/client-icons"; +import {VoiceClient} from "tc-shared/connection/VoiceConnection"; export enum ClientType { CLIENT_VOICE, diff --git a/shared/js/ui/frames/side/music_info.ts b/shared/js/ui/frames/side/music_info.ts index fe80a958..ce8b49c8 100644 --- a/shared/js/ui/frames/side/music_info.ts +++ b/shared/js/ui/frames/side/music_info.ts @@ -1,13 +1,12 @@ import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame"; import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client"; -import {voice} from "tc-shared/connection/ConnectionBase"; -import PlayerState = voice.PlayerState; import {LogCategory} from "tc-shared/log"; import {CommandResult, ErrorID, PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal"; import * as log from "tc-shared/log"; import * as image_preview from "../image_preview"; import {Registry} from "tc-shared/events"; +import {PlayerState} from "tc-shared/connection/VoiceConnection"; export interface MusicSidebarEvents { "open": {}, /* triggers when frame should be shown */ @@ -167,12 +166,14 @@ export class MusicInfo { this.events.on(["bot_change", "bot_property_update"], event => { if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; + /* FIXME: Is this right, using our player state?! */ button_play.toggleClass("hidden", this._current_bot === undefined || this._current_bot.properties.player_state < PlayerState.STOPPING); }); this.events.on(["bot_change", "bot_property_update"], event => { if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return; + /* FIXME: Is this right, using our player state?! */ button_pause.toggleClass("hidden", this._current_bot !== undefined && this._current_bot.properties.player_state >= PlayerState.STOPPING); }); diff --git a/shared/js/ui/react-elements/Icons.tsx b/shared/js/ui/react-elements/Icons.tsx new file mode 100644 index 00000000..5080504f --- /dev/null +++ b/shared/js/ui/react-elements/Icons.tsx @@ -0,0 +1,6 @@ +import {ClientIcon} from "svg-sprites/client-icons"; +import * as React from "react"; + +export const ClientIconRenderer = (props: { icon: ClientIcon, size?: string | number, title?: string }) => ( +
+); \ No newline at end of file diff --git a/shared/js/ui/tree/Channel.tsx b/shared/js/ui/tree/Channel.tsx index fc240261..a1989cb7 100644 --- a/shared/js/ui/tree/Channel.tsx +++ b/shared/js/ui/tree/Channel.tsx @@ -10,6 +10,9 @@ import {EventHandler, ReactEventHandler} from "tc-shared/events"; import {Settings, settings} from "tc-shared/settings"; import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry"; import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer"; +import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons"; +import {ClientIcon} from "svg-sprites/client-icons"; +import {VoiceConnectionStatus} from "tc-shared/connection/VoiceConnection"; const channelStyle = require("./Channel.scss"); const viewStyle = require("./View.scss"); @@ -33,23 +36,47 @@ interface ChannelEntryIconsState { @ReactEventHandler(e => e.props.channel.events) @BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE) class ChannelEntryIcons extends ReactComponentBase { - private static readonly SimpleIcon = (props: { iconClass: string, title: string }) => { - return
- }; + private readonly listenerVoiceStatusChange; + + constructor(props) { + super(props); + + this.listenerVoiceStatusChange = () => { + let stateUpdate = {} as ChannelEntryIconsState; + this.updateVoiceStatus(stateUpdate, this.props.channel.properties.channel_codec); + this.setState(stateUpdate); + } + } + + private serverConnection() { + return this.props.channel.channelTree.client.serverConnection; + } + + componentDidMount() { + const voiceConnection = this.serverConnection().getVoiceConnection(); + voiceConnection.events.on("notify_connection_status_changed", this.listenerVoiceStatusChange); + } + + componentWillUnmount() { + const voiceConnection = this.serverConnection().getVoiceConnection(); + voiceConnection.events.off("notify_connection_status_changed", this.listenerVoiceStatusChange); + } protected defaultState(): ChannelEntryIconsState { const properties = this.props.channel.properties; - const server_connection = this.props.channel.channelTree.client.serverConnection; - return { + const status = { icons_shown: this.props.channel.parsed_channel_name.alignment === "normal", custom_icon_id: properties.channel_icon_id, is_music_quality: properties.channel_codec === 3 || properties.channel_codec === 5, - is_codec_supported: server_connection.support_voice() && server_connection.voice_connection().decoding_supported(properties.channel_codec), + is_codec_supported: false, is_default: properties.channel_flag_default, is_password_protected: properties.channel_flag_password, is_moderated: properties.channel_needed_talk_power !== 0 } + this.updateVoiceStatus(status, this.props.channel.properties.channel_codec); + + return status; } render() { @@ -59,16 +86,16 @@ class ChannelEntryIcons extends ReactComponentBase); + icons.push(); if(this.state.is_password_protected) - icons.push(); //TODO: "client-register" is really the right icon? + icons.push(); if(this.state.is_music_quality) - icons.push(); + icons.push(); if(this.state.is_moderated) - icons.push(); + icons.push(); if(this.state.custom_icon_id) icons.push(); @@ -87,30 +114,46 @@ class ChannelEntryIcons extends ReactComponentBase("notify_properties_updated") private handlePropertiesUpdate(event: ChannelEvents["notify_properties_updated"]) { + let updates = {} as ChannelEntryIconsState; if(typeof event.updated_properties.channel_icon_id !== "undefined") - this.setState({ custom_icon_id: event.updated_properties.channel_icon_id }); + updates.custom_icon_id = event.updated_properties.channel_icon_id; if(typeof event.updated_properties.channel_codec !== "undefined" || typeof event.updated_properties.channel_codec_quality !== "undefined") { const codec = event.channel_properties.channel_codec; - this.setState({ is_music_quality: codec === 3 || codec === 5 }); + updates.is_music_quality = codec === 3 || codec === 5; } if(typeof event.updated_properties.channel_codec !== "undefined") { - const server_connection = this.props.channel.channelTree.client.serverConnection; - this.setState({ is_codec_supported: server_connection.support_voice() && server_connection.voice_connection().decoding_supported(event.channel_properties.channel_codec) }); + this.updateVoiceStatus(updates, event.channel_properties.channel_codec); } if(typeof event.updated_properties.channel_flag_default !== "undefined") - this.setState({ is_default: event.updated_properties.channel_flag_default }); + updates.is_default = event.updated_properties.channel_flag_default; if(typeof event.updated_properties.channel_flag_password !== "undefined") - this.setState({ is_password_protected: event.updated_properties.channel_flag_password }); + updates.is_password_protected = event.updated_properties.channel_flag_password; if(typeof event.updated_properties.channel_needed_talk_power !== "undefined") - this.setState({ is_moderated: event.channel_properties.channel_needed_talk_power !== 0 }); + updates.is_moderated = event.updated_properties.channel_needed_talk_power !== 0; if(typeof event.updated_properties.channel_name !== "undefined") - this.setState({ icons_shown: this.props.channel.parsed_channel_name.alignment === "normal" }); + updates.icons_shown = this.props.channel.parsed_channel_name.alignment === "normal"; + + this.setState(updates); + } + + private updateVoiceStatus(state: ChannelEntryIconsState, currentCodec: number) { + const voiceConnection = this.serverConnection().getVoiceConnection(); + const voiceState = voiceConnection.getConnectionState(); + + switch (voiceState) { + case VoiceConnectionStatus.Connected: + state.is_codec_supported = voiceConnection.decoding_supported(currentCodec); + break; + + default: + state.is_codec_supported = false; + } } } diff --git a/shared/js/ui/view.tsx b/shared/js/ui/view.tsx index fe88577c..993c0713 100644 --- a/shared/js/ui/view.tsx +++ b/shared/js/ui/view.tsx @@ -487,7 +487,7 @@ export class ChannelTree { } //FIXME: Trigger the notify_clients_changed event! - const voice_connection = this.client.serverConnection.voice_connection(); + const voice_connection = this.client.serverConnection.getVoiceConnection(); if(client.get_audio_handle()) { if(!voice_connection) { log.warn(LogCategory.VOICE, tr("Deleting client with a voice handle, but we haven't a voice connection!")); @@ -503,7 +503,7 @@ export class ChannelTree { this.clients.push(client); client.channelTree = this; - const voice_connection = this.client.serverConnection.voice_connection(); + const voice_connection = this.client.serverConnection.getVoiceConnection(); if(voice_connection) client.set_audio_handle(voice_connection.register_client(client.clientId())); } @@ -846,7 +846,7 @@ export class ChannelTree { try { this.selection.reset(); - const voice_connection = this.client.serverConnection ? this.client.serverConnection.voice_connection() : undefined; + const voice_connection = this.client.serverConnection ? this.client.serverConnection.getVoiceConnection() : undefined; for(const client of this.clients) { if(client.get_audio_handle() && voice_connection) { voice_connection.unregister_client(client.get_audio_handle()); diff --git a/web/app/ExternalModalFactory.ts b/web/app/ExternalModalFactory.ts index 51743006..4d1aaa2b 100644 --- a/web/app/ExternalModalFactory.ts +++ b/web/app/ExternalModalFactory.ts @@ -1,14 +1,11 @@ import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import * as ipc from "tc-shared/ipc/BrowserIPC"; -import * as loader from "tc-loader"; -import {Stage} from "tc-loader"; -import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal"; import {ChannelMessage} from "tc-shared/ipc/BrowserIPC"; import {LogCategory, logDebug, logWarn} from "tc-shared/log"; import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage"; -class ExternalModalController extends AbstractExternalModalController { +export class ExternalModalController extends AbstractExternalModalController { private currentWindow: Window; private windowClosedTestInterval: number = 0; private windowClosedTimeout: number; @@ -142,12 +139,4 @@ class ExternalModalController extends AbstractExternalModalController { break; } } -} - -loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { - priority: 50, - name: "external modal controller factory setup", - function: async () => { - setExternalModalControllerFactory((modal, events, userData) => new ExternalModalController(modal, events, userData)); - } -}); \ No newline at end of file +} \ No newline at end of file diff --git a/web/app/connection/CommandParser.ts b/web/app/connection/CommandParser.ts index 4bc8d3a7..5ddcb3b7 100644 --- a/web/app/connection/CommandParser.ts +++ b/web/app/connection/CommandParser.ts @@ -52,7 +52,7 @@ const escapeCharacterMap = { "\x0B": "b" }; -const escapeCommandValue = (value: string) => value.replace(/[\\ \/|\b\f\n\r\t\x07\x08]/g, value => "\\" + escapeCharacterMap[value]); +const escapeCommandValue = (value: string) => value.replace(/[\\ \/|\b\f\n\r\t\x07]/g, value => "\\" + escapeCharacterMap[value]); export function parseCommand(command: string): ParsedCommand { const parts = command.split("|").map(element => element.split(" ").map(e => e.trim()).filter(e => !!e)); diff --git a/web/app/connection/ServerConnection.ts b/web/app/connection/ServerConnection.ts index eacb4e09..5cf2740e 100644 --- a/web/app/connection/ServerConnection.ts +++ b/web/app/connection/ServerConnection.ts @@ -3,7 +3,6 @@ import { CommandOptionDefaults, CommandOptions, ConnectionStateListener, - voice } from "tc-shared/connection/ConnectionBase"; import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler"; import {ServerAddress} from "tc-shared/ui/server"; @@ -18,7 +17,9 @@ import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHa import {VoiceConnection} from "../voice/VoiceHandler"; import {EventType} from "tc-shared/ui/frames/log/Definitions"; import {WrappedWebSocket} from "tc-backend/web/connection/WrappedWebSocket"; -import AbstractVoiceConnection = voice.AbstractVoiceConnection; +import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection"; +import {DummyVoiceConnection} from "tc-shared/connection/DummyVoiceConnection"; +import {ServerConnectionFactory, setServerConnectionFactory} from "tc-shared/connection/ConnectionFactory"; class ReturnListener { resolve: (value?: T | PromiseLike) => void; @@ -42,7 +43,9 @@ export class ServerConnection extends AbstractServerConnection { private returnListeners: ReturnListener[] = []; private _connection_state_listener: ConnectionStateListener; - private _voice_connection: VoiceConnection; + + private dummyVoiceConnection: DummyVoiceConnection; + private voiceConnection: VoiceConnection; private pingStatistics = { thread_id: 0, @@ -68,8 +71,11 @@ export class ServerConnection extends AbstractServerConnection { this.commandHandlerBoss.register_handler(this.defaultCommandHandler); this.command_helper.initialize(); - if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false)) - this._voice_connection = new VoiceConnection(this); + if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false)) { + this.voiceConnection = new VoiceConnection(this); + } else { + this.dummyVoiceConnection = new DummyVoiceConnection(this); + } } destroy() { @@ -94,11 +100,13 @@ export class ServerConnection extends AbstractServerConnection { this.defaultCommandHandler && this.commandHandlerBoss.unregister_handler(this.defaultCommandHandler); this.defaultCommandHandler = undefined; - this._voice_connection && this._voice_connection.destroy(); - this._voice_connection = undefined; + this.voiceConnection && this.voiceConnection.destroy(); + this.voiceConnection = undefined; this.commandHandlerBoss && this.commandHandlerBoss.destroy(); this.commandHandlerBoss = undefined; + + this.events.destroy(); }); } @@ -264,7 +272,7 @@ export class ServerConnection extends AbstractServerConnection { if(this.connectCancelCallback) this.connectCancelCallback(); - if(this.connection_state_ === ConnectionState.UNCONNECTED) + if(this.connectionState === ConnectionState.UNCONNECTED) return; this.updateConnectionState(ConnectionState.DISCONNECTING); @@ -277,8 +285,8 @@ export class ServerConnection extends AbstractServerConnection { } - if(this._voice_connection) - this._voice_connection.drop_rtp_session(); + if(this.voiceConnection) + this.voiceConnection.drop_rtp_session(); if(this.socket) { @@ -327,15 +335,15 @@ export class ServerConnection extends AbstractServerConnection { this.pingStatistics.thread_id = setInterval(() => this.doNextPing(), this.pingStatistics.interval) as any; this.doNextPing(); this.updateConnectionState(ConnectionState.CONNECTED); - if(this._voice_connection) - this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */ + if(this.voiceConnection) + this.voiceConnection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */ } /* devel-block(log-networking-commands) */ group.end(); /* devel-block-end */ } else if(json["type"] === "WebRTC") { - if(this._voice_connection) - this._voice_connection.handleControlPacket(json); + if(this.voiceConnection) + this.voiceConnection.handleControlPacket(json); else log.warn(LogCategory.NETWORKING, tr("Dropping WebRTC command packet, because we haven't a bridge.")) } else if(json["type"] === "ping") { @@ -392,12 +400,12 @@ export class ServerConnection extends AbstractServerConnection { Object.assign(options, CommandOptionDefaults); Object.assign(options, _options); - data = $.isArray(data) ? data : [data || {}]; + data = Array.isArray(data) ? data : [data || {}]; if(data.length == 0) /* we require min one arg to append return_code */ data.push({}); let result = new Promise((resolve, failed) => { - let payload = $.isArray(data) ? data : [data]; + let payload = Array.isArray(data) ? data : [data]; let returnCode = typeof payload[0]["return_code"] === "string" ? payload[0].return_code : ++globalReturnCodeIndex; payload[0].return_code = returnCode; @@ -427,19 +435,14 @@ export class ServerConnection extends AbstractServerConnection { return !!this.socket && this.socket.state === "connected"; } - support_voice(): boolean { - return this._voice_connection !== undefined; - } - - voice_connection(): AbstractVoiceConnection | undefined { - return this._voice_connection; + getVoiceConnection(): AbstractVoiceConnection { + return this.voiceConnection || this.dummyVoiceConnection; } command_handler_boss(): AbstractCommandHandlerBoss { return this.commandHandlerBoss; } - get onconnectionstatechanged() : ConnectionStateListener { return this._connection_state_listener; } @@ -480,14 +483,4 @@ export class ServerConnection extends AbstractServerConnection { native: this.pingStatistics.currentNativeValue }; } -} - -export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection { - return new ServerConnection(handle); /* will be overridden by the client */ -} - -export function destroy_server_connection(handle: AbstractServerConnection) { - if(!(handle instanceof ServerConnection)) - throw "invalid handle"; - handle.destroy(); } \ No newline at end of file diff --git a/web/app/factories/ExternalModal.ts b/web/app/factories/ExternalModal.ts new file mode 100644 index 00000000..cb2ef546 --- /dev/null +++ b/web/app/factories/ExternalModal.ts @@ -0,0 +1,12 @@ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal"; +import {ExternalModalController} from "tc-backend/web/ExternalModalFactory"; + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 50, + name: "external modal controller factory setup", + function: async () => { + setExternalModalControllerFactory((modal, events, userData) => new ExternalModalController(modal, events, userData)); + } +}); \ No newline at end of file diff --git a/web/app/factories/ServerConnection.ts b/web/app/factories/ServerConnection.ts new file mode 100644 index 00000000..30e8fe06 --- /dev/null +++ b/web/app/factories/ServerConnection.ts @@ -0,0 +1,25 @@ +import {ServerConnectionFactory, setServerConnectionFactory} from "tc-shared/connection/ConnectionFactory"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; +import {ServerConnection} from "tc-backend/web/connection/ServerConnection"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 50, + name: "server connection factory setup", + function: async () => { + setServerConnectionFactory(new class implements ServerConnectionFactory { + create(client: ConnectionHandler): AbstractServerConnection { + return new ServerConnection(client); + } + + destroy(instance: AbstractServerConnection) { + if(!(instance instanceof ServerConnection)) + throw "invalid handle"; + + instance.destroy(); + } + }); + } +}); \ No newline at end of file diff --git a/web/app/index.ts b/web/app/index.ts index 72300ecb..6d5bf472 100644 --- a/web/app/index.ts +++ b/web/app/index.ts @@ -1,6 +1,8 @@ import "webrtc-adapter"; import "./index.scss"; import "./FileTransfer"; -import "./ExternalModalFactory"; + +import "./factories/ServerConnection"; +import "./factories/ExternalModal"; export = require("tc-shared/main"); \ No newline at end of file diff --git a/web/app/voice/CodecConverter.ts b/web/app/voice/CodecConverter.ts new file mode 100644 index 00000000..990e0e5c --- /dev/null +++ b/web/app/voice/CodecConverter.ts @@ -0,0 +1,139 @@ +import * as loader from "tc-loader"; +import * as aplayer from "tc-backend/web/audio/player"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {tr} from "tc-shared/i18n/localize"; +import {CodecType} from "tc-backend/web/codec/Codec"; +import {VoiceConnection} from "tc-backend/web/voice/VoiceHandler"; +import {BasicCodec} from "tc-backend/web/codec/BasicCodec"; +import {createErrorModal} from "tc-shared/ui/elements/Modal"; +import {CodecWrapperWorker} from "tc-backend/web/codec/CodecWrapperWorker"; + +class CacheEntry { + instance: BasicCodec; + owner: number; + + last_access: number; +} + +export function codec_supported(type: CodecType) { + return type == CodecType.OPUS_MUSIC || type == CodecType.OPUS_VOICE; +} + +export class CodecPool { + codecIndex: number; + name: string; + type: CodecType; + + entries: CacheEntry[] = []; + maxInstances: number = 2; + + private _supported: boolean = true; + + initialize(cached: number) { + /* test if we're able to use this codec */ + const dummy_client_id = 0xFFEF; + + this.ownCodec(dummy_client_id, _ => {}).then(codec => { + log.trace(LogCategory.VOICE, tr("Releasing codec instance (%o)"), codec); + this.releaseCodec(dummy_client_id); + }).catch(error => { + if(this._supported) { + log.warn(LogCategory.VOICE, tr("Disabling codec support for "), this.name); + createErrorModal(tr("Could not load codec driver"), tr("Could not load or initialize codec ") + this.name + "
" + + "Error: " + JSON.stringify(error) + "").open(); + log.error(LogCategory.VOICE, tr("Failed to initialize the opus codec. Error: %o"), error); + } else { + log.debug(LogCategory.VOICE, tr("Failed to initialize already disabled codec. Error: %o"), error); + } + this._supported = false; + }); + } + + supported() { return this._supported; } + + ownCodec?(clientId: number, callback_encoded: (buffer: Uint8Array) => any, create: boolean = true) : Promise { + return new Promise((resolve, reject) => { + if(!this._supported) { + reject(tr("unsupported codec!")); + return; + } + + let free_slot = 0; + for(let index = 0; index < this.entries.length; index++) { + if(this.entries[index].owner == clientId) { + this.entries[index].last_access = Date.now(); + if(this.entries[index].instance.initialized()) + resolve(this.entries[index].instance); + else { + this.entries[index].instance.initialise().then((flag) => { + //TODO test success flag + this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); + }).catch(reject); + } + return; + } else if(this.entries[index].owner == 0) { + free_slot = index; + } + } + + if(!create) { + resolve(undefined); + return; + } + + if(free_slot == 0){ + free_slot = this.entries.length; + let entry = new CacheEntry(); + entry.instance = new CodecWrapperWorker(this.type); + this.entries.push(entry); + } + this.entries[free_slot].owner = clientId; + this.entries[free_slot].last_access = new Date().getTime(); + this.entries[free_slot].instance.on_encoded_data = callback_encoded; + if(this.entries[free_slot].instance.initialized()) + this.entries[free_slot].instance.reset(); + else { + this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); + return; + } + resolve(this.entries[free_slot].instance); + }); + } + + releaseCodec(clientId: number) { + for(let index = 0; index < this.entries.length; index++) + if(this.entries[index].owner == clientId) this.entries[index].owner = 0; + } + + constructor(index: number, name: string, type: CodecType){ + this.codecIndex = index; + this.name = name; + this.type = type; + + this._supported = this.type !== undefined && codec_supported(this.type); + } +} + +export let codecPool: CodecPool[]; +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + priority: 10, + function: async () => { + aplayer.on_ready(() => { + log.info(LogCategory.VOICE, tr("Initializing voice handler after AudioController has been initialized!")); + + codecPool = [ + new CodecPool(0, tr("Speex Narrowband"), CodecType.SPEEX_NARROWBAND), + new CodecPool(1, tr("Speex Wideband"), CodecType.SPEEX_WIDEBAND), + new CodecPool(2, tr("Speex Ultra Wideband"), CodecType.SPEEX_ULTRA_WIDEBAND), + new CodecPool(3, tr("CELT Mono"), CodecType.CELT_MONO), + new CodecPool(4, tr("Opus Voice"), CodecType.OPUS_VOICE), + new CodecPool(5, tr("Opus Music"), CodecType.OPUS_MUSIC) + ]; + + codecPool[4].initialize(2); + codecPool[5].initialize(2); + }); + }, + name: "registering codec initialisation" +}); diff --git a/web/app/voice/VoiceClient.ts b/web/app/voice/VoiceClient.ts index d719e7ef..7ba3340b 100644 --- a/web/app/voice/VoiceClient.ts +++ b/web/app/voice/VoiceClient.ts @@ -1,11 +1,8 @@ -import {voice} from "tc-shared/connection/ConnectionBase"; -import VoiceClient = voice.VoiceClient; -import PlayerState = voice.PlayerState; import {CodecClientCache} from "../codec/Codec"; import * as aplayer from "../audio/player"; import {LogCategory} from "tc-shared/log"; import * as log from "tc-shared/log"; -import LatencySettings = voice.LatencySettings; +import {LatencySettings, PlayerState, VoiceClient} from "tc-shared/connection/VoiceConnection"; export class VoiceClientController implements VoiceClient { callback_playback: () => any; diff --git a/web/app/voice/VoiceHandler.ts b/web/app/voice/VoiceHandler.ts index beab8f33..09f8f667 100644 --- a/web/app/voice/VoiceHandler.ts +++ b/web/app/voice/VoiceHandler.ts @@ -1,129 +1,15 @@ import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; -import * as loader from "tc-loader"; import * as aplayer from "../audio/player"; -import {BasicCodec} from "../codec/BasicCodec"; -import {CodecType} from "../codec/Codec"; -import {createErrorModal} from "tc-shared/ui/elements/Modal"; -import {CodecWrapperWorker} from "../codec/CodecWrapperWorker"; import {ServerConnection} from "../connection/ServerConnection"; -import {voice} from "tc-shared/connection/ConnectionBase"; import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; import {VoiceClientController} from "./VoiceClient"; import {settings, ValuedSettingsKey} from "tc-shared/settings"; import {CallbackInputConsumer, InputConsumerType, NodeInputConsumer} from "tc-shared/voice/RecorderBase"; -import AbstractVoiceConnection = voice.AbstractVoiceConnection; -import VoiceClient = voice.VoiceClient; import {tr} from "tc-shared/i18n/localize"; import {EventType} from "tc-shared/ui/frames/log/Definitions"; - -export namespace codec { - class CacheEntry { - instance: BasicCodec; - owner: number; - - last_access: number; - } - - export function codec_supported(type: CodecType) { - return type == CodecType.OPUS_MUSIC || type == CodecType.OPUS_VOICE; - } - - export class CodecPool { - codecIndex: number; - name: string; - type: CodecType; - - entries: CacheEntry[] = []; - maxInstances: number = 2; - - private _supported: boolean = true; - - initialize(cached: number) { - /* test if we're able to use this codec */ - const dummy_client_id = 0xFFEF; - - this.ownCodec(dummy_client_id, _ => {}).then(codec => { - log.trace(LogCategory.VOICE, tr("Releasing codec instance (%o)"), codec); - this.releaseCodec(dummy_client_id); - }).catch(error => { - if(this._supported) { - log.warn(LogCategory.VOICE, tr("Disabling codec support for "), this.name); - createErrorModal(tr("Could not load codec driver"), tr("Could not load or initialize codec ") + this.name + "
" + - "Error: " + JSON.stringify(error) + "").open(); - log.error(LogCategory.VOICE, tr("Failed to initialize the opus codec. Error: %o"), error); - } else { - log.debug(LogCategory.VOICE, tr("Failed to initialize already disabled codec. Error: %o"), error); - } - this._supported = false; - }); - } - - supported() { return this._supported; } - - ownCodec?(clientId: number, callback_encoded: (buffer: Uint8Array) => any, create: boolean = true) : Promise { - return new Promise((resolve, reject) => { - if(!this._supported) { - reject(tr("unsupported codec!")); - return; - } - - let free_slot = 0; - for(let index = 0; index < this.entries.length; index++) { - if(this.entries[index].owner == clientId) { - this.entries[index].last_access = Date.now(); - if(this.entries[index].instance.initialized()) - resolve(this.entries[index].instance); - else { - this.entries[index].instance.initialise().then((flag) => { - //TODO test success flag - this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); - }).catch(reject); - } - return; - } else if(this.entries[index].owner == 0) { - free_slot = index; - } - } - - if(!create) { - resolve(undefined); - return; - } - - if(free_slot == 0){ - free_slot = this.entries.length; - let entry = new CacheEntry(); - entry.instance = new CodecWrapperWorker(this.type); - this.entries.push(entry); - } - this.entries[free_slot].owner = clientId; - this.entries[free_slot].last_access = new Date().getTime(); - this.entries[free_slot].instance.on_encoded_data = callback_encoded; - if(this.entries[free_slot].instance.initialized()) - this.entries[free_slot].instance.reset(); - else { - this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); - return; - } - resolve(this.entries[free_slot].instance); - }); - } - - releaseCodec(clientId: number) { - for(let index = 0; index < this.entries.length; index++) - if(this.entries[index].owner == clientId) this.entries[index].owner = 0; - } - - constructor(index: number, name: string, type: CodecType){ - this.codecIndex = index; - this.name = name; - this.type = type; - - this._supported = this.type !== undefined && codec_supported(this.type); - } - } -} +import {AbstractVoiceConnection, VoiceClient, VoiceConnectionStatus} from "tc-shared/connection/VoiceConnection"; +import {codecPool, CodecPool} from "tc-backend/web/voice/CodecConverter"; export enum VoiceEncodeType { JS_ENCODE, @@ -139,10 +25,11 @@ const KEY_VOICE_CONNECTION_TYPE: ValuedSettingsKey = { export class VoiceConnection extends AbstractVoiceConnection { readonly connection: ServerConnection; + connectionState: VoiceConnectionStatus; rtcPeerConnection: RTCPeerConnection; dataChannel: RTCDataChannel; - private _type: VoiceEncodeType = VoiceEncodeType.NATIVE_ENCODE; + private connectionType: VoiceEncodeType = VoiceEncodeType.NATIVE_ENCODE; private localAudioStarted = false; /* @@ -152,10 +39,8 @@ export class VoiceConnection extends AbstractVoiceConnection { local_audio_mute: GainNode; local_audio_stream: MediaStreamAudioDestinationNode; - static codec_pool: codec.CodecPool[]; - static codecSupported(type: number) : boolean { - return this.codec_pool && this.codec_pool.length > type && this.codec_pool[type].supported(); + return !!codecPool && codecPool.length > type && codecPool[type].supported(); } private voice_packet_id: number = 0; @@ -169,9 +54,15 @@ export class VoiceConnection extends AbstractVoiceConnection { constructor(connection: ServerConnection) { super(connection); - this.connection = connection; - this._type = settings.static_global(KEY_VOICE_CONNECTION_TYPE, this._type); + this.connectionState = VoiceConnectionStatus.Disconnected; + + this.connection = connection; + this.connectionType = settings.static_global(KEY_VOICE_CONNECTION_TYPE, this.connectionType); + } + + getConnectionState(): VoiceConnectionStatus { + return this.connectionState; } destroy() { @@ -189,6 +80,7 @@ export class VoiceConnection extends AbstractVoiceConnection { this._audio_clients = undefined; this._audio_source = undefined; }); + this.events.destroy(); } static native_encoding_supported() : boolean { @@ -197,21 +89,17 @@ export class VoiceConnection extends AbstractVoiceConnection { return false; if(!context.prototype.createMediaStreamDestination) - return false; //Required, but not available within edge + return false; /* Required, but not available within edge */ return true; } static javascript_encoding_supported() : boolean { - if(!window.RTCPeerConnection) - return false; - if(!RTCPeerConnection.prototype.createDataChannel) - return false; - return true; + return typeof window.RTCPeerConnection !== "undefined" && typeof window.RTCPeerConnection.prototype.createDataChannel === "function"; } current_encoding_supported() : boolean { - switch (this._type) { + switch (this.connectionType) { case VoiceEncodeType.JS_ENCODE: return VoiceConnection.javascript_encoding_supported(); case VoiceEncodeType.NATIVE_ENCODE: @@ -247,11 +135,13 @@ export class VoiceConnection extends AbstractVoiceConnection { if(this._audio_source === recorder && !enforce) return; - if(recorder) + if(recorder) { await recorder.unmount(); + } - if(this._audio_source) + if(this._audio_source) { await this._audio_source.unmount(); + } this.handleLocalVoiceEnded(); this._audio_source = recorder; @@ -272,7 +162,7 @@ export class VoiceConnection extends AbstractVoiceConnection { } } if(new_input) { - if(this._type == VoiceEncodeType.NATIVE_ENCODE) { + if(this.connectionType == VoiceEncodeType.NATIVE_ENCODE) { if(!this.local_audio_stream) this.setup_native(); /* requires initialized audio */ @@ -311,15 +201,16 @@ export class VoiceConnection extends AbstractVoiceConnection { } }; } - this.connection.client.update_voice_status(undefined); + + this.events.fire("notify_recorder_changed"); } - get_encoder_type() : VoiceEncodeType { return this._type; } + get_encoder_type() : VoiceEncodeType { return this.connectionType; } set_encoder_type(target: VoiceEncodeType) { - if(target == this._type) return; - this._type = target; + if(target == this.connectionType) return; + this.connectionType = target; - if(this._type == VoiceEncodeType.NATIVE_ENCODE) + if(this.connectionType == VoiceEncodeType.NATIVE_ENCODE) this.setup_native(); else this.setup_js(); @@ -331,7 +222,7 @@ export class VoiceConnection extends AbstractVoiceConnection { } voice_send_support() : boolean { - if(this._type == VoiceEncodeType.NATIVE_ENCODE) + if(this.connectionType == VoiceEncodeType.NATIVE_ENCODE) return VoiceConnection.native_encoding_supported() && this.rtcPeerConnection.getLocalStreams().length > 0; else return this.voice_playback_support(); @@ -405,7 +296,7 @@ export class VoiceConnection extends AbstractVoiceConnection { if(!this.current_encoding_supported()) return false; - if(this._type == VoiceEncodeType.NATIVE_ENCODE) + if(this.connectionType == VoiceEncodeType.NATIVE_ENCODE) this.setup_native(); else this.setup_js(); @@ -413,7 +304,7 @@ export class VoiceConnection extends AbstractVoiceConnection { this.drop_rtp_session(); this._ice_use_cache = true; - + this.setConnectionState(VoiceConnectionStatus.Connecting); let config: RTCConfiguration = {}; config.iceServers = []; config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' }); @@ -427,7 +318,7 @@ export class VoiceConnection extends AbstractVoiceConnection { this.dataChannel.binaryType = "arraybuffer"; let sdpConstraints : RTCOfferOptions = {}; - sdpConstraints.offerToReceiveAudio = this._type == VoiceEncodeType.NATIVE_ENCODE; + sdpConstraints.offerToReceiveAudio = this.connectionType == VoiceEncodeType.NATIVE_ENCODE; sdpConstraints.offerToReceiveVideo = false; sdpConstraints.voiceActivityDetection = true; @@ -460,7 +351,7 @@ export class VoiceConnection extends AbstractVoiceConnection { this._ice_use_cache = true; this._ice_cache = []; - this.connection.client.update_voice_status(undefined); + this.setConnectionState(VoiceConnectionStatus.Disconnected); } private registerRemoteICECandidate(candidate: RTCIceCandidate) { @@ -559,7 +450,7 @@ export class VoiceConnection extends AbstractVoiceConnection { private onMainDataChannelOpen(channel) { log.info(LogCategory.VOICE, tr("Got new data channel! (%s)"), this.dataChannel.readyState); - this.connection.client.update_voice_status(); + this.setConnectionState(VoiceConnectionStatus.Connected); } private onMainDataChannelMessage(message: MessageEvent) { @@ -578,7 +469,7 @@ export class VoiceConnection extends AbstractVoiceConnection { return; } - let codec_pool = VoiceConnection.codec_pool[codec]; + let codec_pool = codecPool[codec]; if(!codec_pool) { log.error(LogCategory.VOICE, tr("Could not playback codec %o"), codec); return; @@ -621,7 +512,7 @@ export class VoiceConnection extends AbstractVoiceConnection { } const codec = this._encoder_codec; - VoiceConnection.codec_pool[codec] + codecPool[codec] .ownCodec(chandler.getClientId(), e => this.handleEncodedVoicePacket(e, codec), true) .then(encoder => encoder.encodeSamples(client.get_codec_cache(codec), data)); } @@ -638,7 +529,7 @@ export class VoiceConnection extends AbstractVoiceConnection { log.info(LogCategory.VOICE, tr("Local voice ended")); this.localAudioStarted = false; - if(this._type === VoiceEncodeType.NATIVE_ENCODE) { + if(this.connectionType === VoiceEncodeType.NATIVE_ENCODE) { setTimeout(() => { /* first send all data, than send the stop signal */ this.sendVoiceStopPacket(this._encoder_codec); @@ -672,6 +563,15 @@ export class VoiceConnection extends AbstractVoiceConnection { this.acquire_voice_recorder(undefined, true); /* we can ignore the promise because we should finish this directly */ } + private setConnectionState(state: VoiceConnectionStatus) { + if(this.connectionState === state) + return; + + const oldState = this.connectionState; + this.connectionState = state; + this.events.fire("notify_connection_status_changed", { newStatus: state, oldStatus: oldState }); + } + connected(): boolean { return typeof(this.dataChannel) !== "undefined" && this.dataChannel.readyState === "open"; } @@ -732,26 +632,4 @@ declare global { removeStream(stream: MediaStream): void; createOffer(successCallback?: RTCSessionDescriptionCallback, failureCallback?: RTCPeerConnectionErrorCallback, options?: RTCOfferOptions): Promise; } -} - -loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - priority: 10, - function: async () => { - aplayer.on_ready(() => { - log.info(LogCategory.VOICE, tr("Initializing voice handler after AudioController has been initialized!")); - - VoiceConnection.codec_pool = [ - new codec.CodecPool(0, tr("Speex Narrowband"), CodecType.SPEEX_NARROWBAND), - new codec.CodecPool(1, tr("Speex Wideband"), CodecType.SPEEX_WIDEBAND), - new codec.CodecPool(2, tr("Speex Ultra Wideband"), CodecType.SPEEX_ULTRA_WIDEBAND), - new codec.CodecPool(3, tr("CELT Mono"), CodecType.CELT_MONO), - new codec.CodecPool(4, tr("Opus Voice"), CodecType.OPUS_VOICE), - new codec.CodecPool(5, tr("Opus Music"), CodecType.OPUS_MUSIC) - ]; - - VoiceConnection.codec_pool[4].initialize(2); - VoiceConnection.codec_pool[5].initialize(2); - }); - }, - name: "registering codec initialisation" -}); +} \ No newline at end of file