Some minor changes

master
WolverinDEV 2021-04-27 13:30:33 +02:00
parent e7ff40b0ee
commit 871231cbd5
50 changed files with 843 additions and 330 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,26 +670,42 @@ 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;
} }
videoStreamingCount++;
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()]; const controller = this.clientVideos[client.clientId()];
if(controller.isBroadcasting()) { if(controller.isBroadcasting()) {
videoStreamingCount++;
} else if(!settings.getValue(Settings.KEY_VIDEO_SHOW_ALL_CLIENTS)) {
continue; continue;
} }
videoIds.push(controller.videoId); videoIds.push(controller.videoId);
} }
} }
}
this.updateVisibility(videoStreamingCount !== 0); this.updateVisibility(videoStreamingCount !== 0);
if(this.expended) { if(this.expended) {
@ -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; }

View File

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

View File

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

View File

@ -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,27 +425,50 @@ 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;
return ( case "none":
<div className={cssStyle.actionIcons}> default:
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (screenShown ? "" : cssStyle.hidden) + " " + (screenDisabled ? cssStyle.disabled : "")} break;
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "screen", muted: !screenDisabled })} }
title={screenDisabled ? tr("Unmute screen video") : tr("Mute screen video")}
> switch (props.cameraState) {
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.ShareScreen} /> case "available":
</div> case "ignored":
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (cameraShown ? "" : cssStyle.hidden) + " " + (cameraDisabled ? cssStyle.disabled : "")} buttons.push(
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "camera", muted: !cameraDisabled })} <VideoToggleButton videoId={props.videoId} target={false} broadcastType={"camera"} key={"camera-disabled"} />
title={cameraDisabled ? tr("Unmute camera video") : tr("Mute camera video")} );
> break;
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.VideoMuted} />
</div> 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 : "")} <div className={cssStyle.iconContainer + " " + (props.fullscreenMode === "unavailable" ? cssStyle.hidden : "")}
key={"spotlight"}
onClick={() => { onClick={() => {
if(props.isSpotlight) { if(props.isSpotlight) {
events.fire("action_set_fullscreen", { videoId: props.fullscreenMode === "set" ? undefined : props.videoId }); events.fire("action_set_fullscreen", { videoId: props.fullscreenMode === "set" ? undefined : props.videoId });
@ -381,11 +481,18 @@ const VideoControlButtons = React.memo((props: {
> >
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.Fullscreen} /> <ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.Fullscreen} />
</div> </div>
);
return (
<div className={cssStyle.actionIcons}>
{buttons}
</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,14 +646,18 @@ 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) {
return;
}
const children = [...container.children] as HTMLElement[]; const children = [...container.children] as HTMLElement[];
if(event.direction === "left") { if(event.direction === "left") {
const currentCutOff = container.scrollLeft; const currentCutOff = container.scrollLeft;
@ -560,7 +673,6 @@ const VideoBar = React.memo(() => {
container.scrollLeft = element.offsetLeft - arrowLeft.clientWidth; 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}>
<VideoContainer videoId={videoId} isSpotlight={false} /> <VideoIdContext.Provider value={videoId}>
<VideoContainer isSpotlight={false} />
</VideoIdContext.Provider>
</ErrorBoundary> </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>
) )
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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