Reworked the connection API a bit and the voice unsupported icon now changes with the voice connection state

canary
WolverinDEV 2020-08-10 14:41:34 +02:00
parent ac773f5606
commit 92e5b72677
22 changed files with 604 additions and 347 deletions

View File

@ -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<void>} = {};
@ -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) + " <br>" + "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);
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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"]);

View File

@ -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<ServerConnectionEvents>;
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<ServerConnectionEvents>();
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<void>;
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<CommandResult>;
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<void>;
abstract voice_recorder() : RecorderProfile;
abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise<void>;
abstract get_encoder_codec() : number;
abstract set_encoder_codec(codec: number);
}
}
export class ServerCommand {
command: string;
arguments: any[];

View File

@ -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;
}

View File

@ -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<void> {
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<void> {
this.voiceClients.remove(client as any);
}
voice_recorder(): RecorderProfile {
return this.recorder;
}
}

View File

@ -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<VoiceConnectionEvents>;
readonly connection: AbstractServerConnection;
protected constructor(connection: AbstractServerConnection) {
this.events = new Registry<VoiceConnectionEvents>();
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<void>;
abstract voice_recorder() : RecorderProfile;
abstract acquire_voice_recorder(recorder: RecorderProfile | undefined) : Promise<void>;
abstract get_encoder_codec() : number;
abstract set_encoder_codec(codec: number);
}

View File

@ -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,

View File

@ -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);
});

View File

@ -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 }) => (
<div className={"icon_em " + props.icon} style={{ fontSize: props.size }} title={props.title} />
);

View File

@ -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<ChannelEntryIcons>(e => e.props.channel.events)
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
class ChannelEntryIcons extends ReactComponentBase<ChannelEntryIconsProperties, ChannelEntryIconsState> {
private static readonly SimpleIcon = (props: { iconClass: string, title: string }) => {
return <div className={"icon " + props.iconClass} title={props.title} />
};
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<ChannelEntryIconsProperties,
return null;
if(this.state.is_default)
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-default"} iconClass={"client-channel_default"} title={tr("Default channel")} />);
icons.push(<ClientIconRenderer key={"icon-default"} icon={ClientIcon.ChannelDefault} title={tr("Default channel")} />);
if(this.state.is_password_protected)
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-password"} iconClass={"client-register"} title={tr("The channel is password protected")} />); //TODO: "client-register" is really the right icon?
icons.push(<ClientIconRenderer key={"icon-protected"} icon={ClientIcon.Register} title={tr("The channel is password protected")} />);
if(this.state.is_music_quality)
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-music"} iconClass={"client-music"} title={tr("Music quality")} />);
icons.push(<ClientIconRenderer key={"icon-music"} icon={ClientIcon.Music} title={tr("Music quality")} />);
if(this.state.is_moderated)
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-moderated"} iconClass={"client-moderated"} title={tr("Channel is moderated")} />);
icons.push(<ClientIconRenderer key={"icon-moderated"} icon={ClientIcon.Moderated} title={tr("Channel is moderated")} />);
if(this.state.custom_icon_id)
icons.push(<LocalIconRenderer key={"icon-custom"} icon={this.props.channel.channelTree.client.fileManager.icons.load_icon(this.state.custom_icon_id)} title={tr("Client icon")} />);
@ -87,30 +114,46 @@ class ChannelEntryIcons extends ReactComponentBase<ChannelEntryIconsProperties,
@EventHandler<ChannelEvents>("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;
}
}
}

View File

@ -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());

View File

@ -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));
}
});
}

View File

@ -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));

View File

@ -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<T> {
resolve: (value?: T | PromiseLike<T>) => void;
@ -42,7 +43,9 @@ export class ServerConnection extends AbstractServerConnection {
private returnListeners: ReturnListener<CommandResult>[] = [];
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<CommandResult>((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();
}

View File

@ -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));
}
});

View File

@ -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();
}
});
}
});

View File

@ -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");

View File

@ -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 + "<br>" +
"Error: <code>" + JSON.stringify(error) + "</code>").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<BasicCodec | undefined> {
return new Promise<BasicCodec>((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"
});

View File

@ -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;

View File

@ -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 + "<br>" +
"Error: <code>" + JSON.stringify(error) + "</code>").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<BasicCodec | undefined> {
return new Promise<BasicCodec>((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<number> = {
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<RTCSessionDescription>;
}
}
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"
});
}