Reworked the connection API a bit and the voice unsupported icon now changes with the voice connection state
parent
ac773f5606
commit
92e5b72677
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"]);
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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} />
|
||||
);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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");
|
|
@ -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"
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue