Some minor changes
parent
e7ff40b0ee
commit
871231cbd5
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as fs from "fs-extra";
|
import * as fs from "fs-extra";
|
||||||
import {getKnownCountries} from "./js/i18n/country";
|
import {getKnownCountries} from "./js/i18n/CountryFlag";
|
||||||
|
|
||||||
const kIconsPath = path.join(__dirname, "img", "country-flags");
|
const kIconsPath = path.join(__dirname, "img", "country-flags");
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
|
} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
|
||||||
import {ClientEntry, ClientType, LocalClientEntry} from "tc-shared/tree/Client";
|
import {ClientEntry, ClientType, LocalClientEntry} from "tc-shared/tree/Client";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import * as i18nc from "tc-shared/i18n/country";
|
import * as i18nc from "./i18n/CountryFlag";
|
||||||
|
|
||||||
export type CachedClientInfoCategory = "name" | "description" | "online-state" | "country" | "volume" | "status" | "forum-account" | "group-channel" | "groups-server" | "version";
|
export type CachedClientInfoCategory = "name" | "description" | "online-state" | "country" | "volume" | "status" | "forum-account" | "group-channel" | "groups-server" | "version";
|
||||||
|
|
||||||
|
@ -231,7 +231,7 @@ export class SelectedClientInfo {
|
||||||
|
|
||||||
private initializeClientInfo(client: ClientEntry) {
|
private initializeClientInfo(client: ClientEntry) {
|
||||||
this.currentClientStatus = {
|
this.currentClientStatus = {
|
||||||
type: client instanceof LocalClientEntry ? "self" : client.properties.client_type === ClientType.CLIENT_QUERY ? "query" : "voice",
|
type: client instanceof LocalClientEntry ? "self" : client.getClientType() === ClientType.CLIENT_QUERY ? "query" : "voice",
|
||||||
name: client.properties.client_nickname,
|
name: client.properties.client_nickname,
|
||||||
databaseId: client.properties.client_database_id,
|
databaseId: client.properties.client_database_id,
|
||||||
uniqueId: client.properties.client_unique_identifier,
|
uniqueId: client.properties.client_unique_identifier,
|
||||||
|
|
|
@ -20,14 +20,14 @@ export abstract class AbstractCommandHandler {
|
||||||
abstract handle_command(command: ServerCommand) : boolean;
|
abstract handle_command(command: ServerCommand) : boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExplicitCommandHandler = (command: ServerCommand, consumed: boolean) => void | boolean;
|
export type CommandHandlerCallback = (command: ServerCommand, consumed: boolean) => void | boolean;
|
||||||
export abstract class AbstractCommandHandlerBoss {
|
export abstract class AbstractCommandHandlerBoss {
|
||||||
readonly connection: AbstractServerConnection;
|
readonly connection: AbstractServerConnection;
|
||||||
protected command_handlers: AbstractCommandHandler[] = [];
|
protected command_handlers: AbstractCommandHandler[] = [];
|
||||||
/* TODO: Timeout */
|
/* TODO: Timeout */
|
||||||
protected single_command_handler: SingleCommandHandler[] = [];
|
protected single_command_handler: SingleCommandHandler[] = [];
|
||||||
|
|
||||||
protected explicitHandlers: {[key: string]:ExplicitCommandHandler[]} = {};
|
protected explicitHandlers: {[key: string]:CommandHandlerCallback[]} = {};
|
||||||
|
|
||||||
protected constructor(connection: AbstractServerConnection) {
|
protected constructor(connection: AbstractServerConnection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
@ -38,14 +38,14 @@ export abstract class AbstractCommandHandlerBoss {
|
||||||
this.single_command_handler = undefined;
|
this.single_command_handler = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
register_explicit_handler(command: string, callback: ExplicitCommandHandler) : () => void {
|
registerCommandHandler(command: string, callback: CommandHandlerCallback) : () => void {
|
||||||
this.explicitHandlers[command] = this.explicitHandlers[command] || [];
|
this.explicitHandlers[command] = this.explicitHandlers[command] || [];
|
||||||
this.explicitHandlers[command].push(callback);
|
this.explicitHandlers[command].push(callback);
|
||||||
|
|
||||||
return () => this.explicitHandlers[command].remove(callback);
|
return () => this.explicitHandlers[command].remove(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
unregister_explicit_handler(command: string, callback: ExplicitCommandHandler) {
|
unregisterCommandHandler(command: string, callback: CommandHandlerCallback) {
|
||||||
if(!this.explicitHandlers[command])
|
if(!this.explicitHandlers[command])
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -53,16 +53,17 @@ export abstract class AbstractCommandHandlerBoss {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
register_handler(handler: AbstractCommandHandler) {
|
registerHandler(handler: AbstractCommandHandler) {
|
||||||
if(!handler.volatile_handler_boss && handler.handler_boss)
|
if(!handler.volatile_handler_boss && handler.handler_boss) {
|
||||||
throw "handler already registered";
|
throw "handler already registered";
|
||||||
|
}
|
||||||
|
|
||||||
this.command_handlers.remove(handler); /* just to be sure */
|
this.command_handlers.remove(handler); /* just to be sure */
|
||||||
this.command_handlers.push(handler);
|
this.command_handlers.push(handler);
|
||||||
handler.handler_boss = this;
|
handler.handler_boss = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
unregister_handler(handler: AbstractCommandHandler) {
|
unregisterHandler(handler: AbstractCommandHandler) {
|
||||||
if(!handler.volatile_handler_boss && handler.handler_boss !== this) {
|
if(!handler.volatile_handler_boss && handler.handler_boss !== this) {
|
||||||
logWarn(LogCategory.NETWORKING, tr("Tried to unregister command handler which does not belong to the handler boss"));
|
logWarn(LogCategory.NETWORKING, tr("Tried to unregister command handler which does not belong to the handler boss"));
|
||||||
return;
|
return;
|
||||||
|
@ -73,13 +74,13 @@ export abstract class AbstractCommandHandlerBoss {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
register_single_handler(handler: SingleCommandHandler) {
|
registerSingleHandler(handler: SingleCommandHandler) {
|
||||||
if(typeof handler.command === "string")
|
if(typeof handler.command === "string")
|
||||||
handler.command = [handler.command];
|
handler.command = [handler.command];
|
||||||
this.single_command_handler.push(handler);
|
this.single_command_handler.push(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_single_handler(handler: SingleCommandHandler) {
|
removeSingleHandler(handler: SingleCommandHandler) {
|
||||||
this.single_command_handler.remove(handler);
|
this.single_command_handler.remove(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ export abstract class AbstractCommandHandlerBoss {
|
||||||
return this.command_handlers;
|
return this.command_handlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
invoke_handle(command: ServerCommand) : boolean {
|
invokeCommand(command: ServerCommand) : boolean {
|
||||||
let flag_consumed = false;
|
let flag_consumed = false;
|
||||||
|
|
||||||
for(const handler of this.command_handlers) {
|
for(const handler of this.command_handlers) {
|
||||||
|
|
|
@ -154,7 +154,7 @@ export class ClientInfoResolver {
|
||||||
try {
|
try {
|
||||||
const requestDatabaseIds = Object.keys(this.requestDatabaseIds);
|
const requestDatabaseIds = Object.keys(this.requestDatabaseIds);
|
||||||
if(requestDatabaseIds.length > 0) {
|
if(requestDatabaseIds.length > 0) {
|
||||||
handlers.push(this.handler.serverConnection.command_handler_boss().register_explicit_handler("notifyclientgetnamefromdbid", command => {
|
handlers.push(this.handler.serverConnection.getCommandHandler().registerCommandHandler("notifyclientgetnamefromdbid", command => {
|
||||||
ClientInfoResolver.parseClientInfo(command.arguments).forEach(info => {
|
ClientInfoResolver.parseClientInfo(command.arguments).forEach(info => {
|
||||||
if(this.requestDatabaseIds[info.clientDatabaseId].fullFilled) {
|
if(this.requestDatabaseIds[info.clientDatabaseId].fullFilled) {
|
||||||
return;
|
return;
|
||||||
|
@ -197,7 +197,7 @@ export class ClientInfoResolver {
|
||||||
|
|
||||||
const requestUniqueIds = Object.keys(this.requestUniqueIds);
|
const requestUniqueIds = Object.keys(this.requestUniqueIds);
|
||||||
if(requestUniqueIds.length > 0) {
|
if(requestUniqueIds.length > 0) {
|
||||||
handlers.push(this.handler.serverConnection.command_handler_boss().register_explicit_handler("notifyclientnamefromuid", command => {
|
handlers.push(this.handler.serverConnection.getCommandHandler().registerCommandHandler("notifyclientnamefromuid", command => {
|
||||||
ClientInfoResolver.parseClientInfo(command.arguments).forEach(info => {
|
ClientInfoResolver.parseClientInfo(command.arguments).forEach(info => {
|
||||||
if(this.requestUniqueIds[info.clientUniqueId].fullFilled) {
|
if(this.requestUniqueIds[info.clientUniqueId].fullFilled) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -487,7 +487,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
client = tree.findClient(parseInt(entry["clid"]));
|
client = tree.findClient(parseInt(entry["clid"]));
|
||||||
|
|
||||||
if(!client) {
|
if(!client) {
|
||||||
if(parseInt(entry["client_type_exact"]) == ClientType.CLIENT_MUSIC) {
|
if(parseInt(entry["client_type_exact"]) == 4) {
|
||||||
client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]) as any;
|
client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]) as any;
|
||||||
} else {
|
} else {
|
||||||
client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]);
|
client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]);
|
||||||
|
@ -501,7 +501,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
tree.moveClient(client, channel);
|
tree.moveClient(client, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.connection_handler.areQueriesShown() || client.properties.client_type != ClientType.CLIENT_QUERY) {
|
if(this.connection_handler.areQueriesShown() || client.getClientType() !== ClientType.CLIENT_QUERY) {
|
||||||
const own_channel = this.connection.client.getClient().currentChannel();
|
const own_channel = this.connection.client.getClient().currentChannel();
|
||||||
this.connection_handler.log.log(channel == own_channel ? "client.view.enter.own.channel" : "client.view.enter", {
|
this.connection_handler.log.log(channel == own_channel ? "client.view.enter.own.channel" : "client.view.enter", {
|
||||||
channel_from: old_channel ? old_channel.log_data() : undefined,
|
channel_from: old_channel ? old_channel.log_data() : undefined,
|
||||||
|
@ -584,7 +584,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetChannelId = parseInt(entry["ctid"]);
|
const targetChannelId = parseInt(entry["ctid"]);
|
||||||
if(this.connection_handler.areQueriesShown() || client.properties.client_type != ClientType.CLIENT_QUERY) {
|
if(this.connection_handler.areQueriesShown() || client.getClientType() !== ClientType.CLIENT_QUERY) {
|
||||||
const own_channel = this.connection.client.getClient().currentChannel();
|
const own_channel = this.connection.client.getClient().currentChannel();
|
||||||
let channel_from = tree.findChannel(entry["cfid"]);
|
let channel_from = tree.findChannel(entry["cfid"]);
|
||||||
let channel_to = tree.findChannel(targetChannelId);
|
let channel_to = tree.findChannel(targetChannelId);
|
||||||
|
|
|
@ -24,13 +24,13 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.connection.command_handler_boss().register_handler(this);
|
this.connection.getCommandHandler().registerHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if(this.connection) {
|
if(this.connection) {
|
||||||
const hboss = this.connection.command_handler_boss();
|
const hboss = this.connection.getCommandHandler();
|
||||||
hboss?.unregister_handler(this);
|
hboss?.unregisterHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.infoByUniqueIdRequest = undefined;
|
this.infoByUniqueIdRequest = undefined;
|
||||||
|
@ -173,7 +173,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
let data = {};
|
let data = {};
|
||||||
if(server_id !== undefined) {
|
if(server_id !== undefined) {
|
||||||
|
@ -189,7 +189,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
reject(error);
|
reject(error);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -228,7 +228,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("playlistlist").catch(error => {
|
this.connection.send_command("playlistlist").catch(error => {
|
||||||
if(error instanceof CommandResult) {
|
if(error instanceof CommandResult) {
|
||||||
|
@ -239,7 +239,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
reject(error);
|
reject(error);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -294,7 +294,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}, { process_result: process_result }).catch(error => {
|
this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}, { process_result: process_result }).catch(error => {
|
||||||
if(error instanceof CommandResult) {
|
if(error instanceof CommandResult) {
|
||||||
|
@ -305,7 +305,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
reject(error);
|
reject(error);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -332,7 +332,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("playlistclientlist", {playlist_id: playlist_id}).catch(error => {
|
this.connection.send_command("playlistclientlist", {playlist_id: playlist_id}).catch(error => {
|
||||||
if(error instanceof CommandResult && error.id == ErrorCode.DATABASE_EMPTY_RESULT) {
|
if(error instanceof CommandResult && error.id == ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||||
|
@ -341,7 +341,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
reject(error);
|
reject(error);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -378,10 +378,10 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("servergroupclientlist", {sgid: group_id}).catch(reject).then(() => {
|
this.connection.send_command("servergroupclientlist", {sgid: group_id}).catch(reject).then(() => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -422,10 +422,10 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("playlistinfo", { playlist_id: playlist_id }).catch(reject).then(() => {
|
this.connection.send_command("playlistinfo", { playlist_id: playlist_id }).catch(reject).then(() => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -451,10 +451,10 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("whoami").catch(error => {
|
this.connection.send_command("whoami").catch(error => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -63,7 +63,7 @@ export abstract class AbstractServerConnection {
|
||||||
abstract getVoiceConnection() : AbstractVoiceConnection;
|
abstract getVoiceConnection() : AbstractVoiceConnection;
|
||||||
abstract getVideoConnection() : VideoConnection;
|
abstract getVideoConnection() : VideoConnection;
|
||||||
|
|
||||||
abstract command_handler_boss() : AbstractCommandHandlerBoss;
|
abstract getCommandHandler() : AbstractCommandHandlerBoss;
|
||||||
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
|
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
|
||||||
|
|
||||||
abstract remote_address() : ServerAddress; /* only valid when connected */
|
abstract remote_address() : ServerAddress; /* only valid when connected */
|
||||||
|
@ -89,6 +89,36 @@ export abstract class AbstractServerConnection {
|
||||||
export class ServerCommand {
|
export class ServerCommand {
|
||||||
command: string;
|
command: string;
|
||||||
arguments: any[];
|
arguments: any[];
|
||||||
|
switches: string[];
|
||||||
|
|
||||||
|
constructor(command: string, payload: any[], switches: string[]) {
|
||||||
|
this.command = command;
|
||||||
|
this.arguments = payload;
|
||||||
|
this.switches = switches;
|
||||||
|
}
|
||||||
|
|
||||||
|
getString(key: string, index?: number) {
|
||||||
|
const value = this.arguments[index || 0][key];
|
||||||
|
if(typeof value !== "string") {
|
||||||
|
throw "missing " + key + " at index " + (index || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInt(key: string, index?: number) {
|
||||||
|
const value = this.getString(key, index);
|
||||||
|
const intValue = parseInt(value);
|
||||||
|
if(isNaN(intValue)) {
|
||||||
|
throw "key " + key + " isn't an integer (value: " + value + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
return intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUInt(key: string, index?: number) {
|
||||||
|
return this.getInt(key, index) >>> 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SingleCommandHandler {
|
export interface SingleCommandHandler {
|
||||||
|
|
|
@ -97,11 +97,11 @@ export class PluginCmdRegistry {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
|
||||||
this.handler = new PluginCmdRegistryCommandHandler(connection.serverConnection, this.handlePluginCommand.bind(this));
|
this.handler = new PluginCmdRegistryCommandHandler(connection.serverConnection, this.handlePluginCommand.bind(this));
|
||||||
this.connection.serverConnection.command_handler_boss().register_handler(this.handler);
|
this.connection.serverConnection.getCommandHandler().registerHandler(this.handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.connection.serverConnection.command_handler_boss().unregister_handler(this.handler);
|
this.connection.serverConnection.getCommandHandler().unregisterHandler(this.handler);
|
||||||
|
|
||||||
Object.keys(this.handlerMap).map(e => this.handlerMap[e]).forEach(handler => {
|
Object.keys(this.handlerMap).map(e => this.handlerMap[e]).forEach(handler => {
|
||||||
handler["currentServerConnection"] = undefined;
|
handler["currentServerConnection"] = undefined;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Registry} from "../events";
|
||||||
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
||||||
import {ErrorCode} from "../connection/ErrorCode";
|
import {ErrorCode} from "../connection/ErrorCode";
|
||||||
import {LogCategory, logDebug, logTrace, logWarn} from "../log";
|
import {LogCategory, logDebug, logTrace, logWarn} from "../log";
|
||||||
import {ExplicitCommandHandler} from "../connection/AbstractCommandHandler";
|
import {CommandHandlerCallback} from "../connection/AbstractCommandHandler";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import { tr } from "tc-shared/i18n/localize";
|
||||||
|
|
||||||
export type ServerFeatureSupport = "unsupported" | "supported" | "experimental" | "deprecated";
|
export type ServerFeatureSupport = "unsupported" | "supported" | "experimental" | "deprecated";
|
||||||
|
@ -28,7 +28,7 @@ export interface ServerFeatureEvents {
|
||||||
export class ServerFeatures {
|
export class ServerFeatures {
|
||||||
readonly events: Registry<ServerFeatureEvents>;
|
readonly events: Registry<ServerFeatureEvents>;
|
||||||
private readonly connection: ConnectionHandler;
|
private readonly connection: ConnectionHandler;
|
||||||
private readonly explicitCommandHandler: ExplicitCommandHandler;
|
private readonly explicitCommandHandler: CommandHandlerCallback;
|
||||||
private readonly stateChangeListener: () => void;
|
private readonly stateChangeListener: () => void;
|
||||||
|
|
||||||
private featureAwait: Promise<boolean>;
|
private featureAwait: Promise<boolean>;
|
||||||
|
@ -41,7 +41,7 @@ export class ServerFeatures {
|
||||||
this.events = new Registry<ServerFeatureEvents>();
|
this.events = new Registry<ServerFeatureEvents>();
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
|
||||||
this.connection.getServerConnection().command_handler_boss().register_explicit_handler("notifyfeaturesupport", this.explicitCommandHandler = command => {
|
this.connection.getServerConnection().getCommandHandler().registerCommandHandler("notifyfeaturesupport", this.explicitCommandHandler = command => {
|
||||||
for(const set of command.arguments) {
|
for(const set of command.arguments) {
|
||||||
let support: ServerFeatureSupport;
|
let support: ServerFeatureSupport;
|
||||||
switch (parseInt(set["support"])) {
|
switch (parseInt(set["support"])) {
|
||||||
|
@ -96,7 +96,7 @@ export class ServerFeatures {
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.stateChangeListener();
|
this.stateChangeListener();
|
||||||
this.connection.getServerConnection()?.command_handler_boss()?.unregister_explicit_handler("notifyfeaturesupport", this.explicitCommandHandler);
|
this.connection.getServerConnection()?.getCommandHandler()?.unregisterCommandHandler("notifyfeaturesupport", this.explicitCommandHandler);
|
||||||
|
|
||||||
if(this.featureAwaitCallback) {
|
if(this.featureAwaitCallback) {
|
||||||
this.featureAwaitCallback(false);
|
this.featureAwaitCallback(false);
|
||||||
|
|
|
@ -69,8 +69,17 @@ export interface VideoClient {
|
||||||
showPip(broadcastType: VideoBroadcastType) : Promise<void>;
|
showPip(broadcastType: VideoBroadcastType) : Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VideoBroadcastViewer = {
|
||||||
|
clientId: number,
|
||||||
|
clientName: string,
|
||||||
|
clientUniqueId: string,
|
||||||
|
clientDatabaseId: number,
|
||||||
|
};
|
||||||
|
|
||||||
export interface LocalVideoBroadcastEvents {
|
export interface LocalVideoBroadcastEvents {
|
||||||
notify_state_changed: { oldState: LocalVideoBroadcastState, newState: LocalVideoBroadcastState },
|
notify_state_changed: { oldState: LocalVideoBroadcastState, newState: LocalVideoBroadcastState },
|
||||||
|
notify_clients_joined: { clients: VideoBroadcastViewer[] },
|
||||||
|
notify_clients_left: { clientIds: number[] },
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LocalVideoBroadcastState = {
|
export type LocalVideoBroadcastState = {
|
||||||
|
@ -148,6 +157,8 @@ export interface LocalVideoBroadcast {
|
||||||
getConstraints() : VideoBroadcastConfig | undefined;
|
getConstraints() : VideoBroadcastConfig | undefined;
|
||||||
|
|
||||||
stopBroadcasting();
|
stopBroadcasting();
|
||||||
|
|
||||||
|
getViewer() : VideoBroadcastViewer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoConnection {
|
export interface VideoConnection {
|
||||||
|
|
|
@ -449,19 +449,6 @@ export type RTCSourceTrackType = "audio" | "audio-whisper" | "video" | "video-sc
|
||||||
export type RTCBroadcastableTrackType = Exclude<RTCSourceTrackType, "audio-whisper">;
|
export type RTCBroadcastableTrackType = Exclude<RTCSourceTrackType, "audio-whisper">;
|
||||||
const kRtcSourceTrackTypes: RTCSourceTrackType[] = ["audio", "audio-whisper", "video", "video-screen"];
|
const kRtcSourceTrackTypes: RTCSourceTrackType[] = ["audio", "audio-whisper", "video", "video-screen"];
|
||||||
|
|
||||||
function broadcastableTrackTypeToNumber(type: RTCBroadcastableTrackType) : number {
|
|
||||||
switch (type) {
|
|
||||||
case "video-screen":
|
|
||||||
return 3;
|
|
||||||
case "video":
|
|
||||||
return 2;
|
|
||||||
case "audio":
|
|
||||||
return 1;
|
|
||||||
default:
|
|
||||||
throw tr("invalid target type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TemporaryRtpStream = {
|
type TemporaryRtpStream = {
|
||||||
createTimestamp: number,
|
createTimestamp: number,
|
||||||
timeoutId: number,
|
timeoutId: number,
|
||||||
|
@ -534,16 +521,15 @@ export class RTCConnection {
|
||||||
this.retryCalculator = new RetryTimeCalculator(5000, 30000, 10000);
|
this.retryCalculator = new RetryTimeCalculator(5000, 30000, 10000);
|
||||||
this.audioSupport = audioSupport;
|
this.audioSupport = audioSupport;
|
||||||
|
|
||||||
this.connection.command_handler_boss().register_handler(this.commandHandler);
|
this.connection.getCommandHandler().registerHandler(this.commandHandler);
|
||||||
this.reset(true);
|
this.reset(true);
|
||||||
|
|
||||||
this.connection.events.on("notify_connection_state_changed", event => this.handleConnectionStateChanged(event));
|
this.connection.events.on("notify_connection_state_changed", event => this.handleConnectionStateChanged(event));
|
||||||
|
|
||||||
(window as any).rtp = this;
|
(window as any).rtp = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.connection.command_handler_boss().unregister_handler(this.commandHandler);
|
this.connection.getCommandHandler().unregisterHandler(this.commandHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
isAudioEnabled() : boolean {
|
isAudioEnabled() : boolean {
|
||||||
|
@ -680,6 +666,21 @@ export class RTCConnection {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTrackTypeFromSsrc(ssrc: number) : RTCSourceTrackType | undefined {
|
||||||
|
const mediaId = this.sdpProcessor.getLocalMediaIdFromSsrc(ssrc);
|
||||||
|
if(!mediaId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const type of kRtcSourceTrackTypes) {
|
||||||
|
if(this.currentTransceiver[type]?.mid === mediaId) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public async startVideoBroadcast(type: VideoBroadcastType, config: VideoBroadcastConfig) {
|
public async startVideoBroadcast(type: VideoBroadcastType, config: VideoBroadcastConfig) {
|
||||||
let track: RTCBroadcastableTrackType;
|
let track: RTCBroadcastableTrackType;
|
||||||
let broadcastType: number;
|
let broadcastType: number;
|
||||||
|
|
|
@ -83,6 +83,16 @@ export class SdpProcessor {
|
||||||
return this.rtpLocalChannelMapping[mediaId];
|
return this.rtpLocalChannelMapping[mediaId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLocalMediaIdFromSsrc(ssrc: number) : string | undefined {
|
||||||
|
for(const key of Object.keys(this.rtpLocalChannelMapping)) {
|
||||||
|
if(this.rtpLocalChannelMapping[key] === ssrc) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
processIncomingSdp(sdpString: string, _mode: "offer" | "answer") : string {
|
processIncomingSdp(sdpString: string, _mode: "offer" | "answer") : string {
|
||||||
/* The server somehow does not encode the level id in hex */
|
/* The server somehow does not encode the level id in hex */
|
||||||
sdpString = sdpString.replace(/profile-level-id=4325407/g, "profile-level-id=4d0028");
|
sdpString = sdpString.replace(/profile-level-id=4325407/g, "profile-level-id=4d0028");
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
VideoBroadcastConfig,
|
VideoBroadcastConfig,
|
||||||
VideoBroadcastStatistics,
|
VideoBroadcastStatistics,
|
||||||
VideoBroadcastType,
|
VideoBroadcastType,
|
||||||
|
VideoBroadcastViewer,
|
||||||
VideoClient,
|
VideoClient,
|
||||||
VideoConnection,
|
VideoConnection,
|
||||||
VideoConnectionEvent,
|
VideoConnectionEvent,
|
||||||
|
@ -33,12 +34,14 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
|
||||||
private broadcastStartId: number;
|
private broadcastStartId: number;
|
||||||
|
|
||||||
private localStartPromise: Promise<void>;
|
private localStartPromise: Promise<void>;
|
||||||
|
private subscribedClients: VideoBroadcastViewer[];
|
||||||
|
|
||||||
constructor(handle: RtpVideoConnection, type: VideoBroadcastType) {
|
constructor(handle: RtpVideoConnection, type: VideoBroadcastType) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.broadcastStartId = 0;
|
this.broadcastStartId = 0;
|
||||||
|
|
||||||
|
this.subscribedClients = [];
|
||||||
this.events = new Registry<LocalVideoBroadcastEvents>();
|
this.events = new Registry<LocalVideoBroadcastEvents>();
|
||||||
this.state = { state: "stopped" };
|
this.state = { state: "stopped" };
|
||||||
}
|
}
|
||||||
|
@ -67,12 +70,31 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
|
||||||
const oldState = this.state;
|
const oldState = this.state;
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
this.events.fire("notify_state_changed", { oldState: oldState, newState: newState });
|
this.events.fire("notify_state_changed", { oldState: oldState, newState: newState });
|
||||||
|
|
||||||
|
if(this.subscribedClients.length > 0) {
|
||||||
|
switch (newState.state) {
|
||||||
|
case "broadcasting":
|
||||||
|
case "initializing":
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "failed":
|
||||||
|
case "stopped":
|
||||||
|
default:
|
||||||
|
const clientIds = this.subscribedClients.map(client => client.clientId);
|
||||||
|
this.subscribedClients = [];
|
||||||
|
this.events.fire("notify_clients_left", { clientIds: clientIds });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatistics(): Promise<VideoBroadcastStatistics | undefined> {
|
getStatistics(): Promise<VideoBroadcastStatistics | undefined> {
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getViewer(): VideoBroadcastViewer[] {
|
||||||
|
return this.subscribedClients;
|
||||||
|
}
|
||||||
|
|
||||||
async changeSource(source: VideoSource, constraints: VideoBroadcastConfig): Promise<void> {
|
async changeSource(source: VideoSource, constraints: VideoBroadcastConfig): Promise<void> {
|
||||||
let sourceRef = source.ref();
|
let sourceRef = source.ref();
|
||||||
try {
|
try {
|
||||||
|
@ -333,6 +355,26 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
|
||||||
getConstraints(): VideoBroadcastConfig | undefined {
|
getConstraints(): VideoBroadcastConfig | undefined {
|
||||||
return this.currentConfig;
|
return this.currentConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleClientJoined(client: VideoBroadcastViewer) {
|
||||||
|
const index = this.subscribedClients.findIndex(client_ => client_.clientId === client.clientId);
|
||||||
|
if(index === -1) {
|
||||||
|
this.subscribedClients.push(client);
|
||||||
|
this.events.fire("notify_clients_joined", { clients: [ client ] });
|
||||||
|
} else {
|
||||||
|
this.subscribedClients[index] = client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClientLeave(clientId: number) {
|
||||||
|
const index = this.subscribedClients.findIndex(client => client.clientId === clientId);
|
||||||
|
if(index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscribedClients.splice(index, 1);
|
||||||
|
this.events.fire("notify_clients_left", { clientIds: [ clientId ] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RtpVideoConnection implements VideoConnection {
|
export class RtpVideoConnection implements VideoConnection {
|
||||||
|
@ -355,7 +397,7 @@ export class RtpVideoConnection implements VideoConnection {
|
||||||
this.listener = [];
|
this.listener = [];
|
||||||
|
|
||||||
/* We only have to listen for move events since if the client is leaving the broadcast will be terminated anyways */
|
/* We only have to listen for move events since if the client is leaving the broadcast will be terminated anyways */
|
||||||
this.listener.push(this.rtcConnection.getConnection().command_handler_boss().register_explicit_handler("notifyclientmoved", event => {
|
this.listener.push(this.rtcConnection.getConnection().getCommandHandler().registerCommandHandler("notifyclientmoved", event => {
|
||||||
const localClient = this.rtcConnection.getConnection().client.getClient();
|
const localClient = this.rtcConnection.getConnection().client.getClient();
|
||||||
for(const data of event.arguments) {
|
for(const data of event.arguments) {
|
||||||
const clientId = parseInt(data["clid"]);
|
const clientId = parseInt(data["clid"]);
|
||||||
|
@ -380,7 +422,7 @@ export class RtpVideoConnection implements VideoConnection {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.listener.push(this.rtcConnection.getConnection().command_handler_boss().register_explicit_handler("notifybroadcastvideo", event => {
|
this.listener.push(this.rtcConnection.getConnection().getCommandHandler().registerCommandHandler("notifybroadcastvideo", event => {
|
||||||
const assignedClients: { clientId: number, broadcastType: VideoBroadcastType }[] = [];
|
const assignedClients: { clientId: number, broadcastType: VideoBroadcastType }[] = [];
|
||||||
for(const data of event.arguments) {
|
for(const data of event.arguments) {
|
||||||
if(!("bid" in data)) {
|
if(!("bid" in data)) {
|
||||||
|
@ -428,6 +470,76 @@ export class RtpVideoConnection implements VideoConnection {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.listener.push(this.rtcConnection.getConnection().getCommandHandler().registerCommandHandler("notifystreamjoined", command => {
|
||||||
|
const streamId = command.getUInt("streamid");
|
||||||
|
const clientInfo: VideoBroadcastViewer = {
|
||||||
|
clientId: command.getUInt("clid"),
|
||||||
|
clientName: command.getString("clname"),
|
||||||
|
clientUniqueId: command.getString("cluid"),
|
||||||
|
clientDatabaseId: command.getUInt("cldbid")
|
||||||
|
};
|
||||||
|
|
||||||
|
if(clientInfo.clientId === this.rtcConnection.getConnection().client.getClientId()) {
|
||||||
|
/* Just our local video preview */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastType = this.rtcConnection.getTrackTypeFromSsrc(streamId);
|
||||||
|
if(!broadcastType) {
|
||||||
|
logError(LogCategory.NETWORKING, tr("Received stream join notify for invalid stream (ssrc: %o)"), streamId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let broadcast: LocalRtpVideoBroadcast;
|
||||||
|
switch (broadcastType) {
|
||||||
|
case "video":
|
||||||
|
broadcast = this.broadcasts["camera"];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "video-screen":
|
||||||
|
broadcast = this.broadcasts["screen"];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
case "audio-whisper":
|
||||||
|
default:
|
||||||
|
logError(LogCategory.NETWORKING, tr("Received stream join notify for invalid stream type: %s"), broadcastType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast.handleClientJoined(clientInfo);
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.listener.push(this.rtcConnection.getConnection().getCommandHandler().registerCommandHandler("notifystreamleft", command => {
|
||||||
|
const streamId = command.getUInt("streamid");
|
||||||
|
const clientId = command.getUInt("clid");
|
||||||
|
|
||||||
|
const broadcastType = this.rtcConnection.getTrackTypeFromSsrc(streamId);
|
||||||
|
if(!broadcastType) {
|
||||||
|
logError(LogCategory.NETWORKING, tr("Received stream leave notify for invalid stream (ssrc: %o)"), streamId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let broadcast: LocalRtpVideoBroadcast;
|
||||||
|
switch (broadcastType) {
|
||||||
|
case "video":
|
||||||
|
broadcast = this.broadcasts["camera"];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "video-screen":
|
||||||
|
broadcast = this.broadcasts["screen"];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
case "audio-whisper":
|
||||||
|
default:
|
||||||
|
logError(LogCategory.NETWORKING, tr("Received stream leave notify for invalid stream type: %s"), broadcastType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast.handleClientLeave(clientId);
|
||||||
|
}));
|
||||||
|
|
||||||
this.listener.push(this.rtcConnection.getConnection().events.on("notify_connection_state_changed", event => {
|
this.listener.push(this.rtcConnection.getConnection().events.on("notify_connection_state_changed", event => {
|
||||||
if(event.newState !== ConnectionState.CONNECTED) {
|
if(event.newState !== ConnectionState.CONNECTED) {
|
||||||
Object.values(this.broadcasts).forEach(broadcast => broadcast.stopBroadcasting(true));
|
Object.values(this.broadcasts).forEach(broadcast => broadcast.stopBroadcasting(true));
|
||||||
|
|
|
@ -82,11 +82,6 @@ export class RtpVideoClient implements VideoClient {
|
||||||
await this.handle.getConnection().send_command("broadcastvideojoin", {
|
await this.handle.getConnection().send_command("broadcastvideojoin", {
|
||||||
bid: this.broadcastIds[broadcastType],
|
bid: this.broadcastIds[broadcastType],
|
||||||
bt: broadcastType === "camera" ? 0 : 1
|
bt: broadcastType === "camera" ? 0 : 1
|
||||||
}).then(() => {
|
|
||||||
/* the broadcast state should switch automatically to running since we got an RTP stream now */
|
|
||||||
if(this.trackStates[broadcastType] === VideoBroadcastState.Initializing) {
|
|
||||||
throw tr("failed to receive stream");
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.joinedStates[broadcastType] = false;
|
this.joinedStates[broadcastType] = false;
|
||||||
this.updateBroadcastState(broadcastType);
|
this.updateBroadcastState(broadcastType);
|
||||||
|
|
|
@ -395,9 +395,9 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
||||||
|
|
||||||
/* TODO: Permission listener for text send power! */
|
/* TODO: Permission listener for text send power! */
|
||||||
|
|
||||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationhistory", this.handleConversationHistory.bind(this)));
|
this.listenerConnection.push(connection.serverConnection.getCommandHandler().registerCommandHandler("notifyconversationhistory", this.handleConversationHistory.bind(this)));
|
||||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationindex", this.handleConversationIndex.bind(this)));
|
this.listenerConnection.push(connection.serverConnection.getCommandHandler().registerCommandHandler("notifyconversationindex", this.handleConversationIndex.bind(this)));
|
||||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationmessagedelete", this.handleConversationMessageDelete.bind(this)));
|
this.listenerConnection.push(connection.serverConnection.getCommandHandler().registerCommandHandler("notifyconversationmessagedelete", this.handleConversationMessageDelete.bind(this)));
|
||||||
|
|
||||||
this.listenerConnection.push(this.connection.channelTree.events.on("notify_channel_list_received", () => {
|
this.listenerConnection.push(this.connection.channelTree.events.on("notify_channel_list_received", () => {
|
||||||
this.queryUnreadFlags();
|
this.queryUnreadFlags();
|
||||||
|
|
|
@ -64,13 +64,13 @@ class FileCommandHandler extends AbstractCommandHandler {
|
||||||
super(manager.connectionHandler.serverConnection);
|
super(manager.connectionHandler.serverConnection);
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
|
||||||
this.connection.command_handler_boss().register_handler(this);
|
this.connection.getCommandHandler().registerHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if(this.connection) {
|
if(this.connection) {
|
||||||
const hboss = this.connection.command_handler_boss();
|
const hboss = this.connection.getCommandHandler();
|
||||||
if(hboss) hboss.unregister_handler(this);
|
if(hboss) hboss.unregisterHandler(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
"key": "de_gt",
|
||||||
|
"country_code": "de",
|
||||||
|
"path": "de_google_translate.translation",
|
||||||
|
|
||||||
|
"name": "German translation, based on Google Translate",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Markus Hadenfeldt",
|
||||||
|
"email": "i18n.client@teaspeak.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {CountryFlag} from "svg-sprites/country-flags";
|
||||||
|
import {AbstractTranslationResolver} from "tc-shared/i18n/Translation";
|
||||||
|
|
||||||
|
export type I18NContributor = {
|
||||||
|
name: string,
|
||||||
|
email: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TranslationResolverCreateResult = {
|
||||||
|
status: "success",
|
||||||
|
resolver: AbstractTranslationResolver
|
||||||
|
} | {
|
||||||
|
status: "error",
|
||||||
|
message: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class I18NTranslation {
|
||||||
|
abstract getId() : string;
|
||||||
|
abstract getName() : string;
|
||||||
|
|
||||||
|
abstract getCountry() : CountryFlag;
|
||||||
|
abstract getDescription() : string;
|
||||||
|
abstract getContributors() : I18NContributor[];
|
||||||
|
|
||||||
|
abstract createTranslationResolver() : Promise<TranslationResolverCreateResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class I18NRepository {
|
||||||
|
abstract getName() : string;
|
||||||
|
abstract getDescription() : string;
|
||||||
|
|
||||||
|
abstract getTranslations() : Promise<I18NTranslation[]>;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import {LogCategory, logError} from "tc-shared/log";
|
||||||
|
|
||||||
|
export abstract class AbstractTranslationResolver {
|
||||||
|
private translationCache: { [key: string]: string };
|
||||||
|
|
||||||
|
protected constructor() {
|
||||||
|
this.translationCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the target message.
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
|
public translateMessage(message: string) {
|
||||||
|
/* typeof is the fastest method */
|
||||||
|
if(typeof this.translationCache === "string") {
|
||||||
|
return this.translationCache[message];
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = this.translationCache[message] = this.resolveTranslation(message);
|
||||||
|
} catch (error) {
|
||||||
|
/* Don't translate this message because it could cause an infinite loop */
|
||||||
|
logError(LogCategory.I18N, "Failed to resolve translation message: %o", error);
|
||||||
|
result = this.translationCache[message] = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected invalidateCache() {
|
||||||
|
this.translationCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a translation into the cache.
|
||||||
|
* @param message
|
||||||
|
* @param translation
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected registerTranslation(message: string, translation: string) {
|
||||||
|
this.translationCache[message] = translation;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract resolveTranslation(message: string) : string;
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ export async function requestMediaStreamWithConstraints(constraints: MediaTrackC
|
||||||
const beginTimestamp = Date.now();
|
const beginTimestamp = Date.now();
|
||||||
try {
|
try {
|
||||||
logInfo(LogCategory.AUDIO, tr("Requesting a %s stream for device %s in group %s"), type, constraints.deviceId, constraints.groupId);
|
logInfo(LogCategory.AUDIO, tr("Requesting a %s stream for device %s in group %s"), type, constraints.deviceId, constraints.groupId);
|
||||||
return await navigator.mediaDevices.getUserMedia(type === "audio" ? {audio: constraints} : {video: constraints});
|
return await navigator.mediaDevices.getUserMedia(type === "audio" ? { audio: constraints } : { video: constraints });
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
if('name' in error) {
|
if('name' in error) {
|
||||||
if(error.name === "NotAllowedError") {
|
if(error.name === "NotAllowedError") {
|
||||||
|
|
|
@ -201,7 +201,7 @@ class InternalSubscribedPlaylist extends SubscribedPlaylist {
|
||||||
this.listenerConnection = [];
|
this.listenerConnection = [];
|
||||||
|
|
||||||
const serverConnection = this.handle.connection.serverConnection;
|
const serverConnection = this.handle.connection.serverConnection;
|
||||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongadd", command => {
|
this.listenerConnection.push(serverConnection.getCommandHandler().registerCommandHandler("notifyplaylistsongadd", command => {
|
||||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||||
if(isNaN(playlistId)) {
|
if(isNaN(playlistId)) {
|
||||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song add notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
logWarn(LogCategory.NETWORKING, tr("Received a playlist song add notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||||
|
@ -220,7 +220,7 @@ class InternalSubscribedPlaylist extends SubscribedPlaylist {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongremove", command => {
|
this.listenerConnection.push(serverConnection.getCommandHandler().registerCommandHandler("notifyplaylistsongremove", command => {
|
||||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||||
if(isNaN(playlistId)) {
|
if(isNaN(playlistId)) {
|
||||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song remove notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
logWarn(LogCategory.NETWORKING, tr("Received a playlist song remove notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||||
|
@ -243,7 +243,7 @@ class InternalSubscribedPlaylist extends SubscribedPlaylist {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongreorder", command => {
|
this.listenerConnection.push(serverConnection.getCommandHandler().registerCommandHandler("notifyplaylistsongreorder", command => {
|
||||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||||
if(isNaN(playlistId)) {
|
if(isNaN(playlistId)) {
|
||||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song reorder notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
logWarn(LogCategory.NETWORKING, tr("Received a playlist song reorder notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||||
|
@ -283,7 +283,7 @@ class InternalSubscribedPlaylist extends SubscribedPlaylist {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongloaded", command => {
|
this.listenerConnection.push(serverConnection.getCommandHandler().registerCommandHandler("notifyplaylistsongloaded", command => {
|
||||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||||
if(isNaN(playlistId)) {
|
if(isNaN(playlistId)) {
|
||||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song loaded notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
logWarn(LogCategory.NETWORKING, tr("Received a playlist song loaded notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||||
|
@ -470,7 +470,7 @@ export class PlaylistManager {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.listenerConnection = [];
|
this.listenerConnection = [];
|
||||||
|
|
||||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsonglist", command => {
|
this.listenerConnection.push(connection.serverConnection.getCommandHandler().registerCommandHandler("notifyplaylistsonglist", command => {
|
||||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||||
if(isNaN(playlistId)) {
|
if(isNaN(playlistId)) {
|
||||||
logWarn(LogCategory.NETWORKING, tr("Received playlist song list notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
logWarn(LogCategory.NETWORKING, tr("Received playlist song list notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||||
|
|
|
@ -168,7 +168,7 @@ export class GroupManager extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
client.serverConnection.command_handler_boss().register_handler(this);
|
client.serverConnection.getCommandHandler().registerHandler(this);
|
||||||
client.events().on("notify_connection_state_changed", this.connectionStateListener);
|
client.events().on("notify_connection_state_changed", this.connectionStateListener);
|
||||||
|
|
||||||
this.reset();
|
this.reset();
|
||||||
|
@ -177,7 +177,7 @@ export class GroupManager extends AbstractCommandHandler {
|
||||||
destroy() {
|
destroy() {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.connectionHandler.events().off("notify_connection_state_changed", this.connectionStateListener);
|
this.connectionHandler.events().off("notify_connection_state_changed", this.connectionStateListener);
|
||||||
this.connectionHandler.serverConnection?.command_handler_boss().unregister_handler(this);
|
this.connectionHandler.serverConnection?.getCommandHandler().unregisterHandler(this);
|
||||||
this.serverGroups = undefined;
|
this.serverGroups = undefined;
|
||||||
this.channelGroups = undefined;
|
this.channelGroups = undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -260,13 +260,13 @@ export class PermissionManager extends AbstractCommandHandler {
|
||||||
|
|
||||||
//FIXME? Dont register the handler like this?
|
//FIXME? Dont register the handler like this?
|
||||||
this.volatile_handler_boss = true;
|
this.volatile_handler_boss = true;
|
||||||
client.serverConnection.command_handler_boss().register_handler(this);
|
client.serverConnection.getCommandHandler().registerHandler(this);
|
||||||
|
|
||||||
this.handle = client;
|
this.handle = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.handle.serverConnection && this.handle.serverConnection.command_handler_boss().unregister_handler(this);
|
this.handle.serverConnection && this.handle.serverConnection.getCommandHandler().unregisterHandler(this);
|
||||||
this.needed_permission_change_listener = {};
|
this.needed_permission_change_listener = {};
|
||||||
|
|
||||||
this.permissionList = undefined;
|
this.permissionList = undefined;
|
||||||
|
@ -712,10 +712,10 @@ export class PermissionManager extends AbstractCommandHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.registerSingleHandler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("permfind", permission_ids.map(e => { return {permid: e }})).catch(error => {
|
this.connection.send_command("permfind", permission_ids.map(e => { return {permid: e }})).catch(error => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.removeSingleHandler(single_handler);
|
||||||
|
|
||||||
if(error instanceof CommandResult && error.id == ErrorCode.DATABASE_EMPTY_RESULT) {
|
if(error instanceof CommandResult && error.id == ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||||
resolve([]);
|
resolve([]);
|
||||||
|
|
|
@ -23,7 +23,7 @@ class NameHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
executeHandshake() {
|
executeHandshake() {
|
||||||
this.connection.command_handler_boss().register_handler(this.handler);
|
this.connection.getCommandHandler().registerHandler(this.handler);
|
||||||
this.connection.send_command("handshakebegin", {
|
this.connection.send_command("handshakebegin", {
|
||||||
intention: 0,
|
intention: 0,
|
||||||
authentication_method: this.identity.type(),
|
authentication_method: this.identity.type(),
|
||||||
|
@ -37,12 +37,12 @@ class NameHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected trigger_fail(message: string) {
|
protected trigger_fail(message: string) {
|
||||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||||
super.trigger_fail(message);
|
super.trigger_fail(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected trigger_success() {
|
protected trigger_success() {
|
||||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||||
super.trigger_success();
|
super.trigger_success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
executeHandshake() {
|
executeHandshake() {
|
||||||
this.connection.command_handler_boss().register_handler(this.handler);
|
this.connection.getCommandHandler().registerHandler(this.handler);
|
||||||
this.connection.send_command("handshakebegin", {
|
this.connection.send_command("handshakebegin", {
|
||||||
intention: 0,
|
intention: 0,
|
||||||
authentication_method: this.identity.type(),
|
authentication_method: this.identity.type(),
|
||||||
|
@ -51,12 +51,12 @@ class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected trigger_fail(message: string) {
|
protected trigger_fail(message: string) {
|
||||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||||
super.trigger_fail(message);
|
super.trigger_fail(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected trigger_success() {
|
protected trigger_success() {
|
||||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||||
super.trigger_success();
|
super.trigger_success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,7 +237,7 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
executeHandshake() {
|
executeHandshake() {
|
||||||
this.connection.command_handler_boss().register_handler(this.handler);
|
this.connection.getCommandHandler().registerHandler(this.handler);
|
||||||
this.connection.send_command("handshakebegin", {
|
this.connection.send_command("handshakebegin", {
|
||||||
intention: 0,
|
intention: 0,
|
||||||
authentication_method: this.identity.type(),
|
authentication_method: this.identity.type(),
|
||||||
|
@ -271,12 +271,12 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected trigger_fail(message: string) {
|
protected trigger_fail(message: string) {
|
||||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||||
super.trigger_fail(message);
|
super.trigger_fail(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected trigger_success() {
|
protected trigger_success() {
|
||||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||||
super.trigger_success();
|
super.trigger_success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,18 +31,19 @@ import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
|
||||||
import {spawnServerGroupAssignments} from "tc-shared/ui/modal/group-assignment/Controller";
|
import {spawnServerGroupAssignments} from "tc-shared/ui/modal/group-assignment/Controller";
|
||||||
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
|
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
|
||||||
|
|
||||||
|
/* Must be the same as the TeaSpeak servers enum values */
|
||||||
export enum ClientType {
|
export enum ClientType {
|
||||||
CLIENT_VOICE,
|
CLIENT_VOICE = 0,
|
||||||
CLIENT_QUERY,
|
CLIENT_QUERY = 1,
|
||||||
CLIENT_INTERNAL,
|
CLIENT_WEB = 3,
|
||||||
CLIENT_WEB,
|
CLIENT_MUSIC = 4,
|
||||||
CLIENT_MUSIC,
|
CLIENT_TEASPEAK = 5,
|
||||||
CLIENT_UNDEFINED
|
CLIENT_UNDEFINED = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClientProperties {
|
export class ClientProperties {
|
||||||
client_type: ClientType = ClientType.CLIENT_VOICE; //TeamSpeaks type
|
client_type: ClientType = ClientType.CLIENT_VOICE; //TeamSpeaks type
|
||||||
client_type_exact: ClientType = ClientType.CLIENT_VOICE;
|
client_type_exact: ClientType = ClientType.CLIENT_UNDEFINED;
|
||||||
|
|
||||||
client_database_id: number = 0;
|
client_database_id: number = 0;
|
||||||
client_version: string = "";
|
client_version: string = "";
|
||||||
|
@ -912,6 +913,44 @@ export class ClientEntry<Events extends ClientEvents = ClientEvents> extends Cha
|
||||||
getAudioVolume() {
|
getAudioVolume() {
|
||||||
return this.voiceVolume;
|
return this.voiceVolume;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getClientType() : ClientType {
|
||||||
|
if(this.properties.client_type_exact === ClientType.CLIENT_UNDEFINED) {
|
||||||
|
/* We're on a TS3 server */
|
||||||
|
switch(this.properties.client_type) {
|
||||||
|
case 0:
|
||||||
|
return ClientType.CLIENT_VOICE;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return ClientType.CLIENT_QUERY;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ClientType.CLIENT_UNDEFINED;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch(this.properties.client_type_exact) {
|
||||||
|
case 0:
|
||||||
|
return ClientType.CLIENT_VOICE;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return ClientType.CLIENT_QUERY;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return ClientType.CLIENT_WEB;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
return ClientType.CLIENT_MUSIC;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
return ClientType.CLIENT_TEASPEAK;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
/* 2 is the internal client type which should never be visible for the target user */
|
||||||
|
default:
|
||||||
|
return ClientType.CLIENT_UNDEFINED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LocalClientEntry extends ClientEntry {
|
export class LocalClientEntry extends ClientEntry {
|
||||||
|
|
|
@ -307,7 +307,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
const connection = this.channelTree.client.serverConnection;
|
const connection = this.channelTree.client.serverConnection;
|
||||||
|
|
||||||
let result: ServerConnectionInfoResult = { status: "error", message: "missing notify" };
|
let result: ServerConnectionInfoResult = { status: "error", message: "missing notify" };
|
||||||
const handlerUnregister = connection.command_handler_boss().register_explicit_handler("notifyserverconnectioninfo", command => {
|
const handlerUnregister = connection.getCommandHandler().registerCommandHandler("notifyserverconnectioninfo", command => {
|
||||||
const payload = command.arguments[0];
|
const payload = command.arguments[0];
|
||||||
|
|
||||||
const info = {} as any;
|
const info = {} as any;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||||
import {EventHandler} from "../../../events";
|
import {EventHandler} from "tc-events";
|
||||||
import {LogCategory, logError} from "../../../log";
|
import {LogCategory, logError} from "../../../log";
|
||||||
import {tr} from "../../../i18n/localize";
|
import {tr} from "../../../i18n/localize";
|
||||||
import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions";
|
import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions";
|
||||||
|
|
|
@ -447,6 +447,7 @@ class ChannelVideoController {
|
||||||
this.events.on("query_videos", () => this.notifyVideoList());
|
this.events.on("query_videos", () => this.notifyVideoList());
|
||||||
this.events.on("query_spotlight", () => this.notifySpotlight());
|
this.events.on("query_spotlight", () => this.notifySpotlight());
|
||||||
this.events.on("query_subscribe_info", () => this.notifySubscribeInfo());
|
this.events.on("query_subscribe_info", () => this.notifySubscribeInfo());
|
||||||
|
this.events.on("query_viewer_count", () => this.notifyViewerCount());
|
||||||
|
|
||||||
this.events.on("query_video_info", event => {
|
this.events.on("query_video_info", event => {
|
||||||
const controller = this.findVideoById(event.videoId);
|
const controller = this.findVideoById(event.videoId);
|
||||||
|
@ -492,6 +493,14 @@ class ChannelVideoController {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
events.push(this.videoConnection.getLocalBroadcast("camera").getEvents().on([ "notify_clients_left", "notify_clients_joined", "notify_state_changed" ], () => {
|
||||||
|
this.notifyViewerCount();
|
||||||
|
}));
|
||||||
|
|
||||||
|
events.push(this.videoConnection.getLocalBroadcast("screen").getEvents().on([ "notify_clients_left", "notify_clients_joined", "notify_state_changed" ], () => {
|
||||||
|
this.notifyViewerCount();
|
||||||
|
}));
|
||||||
|
|
||||||
const channelTree = this.connection.channelTree;
|
const channelTree = this.connection.channelTree;
|
||||||
events.push(channelTree.events.on("notify_tree_reset", () => {
|
events.push(channelTree.events.on("notify_tree_reset", () => {
|
||||||
this.resetClientVideos();
|
this.resetClientVideos();
|
||||||
|
@ -512,6 +521,7 @@ class ChannelVideoController {
|
||||||
this.notifyVideoList();
|
this.notifyVideoList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(event.newChannel.channelId === this.currentChannelId) {
|
if(event.newChannel.channelId === this.currentChannelId) {
|
||||||
this.createClientVideo(event.client);
|
this.createClientVideo(event.client);
|
||||||
this.notifyVideoList();
|
this.notifyVideoList();
|
||||||
|
@ -527,6 +537,7 @@ class ChannelVideoController {
|
||||||
if(this.destroyClientVideo(event.client.clientId())) {
|
if(this.destroyClientVideo(event.client.clientId())) {
|
||||||
this.notifyVideoList();
|
this.notifyVideoList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(event.client instanceof LocalClientEntry) {
|
if(event.client instanceof LocalClientEntry) {
|
||||||
this.resetClientVideos();
|
this.resetClientVideos();
|
||||||
}
|
}
|
||||||
|
@ -541,6 +552,7 @@ class ChannelVideoController {
|
||||||
this.createClientVideo(event.client);
|
this.createClientVideo(event.client);
|
||||||
this.notifyVideoList();
|
this.notifyVideoList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(event.client instanceof LocalClientEntry) {
|
if(event.client instanceof LocalClientEntry) {
|
||||||
this.updateLocalChannel(event.client);
|
this.updateLocalChannel(event.client);
|
||||||
}
|
}
|
||||||
|
@ -572,7 +584,7 @@ class ChannelVideoController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static shouldIgnoreClient(client: ClientEntry) {
|
private static shouldIgnoreClient(client: ClientEntry) {
|
||||||
return (client instanceof MusicClientEntry || client.properties.client_type_exact === ClientType.CLIENT_QUERY);
|
return (client instanceof MusicClientEntry || client.getClientType() === ClientType.CLIENT_QUERY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLocalChannel(localClient: ClientEntry) {
|
private updateLocalChannel(localClient: ClientEntry) {
|
||||||
|
@ -658,25 +670,41 @@ class ChannelVideoController {
|
||||||
|
|
||||||
const channel = this.connection.channelTree.findChannel(this.currentChannelId);
|
const channel = this.connection.channelTree.findChannel(this.currentChannelId);
|
||||||
if(channel) {
|
if(channel) {
|
||||||
const clients = channel.channelClientsOrdered();
|
const clients = channel.channelClientsOrdered().filter(client => {
|
||||||
for(const client of clients) {
|
|
||||||
if(client instanceof LocalClientEntry) {
|
if(client instanceof LocalClientEntry) {
|
||||||
continue;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.clientVideos[client.clientId()]) {
|
if(!this.clientVideos[client.clientId()]) {
|
||||||
/* should not be possible (Is only possible for the local client) */
|
/* should not be possible (Is only possible for the local client) */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Firstly add all clients with video */
|
||||||
|
for(const client of clients) {
|
||||||
|
const controller = this.clientVideos[client.clientId()];
|
||||||
|
if(!controller.isBroadcasting()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = this.clientVideos[client.clientId()];
|
videoStreamingCount++;
|
||||||
if(controller.isBroadcasting()) {
|
|
||||||
videoStreamingCount++;
|
|
||||||
} else if(!settings.getValue(Settings.KEY_VIDEO_SHOW_ALL_CLIENTS)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
videoIds.push(controller.videoId);
|
videoIds.push(controller.videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Secondly add all other clients (if wanted) */
|
||||||
|
if(settings.getValue(Settings.KEY_VIDEO_SHOW_ALL_CLIENTS)) {
|
||||||
|
for(const client of clients) {
|
||||||
|
const controller = this.clientVideos[client.clientId()];
|
||||||
|
if(controller.isBroadcasting()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoIds.push(controller.videoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateVisibility(videoStreamingCount !== 0);
|
this.updateVisibility(videoStreamingCount !== 0);
|
||||||
|
@ -720,6 +748,41 @@ class ChannelVideoController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyViewerCount() {
|
||||||
|
let cameraViewers, screenViewers;
|
||||||
|
{
|
||||||
|
let broadcast = this.videoConnection.getLocalBroadcast("camera");
|
||||||
|
switch (broadcast.getState().state) {
|
||||||
|
case "initializing":
|
||||||
|
case "broadcasting":
|
||||||
|
cameraViewers = broadcast.getViewer().length;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stopped":
|
||||||
|
case "failed":
|
||||||
|
default:
|
||||||
|
cameraViewers = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let broadcast = this.videoConnection.getLocalBroadcast("screen");
|
||||||
|
switch (broadcast.getState().state) {
|
||||||
|
case "initializing":
|
||||||
|
case "broadcasting":
|
||||||
|
screenViewers = broadcast.getViewer().length;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stopped":
|
||||||
|
case "failed":
|
||||||
|
default:
|
||||||
|
screenViewers = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.events.fire_react("notify_viewer_count", { camera: cameraViewers, screen: screenViewers });
|
||||||
|
}
|
||||||
|
|
||||||
private updateVisibility(target: boolean) {
|
private updateVisibility(target: boolean) {
|
||||||
if(this.currentlyVisible === target) { return; }
|
if(this.currentlyVisible === target) { return; }
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,8 @@ export interface ChannelVideoEvents {
|
||||||
query_video_statistics: { videoId: string, broadcastType: VideoBroadcastType },
|
query_video_statistics: { videoId: string, broadcastType: VideoBroadcastType },
|
||||||
query_spotlight: {},
|
query_spotlight: {},
|
||||||
query_video_stream: { videoId: string, broadcastType: VideoBroadcastType },
|
query_video_stream: { videoId: string, broadcastType: VideoBroadcastType },
|
||||||
query_subscribe_info: {}
|
query_subscribe_info: {},
|
||||||
|
query_viewer_count: {},
|
||||||
|
|
||||||
notify_expended: { expended: boolean },
|
notify_expended: { expended: boolean },
|
||||||
notify_videos: {
|
notify_videos: {
|
||||||
|
@ -107,10 +108,6 @@ export interface ChannelVideoEvents {
|
||||||
videoId: string,
|
videoId: string,
|
||||||
statusIcon: ClientIcon
|
statusIcon: ClientIcon
|
||||||
},
|
},
|
||||||
notify_video_arrows: {
|
|
||||||
left: boolean,
|
|
||||||
right: boolean
|
|
||||||
},
|
|
||||||
notify_spotlight: {
|
notify_spotlight: {
|
||||||
videoId: string[]
|
videoId: string[]
|
||||||
},
|
},
|
||||||
|
@ -126,6 +123,10 @@ export interface ChannelVideoEvents {
|
||||||
},
|
},
|
||||||
notify_subscribe_info: {
|
notify_subscribe_info: {
|
||||||
info: VideoSubscribeInfo
|
info: VideoSubscribeInfo
|
||||||
|
},
|
||||||
|
notify_viewer_count: {
|
||||||
|
camera: number | undefined,
|
||||||
|
screen: number | undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,9 @@ $small_height: 10em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
height: $small_height;
|
height: $small_height;
|
||||||
width: 100%;
|
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 16em;
|
||||||
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -136,9 +138,6 @@ $small_height: 10em;
|
||||||
margin-left: .5em;
|
margin-left: .5em;
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
|
|
||||||
/* TODO: Min with of two video +4em for one of the arrows */
|
|
||||||
min-width: 6em;
|
|
||||||
|
|
||||||
.videos {
|
.videos {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -244,6 +243,7 @@ $small_height: 10em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FIXME: Unify the overlays (Bottom left, Bottom right, and Top right) */
|
||||||
.videoContainer, :global(.react-grid-item.react-grid-placeholder) {
|
.videoContainer, :global(.react-grid-item.react-grid-placeholder) {
|
||||||
/* Note: don't use margin here since it might */
|
/* Note: don't use margin here since it might */
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -401,13 +401,65 @@ $small_height: 10em;
|
||||||
font-weight: normal!important;
|
font-weight: normal!important;
|
||||||
|
|
||||||
@include text-dotdotdot();
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
|
||||||
&.local {
|
&.local {
|
||||||
|
.name {
|
||||||
color: #147114;
|
color: #147114;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.videoViewerCount {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
border-bottom-left-radius: .2em;
|
||||||
|
background-color: #35353580;
|
||||||
|
|
||||||
|
padding: .1em .3em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
max-width: 70%;
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
color: #999;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
margin-right: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
align-self: center;
|
||||||
|
margin-left: .25em;
|
||||||
|
font-weight: normal!important;
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.actionIcons {
|
.actionIcons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
|
|
|
@ -4,30 +4,30 @@ import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {
|
import {
|
||||||
ChannelVideoEvents,
|
ChannelVideoEvents, ChannelVideoInfo,
|
||||||
ChannelVideoInfo,
|
|
||||||
ChannelVideoStreamState,
|
ChannelVideoStreamState,
|
||||||
kLocalVideoId,
|
kLocalVideoId, makeVideoAutoplay,
|
||||||
makeVideoAutoplay,
|
|
||||||
VideoStreamState,
|
VideoStreamState,
|
||||||
VideoSubscribeInfo
|
VideoSubscribeInfo
|
||||||
} from "./Definitions";
|
} from "./Definitions";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
|
||||||
import ResizeObserver from "resize-observer-polyfill";
|
import ResizeObserver from "resize-observer-polyfill";
|
||||||
import {LogCategory, logTrace, logWarn} from "tc-shared/log";
|
import {LogCategory, logTrace, logWarn} from "tc-shared/log";
|
||||||
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
|
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
|
||||||
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
|
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
|
||||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
import {Spotlight, SpotlightDimensions, SpotlightDimensionsContext} from "./RendererSpotlight";
|
import {Spotlight, SpotlightDimensions, SpotlightDimensionsContext} from "./RendererSpotlight";
|
||||||
import * as _ from "lodash";
|
import * as _ from "lodash";
|
||||||
|
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||||
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
|
|
||||||
|
|
||||||
const SubscribeContext = React.createContext<VideoSubscribeInfo>(undefined);
|
const SubscribeContext = React.createContext<VideoSubscribeInfo>(undefined);
|
||||||
const EventContext = React.createContext<Registry<ChannelVideoEvents>>(undefined);
|
const EventContext = React.createContext<Registry<ChannelVideoEvents>>(undefined);
|
||||||
const HandlerIdContext = React.createContext<string>(undefined);
|
const HandlerIdContext = React.createContext<string>(undefined);
|
||||||
|
export const VideoIdContext = React.createContext<string>(undefined);
|
||||||
|
|
||||||
export const RendererVideoEventContext = EventContext;
|
export const RendererVideoEventContext = EventContext;
|
||||||
|
|
||||||
|
@ -47,16 +47,64 @@ const ExpendArrow = React.memo(() => {
|
||||||
<div className={cssStyle.expendArrow} onClick={() => events.fire("action_toggle_expended", { expended: !expended })}>
|
<div className={cssStyle.expendArrow} onClick={() => events.fire("action_toggle_expended", { expended: !expended })}>
|
||||||
<ClientIconRenderer icon={ClientIcon.DoubleArrow} className={cssStyle.icon} />
|
<ClientIconRenderer icon={ClientIcon.DoubleArrow} className={cssStyle.icon} />
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const VideoViewerCount = React.memo(() => {
|
||||||
|
const videoId = useContext(VideoIdContext);
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
if(videoId !== kLocalVideoId) {
|
||||||
|
/* Currently one we can see our own video viewer */
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ viewer, setViewer ] = useState<{ camera: number | undefined, screen: number | undefined }>(() => {
|
||||||
|
events.fire("query_viewer_count");
|
||||||
|
return { screen: undefined, camera: undefined };
|
||||||
|
});
|
||||||
|
|
||||||
|
events.reactUse("notify_viewer_count", event => setViewer({ camera: event.camera, screen: event.screen }));
|
||||||
|
|
||||||
|
let info = [];
|
||||||
|
if(typeof viewer.camera === "number") {
|
||||||
|
info.push(
|
||||||
|
<div className={cssStyle.entry} key={"camera"} title={tra("{} Camera viewers", viewer.camera)}>
|
||||||
|
<div className={cssStyle.value}>{viewer.camera}</div>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.VideoMuted} className={cssStyle.icon} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof viewer.screen === "number") {
|
||||||
|
info.push(
|
||||||
|
<div className={cssStyle.entry} key={"screen"} title={tra("{} Screen viewers", viewer.screen)}>
|
||||||
|
<div className={cssStyle.value}>{viewer.screen}</div>
|
||||||
|
<ClientIconRenderer icon={ClientIcon.ShareScreen} className={cssStyle.icon} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(info.length === 0) {
|
||||||
|
/* We're not streaming any video */
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cssStyle.videoViewerCount}
|
||||||
|
onClick={() => {
|
||||||
|
/* TODO! */
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{info}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const VideoInfo = React.memo((props: { videoId: string }) => {
|
const VideoClientInfo = React.memo((props: { videoId: string }) => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const handlerId = useContext(HandlerIdContext);
|
const handlerId = useContext(HandlerIdContext);
|
||||||
|
|
||||||
const localVideo = props.videoId === kLocalVideoId;
|
|
||||||
const nameClassList = cssStyle.name + " " + (localVideo ? cssStyle.local : "");
|
|
||||||
|
|
||||||
const [ info, setInfo ] = useState<"loading" | ChannelVideoInfo>(() => {
|
const [ info, setInfo ] = useState<"loading" | ChannelVideoInfo>(() => {
|
||||||
events.fire("query_video_info", { videoId: props.videoId });
|
events.fire("query_video_info", { videoId: props.videoId });
|
||||||
return "loading";
|
return "loading";
|
||||||
|
@ -79,59 +127,23 @@ const VideoInfo = React.memo((props: { videoId: string }) => {
|
||||||
|
|
||||||
let clientName;
|
let clientName;
|
||||||
if(info === "loading") {
|
if(info === "loading") {
|
||||||
clientName = <div className={nameClassList} key={"loading"}><Translatable>loading</Translatable> {props.videoId} <LoadingDots /></div>;
|
clientName = (
|
||||||
|
<div className={cssStyle.name} key={"loading"}>
|
||||||
|
<Translatable>loading</Translatable> {props.videoId} <LoadingDots />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
clientName = <ClientTag clientName={info.clientName} clientUniqueId={info.clientUniqueId} clientId={info.clientId} handlerId={handlerId} className={nameClassList} key={"loaded"} />;
|
clientName = <ClientTag clientName={info.clientName} clientUniqueId={info.clientUniqueId} clientId={info.clientId} handlerId={handlerId} className={cssStyle.name} key={"loaded"} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.info}>
|
<div className={joinClassList(cssStyle.info, props.videoId === kLocalVideoId && cssStyle.local)}>
|
||||||
<ClientIconRenderer icon={statusIcon} className={cssStyle.icon} />
|
<ClientIconRenderer icon={statusIcon} className={cssStyle.icon} />
|
||||||
{clientName}
|
{clientName}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const VideoStreamReplay = React.memo((props: { stream: MediaStream | undefined, className: string, streamType: VideoBroadcastType }) => {
|
|
||||||
const refVideo = useRef<HTMLVideoElement>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelAutoplay;
|
|
||||||
const video = refVideo.current;
|
|
||||||
if(props.stream) {
|
|
||||||
video.style.opacity = "1";
|
|
||||||
video.srcObject = props.stream;
|
|
||||||
video.muted = true;
|
|
||||||
cancelAutoplay = makeVideoAutoplay(video);
|
|
||||||
} else {
|
|
||||||
video.style.opacity = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const video = refVideo.current;
|
|
||||||
if(video) {
|
|
||||||
video.onpause = undefined;
|
|
||||||
video.onended = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cancelAutoplay) {
|
|
||||||
cancelAutoplay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [ props.stream ]);
|
|
||||||
|
|
||||||
let title;
|
|
||||||
if(props.streamType === "camera") {
|
|
||||||
title = useTr("Camera");
|
|
||||||
} else {
|
|
||||||
title = useTr("Screen");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<video ref={refVideo} className={cssStyle.video + " " + props.className} title={title} x-stream-type={props.streamType} />
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
const VideoSubscribeContextProvider = (props: { children?: React.ReactElement | React.ReactElement[] }) => {
|
const VideoSubscribeContextProvider = (props: { children?: React.ReactElement | React.ReactElement[] }) => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
|
@ -225,13 +237,44 @@ const VideoStreamAvailableRenderer = (props: { videoId: string, mode: VideoBroad
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const VideoStreamRenderer = (props: { videoId: string, streamType: VideoBroadcastType, className?: string }) => {
|
const MediaStreamVideoRenderer = React.memo((props: { stream: MediaStream | undefined, className: string, title: string }) => {
|
||||||
|
const refVideo = useRef<HTMLVideoElement>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelAutoplay;
|
||||||
|
const video = refVideo.current;
|
||||||
|
if(props.stream) {
|
||||||
|
video.style.opacity = "1";
|
||||||
|
video.srcObject = props.stream;
|
||||||
|
video.muted = true;
|
||||||
|
cancelAutoplay = makeVideoAutoplay(video);
|
||||||
|
} else {
|
||||||
|
video.style.opacity = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const video = refVideo.current;
|
||||||
|
if(video) {
|
||||||
|
video.onpause = undefined;
|
||||||
|
video.onended = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(cancelAutoplay) {
|
||||||
|
cancelAutoplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ props.stream ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video ref={refVideo} className={cssStyle.video + " " + props.className} title={props.title} />
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const VideoStreamPlayer = (props: { videoId: string, streamType: VideoBroadcastType, className?: string }) => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const [ state, setState ] = useState<VideoStreamState>(() => {
|
const [ state, setState ] = useState<VideoStreamState>(() => {
|
||||||
events.fire("query_video_stream", { videoId: props.videoId, broadcastType: props.streamType });
|
events.fire("query_video_stream", { videoId: props.videoId, broadcastType: props.streamType });
|
||||||
return {
|
return { state: "disconnected", }
|
||||||
state: "disconnected",
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
events.reactUse("notify_video_stream", event => {
|
events.reactUse("notify_video_stream", event => {
|
||||||
if(event.videoId === props.videoId && event.broadcastType === props.streamType) {
|
if(event.videoId === props.videoId && event.broadcastType === props.streamType) {
|
||||||
|
@ -243,23 +286,34 @@ const VideoStreamRenderer = (props: { videoId: string, streamType: VideoBroadcas
|
||||||
case "disconnected":
|
case "disconnected":
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||||
<div><Translatable>No video stream</Translatable></div>
|
<div>
|
||||||
|
<Translatable>No video stream</Translatable>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "connecting":
|
case "connecting":
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.text} key={"info-initializing"}>
|
<div className={cssStyle.text} key={"info-initializing"}>
|
||||||
<div><Translatable>connecting</Translatable> <LoadingDots /></div>
|
<div>
|
||||||
|
<Translatable>connecting</Translatable> <LoadingDots />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "connected":
|
case "connected":
|
||||||
return <VideoStreamReplay stream={state.stream} className={props.className} streamType={props.streamType} key={"connected"} />;
|
return (
|
||||||
|
<MediaStreamVideoRenderer
|
||||||
|
stream={state.stream}
|
||||||
|
className={props.className}
|
||||||
|
title={props.streamType === "camera" ? tr("Camera") : tr("Screen")}
|
||||||
|
key={"connected"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case "failed":
|
case "failed":
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.text + " " + cssStyle.error} key={"error"}>
|
<div className={joinClassList(cssStyle.text, cssStyle.error)} key={"error"}>
|
||||||
<div><Translatable>Stream replay failed</Translatable></div>
|
<div><Translatable>Stream replay failed</Translatable></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -275,7 +329,7 @@ const VideoStreamRenderer = (props: { videoId: string, streamType: VideoBroadcas
|
||||||
|
|
||||||
const VideoPlayer = React.memo((props: { videoId: string, cameraState: ChannelVideoStreamState, screenState: ChannelVideoStreamState }) => {
|
const VideoPlayer = React.memo((props: { videoId: string, cameraState: ChannelVideoStreamState, screenState: ChannelVideoStreamState }) => {
|
||||||
const streamElements = [];
|
const streamElements = [];
|
||||||
const streamClasses = [cssStyle.videoPrimary, cssStyle.videoSecondary];
|
const streamClasses = [ cssStyle.videoPrimary, cssStyle.videoSecondary ];
|
||||||
|
|
||||||
if(props.cameraState === "none" && props.screenState === "none") {
|
if(props.cameraState === "none" && props.screenState === "none") {
|
||||||
/* No video available. Will be handled bellow */
|
/* No video available. Will be handled bellow */
|
||||||
|
@ -302,7 +356,7 @@ const VideoPlayer = React.memo((props: { videoId: string, cameraState: ChannelVi
|
||||||
);
|
);
|
||||||
} else if(props.screenState === "streaming") {
|
} else if(props.screenState === "streaming") {
|
||||||
streamElements.push(
|
streamElements.push(
|
||||||
<VideoStreamRenderer key={"stream-screen"} videoId={props.videoId} streamType={"screen"} className={streamClasses.pop_front()} />
|
<VideoStreamPlayer key={"stream-screen"} videoId={props.videoId} streamType={"screen"} className={streamClasses.pop_front()} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +372,7 @@ const VideoPlayer = React.memo((props: { videoId: string, cameraState: ChannelVi
|
||||||
);
|
);
|
||||||
} else if(props.cameraState === "streaming") {
|
} else if(props.cameraState === "streaming") {
|
||||||
streamElements.push(
|
streamElements.push(
|
||||||
<VideoStreamRenderer key={"stream-camera"} videoId={props.videoId} streamType={"camera"} className={streamClasses.pop_front()} />
|
<VideoStreamPlayer key={"stream-camera"} videoId={props.videoId} streamType={"camera"} className={streamClasses.pop_front()} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -339,6 +393,29 @@ const VideoPlayer = React.memo((props: { videoId: string, cameraState: ChannelVi
|
||||||
return <>{streamElements}</>;
|
return <>{streamElements}</>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const VideoToggleButton = React.memo((props: { videoId: string, broadcastType: VideoBroadcastType, target: boolean }) => {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
|
let title;
|
||||||
|
let icon: ClientIcon;
|
||||||
|
if(props.broadcastType === "camera") {
|
||||||
|
title = props.target ? useTr("Unmute screen video") : useTr("Mute screen video");
|
||||||
|
icon = ClientIcon.ShareScreen;
|
||||||
|
} else {
|
||||||
|
title = props.target ? useTr("Unmute camera video") : useTr("Mute camera video");
|
||||||
|
icon = ClientIcon.VideoMuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={joinClassList(cssStyle.iconContainer, cssStyle.toggle, !props.target && cssStyle.disabled)}
|
||||||
|
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: props.broadcastType, muted: props.target })}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<ClientIconRenderer className={cssStyle.icon} icon={icon} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
const VideoControlButtons = React.memo((props: {
|
const VideoControlButtons = React.memo((props: {
|
||||||
videoId: string,
|
videoId: string,
|
||||||
cameraState: ChannelVideoStreamState,
|
cameraState: ChannelVideoStreamState,
|
||||||
|
@ -348,44 +425,74 @@ const VideoControlButtons = React.memo((props: {
|
||||||
}) => {
|
}) => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
|
|
||||||
const screenShown = props.screenState !== "none" && props.videoId !== kLocalVideoId;
|
let buttons = [];
|
||||||
const cameraShown = props.cameraState !== "none" && props.videoId !== kLocalVideoId;
|
if(props.videoId !== kLocalVideoId) {
|
||||||
|
switch (props.screenState) {
|
||||||
|
case "available":
|
||||||
|
case "ignored":
|
||||||
|
buttons.push(
|
||||||
|
<VideoToggleButton videoId={props.videoId} target={false} broadcastType={"screen"} key={"screen-disabled"} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
const screenDisabled = props.screenState === "ignored" || props.screenState === "available";
|
case "streaming":
|
||||||
const cameraDisabled = props.cameraState === "ignored" || props.cameraState === "available";
|
buttons.push(
|
||||||
|
<VideoToggleButton videoId={props.videoId} target={true} broadcastType={"screen"} key={"screen-enabled"} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "none":
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (props.cameraState) {
|
||||||
|
case "available":
|
||||||
|
case "ignored":
|
||||||
|
buttons.push(
|
||||||
|
<VideoToggleButton videoId={props.videoId} target={false} broadcastType={"camera"} key={"camera-disabled"} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "streaming":
|
||||||
|
buttons.push(
|
||||||
|
<VideoToggleButton videoId={props.videoId} target={true} broadcastType={"camera"} key={"camera-enabled"} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "none":
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
<div className={cssStyle.iconContainer + " " + (props.fullscreenMode === "unavailable" ? cssStyle.hidden : "")}
|
||||||
|
key={"spotlight"}
|
||||||
|
onClick={() => {
|
||||||
|
if(props.isSpotlight) {
|
||||||
|
events.fire("action_set_fullscreen", { videoId: props.fullscreenMode === "set" ? undefined : props.videoId });
|
||||||
|
} else {
|
||||||
|
events.fire("action_toggle_spotlight", { videoIds: [ props.videoId ], expend: true, enabled: true });
|
||||||
|
events.fire("action_focus_spotlight", { });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={props.isSpotlight ? tr("Toggle fullscreen") : tr("Toggle spotlight")}
|
||||||
|
>
|
||||||
|
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.Fullscreen} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.actionIcons}>
|
<div className={cssStyle.actionIcons}>
|
||||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (screenShown ? "" : cssStyle.hidden) + " " + (screenDisabled ? cssStyle.disabled : "")}
|
{buttons}
|
||||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "screen", muted: !screenDisabled })}
|
|
||||||
title={screenDisabled ? tr("Unmute screen video") : tr("Mute screen video")}
|
|
||||||
>
|
|
||||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.ShareScreen} />
|
|
||||||
</div>
|
|
||||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (cameraShown ? "" : cssStyle.hidden) + " " + (cameraDisabled ? cssStyle.disabled : "")}
|
|
||||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "camera", muted: !cameraDisabled })}
|
|
||||||
title={cameraDisabled ? tr("Unmute camera video") : tr("Mute camera video")}
|
|
||||||
>
|
|
||||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.VideoMuted} />
|
|
||||||
</div>
|
|
||||||
<div className={cssStyle.iconContainer + " " + (props.fullscreenMode === "unavailable" ? cssStyle.hidden : "")}
|
|
||||||
onClick={() => {
|
|
||||||
if(props.isSpotlight) {
|
|
||||||
events.fire("action_set_fullscreen", { videoId: props.fullscreenMode === "set" ? undefined : props.videoId });
|
|
||||||
} else {
|
|
||||||
events.fire("action_toggle_spotlight", { videoIds: [ props.videoId ], expend: true, enabled: true });
|
|
||||||
events.fire("action_focus_spotlight", { });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={props.isSpotlight ? tr("Toggle fullscreen") : tr("Toggle spotlight")}
|
|
||||||
>
|
|
||||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.Fullscreen} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolean }) => {
|
export const VideoContainer = React.memo((props: { isSpotlight: boolean }) => {
|
||||||
|
const videoId = useContext(VideoIdContext);
|
||||||
|
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const refContainer = useRef<HTMLDivElement>();
|
const refContainer = useRef<HTMLDivElement>();
|
||||||
const fullscreenCapable = "requestFullscreen" in HTMLElement.prototype;
|
const fullscreenCapable = "requestFullscreen" in HTMLElement.prototype;
|
||||||
|
@ -394,12 +501,12 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
|
|
||||||
const [ cameraState, setCameraState ] = useState<ChannelVideoStreamState>("none");
|
const [ cameraState, setCameraState ] = useState<ChannelVideoStreamState>("none");
|
||||||
const [ screenState, setScreenState ] = useState<ChannelVideoStreamState>(() => {
|
const [ screenState, setScreenState ] = useState<ChannelVideoStreamState>(() => {
|
||||||
events.fire("query_video", { videoId: props.videoId });
|
events.fire("query_video", { videoId: videoId });
|
||||||
return "none";
|
return "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
events.reactUse("notify_video", event => {
|
events.reactUse("notify_video", event => {
|
||||||
if(event.videoId === props.videoId) {
|
if(event.videoId === videoId) {
|
||||||
setCameraState(event.cameraStream);
|
setCameraState(event.cameraStream);
|
||||||
setScreenState(event.screenStream);
|
setScreenState(event.screenStream);
|
||||||
}
|
}
|
||||||
|
@ -424,7 +531,7 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
}, [ isFullscreen ]);
|
}, [ isFullscreen ]);
|
||||||
|
|
||||||
events.reactUse("action_set_fullscreen", event => {
|
events.reactUse("action_set_fullscreen", event => {
|
||||||
if(event.videoId === props.videoId) {
|
if(event.videoId === videoId) {
|
||||||
if(!refContainer.current) { return; }
|
if(!refContainer.current) { return; }
|
||||||
|
|
||||||
refContainer.current.requestFullscreen().then(() => {
|
refContainer.current.requestFullscreen().then(() => {
|
||||||
|
@ -448,9 +555,9 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
if(isFullscreen) {
|
if(isFullscreen) {
|
||||||
events.fire("action_set_fullscreen", { videoId: undefined });
|
events.fire("action_set_fullscreen", { videoId: undefined });
|
||||||
} else if(props.isSpotlight) {
|
} else if(props.isSpotlight) {
|
||||||
events.fire("action_set_fullscreen", { videoId: props.videoId });
|
events.fire("action_set_fullscreen", { videoId: videoId });
|
||||||
} else {
|
} else {
|
||||||
events.fire("action_toggle_spotlight", { videoIds: [ props.videoId ], expend: true, enabled: true });
|
events.fire("action_toggle_spotlight", { videoIds: [ videoId ], expend: true, enabled: true });
|
||||||
events.fire("action_focus_spotlight", { });
|
events.fire("action_focus_spotlight", { });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -467,7 +574,7 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
label: tr("Popout Video"),
|
label: tr("Popout Video"),
|
||||||
icon: ClientIcon.Fullscreen,
|
icon: ClientIcon.Fullscreen,
|
||||||
click: () => {
|
click: () => {
|
||||||
events.fire("action_set_pip", { videoId: props.videoId, broadcastType: streamType as any });
|
events.fire("action_set_pip", { videoId: videoId, broadcastType: streamType as any });
|
||||||
},
|
},
|
||||||
visible: !!streamType && "requestPictureInPicture" in HTMLVideoElement.prototype
|
visible: !!streamType && "requestPictureInPicture" in HTMLVideoElement.prototype
|
||||||
},
|
},
|
||||||
|
@ -476,7 +583,7 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
label: isFullscreen ? tr("Release fullscreen") : tr("Show in fullscreen"),
|
label: isFullscreen ? tr("Release fullscreen") : tr("Show in fullscreen"),
|
||||||
icon: ClientIcon.Fullscreen,
|
icon: ClientIcon.Fullscreen,
|
||||||
click: () => {
|
click: () => {
|
||||||
events.fire("action_set_fullscreen", { videoId: isFullscreen ? undefined : props.videoId });
|
events.fire("action_set_fullscreen", { videoId: isFullscreen ? undefined : videoId });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -484,7 +591,7 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
label: props.isSpotlight ? tr("Release spotlight") : tr("Put client in spotlight"),
|
label: props.isSpotlight ? tr("Release spotlight") : tr("Put client in spotlight"),
|
||||||
icon: ClientIcon.Fullscreen,
|
icon: ClientIcon.Fullscreen,
|
||||||
click: () => {
|
click: () => {
|
||||||
events.fire("action_toggle_spotlight", { videoIds: [ props.videoId ], expend: true, enabled: !props.isSpotlight });
|
events.fire("action_toggle_spotlight", { videoIds: [ videoId ], expend: true, enabled: !props.isSpotlight });
|
||||||
events.fire("action_focus_spotlight", { });
|
events.fire("action_focus_spotlight", { });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -492,10 +599,11 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
}}
|
}}
|
||||||
ref={refContainer}
|
ref={refContainer}
|
||||||
>
|
>
|
||||||
<VideoPlayer videoId={props.videoId} cameraState={cameraState} screenState={screenState} />
|
<VideoPlayer videoId={videoId} cameraState={cameraState} screenState={screenState} />
|
||||||
<VideoInfo videoId={props.videoId} />
|
<VideoClientInfo videoId={videoId} />
|
||||||
|
<VideoViewerCount />
|
||||||
<VideoControlButtons
|
<VideoControlButtons
|
||||||
videoId={props.videoId}
|
videoId={videoId}
|
||||||
cameraState={cameraState}
|
cameraState={cameraState}
|
||||||
screenState={screenState}
|
screenState={screenState}
|
||||||
isSpotlight={props.isSpotlight}
|
isSpotlight={props.isSpotlight}
|
||||||
|
@ -505,13 +613,11 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const VideoBarArrow = React.memo((props: { direction: "left" | "right", containerRef: React.RefObject<HTMLDivElement> }) => {
|
const VideoBarArrow = React.memo((props: { direction: "left" | "right", shown: boolean, containerRef: React.RefObject<HTMLDivElement> }) => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const [ shown, setShown ] = useState(false);
|
|
||||||
events.reactUse("notify_video_arrows", event => setShown(event[props.direction]));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.arrow + " " + cssStyle[props.direction] + " " + (shown ? "" : cssStyle.hidden)} ref={props.containerRef}>
|
<div className={cssStyle.arrow + " " + cssStyle[props.direction] + " " + (props.shown ? "" : cssStyle.hidden)} ref={props.containerRef}>
|
||||||
<div className={cssStyle.iconContainer} onClick={() => events.fire("action_video_scroll", { direction: props.direction })}>
|
<div className={cssStyle.iconContainer} onClick={() => events.fire("action_video_scroll", { direction: props.direction })}>
|
||||||
<ClientIconRenderer icon={ClientIcon.SimpleArrow} className={cssStyle.icon} />
|
<ClientIconRenderer icon={ClientIcon.SimpleArrow} className={cssStyle.icon} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -525,9 +631,12 @@ const VideoBar = React.memo(() => {
|
||||||
const refArrowRight = useRef<HTMLDivElement>();
|
const refArrowRight = useRef<HTMLDivElement>();
|
||||||
const refArrowLeft = useRef<HTMLDivElement>();
|
const refArrowLeft = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const [ videos, setVideos ] = useState<"loading" | string[]>(() => {
|
const [ arrowLeftShown, setArrowLeftShown ] = useState(false);
|
||||||
|
const [ arrowRightShown, setArrowRightShown ] = useState(false);
|
||||||
|
|
||||||
|
const [ videos, setVideos ] = useState<string[]>(() => {
|
||||||
events.fire("query_videos");
|
events.fire("query_videos");
|
||||||
return "loading";
|
return [];
|
||||||
});
|
});
|
||||||
events.reactUse("notify_videos", event => setVideos(event.videoIds));
|
events.reactUse("notify_videos", event => setVideos(event.videoIds));
|
||||||
|
|
||||||
|
@ -537,29 +646,32 @@ const VideoBar = React.memo(() => {
|
||||||
|
|
||||||
const rightEndReached = container.scrollLeft + container.clientWidth + 1 >= container.scrollWidth;
|
const rightEndReached = container.scrollLeft + container.clientWidth + 1 >= container.scrollWidth;
|
||||||
const leftEndReached = container.scrollLeft <= .9;
|
const leftEndReached = container.scrollLeft <= .9;
|
||||||
events.fire("notify_video_arrows", { left: !leftEndReached, right: !rightEndReached });
|
setArrowLeftShown(!leftEndReached);
|
||||||
|
setArrowRightShown(!rightEndReached);
|
||||||
}, [ refVideos ]);
|
}, [ refVideos ]);
|
||||||
|
|
||||||
events.reactUse("action_video_scroll", event => {
|
events.reactUse("action_video_scroll", event => {
|
||||||
const container = refVideos.current;
|
const container = refVideos.current;
|
||||||
const arrowLeft = refArrowLeft.current;
|
const arrowLeft = refArrowLeft.current;
|
||||||
const arrowRight = refArrowRight.current;
|
const arrowRight = refArrowRight.current;
|
||||||
if(container && arrowLeft && arrowRight) {
|
if(!container || !arrowLeft || !arrowRight) {
|
||||||
const children = [...container.children] as HTMLElement[];
|
return;
|
||||||
if(event.direction === "left") {
|
}
|
||||||
const currentCutOff = container.scrollLeft;
|
|
||||||
const element = children.filter(element => element.offsetLeft >= currentCutOff)
|
|
||||||
.sort((a, b) => a.offsetLeft - b.offsetLeft)[0];
|
|
||||||
|
|
||||||
container.scrollLeft = (element.offsetLeft + element.clientWidth) - (container.clientWidth - arrowRight.clientWidth);
|
const children = [...container.children] as HTMLElement[];
|
||||||
} else {
|
if(event.direction === "left") {
|
||||||
const currentCutOff = container.scrollLeft + container.clientWidth;
|
const currentCutOff = container.scrollLeft;
|
||||||
const element = children.filter(element => element.offsetLeft <= currentCutOff)
|
const element = children.filter(element => element.offsetLeft >= currentCutOff)
|
||||||
.sort((a, b) => a.offsetLeft - b.offsetLeft)
|
.sort((a, b) => a.offsetLeft - b.offsetLeft)[0];
|
||||||
.last();
|
|
||||||
|
|
||||||
container.scrollLeft = element.offsetLeft - arrowLeft.clientWidth;
|
container.scrollLeft = (element.offsetLeft + element.clientWidth) - (container.clientWidth - arrowRight.clientWidth);
|
||||||
}
|
} else {
|
||||||
|
const currentCutOff = container.scrollLeft + container.clientWidth;
|
||||||
|
const element = children.filter(element => element.offsetLeft <= currentCutOff)
|
||||||
|
.sort((a, b) => a.offsetLeft - b.offsetLeft)
|
||||||
|
.last();
|
||||||
|
|
||||||
|
container.scrollLeft = element.offsetLeft - arrowLeft.clientWidth;
|
||||||
}
|
}
|
||||||
updateScrollButtons();
|
updateScrollButtons();
|
||||||
}, undefined, [ updateScrollButtons ]);
|
}, undefined, [ updateScrollButtons ]);
|
||||||
|
@ -587,16 +699,16 @@ const VideoBar = React.memo(() => {
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.videoBar}>
|
<div className={cssStyle.videoBar}>
|
||||||
<div className={cssStyle.videos} ref={refVideos}>
|
<div className={cssStyle.videos} ref={refVideos}>
|
||||||
{videos === "loading" ? undefined :
|
{videos.map(videoId => (
|
||||||
videos.map(videoId => (
|
<ErrorBoundary key={videoId}>
|
||||||
<ErrorBoundary key={videoId}>
|
<VideoIdContext.Provider value={videoId}>
|
||||||
<VideoContainer videoId={videoId} isSpotlight={false} />
|
<VideoContainer isSpotlight={false} />
|
||||||
</ErrorBoundary>
|
</VideoIdContext.Provider>
|
||||||
))
|
</ErrorBoundary>
|
||||||
}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<VideoBarArrow direction={"left"} containerRef={refArrowLeft} />
|
<VideoBarArrow direction={"left"} containerRef={refArrowLeft} shown={arrowLeftShown} />
|
||||||
<VideoBarArrow direction={"right"} containerRef={refArrowRight} />
|
<VideoBarArrow direction={"right"} containerRef={refArrowRight} shown={arrowRightShown} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ import GridLayout from "react-grid-layout";
|
||||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||||
import * as _ from "lodash";
|
import * as _ from "lodash";
|
||||||
import {FontSizeObserver} from "tc-shared/ui/react-elements/FontSize";
|
import {FontSizeObserver} from "tc-shared/ui/react-elements/FontSize";
|
||||||
import {RendererVideoEventContext, VideoContainer} from "tc-shared/ui/frames/video/Renderer";
|
import {RendererVideoEventContext, VideoContainer, VideoIdContext} from "tc-shared/ui/frames/video/Renderer";
|
||||||
|
|
||||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!react-resizable/css/styles.css";
|
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!react-resizable/css/styles.css";
|
||||||
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!react-grid-layout/css/styles.css";
|
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!react-grid-layout/css/styles.css";
|
||||||
|
@ -37,7 +37,11 @@ const SpotlightSingle = () => {
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
if(videoId) {
|
if(videoId) {
|
||||||
body = <VideoContainer videoId={videoId} key={"video-" + videoId} isSpotlight={true} />;
|
body = (
|
||||||
|
<VideoIdContext.Provider value={videoId} key={"video-" + videoId}>
|
||||||
|
<VideoContainer isSpotlight={true} />
|
||||||
|
</VideoIdContext.Provider>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
body = (
|
body = (
|
||||||
<div className={cssStyle.videoContainer + " " + cssStyle.outlined} key={"no-video"}>
|
<div className={cssStyle.videoContainer + " " + cssStyle.outlined} key={"no-video"}>
|
||||||
|
@ -187,7 +191,9 @@ class SpotlightGridController extends React.PureComponent<{ fontSize: number },
|
||||||
{this.state.layout.map(entry => (
|
{this.state.layout.map(entry => (
|
||||||
<div className={cssStyle.videoContainer} key={entry.i}>
|
<div className={cssStyle.videoContainer} key={entry.i}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<VideoContainer videoId={entry.i} key={entry.i} isSpotlight={true} />
|
<VideoIdContext.Provider value={entry.i} key={"video-" + entry.i}>
|
||||||
|
<VideoContainer key={entry.i} isSpotlight={true} />
|
||||||
|
</VideoIdContext.Provider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -191,11 +191,11 @@ export function openBanList(client: ConnectionHandler) {
|
||||||
width: '60em'
|
width: '60em'
|
||||||
});
|
});
|
||||||
|
|
||||||
client.serverConnection.command_handler_boss().register_single_handler(single_ban_handler);
|
client.serverConnection.getCommandHandler().registerSingleHandler(single_ban_handler);
|
||||||
client.serverConnection.command_handler_boss().register_single_handler(single_trigger_handler);
|
client.serverConnection.getCommandHandler().registerSingleHandler(single_trigger_handler);
|
||||||
modal.close_listener.push(() => {
|
modal.close_listener.push(() => {
|
||||||
client.serverConnection.command_handler_boss().remove_single_handler(single_ban_handler);
|
client.serverConnection.getCommandHandler().removeSingleHandler(single_ban_handler);
|
||||||
client.serverConnection.command_handler_boss().remove_single_handler(single_trigger_handler);
|
client.serverConnection.getCommandHandler().removeSingleHandler(single_trigger_handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: Test without dividerfy!
|
//TODO: Test without dividerfy!
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {ClientConnectionInfo, ClientEntry} from "../../tree/Client";
|
||||||
import PermissionType from "../../permission/PermissionType";
|
import PermissionType from "../../permission/PermissionType";
|
||||||
import {createInfoModal, createModal, Modal} from "../../ui/elements/Modal";
|
import {createInfoModal, createModal, Modal} from "../../ui/elements/Modal";
|
||||||
import {copyToClipboard} from "../../utils/helpers";
|
import {copyToClipboard} from "../../utils/helpers";
|
||||||
import * as i18nc from "../../i18n/country";
|
import * as i18nc from "../../i18n/CountryFlag";
|
||||||
import * as tooltip from "../../ui/elements/Tooltip";
|
import * as tooltip from "../../ui/elements/Tooltip";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {format_number, network} from "../../ui/frames/chat";
|
import {format_number, network} from "../../ui/frames/chat";
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {CommandResult} from "../../connection/ServerConnectionDeclaration";
|
||||||
import {LogCategory, logError, logWarn} from "../../log";
|
import {LogCategory, logError, logWarn} from "../../log";
|
||||||
import {tr, tra} from "../../i18n/localize";
|
import {tr, tra} from "../../i18n/localize";
|
||||||
import * as tooltip from "../../ui/elements/Tooltip";
|
import * as tooltip from "../../ui/elements/Tooltip";
|
||||||
import * as i18nc from "../../i18n/country";
|
import * as i18nc from "../../i18n/CountryFlag";
|
||||||
import {find} from "../../permission/PermissionManager";
|
import {find} from "../../permission/PermissionManager";
|
||||||
import * as htmltags from "../../ui/htmltags";
|
import * as htmltags from "../../ui/htmltags";
|
||||||
import {ErrorCode} from "../../connection/ErrorCode";
|
import {ErrorCode} from "../../connection/ErrorCode";
|
||||||
|
|
|
@ -35,14 +35,14 @@ export function spawnQueryCreate(connection: ConnectionHandler, callback_created
|
||||||
},
|
},
|
||||||
command: "notifyquerycreated"
|
command: "notifyquerycreated"
|
||||||
};
|
};
|
||||||
connection.serverConnection.command_handler_boss().register_single_handler(single_handler);
|
connection.serverConnection.getCommandHandler().registerSingleHandler(single_handler);
|
||||||
connection.serverConnection.send_command("querycreate", {
|
connection.serverConnection.send_command("querycreate", {
|
||||||
client_login_name: name
|
client_login_name: name
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (error instanceof CommandResult)
|
if (error instanceof CommandResult)
|
||||||
error = error.extra_message || error.message;
|
error = error.extra_message || error.message;
|
||||||
createErrorModal(tr("Unable to create account"), tr("Failed to create account<br>Message: ") + error).open();
|
createErrorModal(tr("Unable to create account"), tr("Failed to create account<br>Message: ") + error).open();
|
||||||
}).then(() => connection.serverConnection.command_handler_boss().remove_single_handler(single_handler));
|
}).then(() => connection.serverConnection.getCommandHandler().removeSingleHandler(single_handler));
|
||||||
|
|
||||||
modal.close();
|
modal.close();
|
||||||
});
|
});
|
||||||
|
|
|
@ -394,13 +394,13 @@ export function spawnQueryManage(client: ConnectionHandler) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client.serverConnection.command_handler_boss().register_single_handler(single_handler);
|
client.serverConnection.getCommandHandler().registerSingleHandler(single_handler);
|
||||||
|
|
||||||
client.serverConnection.send_command("querychangepassword", {
|
client.serverConnection.send_command("querychangepassword", {
|
||||||
client_login_name: selected_query.username,
|
client_login_name: selected_query.username,
|
||||||
client_login_password: result
|
client_login_password: result
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
client.serverConnection.command_handler_boss().remove_single_handler(single_handler);
|
client.serverConnection.getCommandHandler().removeSingleHandler(single_handler);
|
||||||
if (error instanceof CommandResult)
|
if (error instanceof CommandResult)
|
||||||
error = error.extra_message || error.message;
|
error = error.extra_message || error.message;
|
||||||
createErrorModal(tr("Unable to change password"), formatMessage(tr("Failed to change password{:br:}Message: {}"), error)).open();
|
createErrorModal(tr("Unable to change password"), formatMessage(tr("Failed to change password{:br:}Message: {}"), error)).open();
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {LogCategory, logDebug, logError, logTrace, logWarn} from "tc-shared/log"
|
||||||
import * as i18n from "tc-shared/i18n/localize";
|
import * as i18n from "tc-shared/i18n/localize";
|
||||||
import {RepositoryTranslation, TranslationRepository} from "tc-shared/i18n/localize";
|
import {RepositoryTranslation, TranslationRepository} from "tc-shared/i18n/localize";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import * as i18nc from "tc-shared/i18n/country";
|
import * as i18nc from "../../i18n/CountryFlag";
|
||||||
import * as forum from "tc-shared/profiles/identities/teaspeak-forum";
|
import * as forum from "tc-shared/profiles/identities/teaspeak-forum";
|
||||||
import {formatMessage, set_icon_size} from "tc-shared/ui/frames/chat";
|
import {formatMessage, set_icon_size} from "tc-shared/ui/frames/chat";
|
||||||
import {spawnTeamSpeakIdentityImport, spawnTeamSpeakIdentityImprove} from "tc-shared/ui/modal/ModalIdentity";
|
import {spawnTeamSpeakIdentityImport, spawnTeamSpeakIdentityImprove} from "tc-shared/ui/modal/ModalIdentity";
|
||||||
|
|
|
@ -291,7 +291,7 @@ class Controller {
|
||||||
this.assignmentLoadEnqueued = false;
|
this.assignmentLoadEnqueued = false;
|
||||||
|
|
||||||
let resultSet = false;
|
let resultSet = false;
|
||||||
const unregisterCallback = this.handler.serverConnection.command_handler_boss().register_explicit_handler("notifyservergroupsbyclientid", command => {
|
const unregisterCallback = this.handler.serverConnection.getCommandHandler().registerCommandHandler("notifyservergroupsbyclientid", command => {
|
||||||
const payload = command.arguments;
|
const payload = command.arguments;
|
||||||
const clientId = parseInt(payload[0].cldbid);
|
const clientId = parseInt(payload[0].cldbid);
|
||||||
if(isNaN(clientId) || clientId !== this.clientDatabaseId) {
|
if(isNaN(clientId) || clientId !== this.clientDatabaseId) {
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
&.entry {
|
&.entry {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
|
|
||||||
:global .icon {
|
.icon {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
margin-right: .25em;
|
margin-right: .25em;
|
||||||
|
|
|
@ -101,7 +101,7 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
||||||
hidden={this.props.hidden}
|
hidden={this.props.hidden}
|
||||||
onContextMenu={e => this.onContextMenu(e)}
|
onContextMenu={e => this.onContextMenu(e)}
|
||||||
>
|
>
|
||||||
<IconRenderer icon={this.props.icon}/>
|
<IconRenderer icon={this.props.icon} className={cssStyle.icon}/>
|
||||||
<a><Translatable trIgnore={true}>{this.props.description}</Translatable></a>
|
<a><Translatable trIgnore={true}>{this.props.description}</Translatable></a>
|
||||||
{rightItem}
|
{rightItem}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
import {getCountryFlag, getCountryName} from "tc-shared/i18n/country";
|
import {getCountryFlag, getCountryName} from "../../i18n/CountryFlag";
|
||||||
|
|
||||||
const cssStyle = require("./CountryIcon.scss");
|
const cssStyle = require("./CountryIcon.scss");
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
setupDragData
|
setupDragData
|
||||||
} from "tc-shared/ui/tree/DragHelper";
|
} from "tc-shared/ui/tree/DragHelper";
|
||||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
|
import {CallOnce} from "tc-shared/proto";
|
||||||
|
|
||||||
function isEquivalent(a, b) {
|
function isEquivalent(a, b) {
|
||||||
const typeA = typeof a;
|
const typeA = typeof a;
|
||||||
|
@ -757,25 +758,17 @@ export abstract class RDPEntry {
|
||||||
unread: boolean = false;
|
unread: boolean = false;
|
||||||
|
|
||||||
private renderedInstance: React.ReactElement;
|
private renderedInstance: React.ReactElement;
|
||||||
private destroyed = false;
|
|
||||||
|
|
||||||
protected constructor(handle: RDPChannelTree, entryId: number) {
|
protected constructor(handle: RDPChannelTree, entryId: number) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
this.entryId = entryId;
|
this.entryId = entryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
destroy() {
|
destroy() {
|
||||||
if(this.destroyed) {
|
|
||||||
throw "can not destry an entry twice";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderedInstance = undefined;
|
this.renderedInstance = undefined;
|
||||||
this.destroyed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* returns true if this element does not longer exists, but it's still rendered */
|
|
||||||
isDestroyed() { return this.destroyed; }
|
|
||||||
|
|
||||||
getEvents() : Registry<ChannelTreeUIEvents> { return this.handle.events; }
|
getEvents() : Registry<ChannelTreeUIEvents> { return this.handle.events; }
|
||||||
getHandlerId() : string { return this.handle.handlerId; }
|
getHandlerId() : string { return this.handle.handlerId; }
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {RDPEntry} from "tc-shared/ui/tree/RendererDataProvider";
|
import {RDPEntry} from "tc-shared/ui/tree/RendererDataProvider";
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ class JavascriptInput implements AbstractInput {
|
||||||
private currentAudioStream: MediaStreamAudioSourceNode;
|
private currentAudioStream: MediaStreamAudioSourceNode;
|
||||||
|
|
||||||
private audioContext: AudioContext;
|
private audioContext: AudioContext;
|
||||||
private sourceNode: AudioNode; /* last node which could be connected to the target; target might be the _consumer_node */
|
private audioSourceNode: AudioNode; /* last node which could be connected to the target; target might be the _consumer_node */
|
||||||
private audioNodeCallbackConsumer: ScriptProcessorNode;
|
private audioNodeCallbackConsumer: ScriptProcessorNode;
|
||||||
private readonly audioScriptProcessorCallback;
|
private readonly audioScriptProcessorCallback;
|
||||||
private audioNodeVolume: GainNode;
|
private audioNodeVolume: GainNode;
|
||||||
|
@ -167,8 +167,12 @@ class JavascriptInput implements AbstractInput {
|
||||||
return InputStartError.EBUSY;
|
return InputStartError.EBUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* do it async since if the doStart fails on the first iteration, we're setting the start promise, after it's getting cleared */
|
try {
|
||||||
return await (this.startPromise = Promise.resolve().then(() => this.doStart()));
|
this.startPromise = this.doStart();
|
||||||
|
return await this.startPromise;
|
||||||
|
} finally {
|
||||||
|
this.startPromise = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doStart() : Promise<InputStartError | true> {
|
private async doStart() : Promise<InputStartError | true> {
|
||||||
|
@ -231,8 +235,6 @@ class JavascriptInput implements AbstractInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
this.startPromise = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,24 +385,24 @@ class JavascriptInput implements AbstractInput {
|
||||||
async setConsumer(consumer: InputConsumer) {
|
async setConsumer(consumer: InputConsumer) {
|
||||||
if(this.consumer) {
|
if(this.consumer) {
|
||||||
if(this.consumer.type == InputConsumerType.NODE) {
|
if(this.consumer.type == InputConsumerType.NODE) {
|
||||||
if(this.sourceNode) {
|
if(this.audioSourceNode) {
|
||||||
this.consumer.callbackDisconnect(this.sourceNode);
|
this.consumer.callbackDisconnect(this.audioSourceNode);
|
||||||
}
|
}
|
||||||
} else if(this.consumer.type === InputConsumerType.CALLBACK) {
|
} else if(this.consumer.type === InputConsumerType.CALLBACK) {
|
||||||
if(this.sourceNode) {
|
if(this.audioSourceNode) {
|
||||||
this.sourceNode.disconnect(this.audioNodeCallbackConsumer);
|
this.audioSourceNode.disconnect(this.audioNodeCallbackConsumer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(consumer) {
|
if(consumer) {
|
||||||
if(consumer.type == InputConsumerType.CALLBACK) {
|
if(consumer.type == InputConsumerType.CALLBACK) {
|
||||||
if(this.sourceNode) {
|
if(this.audioSourceNode) {
|
||||||
this.sourceNode.connect(this.audioNodeCallbackConsumer);
|
this.audioSourceNode.connect(this.audioNodeCallbackConsumer);
|
||||||
}
|
}
|
||||||
} else if(consumer.type == InputConsumerType.NODE) {
|
} else if(consumer.type == InputConsumerType.NODE) {
|
||||||
if(this.sourceNode) {
|
if(this.audioSourceNode) {
|
||||||
consumer.callbackNode(this.sourceNode);
|
consumer.callbackNode(this.audioSourceNode);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw "native callback consumers are not supported!";
|
throw "native callback consumers are not supported!";
|
||||||
|
@ -413,22 +415,22 @@ class JavascriptInput implements AbstractInput {
|
||||||
if(this.consumer) {
|
if(this.consumer) {
|
||||||
if(this.consumer.type == InputConsumerType.NODE) {
|
if(this.consumer.type == InputConsumerType.NODE) {
|
||||||
const node_consumer = this.consumer as NodeInputConsumer;
|
const node_consumer = this.consumer as NodeInputConsumer;
|
||||||
if(this.sourceNode) {
|
if(this.audioSourceNode) {
|
||||||
node_consumer.callbackDisconnect(this.sourceNode);
|
node_consumer.callbackDisconnect(this.audioSourceNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newNode) {
|
if(newNode) {
|
||||||
node_consumer.callbackNode(newNode);
|
node_consumer.callbackNode(newNode);
|
||||||
}
|
}
|
||||||
} else if(this.consumer.type == InputConsumerType.CALLBACK) {
|
} else if(this.consumer.type == InputConsumerType.CALLBACK) {
|
||||||
this.sourceNode.disconnect(this.audioNodeCallbackConsumer);
|
this.audioSourceNode.disconnect(this.audioNodeCallbackConsumer);
|
||||||
if(newNode) {
|
if(newNode) {
|
||||||
newNode.connect(this.audioNodeCallbackConsumer);
|
newNode.connect(this.audioNodeCallbackConsumer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sourceNode = newNode;
|
this.audioSourceNode = newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentConsumer(): InputConsumer | undefined {
|
currentConsumer(): InputConsumer | undefined {
|
||||||
|
@ -480,7 +482,7 @@ class JavascriptInput implements AbstractInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputProcessor(): InputProcessor {
|
getInputProcessor(): InputProcessor {
|
||||||
return new JavaScriptInputProcessor();
|
return JavaScriptInputProcessor.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
createLevelMeter(): LevelMeter {
|
createLevelMeter(): LevelMeter {
|
||||||
|
@ -489,6 +491,8 @@ class JavascriptInput implements AbstractInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
class JavaScriptInputProcessor implements InputProcessor {
|
class JavaScriptInputProcessor implements InputProcessor {
|
||||||
|
static readonly Instance = new JavaScriptInputProcessor();
|
||||||
|
|
||||||
applyProcessorConfig<T extends InputProcessorType>(processor: T, config: InputProcessorConfigMapping[T]) {
|
applyProcessorConfig<T extends InputProcessorType>(processor: T, config: InputProcessorConfigMapping[T]) {
|
||||||
throw tr("target processor is not supported");
|
throw tr("target processor is not supported");
|
||||||
}
|
}
|
||||||
|
@ -559,8 +563,8 @@ class JavascriptLevelMeter implements LevelMeter {
|
||||||
/* starting stream */
|
/* starting stream */
|
||||||
const _result = await requestMediaStream(this._device.deviceId, this._device.groupId, "audio");
|
const _result = await requestMediaStream(this._device.deviceId, this._device.groupId, "audio");
|
||||||
if(!(_result instanceof MediaStream)){
|
if(!(_result instanceof MediaStream)){
|
||||||
if(_result === InputStartError.ENOTALLOWED)
|
if(_result === InputStartError.ENOTALLOWED) {
|
||||||
throw tr("No permissions");
|
throw tr("No permissions");}
|
||||||
if(_result === InputStartError.ENOTSUPPORTED)
|
if(_result === InputStartError.ENOTSUPPORTED)
|
||||||
throw tr("Not supported");
|
throw tr("Not supported");
|
||||||
if(_result === InputStartError.EBUSY)
|
if(_result === InputStartError.EBUSY)
|
||||||
|
@ -630,7 +634,8 @@ class JavascriptLevelMeter implements LevelMeter {
|
||||||
this._analyser_node.getByteTimeDomainData(this._analyse_buffer);
|
this._analyser_node.getByteTimeDomainData(this._analyse_buffer);
|
||||||
|
|
||||||
this._current_level = JThresholdFilter.calculateAudioLevel(this._analyse_buffer, this._analyser_node.fftSize, this._current_level, .75);
|
this._current_level = JThresholdFilter.calculateAudioLevel(this._analyse_buffer, this._analyser_node.fftSize, this._current_level, .75);
|
||||||
if(this._callback)
|
if(this._callback) {
|
||||||
this._callback(this._current_level);
|
this._callback(this._current_level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,4 @@
|
||||||
export interface ParsedCommand {
|
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||||
command?: string;
|
|
||||||
|
|
||||||
payload: {[key: string]: string}[]
|
|
||||||
switches: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function unescapeCommandValue(value: string) : string {
|
function unescapeCommandValue(value: string) : string {
|
||||||
let result = "", index = 0, lastIndex = 0;
|
let result = "", index = 0, lastIndex = 0;
|
||||||
|
@ -55,7 +50,7 @@ const escapeCharacterMap = {
|
||||||
|
|
||||||
const escapeCommandValue = (value: string) => value.replace(/[\\ \/|\b\f\n\r\t\x07]/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 {
|
export function parseCommand(command: string): ServerCommand {
|
||||||
const parts = command.split("|").map(element => element.split(" ").map(e => e.trim()).filter(e => !!e));
|
const parts = command.split("|").map(element => element.split(" ").map(e => e.trim()).filter(e => !!e));
|
||||||
|
|
||||||
let cmd;
|
let cmd;
|
||||||
|
@ -84,11 +79,7 @@ export function parseCommand(command: string): ParsedCommand {
|
||||||
payloads.push(payload)
|
payloads.push(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return new ServerCommand(cmd, payloads, switches);
|
||||||
command: cmd,
|
|
||||||
payload: payloads,
|
|
||||||
switches: switches
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCommand(data: any | any[], switches?: string[], command?: string) {
|
export function buildCommand(data: any | any[], switches?: string[], command?: string) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
AbstractServerConnection,
|
AbstractServerConnection,
|
||||||
CommandOptionDefaults,
|
CommandOptionDefaults,
|
||||||
CommandOptions, ConnectionPing,
|
CommandOptions, ConnectionPing,
|
||||||
ConnectionStatistics,
|
ConnectionStatistics, ServerCommand,
|
||||||
} from "tc-shared/connection/ConnectionBase";
|
} from "tc-shared/connection/ConnectionBase";
|
||||||
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
|
||||||
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
||||||
|
@ -71,7 +71,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
this.commandHandlerBoss = new ServerConnectionCommandBoss(this);
|
this.commandHandlerBoss = new ServerConnectionCommandBoss(this);
|
||||||
this.defaultCommandHandler = new ConnectionCommandHandler(this);
|
this.defaultCommandHandler = new ConnectionCommandHandler(this);
|
||||||
|
|
||||||
this.commandHandlerBoss.register_handler(this.defaultCommandHandler);
|
this.commandHandlerBoss.registerHandler(this.defaultCommandHandler);
|
||||||
this.command_helper.initialize();
|
this.command_helper.initialize();
|
||||||
|
|
||||||
this.rtcConnection = new RTCConnection(this, true);
|
this.rtcConnection = new RTCConnection(this, true);
|
||||||
|
@ -101,7 +101,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
this.rtcConnection.destroy();
|
this.rtcConnection.destroy();
|
||||||
this.command_helper.destroy();
|
this.command_helper.destroy();
|
||||||
|
|
||||||
this.defaultCommandHandler && this.commandHandlerBoss.unregister_handler(this.defaultCommandHandler);
|
this.defaultCommandHandler && this.commandHandlerBoss.unregisterHandler(this.defaultCommandHandler);
|
||||||
this.defaultCommandHandler = undefined;
|
this.defaultCommandHandler = undefined;
|
||||||
|
|
||||||
this.voiceConnection && this.voiceConnection.destroy();
|
this.voiceConnection && this.voiceConnection.destroy();
|
||||||
|
@ -330,10 +330,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
|
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
|
||||||
/* devel-block-end */
|
/* devel-block-end */
|
||||||
|
|
||||||
this.commandHandlerBoss.invoke_handle({
|
this.commandHandlerBoss.invokeCommand(new ServerCommand(json["command"], json["data"], []));
|
||||||
command: json["command"],
|
|
||||||
arguments: json["data"]
|
|
||||||
});
|
|
||||||
|
|
||||||
if(json["command"] === "initserver") {
|
if(json["command"] === "initserver") {
|
||||||
this.handleServerInit();
|
this.handleServerInit();
|
||||||
|
@ -344,10 +341,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
} else if(json["type"] === "command-raw") {
|
} else if(json["type"] === "command-raw") {
|
||||||
const command = parseCommand(json["payload"]);
|
const command = parseCommand(json["payload"]);
|
||||||
logTrace(LogCategory.NETWORKING, tr("Received command %s"), command.command);
|
logTrace(LogCategory.NETWORKING, tr("Received command %s"), command.command);
|
||||||
this.commandHandlerBoss.invoke_handle({
|
this.commandHandlerBoss.invokeCommand(command);
|
||||||
command: command.command,
|
|
||||||
arguments: command.payload
|
|
||||||
});
|
|
||||||
|
|
||||||
if(command.command === "initserver") {
|
if(command.command === "initserver") {
|
||||||
this.handleServerInit();
|
this.handleServerInit();
|
||||||
|
@ -471,7 +465,7 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
return this.videoConnection;
|
return this.videoConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
command_handler_boss(): AbstractCommandHandlerBoss {
|
getCommandHandler(): AbstractCommandHandlerBoss {
|
||||||
return this.commandHandlerBoss;
|
return this.commandHandlerBoss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.listenerCallbacks.push(
|
this.listenerCallbacks.push(
|
||||||
this.rtcConnection.getConnection().command_handler_boss().register_explicit_handler("notifyclientmoved", event => {
|
this.rtcConnection.getConnection().getCommandHandler().registerCommandHandler("notifyclientmoved", event => {
|
||||||
const localClientId = this.rtcConnection.getConnection().client.getClientId();
|
const localClientId = this.rtcConnection.getConnection().client.getClientId();
|
||||||
for(const data of event.arguments) {
|
for(const data of event.arguments) {
|
||||||
if(parseInt(data["clid"]) === localClientId) {
|
if(parseInt(data["clid"]) === localClientId) {
|
||||||
|
|
Loading…
Reference in New Issue