Some minor changes
parent
e7ff40b0ee
commit
871231cbd5
|
@ -3,7 +3,7 @@
|
|||
|
||||
import * as path from "path";
|
||||
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");
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
|
||||
import {ClientEntry, ClientType, LocalClientEntry} from "tc-shared/tree/Client";
|
||||
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";
|
||||
|
||||
|
@ -231,7 +231,7 @@ export class SelectedClientInfo {
|
|||
|
||||
private initializeClientInfo(client: ClientEntry) {
|
||||
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,
|
||||
databaseId: client.properties.client_database_id,
|
||||
uniqueId: client.properties.client_unique_identifier,
|
||||
|
|
|
@ -20,14 +20,14 @@ export abstract class AbstractCommandHandler {
|
|||
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 {
|
||||
readonly connection: AbstractServerConnection;
|
||||
protected command_handlers: AbstractCommandHandler[] = [];
|
||||
/* TODO: Timeout */
|
||||
protected single_command_handler: SingleCommandHandler[] = [];
|
||||
|
||||
protected explicitHandlers: {[key: string]:ExplicitCommandHandler[]} = {};
|
||||
protected explicitHandlers: {[key: string]:CommandHandlerCallback[]} = {};
|
||||
|
||||
protected constructor(connection: AbstractServerConnection) {
|
||||
this.connection = connection;
|
||||
|
@ -38,14 +38,14 @@ export abstract class AbstractCommandHandlerBoss {
|
|||
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].push(callback);
|
||||
|
||||
return () => this.explicitHandlers[command].remove(callback);
|
||||
}
|
||||
|
||||
unregister_explicit_handler(command: string, callback: ExplicitCommandHandler) {
|
||||
unregisterCommandHandler(command: string, callback: CommandHandlerCallback) {
|
||||
if(!this.explicitHandlers[command])
|
||||
return false;
|
||||
|
||||
|
@ -53,16 +53,17 @@ export abstract class AbstractCommandHandlerBoss {
|
|||
return true;
|
||||
}
|
||||
|
||||
register_handler(handler: AbstractCommandHandler) {
|
||||
if(!handler.volatile_handler_boss && handler.handler_boss)
|
||||
registerHandler(handler: AbstractCommandHandler) {
|
||||
if(!handler.volatile_handler_boss && handler.handler_boss) {
|
||||
throw "handler already registered";
|
||||
}
|
||||
|
||||
this.command_handlers.remove(handler); /* just to be sure */
|
||||
this.command_handlers.push(handler);
|
||||
handler.handler_boss = this;
|
||||
}
|
||||
|
||||
unregister_handler(handler: AbstractCommandHandler) {
|
||||
unregisterHandler(handler: AbstractCommandHandler) {
|
||||
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"));
|
||||
return;
|
||||
|
@ -73,13 +74,13 @@ export abstract class AbstractCommandHandlerBoss {
|
|||
}
|
||||
|
||||
|
||||
register_single_handler(handler: SingleCommandHandler) {
|
||||
registerSingleHandler(handler: SingleCommandHandler) {
|
||||
if(typeof handler.command === "string")
|
||||
handler.command = [handler.command];
|
||||
this.single_command_handler.push(handler);
|
||||
}
|
||||
|
||||
remove_single_handler(handler: SingleCommandHandler) {
|
||||
removeSingleHandler(handler: SingleCommandHandler) {
|
||||
this.single_command_handler.remove(handler);
|
||||
}
|
||||
|
||||
|
@ -87,7 +88,7 @@ export abstract class AbstractCommandHandlerBoss {
|
|||
return this.command_handlers;
|
||||
}
|
||||
|
||||
invoke_handle(command: ServerCommand) : boolean {
|
||||
invokeCommand(command: ServerCommand) : boolean {
|
||||
let flag_consumed = false;
|
||||
|
||||
for(const handler of this.command_handlers) {
|
||||
|
|
|
@ -154,7 +154,7 @@ export class ClientInfoResolver {
|
|||
try {
|
||||
const requestDatabaseIds = Object.keys(this.requestDatabaseIds);
|
||||
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 => {
|
||||
if(this.requestDatabaseIds[info.clientDatabaseId].fullFilled) {
|
||||
return;
|
||||
|
@ -197,7 +197,7 @@ export class ClientInfoResolver {
|
|||
|
||||
const requestUniqueIds = Object.keys(this.requestUniqueIds);
|
||||
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 => {
|
||||
if(this.requestUniqueIds[info.clientUniqueId].fullFilled) {
|
||||
return;
|
||||
|
|
|
@ -487,7 +487,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
client = tree.findClient(parseInt(entry["clid"]));
|
||||
|
||||
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;
|
||||
} else {
|
||||
client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]);
|
||||
|
@ -501,7 +501,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
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();
|
||||
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,
|
||||
|
@ -584,7 +584,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
}
|
||||
|
||||
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();
|
||||
let channel_from = tree.findChannel(entry["cfid"]);
|
||||
let channel_to = tree.findChannel(targetChannelId);
|
||||
|
|
|
@ -24,13 +24,13 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
}
|
||||
|
||||
initialize() {
|
||||
this.connection.command_handler_boss().register_handler(this);
|
||||
this.connection.getCommandHandler().registerHandler(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this.connection) {
|
||||
const hboss = this.connection.command_handler_boss();
|
||||
hboss?.unregister_handler(this);
|
||||
const hboss = this.connection.getCommandHandler();
|
||||
hboss?.unregisterHandler(this);
|
||||
}
|
||||
|
||||
this.infoByUniqueIdRequest = undefined;
|
||||
|
@ -173,7 +173,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
return true;
|
||||
}
|
||||
};
|
||||
this.handler_boss.register_single_handler(single_handler);
|
||||
this.handler_boss.registerSingleHandler(single_handler);
|
||||
|
||||
let data = {};
|
||||
if(server_id !== undefined) {
|
||||
|
@ -189,7 +189,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
}
|
||||
reject(error);
|
||||
}).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;
|
||||
}
|
||||
};
|
||||
this.handler_boss.register_single_handler(single_handler);
|
||||
this.handler_boss.registerSingleHandler(single_handler);
|
||||
|
||||
this.connection.send_command("playlistlist").catch(error => {
|
||||
if(error instanceof CommandResult) {
|
||||
|
@ -239,7 +239,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
}
|
||||
reject(error);
|
||||
}).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 => {
|
||||
if(error instanceof CommandResult) {
|
||||
|
@ -305,7 +305,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
}
|
||||
reject(error);
|
||||
}).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;
|
||||
}
|
||||
};
|
||||
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 => {
|
||||
if(error instanceof CommandResult && error.id == ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||
|
@ -341,7 +341,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
}
|
||||
reject(error);
|
||||
}).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;
|
||||
}
|
||||
};
|
||||
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.handler_boss.remove_single_handler(single_handler);
|
||||
this.handler_boss.removeSingleHandler(single_handler);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -422,10 +422,10 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
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.handler_boss.remove_single_handler(single_handler);
|
||||
this.handler_boss.removeSingleHandler(single_handler);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -451,10 +451,10 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
return true;
|
||||
}
|
||||
};
|
||||
this.handler_boss.register_single_handler(single_handler);
|
||||
this.handler_boss.registerSingleHandler(single_handler);
|
||||
|
||||
this.connection.send_command("whoami").catch(error => {
|
||||
this.handler_boss.remove_single_handler(single_handler);
|
||||
this.handler_boss.removeSingleHandler(single_handler);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,7 +63,7 @@ export abstract class AbstractServerConnection {
|
|||
abstract getVoiceConnection() : AbstractVoiceConnection;
|
||||
abstract getVideoConnection() : VideoConnection;
|
||||
|
||||
abstract command_handler_boss() : AbstractCommandHandlerBoss;
|
||||
abstract getCommandHandler() : AbstractCommandHandlerBoss;
|
||||
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
|
||||
|
||||
abstract remote_address() : ServerAddress; /* only valid when connected */
|
||||
|
@ -89,6 +89,36 @@ export abstract class AbstractServerConnection {
|
|||
export class ServerCommand {
|
||||
command: string;
|
||||
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 {
|
||||
|
|
|
@ -97,11 +97,11 @@ export class PluginCmdRegistry {
|
|||
this.connection = connection;
|
||||
|
||||
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() {
|
||||
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 => {
|
||||
handler["currentServerConnection"] = undefined;
|
||||
|
|
|
@ -3,7 +3,7 @@ import {Registry} from "../events";
|
|||
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "../connection/ErrorCode";
|
||||
import {LogCategory, logDebug, logTrace, logWarn} from "../log";
|
||||
import {ExplicitCommandHandler} from "../connection/AbstractCommandHandler";
|
||||
import {CommandHandlerCallback} from "../connection/AbstractCommandHandler";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
|
||||
export type ServerFeatureSupport = "unsupported" | "supported" | "experimental" | "deprecated";
|
||||
|
@ -28,7 +28,7 @@ export interface ServerFeatureEvents {
|
|||
export class ServerFeatures {
|
||||
readonly events: Registry<ServerFeatureEvents>;
|
||||
private readonly connection: ConnectionHandler;
|
||||
private readonly explicitCommandHandler: ExplicitCommandHandler;
|
||||
private readonly explicitCommandHandler: CommandHandlerCallback;
|
||||
private readonly stateChangeListener: () => void;
|
||||
|
||||
private featureAwait: Promise<boolean>;
|
||||
|
@ -41,7 +41,7 @@ export class ServerFeatures {
|
|||
this.events = new Registry<ServerFeatureEvents>();
|
||||
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) {
|
||||
let support: ServerFeatureSupport;
|
||||
switch (parseInt(set["support"])) {
|
||||
|
@ -96,7 +96,7 @@ export class ServerFeatures {
|
|||
|
||||
destroy() {
|
||||
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) {
|
||||
this.featureAwaitCallback(false);
|
||||
|
|
|
@ -69,8 +69,17 @@ export interface VideoClient {
|
|||
showPip(broadcastType: VideoBroadcastType) : Promise<void>;
|
||||
}
|
||||
|
||||
export type VideoBroadcastViewer = {
|
||||
clientId: number,
|
||||
clientName: string,
|
||||
clientUniqueId: string,
|
||||
clientDatabaseId: number,
|
||||
};
|
||||
|
||||
export interface LocalVideoBroadcastEvents {
|
||||
notify_state_changed: { oldState: LocalVideoBroadcastState, newState: LocalVideoBroadcastState },
|
||||
notify_clients_joined: { clients: VideoBroadcastViewer[] },
|
||||
notify_clients_left: { clientIds: number[] },
|
||||
}
|
||||
|
||||
export type LocalVideoBroadcastState = {
|
||||
|
@ -148,6 +157,8 @@ export interface LocalVideoBroadcast {
|
|||
getConstraints() : VideoBroadcastConfig | undefined;
|
||||
|
||||
stopBroadcasting();
|
||||
|
||||
getViewer() : VideoBroadcastViewer[];
|
||||
}
|
||||
|
||||
export interface VideoConnection {
|
||||
|
|
|
@ -449,19 +449,6 @@ export type RTCSourceTrackType = "audio" | "audio-whisper" | "video" | "video-sc
|
|||
export type RTCBroadcastableTrackType = Exclude<RTCSourceTrackType, "audio-whisper">;
|
||||
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 = {
|
||||
createTimestamp: number,
|
||||
timeoutId: number,
|
||||
|
@ -534,16 +521,15 @@ export class RTCConnection {
|
|||
this.retryCalculator = new RetryTimeCalculator(5000, 30000, 10000);
|
||||
this.audioSupport = audioSupport;
|
||||
|
||||
this.connection.command_handler_boss().register_handler(this.commandHandler);
|
||||
this.connection.getCommandHandler().registerHandler(this.commandHandler);
|
||||
this.reset(true);
|
||||
|
||||
this.connection.events.on("notify_connection_state_changed", event => this.handleConnectionStateChanged(event));
|
||||
|
||||
(window as any).rtp = this;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.connection.command_handler_boss().unregister_handler(this.commandHandler);
|
||||
this.connection.getCommandHandler().unregisterHandler(this.commandHandler);
|
||||
}
|
||||
|
||||
isAudioEnabled() : boolean {
|
||||
|
@ -680,6 +666,21 @@ export class RTCConnection {
|
|||
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) {
|
||||
let track: RTCBroadcastableTrackType;
|
||||
let broadcastType: number;
|
||||
|
|
|
@ -83,6 +83,16 @@ export class SdpProcessor {
|
|||
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 {
|
||||
/* The server somehow does not encode the level id in hex */
|
||||
sdpString = sdpString.replace(/profile-level-id=4325407/g, "profile-level-id=4d0028");
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
VideoBroadcastConfig,
|
||||
VideoBroadcastStatistics,
|
||||
VideoBroadcastType,
|
||||
VideoBroadcastViewer,
|
||||
VideoClient,
|
||||
VideoConnection,
|
||||
VideoConnectionEvent,
|
||||
|
@ -33,12 +34,14 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
|
|||
private broadcastStartId: number;
|
||||
|
||||
private localStartPromise: Promise<void>;
|
||||
private subscribedClients: VideoBroadcastViewer[];
|
||||
|
||||
constructor(handle: RtpVideoConnection, type: VideoBroadcastType) {
|
||||
this.handle = handle;
|
||||
this.type = type;
|
||||
this.broadcastStartId = 0;
|
||||
|
||||
this.subscribedClients = [];
|
||||
this.events = new Registry<LocalVideoBroadcastEvents>();
|
||||
this.state = { state: "stopped" };
|
||||
}
|
||||
|
@ -67,12 +70,31 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
|
|||
const oldState = this.state;
|
||||
this.state = 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> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
getViewer(): VideoBroadcastViewer[] {
|
||||
return this.subscribedClients;
|
||||
}
|
||||
|
||||
async changeSource(source: VideoSource, constraints: VideoBroadcastConfig): Promise<void> {
|
||||
let sourceRef = source.ref();
|
||||
try {
|
||||
|
@ -333,6 +355,26 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
|
|||
getConstraints(): VideoBroadcastConfig | undefined {
|
||||
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 {
|
||||
|
@ -355,7 +397,7 @@ export class RtpVideoConnection implements VideoConnection {
|
|||
this.listener = [];
|
||||
|
||||
/* 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();
|
||||
for(const data of event.arguments) {
|
||||
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 }[] = [];
|
||||
for(const data of event.arguments) {
|
||||
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 => {
|
||||
if(event.newState !== ConnectionState.CONNECTED) {
|
||||
Object.values(this.broadcasts).forEach(broadcast => broadcast.stopBroadcasting(true));
|
||||
|
|
|
@ -82,11 +82,6 @@ export class RtpVideoClient implements VideoClient {
|
|||
await this.handle.getConnection().send_command("broadcastvideojoin", {
|
||||
bid: this.broadcastIds[broadcastType],
|
||||
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 => {
|
||||
this.joinedStates[broadcastType] = false;
|
||||
this.updateBroadcastState(broadcastType);
|
||||
|
|
|
@ -395,9 +395,9 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
|||
|
||||
/* 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.command_handler_boss().register_explicit_handler("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("notifyconversationhistory", this.handleConversationHistory.bind(this)));
|
||||
this.listenerConnection.push(connection.serverConnection.getCommandHandler().registerCommandHandler("notifyconversationindex", this.handleConversationIndex.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.queryUnreadFlags();
|
||||
|
|
|
@ -64,13 +64,13 @@ class FileCommandHandler extends AbstractCommandHandler {
|
|||
super(manager.connectionHandler.serverConnection);
|
||||
this.manager = manager;
|
||||
|
||||
this.connection.command_handler_boss().register_handler(this);
|
||||
this.connection.getCommandHandler().registerHandler(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this.connection) {
|
||||
const hboss = this.connection.command_handler_boss();
|
||||
if(hboss) hboss.unregister_handler(this);
|
||||
const hboss = this.connection.getCommandHandler();
|
||||
if(hboss) hboss.unregisterHandler(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
"key": "de_gt",
|
||||
"country_code": "de",
|
||||
"path": "de_google_translate.translation",
|
||||
|
||||
"name": "German translation, based on Google Translate",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||
"email": "gtr.i18n.client@teaspeak.de"
|
||||
},
|
||||
{
|
||||
"name": "Markus Hadenfeldt",
|
||||
"email": "i18n.client@teaspeak.de"
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
import {CountryFlag} from "svg-sprites/country-flags";
|
||||
import {AbstractTranslationResolver} from "tc-shared/i18n/Translation";
|
||||
|
||||
export type I18NContributor = {
|
||||
name: string,
|
||||
email: string
|
||||
};
|
||||
|
||||
export type TranslationResolverCreateResult = {
|
||||
status: "success",
|
||||
resolver: AbstractTranslationResolver
|
||||
} | {
|
||||
status: "error",
|
||||
message: string
|
||||
};
|
||||
|
||||
export abstract class I18NTranslation {
|
||||
abstract getId() : string;
|
||||
abstract getName() : string;
|
||||
|
||||
abstract getCountry() : CountryFlag;
|
||||
abstract getDescription() : string;
|
||||
abstract getContributors() : I18NContributor[];
|
||||
|
||||
abstract createTranslationResolver() : Promise<TranslationResolverCreateResult>;
|
||||
}
|
||||
|
||||
export abstract class I18NRepository {
|
||||
abstract getName() : string;
|
||||
abstract getDescription() : string;
|
||||
|
||||
abstract getTranslations() : Promise<I18NTranslation[]>;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import {LogCategory, logError} from "tc-shared/log";
|
||||
|
||||
export abstract class AbstractTranslationResolver {
|
||||
private translationCache: { [key: string]: string };
|
||||
|
||||
protected constructor() {
|
||||
this.translationCache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the target message.
|
||||
* @param message
|
||||
*/
|
||||
public translateMessage(message: string) {
|
||||
/* typeof is the fastest method */
|
||||
if(typeof this.translationCache === "string") {
|
||||
return this.translationCache[message];
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = this.translationCache[message] = this.resolveTranslation(message);
|
||||
} catch (error) {
|
||||
/* Don't translate this message because it could cause an infinite loop */
|
||||
logError(LogCategory.I18N, "Failed to resolve translation message: %o", error);
|
||||
result = this.translationCache[message] = message;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected invalidateCache() {
|
||||
this.translationCache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a translation into the cache.
|
||||
* @param message
|
||||
* @param translation
|
||||
* @protected
|
||||
*/
|
||||
protected registerTranslation(message: string, translation: string) {
|
||||
this.translationCache[message] = translation;
|
||||
}
|
||||
|
||||
protected abstract resolveTranslation(message: string) : string;
|
||||
}
|
|
@ -201,7 +201,7 @@ class InternalSubscribedPlaylist extends SubscribedPlaylist {
|
|||
this.listenerConnection = [];
|
||||
|
||||
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"]);
|
||||
if(isNaN(playlistId)) {
|
||||
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"]);
|
||||
if(isNaN(playlistId)) {
|
||||
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"]);
|
||||
if(isNaN(playlistId)) {
|
||||
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"]);
|
||||
if(isNaN(playlistId)) {
|
||||
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.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"]);
|
||||
if(isNaN(playlistId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Received playlist song list notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||
|
|
|
@ -168,7 +168,7 @@ export class GroupManager extends AbstractCommandHandler {
|
|||
}
|
||||
};
|
||||
|
||||
client.serverConnection.command_handler_boss().register_handler(this);
|
||||
client.serverConnection.getCommandHandler().registerHandler(this);
|
||||
client.events().on("notify_connection_state_changed", this.connectionStateListener);
|
||||
|
||||
this.reset();
|
||||
|
@ -177,7 +177,7 @@ export class GroupManager extends AbstractCommandHandler {
|
|||
destroy() {
|
||||
this.reset();
|
||||
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.channelGroups = undefined;
|
||||
}
|
||||
|
|
|
@ -260,13 +260,13 @@ export class PermissionManager extends AbstractCommandHandler {
|
|||
|
||||
//FIXME? Dont register the handler like this?
|
||||
this.volatile_handler_boss = true;
|
||||
client.serverConnection.command_handler_boss().register_handler(this);
|
||||
client.serverConnection.getCommandHandler().registerHandler(this);
|
||||
|
||||
this.handle = client;
|
||||
}
|
||||
|
||||
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.permissionList = undefined;
|
||||
|
@ -712,10 +712,10 @@ export class PermissionManager extends AbstractCommandHandler {
|
|||
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.handler_boss.remove_single_handler(single_handler);
|
||||
this.handler_boss.removeSingleHandler(single_handler);
|
||||
|
||||
if(error instanceof CommandResult && error.id == ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||
resolve([]);
|
||||
|
|
|
@ -23,7 +23,7 @@ class NameHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
|||
}
|
||||
|
||||
executeHandshake() {
|
||||
this.connection.command_handler_boss().register_handler(this.handler);
|
||||
this.connection.getCommandHandler().registerHandler(this.handler);
|
||||
this.connection.send_command("handshakebegin", {
|
||||
intention: 0,
|
||||
authentication_method: this.identity.type(),
|
||||
|
@ -37,12 +37,12 @@ class NameHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
|||
}
|
||||
|
||||
protected trigger_fail(message: string) {
|
||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
||||
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||
super.trigger_fail(message);
|
||||
}
|
||||
|
||||
protected trigger_success() {
|
||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
||||
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||
super.trigger_success();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
|||
}
|
||||
|
||||
executeHandshake() {
|
||||
this.connection.command_handler_boss().register_handler(this.handler);
|
||||
this.connection.getCommandHandler().registerHandler(this.handler);
|
||||
this.connection.send_command("handshakebegin", {
|
||||
intention: 0,
|
||||
authentication_method: this.identity.type(),
|
||||
|
@ -51,12 +51,12 @@ class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
|||
}
|
||||
|
||||
protected trigger_fail(message: string) {
|
||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
||||
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||
super.trigger_fail(message);
|
||||
}
|
||||
|
||||
protected trigger_success() {
|
||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
||||
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||
super.trigger_success();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,7 +237,7 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
|||
}
|
||||
|
||||
executeHandshake() {
|
||||
this.connection.command_handler_boss().register_handler(this.handler);
|
||||
this.connection.getCommandHandler().registerHandler(this.handler);
|
||||
this.connection.send_command("handshakebegin", {
|
||||
intention: 0,
|
||||
authentication_method: this.identity.type(),
|
||||
|
@ -271,12 +271,12 @@ export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
|||
}
|
||||
|
||||
protected trigger_fail(message: string) {
|
||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
||||
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||
super.trigger_fail(message);
|
||||
}
|
||||
|
||||
protected trigger_success() {
|
||||
this.connection.command_handler_boss().unregister_handler(this.handler);
|
||||
this.connection.getCommandHandler().unregisterHandler(this.handler);
|
||||
super.trigger_success();
|
||||
}
|
||||
|
||||
|
|
|
@ -31,18 +31,19 @@ import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
|
|||
import {spawnServerGroupAssignments} from "tc-shared/ui/modal/group-assignment/Controller";
|
||||
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
|
||||
|
||||
/* Must be the same as the TeaSpeak servers enum values */
|
||||
export enum ClientType {
|
||||
CLIENT_VOICE,
|
||||
CLIENT_QUERY,
|
||||
CLIENT_INTERNAL,
|
||||
CLIENT_WEB,
|
||||
CLIENT_MUSIC,
|
||||
CLIENT_UNDEFINED
|
||||
CLIENT_VOICE = 0,
|
||||
CLIENT_QUERY = 1,
|
||||
CLIENT_WEB = 3,
|
||||
CLIENT_MUSIC = 4,
|
||||
CLIENT_TEASPEAK = 5,
|
||||
CLIENT_UNDEFINED = 5
|
||||
}
|
||||
|
||||
export class ClientProperties {
|
||||
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_version: string = "";
|
||||
|
@ -912,6 +913,44 @@ export class ClientEntry<Events extends ClientEvents = ClientEvents> extends Cha
|
|||
getAudioVolume() {
|
||||
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 {
|
||||
|
|
|
@ -307,7 +307,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
const connection = this.channelTree.client.serverConnection;
|
||||
|
||||
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 info = {} as any;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||
import {EventHandler} from "../../../events";
|
||||
import {EventHandler} from "tc-events";
|
||||
import {LogCategory, logError} from "../../../log";
|
||||
import {tr} from "../../../i18n/localize";
|
||||
import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions";
|
||||
|
|
|
@ -447,6 +447,7 @@ class ChannelVideoController {
|
|||
this.events.on("query_videos", () => this.notifyVideoList());
|
||||
this.events.on("query_spotlight", () => this.notifySpotlight());
|
||||
this.events.on("query_subscribe_info", () => this.notifySubscribeInfo());
|
||||
this.events.on("query_viewer_count", () => this.notifyViewerCount());
|
||||
|
||||
this.events.on("query_video_info", event => {
|
||||
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;
|
||||
events.push(channelTree.events.on("notify_tree_reset", () => {
|
||||
this.resetClientVideos();
|
||||
|
@ -512,6 +521,7 @@ class ChannelVideoController {
|
|||
this.notifyVideoList();
|
||||
}
|
||||
}
|
||||
|
||||
if(event.newChannel.channelId === this.currentChannelId) {
|
||||
this.createClientVideo(event.client);
|
||||
this.notifyVideoList();
|
||||
|
@ -527,6 +537,7 @@ class ChannelVideoController {
|
|||
if(this.destroyClientVideo(event.client.clientId())) {
|
||||
this.notifyVideoList();
|
||||
}
|
||||
|
||||
if(event.client instanceof LocalClientEntry) {
|
||||
this.resetClientVideos();
|
||||
}
|
||||
|
@ -541,6 +552,7 @@ class ChannelVideoController {
|
|||
this.createClientVideo(event.client);
|
||||
this.notifyVideoList();
|
||||
}
|
||||
|
||||
if(event.client instanceof LocalClientEntry) {
|
||||
this.updateLocalChannel(event.client);
|
||||
}
|
||||
|
@ -572,7 +584,7 @@ class ChannelVideoController {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -658,26 +670,42 @@ class ChannelVideoController {
|
|||
|
||||
const channel = this.connection.channelTree.findChannel(this.currentChannelId);
|
||||
if(channel) {
|
||||
const clients = channel.channelClientsOrdered();
|
||||
for(const client of clients) {
|
||||
const clients = channel.channelClientsOrdered().filter(client => {
|
||||
if(client instanceof LocalClientEntry) {
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this.clientVideos[client.clientId()]) {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
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()];
|
||||
if(controller.isBroadcasting()) {
|
||||
videoStreamingCount++;
|
||||
} else if(!settings.getValue(Settings.KEY_VIDEO_SHOW_ALL_CLIENTS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
videoIds.push(controller.videoId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateVisibility(videoStreamingCount !== 0);
|
||||
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) {
|
||||
if(this.currentlyVisible === target) { return; }
|
||||
|
||||
|
|
|
@ -87,7 +87,8 @@ export interface ChannelVideoEvents {
|
|||
query_video_statistics: { videoId: string, broadcastType: VideoBroadcastType },
|
||||
query_spotlight: {},
|
||||
query_video_stream: { videoId: string, broadcastType: VideoBroadcastType },
|
||||
query_subscribe_info: {}
|
||||
query_subscribe_info: {},
|
||||
query_viewer_count: {},
|
||||
|
||||
notify_expended: { expended: boolean },
|
||||
notify_videos: {
|
||||
|
@ -107,10 +108,6 @@ export interface ChannelVideoEvents {
|
|||
videoId: string,
|
||||
statusIcon: ClientIcon
|
||||
},
|
||||
notify_video_arrows: {
|
||||
left: boolean,
|
||||
right: boolean
|
||||
},
|
||||
notify_spotlight: {
|
||||
videoId: string[]
|
||||
},
|
||||
|
@ -126,6 +123,10 @@ export interface ChannelVideoEvents {
|
|||
},
|
||||
notify_subscribe_info: {
|
||||
info: VideoSubscribeInfo
|
||||
},
|
||||
notify_viewer_count: {
|
||||
camera: number | undefined,
|
||||
screen: number | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -124,7 +124,9 @@ $small_height: 10em;
|
|||
position: relative;
|
||||
|
||||
height: $small_height;
|
||||
width: 100%;
|
||||
|
||||
max-width: 100%;
|
||||
min-width: 16em;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
@ -136,9 +138,6 @@ $small_height: 10em;
|
|||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
|
||||
/* TODO: Min with of two video +4em for one of the arrows */
|
||||
min-width: 6em;
|
||||
|
||||
.videos {
|
||||
display: flex;
|
||||
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) {
|
||||
/* Note: don't use margin here since it might */
|
||||
position: relative;
|
||||
|
@ -401,13 +401,65 @@ $small_height: 10em;
|
|||
font-weight: normal!important;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
&.local {
|
||||
.name {
|
||||
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 {
|
||||
position: absolute;
|
||||
|
||||
|
|
|
@ -4,30 +4,30 @@ import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
|||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {
|
||||
ChannelVideoEvents,
|
||||
ChannelVideoInfo,
|
||||
ChannelVideoEvents, ChannelVideoInfo,
|
||||
ChannelVideoStreamState,
|
||||
kLocalVideoId,
|
||||
makeVideoAutoplay,
|
||||
kLocalVideoId, makeVideoAutoplay,
|
||||
VideoStreamState,
|
||||
VideoSubscribeInfo
|
||||
} from "./Definitions";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import {LogCategory, logTrace, logWarn} from "tc-shared/log";
|
||||
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
|
||||
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
|
||||
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 * 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 EventContext = React.createContext<Registry<ChannelVideoEvents>>(undefined);
|
||||
const HandlerIdContext = React.createContext<string>(undefined);
|
||||
export const VideoIdContext = React.createContext<string>(undefined);
|
||||
|
||||
export const RendererVideoEventContext = EventContext;
|
||||
|
||||
|
@ -47,16 +47,64 @@ const ExpendArrow = React.memo(() => {
|
|||
<div className={cssStyle.expendArrow} onClick={() => events.fire("action_toggle_expended", { expended: !expended })}>
|
||||
<ClientIconRenderer icon={ClientIcon.DoubleArrow} className={cssStyle.icon} />
|
||||
</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 handlerId = useContext(HandlerIdContext);
|
||||
|
||||
const localVideo = props.videoId === kLocalVideoId;
|
||||
const nameClassList = cssStyle.name + " " + (localVideo ? cssStyle.local : "");
|
||||
|
||||
const [ info, setInfo ] = useState<"loading" | ChannelVideoInfo>(() => {
|
||||
events.fire("query_video_info", { videoId: props.videoId });
|
||||
return "loading";
|
||||
|
@ -79,59 +127,23 @@ const VideoInfo = React.memo((props: { videoId: string }) => {
|
|||
|
||||
let clientName;
|
||||
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 {
|
||||
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 (
|
||||
<div className={cssStyle.info}>
|
||||
<div className={joinClassList(cssStyle.info, props.videoId === kLocalVideoId && cssStyle.local)}>
|
||||
<ClientIconRenderer icon={statusIcon} className={cssStyle.icon} />
|
||||
{clientName}
|
||||
</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 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 [ state, setState ] = useState<VideoStreamState>(() => {
|
||||
events.fire("query_video_stream", { videoId: props.videoId, broadcastType: props.streamType });
|
||||
return {
|
||||
state: "disconnected",
|
||||
}
|
||||
return { state: "disconnected", }
|
||||
});
|
||||
events.reactUse("notify_video_stream", event => {
|
||||
if(event.videoId === props.videoId && event.broadcastType === props.streamType) {
|
||||
|
@ -243,23 +286,34 @@ const VideoStreamRenderer = (props: { videoId: string, streamType: VideoBroadcas
|
|||
case "disconnected":
|
||||
return (
|
||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||
<div><Translatable>No video stream</Translatable></div>
|
||||
<div>
|
||||
<Translatable>No video stream</Translatable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "connecting":
|
||||
return (
|
||||
<div className={cssStyle.text} key={"info-initializing"}>
|
||||
<div><Translatable>connecting</Translatable> <LoadingDots /></div>
|
||||
<div>
|
||||
<Translatable>connecting</Translatable> <LoadingDots />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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":
|
||||
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>
|
||||
);
|
||||
|
@ -302,7 +356,7 @@ const VideoPlayer = React.memo((props: { videoId: string, cameraState: ChannelVi
|
|||
);
|
||||
} else if(props.screenState === "streaming") {
|
||||
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") {
|
||||
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}</>;
|
||||
});
|
||||
|
||||
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: {
|
||||
videoId: string,
|
||||
cameraState: ChannelVideoStreamState,
|
||||
|
@ -348,27 +425,50 @@ const VideoControlButtons = React.memo((props: {
|
|||
}) => {
|
||||
const events = useContext(EventContext);
|
||||
|
||||
const screenShown = props.screenState !== "none" && props.videoId !== kLocalVideoId;
|
||||
const cameraShown = props.cameraState !== "none" && props.videoId !== kLocalVideoId;
|
||||
let buttons = [];
|
||||
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";
|
||||
const cameraDisabled = props.cameraState === "ignored" || props.cameraState === "available";
|
||||
case "streaming":
|
||||
buttons.push(
|
||||
<VideoToggleButton videoId={props.videoId} target={true} broadcastType={"screen"} key={"screen-enabled"} />
|
||||
);
|
||||
break;
|
||||
|
||||
return (
|
||||
<div className={cssStyle.actionIcons}>
|
||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (screenShown ? "" : cssStyle.hidden) + " " + (screenDisabled ? cssStyle.disabled : "")}
|
||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "screen", muted: !screenDisabled })}
|
||||
title={screenDisabled ? tr("Unmute screen video") : tr("Mute screen video")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.ShareScreen} />
|
||||
</div>
|
||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (cameraShown ? "" : cssStyle.hidden) + " " + (cameraDisabled ? cssStyle.disabled : "")}
|
||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "camera", muted: !cameraDisabled })}
|
||||
title={cameraDisabled ? tr("Unmute camera video") : tr("Mute camera video")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.VideoMuted} />
|
||||
</div>
|
||||
case "none":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (props.cameraState) {
|
||||
case "available":
|
||||
case "ignored":
|
||||
buttons.push(
|
||||
<VideoToggleButton videoId={props.videoId} target={false} broadcastType={"camera"} key={"camera-disabled"} />
|
||||
);
|
||||
break;
|
||||
|
||||
case "streaming":
|
||||
buttons.push(
|
||||
<VideoToggleButton videoId={props.videoId} target={true} broadcastType={"camera"} key={"camera-enabled"} />
|
||||
);
|
||||
break;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<div className={cssStyle.iconContainer + " " + (props.fullscreenMode === "unavailable" ? cssStyle.hidden : "")}
|
||||
key={"spotlight"}
|
||||
onClick={() => {
|
||||
if(props.isSpotlight) {
|
||||
events.fire("action_set_fullscreen", { videoId: props.fullscreenMode === "set" ? undefined : props.videoId });
|
||||
|
@ -381,11 +481,18 @@ const VideoControlButtons = React.memo((props: {
|
|||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.Fullscreen} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.actionIcons}>
|
||||
{buttons}
|
||||
</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 refContainer = useRef<HTMLDivElement>();
|
||||
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 [ screenState, setScreenState ] = useState<ChannelVideoStreamState>(() => {
|
||||
events.fire("query_video", { videoId: props.videoId });
|
||||
events.fire("query_video", { videoId: videoId });
|
||||
return "none";
|
||||
});
|
||||
|
||||
events.reactUse("notify_video", event => {
|
||||
if(event.videoId === props.videoId) {
|
||||
if(event.videoId === videoId) {
|
||||
setCameraState(event.cameraStream);
|
||||
setScreenState(event.screenStream);
|
||||
}
|
||||
|
@ -424,7 +531,7 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
|||
}, [ isFullscreen ]);
|
||||
|
||||
events.reactUse("action_set_fullscreen", event => {
|
||||
if(event.videoId === props.videoId) {
|
||||
if(event.videoId === videoId) {
|
||||
if(!refContainer.current) { return; }
|
||||
|
||||
refContainer.current.requestFullscreen().then(() => {
|
||||
|
@ -448,9 +555,9 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
|||
if(isFullscreen) {
|
||||
events.fire("action_set_fullscreen", { videoId: undefined });
|
||||
} else if(props.isSpotlight) {
|
||||
events.fire("action_set_fullscreen", { videoId: props.videoId });
|
||||
events.fire("action_set_fullscreen", { videoId: videoId });
|
||||
} 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", { });
|
||||
}
|
||||
}}
|
||||
|
@ -467,7 +574,7 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
|||
label: tr("Popout Video"),
|
||||
icon: ClientIcon.Fullscreen,
|
||||
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
|
||||
},
|
||||
|
@ -476,7 +583,7 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
|||
label: isFullscreen ? tr("Release fullscreen") : tr("Show in fullscreen"),
|
||||
icon: ClientIcon.Fullscreen,
|
||||
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"),
|
||||
icon: ClientIcon.Fullscreen,
|
||||
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", { });
|
||||
}
|
||||
}
|
||||
|
@ -492,10 +599,11 @@ export const VideoContainer = React.memo((props: { videoId: string, isSpotlight:
|
|||
}}
|
||||
ref={refContainer}
|
||||
>
|
||||
<VideoPlayer videoId={props.videoId} cameraState={cameraState} screenState={screenState} />
|
||||
<VideoInfo videoId={props.videoId} />
|
||||
<VideoPlayer videoId={videoId} cameraState={cameraState} screenState={screenState} />
|
||||
<VideoClientInfo videoId={videoId} />
|
||||
<VideoViewerCount />
|
||||
<VideoControlButtons
|
||||
videoId={props.videoId}
|
||||
videoId={videoId}
|
||||
cameraState={cameraState}
|
||||
screenState={screenState}
|
||||
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 [ shown, setShown ] = useState(false);
|
||||
events.reactUse("notify_video_arrows", event => setShown(event[props.direction]));
|
||||
|
||||
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 })}>
|
||||
<ClientIconRenderer icon={ClientIcon.SimpleArrow} className={cssStyle.icon} />
|
||||
</div>
|
||||
|
@ -525,9 +631,12 @@ const VideoBar = React.memo(() => {
|
|||
const refArrowRight = 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");
|
||||
return "loading";
|
||||
return [];
|
||||
});
|
||||
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 leftEndReached = container.scrollLeft <= .9;
|
||||
events.fire("notify_video_arrows", { left: !leftEndReached, right: !rightEndReached });
|
||||
setArrowLeftShown(!leftEndReached);
|
||||
setArrowRightShown(!rightEndReached);
|
||||
}, [ refVideos ]);
|
||||
|
||||
events.reactUse("action_video_scroll", event => {
|
||||
const container = refVideos.current;
|
||||
const arrowLeft = refArrowLeft.current;
|
||||
const arrowRight = refArrowRight.current;
|
||||
if(container && arrowLeft && arrowRight) {
|
||||
if(!container || !arrowLeft || !arrowRight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const children = [...container.children] as HTMLElement[];
|
||||
if(event.direction === "left") {
|
||||
const currentCutOff = container.scrollLeft;
|
||||
|
@ -560,7 +673,6 @@ const VideoBar = React.memo(() => {
|
|||
|
||||
container.scrollLeft = element.offsetLeft - arrowLeft.clientWidth;
|
||||
}
|
||||
}
|
||||
updateScrollButtons();
|
||||
}, undefined, [ updateScrollButtons ]);
|
||||
|
||||
|
@ -587,16 +699,16 @@ const VideoBar = React.memo(() => {
|
|||
return (
|
||||
<div className={cssStyle.videoBar}>
|
||||
<div className={cssStyle.videos} ref={refVideos}>
|
||||
{videos === "loading" ? undefined :
|
||||
videos.map(videoId => (
|
||||
{videos.map(videoId => (
|
||||
<ErrorBoundary key={videoId}>
|
||||
<VideoContainer videoId={videoId} isSpotlight={false} />
|
||||
<VideoIdContext.Provider value={videoId}>
|
||||
<VideoContainer isSpotlight={false} />
|
||||
</VideoIdContext.Provider>
|
||||
</ErrorBoundary>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</div>
|
||||
<VideoBarArrow direction={"left"} containerRef={refArrowLeft} />
|
||||
<VideoBarArrow direction={"right"} containerRef={refArrowRight} />
|
||||
<VideoBarArrow direction={"left"} containerRef={refArrowLeft} shown={arrowLeftShown} />
|
||||
<VideoBarArrow direction={"right"} containerRef={refArrowRight} shown={arrowRightShown} />
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ import GridLayout from "react-grid-layout";
|
|||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||
import * as _ from "lodash";
|
||||
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-grid-layout/css/styles.css";
|
||||
|
@ -37,7 +37,11 @@ const SpotlightSingle = () => {
|
|||
|
||||
let body;
|
||||
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 {
|
||||
body = (
|
||||
<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 => (
|
||||
<div className={cssStyle.videoContainer} key={entry.i}>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -191,11 +191,11 @@ export function openBanList(client: ConnectionHandler) {
|
|||
width: '60em'
|
||||
});
|
||||
|
||||
client.serverConnection.command_handler_boss().register_single_handler(single_ban_handler);
|
||||
client.serverConnection.command_handler_boss().register_single_handler(single_trigger_handler);
|
||||
client.serverConnection.getCommandHandler().registerSingleHandler(single_ban_handler);
|
||||
client.serverConnection.getCommandHandler().registerSingleHandler(single_trigger_handler);
|
||||
modal.close_listener.push(() => {
|
||||
client.serverConnection.command_handler_boss().remove_single_handler(single_ban_handler);
|
||||
client.serverConnection.command_handler_boss().remove_single_handler(single_trigger_handler);
|
||||
client.serverConnection.getCommandHandler().removeSingleHandler(single_ban_handler);
|
||||
client.serverConnection.getCommandHandler().removeSingleHandler(single_trigger_handler);
|
||||
});
|
||||
|
||||
//TODO: Test without dividerfy!
|
||||
|
|
|
@ -2,7 +2,7 @@ import {ClientConnectionInfo, ClientEntry} from "../../tree/Client";
|
|||
import PermissionType from "../../permission/PermissionType";
|
||||
import {createInfoModal, createModal, Modal} from "../../ui/elements/Modal";
|
||||
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 moment from "moment";
|
||||
import {format_number, network} from "../../ui/frames/chat";
|
||||
|
|
|
@ -6,7 +6,7 @@ import {CommandResult} from "../../connection/ServerConnectionDeclaration";
|
|||
import {LogCategory, logError, logWarn} from "../../log";
|
||||
import {tr, tra} from "../../i18n/localize";
|
||||
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 * as htmltags from "../../ui/htmltags";
|
||||
import {ErrorCode} from "../../connection/ErrorCode";
|
||||
|
|
|
@ -35,14 +35,14 @@ export function spawnQueryCreate(connection: ConnectionHandler, callback_created
|
|||
},
|
||||
command: "notifyquerycreated"
|
||||
};
|
||||
connection.serverConnection.command_handler_boss().register_single_handler(single_handler);
|
||||
connection.serverConnection.getCommandHandler().registerSingleHandler(single_handler);
|
||||
connection.serverConnection.send_command("querycreate", {
|
||||
client_login_name: name
|
||||
}).catch(error => {
|
||||
if (error instanceof CommandResult)
|
||||
error = error.extra_message || error.message;
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -394,13 +394,13 @@ export function spawnQueryManage(client: ConnectionHandler) {
|
|||
return true;
|
||||
}
|
||||
};
|
||||
client.serverConnection.command_handler_boss().register_single_handler(single_handler);
|
||||
client.serverConnection.getCommandHandler().registerSingleHandler(single_handler);
|
||||
|
||||
client.serverConnection.send_command("querychangepassword", {
|
||||
client_login_name: selected_query.username,
|
||||
client_login_password: result
|
||||
}).catch(error => {
|
||||
client.serverConnection.command_handler_boss().remove_single_handler(single_handler);
|
||||
client.serverConnection.getCommandHandler().removeSingleHandler(single_handler);
|
||||
if (error instanceof CommandResult)
|
||||
error = error.extra_message || error.message;
|
||||
createErrorModal(tr("Unable to change password"), formatMessage(tr("Failed to change password{:br:}Message: {}"), error)).open();
|
||||
|
|
|
@ -13,7 +13,7 @@ import {LogCategory, logDebug, logError, logTrace, logWarn} from "tc-shared/log"
|
|||
import * as i18n from "tc-shared/i18n/localize";
|
||||
import {RepositoryTranslation, TranslationRepository} from "tc-shared/i18n/localize";
|
||||
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 {formatMessage, set_icon_size} from "tc-shared/ui/frames/chat";
|
||||
import {spawnTeamSpeakIdentityImport, spawnTeamSpeakIdentityImprove} from "tc-shared/ui/modal/ModalIdentity";
|
||||
|
|
|
@ -291,7 +291,7 @@ class Controller {
|
|||
this.assignmentLoadEnqueued = 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 clientId = parseInt(payload[0].cldbid);
|
||||
if(isNaN(clientId) || clientId !== this.clientDatabaseId) {
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
&.entry {
|
||||
padding-left: 2em;
|
||||
|
||||
:global .icon {
|
||||
.icon {
|
||||
align-self: center;
|
||||
|
||||
margin-right: .25em;
|
||||
|
|
|
@ -101,7 +101,7 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
|||
hidden={this.props.hidden}
|
||||
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>
|
||||
{rightItem}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
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");
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
setupDragData
|
||||
} from "tc-shared/ui/tree/DragHelper";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
import {CallOnce} from "tc-shared/proto";
|
||||
|
||||
function isEquivalent(a, b) {
|
||||
const typeA = typeof a;
|
||||
|
@ -757,25 +758,17 @@ export abstract class RDPEntry {
|
|||
unread: boolean = false;
|
||||
|
||||
private renderedInstance: React.ReactElement;
|
||||
private destroyed = false;
|
||||
|
||||
protected constructor(handle: RDPChannelTree, entryId: number) {
|
||||
this.handle = handle;
|
||||
this.entryId = entryId;
|
||||
}
|
||||
|
||||
@CallOnce
|
||||
destroy() {
|
||||
if(this.destroyed) {
|
||||
throw "can not destry an entry twice";
|
||||
}
|
||||
|
||||
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; }
|
||||
getHandlerId() : string { return this.handle.handlerId; }
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import * as React from "react";
|
||||
import {RDPEntry} from "tc-shared/ui/tree/RendererDataProvider";
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class JavascriptInput implements AbstractInput {
|
|||
private currentAudioStream: MediaStreamAudioSourceNode;
|
||||
|
||||
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 readonly audioScriptProcessorCallback;
|
||||
private audioNodeVolume: GainNode;
|
||||
|
@ -167,8 +167,12 @@ class JavascriptInput implements AbstractInput {
|
|||
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 */
|
||||
return await (this.startPromise = Promise.resolve().then(() => this.doStart()));
|
||||
try {
|
||||
this.startPromise = this.doStart();
|
||||
return await this.startPromise;
|
||||
} finally {
|
||||
this.startPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async doStart() : Promise<InputStartError | true> {
|
||||
|
@ -231,8 +235,6 @@ class JavascriptInput implements AbstractInput {
|
|||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
this.startPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,24 +385,24 @@ class JavascriptInput implements AbstractInput {
|
|||
async setConsumer(consumer: InputConsumer) {
|
||||
if(this.consumer) {
|
||||
if(this.consumer.type == InputConsumerType.NODE) {
|
||||
if(this.sourceNode) {
|
||||
this.consumer.callbackDisconnect(this.sourceNode);
|
||||
if(this.audioSourceNode) {
|
||||
this.consumer.callbackDisconnect(this.audioSourceNode);
|
||||
}
|
||||
} else if(this.consumer.type === InputConsumerType.CALLBACK) {
|
||||
if(this.sourceNode) {
|
||||
this.sourceNode.disconnect(this.audioNodeCallbackConsumer);
|
||||
if(this.audioSourceNode) {
|
||||
this.audioSourceNode.disconnect(this.audioNodeCallbackConsumer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(consumer) {
|
||||
if(consumer.type == InputConsumerType.CALLBACK) {
|
||||
if(this.sourceNode) {
|
||||
this.sourceNode.connect(this.audioNodeCallbackConsumer);
|
||||
if(this.audioSourceNode) {
|
||||
this.audioSourceNode.connect(this.audioNodeCallbackConsumer);
|
||||
}
|
||||
} else if(consumer.type == InputConsumerType.NODE) {
|
||||
if(this.sourceNode) {
|
||||
consumer.callbackNode(this.sourceNode);
|
||||
if(this.audioSourceNode) {
|
||||
consumer.callbackNode(this.audioSourceNode);
|
||||
}
|
||||
} else {
|
||||
throw "native callback consumers are not supported!";
|
||||
|
@ -413,22 +415,22 @@ class JavascriptInput implements AbstractInput {
|
|||
if(this.consumer) {
|
||||
if(this.consumer.type == InputConsumerType.NODE) {
|
||||
const node_consumer = this.consumer as NodeInputConsumer;
|
||||
if(this.sourceNode) {
|
||||
node_consumer.callbackDisconnect(this.sourceNode);
|
||||
if(this.audioSourceNode) {
|
||||
node_consumer.callbackDisconnect(this.audioSourceNode);
|
||||
}
|
||||
|
||||
if(newNode) {
|
||||
node_consumer.callbackNode(newNode);
|
||||
}
|
||||
} else if(this.consumer.type == InputConsumerType.CALLBACK) {
|
||||
this.sourceNode.disconnect(this.audioNodeCallbackConsumer);
|
||||
this.audioSourceNode.disconnect(this.audioNodeCallbackConsumer);
|
||||
if(newNode) {
|
||||
newNode.connect(this.audioNodeCallbackConsumer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.sourceNode = newNode;
|
||||
this.audioSourceNode = newNode;
|
||||
}
|
||||
|
||||
currentConsumer(): InputConsumer | undefined {
|
||||
|
@ -480,7 +482,7 @@ class JavascriptInput implements AbstractInput {
|
|||
}
|
||||
|
||||
getInputProcessor(): InputProcessor {
|
||||
return new JavaScriptInputProcessor();
|
||||
return JavaScriptInputProcessor.Instance;
|
||||
}
|
||||
|
||||
createLevelMeter(): LevelMeter {
|
||||
|
@ -489,6 +491,8 @@ class JavascriptInput implements AbstractInput {
|
|||
}
|
||||
|
||||
class JavaScriptInputProcessor implements InputProcessor {
|
||||
static readonly Instance = new JavaScriptInputProcessor();
|
||||
|
||||
applyProcessorConfig<T extends InputProcessorType>(processor: T, config: InputProcessorConfigMapping[T]) {
|
||||
throw tr("target processor is not supported");
|
||||
}
|
||||
|
@ -559,8 +563,8 @@ class JavascriptLevelMeter implements LevelMeter {
|
|||
/* starting stream */
|
||||
const _result = await requestMediaStream(this._device.deviceId, this._device.groupId, "audio");
|
||||
if(!(_result instanceof MediaStream)){
|
||||
if(_result === InputStartError.ENOTALLOWED)
|
||||
throw tr("No permissions");
|
||||
if(_result === InputStartError.ENOTALLOWED) {
|
||||
throw tr("No permissions");}
|
||||
if(_result === InputStartError.ENOTSUPPORTED)
|
||||
throw tr("Not supported");
|
||||
if(_result === InputStartError.EBUSY)
|
||||
|
@ -630,7 +634,8 @@ class JavascriptLevelMeter implements LevelMeter {
|
|||
this._analyser_node.getByteTimeDomainData(this._analyse_buffer);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,4 @@
|
|||
export interface ParsedCommand {
|
||||
command?: string;
|
||||
|
||||
payload: {[key: string]: string}[]
|
||||
switches: string[]
|
||||
}
|
||||
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||
|
||||
function unescapeCommandValue(value: string) : string {
|
||||
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]);
|
||||
|
||||
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));
|
||||
|
||||
let cmd;
|
||||
|
@ -84,11 +79,7 @@ export function parseCommand(command: string): ParsedCommand {
|
|||
payloads.push(payload)
|
||||
});
|
||||
|
||||
return {
|
||||
command: cmd,
|
||||
payload: payloads,
|
||||
switches: switches
|
||||
}
|
||||
return new ServerCommand(cmd, payloads, switches);
|
||||
}
|
||||
|
||||
export function buildCommand(data: any | any[], switches?: string[], command?: string) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import {
|
|||
AbstractServerConnection,
|
||||
CommandOptionDefaults,
|
||||
CommandOptions, ConnectionPing,
|
||||
ConnectionStatistics,
|
||||
ConnectionStatistics, ServerCommand,
|
||||
} from "tc-shared/connection/ConnectionBase";
|
||||
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
|
||||
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
||||
|
@ -71,7 +71,7 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
this.commandHandlerBoss = new ServerConnectionCommandBoss(this);
|
||||
this.defaultCommandHandler = new ConnectionCommandHandler(this);
|
||||
|
||||
this.commandHandlerBoss.register_handler(this.defaultCommandHandler);
|
||||
this.commandHandlerBoss.registerHandler(this.defaultCommandHandler);
|
||||
this.command_helper.initialize();
|
||||
|
||||
this.rtcConnection = new RTCConnection(this, true);
|
||||
|
@ -101,7 +101,7 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
this.rtcConnection.destroy();
|
||||
this.command_helper.destroy();
|
||||
|
||||
this.defaultCommandHandler && this.commandHandlerBoss.unregister_handler(this.defaultCommandHandler);
|
||||
this.defaultCommandHandler && this.commandHandlerBoss.unregisterHandler(this.defaultCommandHandler);
|
||||
this.defaultCommandHandler = undefined;
|
||||
|
||||
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();
|
||||
/* devel-block-end */
|
||||
|
||||
this.commandHandlerBoss.invoke_handle({
|
||||
command: json["command"],
|
||||
arguments: json["data"]
|
||||
});
|
||||
this.commandHandlerBoss.invokeCommand(new ServerCommand(json["command"], json["data"], []));
|
||||
|
||||
if(json["command"] === "initserver") {
|
||||
this.handleServerInit();
|
||||
|
@ -344,10 +341,7 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
} else if(json["type"] === "command-raw") {
|
||||
const command = parseCommand(json["payload"]);
|
||||
logTrace(LogCategory.NETWORKING, tr("Received command %s"), command.command);
|
||||
this.commandHandlerBoss.invoke_handle({
|
||||
command: command.command,
|
||||
arguments: command.payload
|
||||
});
|
||||
this.commandHandlerBoss.invokeCommand(command);
|
||||
|
||||
if(command.command === "initserver") {
|
||||
this.handleServerInit();
|
||||
|
@ -471,7 +465,7 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
return this.videoConnection;
|
||||
}
|
||||
|
||||
command_handler_boss(): AbstractCommandHandlerBoss {
|
||||
getCommandHandler(): AbstractCommandHandlerBoss {
|
||||
return this.commandHandlerBoss;
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
|||
);
|
||||
|
||||
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();
|
||||
for(const data of event.arguments) {
|
||||
if(parseInt(data["clid"]) === localClientId) {
|
||||
|
|
Loading…
Reference in New Issue