Fixed the video bar using the new full react app layout

master
WolverinDEV 2021-01-06 17:22:29 +01:00
parent 2211da243d
commit 4d9df41c35
14 changed files with 363 additions and 438 deletions

View File

@ -1,13 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="../css/modals.scss"/>
<meta charset="UTF-8">
<title>TeaSpeak-Web client templates</title>
</head>
<body>
<script class="jsrender-template" id="tmpl_main" type="text/html">
<div id="contextMenu" class="context-menu"></div>
<div class="overlay-image-preview hidden" id="overlay-image-preview">
<div class="container-menu-bar">
<div class="entry button-open-in-window">
@ -30,154 +28,6 @@
</div>
</script>
<script class="jsrender-template" id="tmpl_frame_chat" type="text/html">
<div class="container-chat-frame">
<div class="container-info"></div>
<div class="container-chat"></div>
</div>
</script>
<script class="jsrender-template" id="tmpl_frame_chat_music_info" type="text/html">
<div class="container-music-info">
<div class="player">
<div class="container-thumbnail">
<div class="thumbnail">
<!-- https://i.ytimg.com/vi/DeXoACwOT1o/maxresdefault.jpg -->
<!-- <img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%"> -->
<img src="" alt="thumbnail">
</div>
</div>
<div class="container-song-info">
<a class="song-name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</a>
<a class="song-description"></a>
</div>
<div class="container-timeline">
<div class="timestamps">
<div class="current">00:01:13</div>
<div class="max">00:03:22</div>
</div>
<div class="timeline">
<div class="indicator indicator-buffered"></div>
<div class="indicator indicator-playtime"></div>
<div class="thumb"><div class="dot"></div></div>
</div>
<div class="control-buttons">
<div class="button button-rewind">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path transform="rotate(180, 256, 256)" d="M504.171,239.489l-234.667-192c-6.357-5.227-15.189-6.293-22.656-2.773c-7.424,3.541-12.181,11.051-12.181,19.285v146.987
L34.837,47.489c-6.379-5.227-15.189-6.293-22.656-2.773C4.757,48.257,0,55.767,0,64.001v384c0,8.235,4.757,15.744,12.181,19.285
c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l199.829-163.499v146.987
c0,8.235,4.757,15.744,12.181,19.285c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l234.667-192
c4.949-4.053,7.829-10.112,7.829-16.512S509.12,243.543,504.171,239.489z"/>
</svg>
</div>
<div class="button button-play">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path d="M500.203,236.907L30.869,2.24c-6.613-3.285-14.443-2.944-20.736,0.939C3.84,7.083,0,13.931,0,21.333v469.333
c0,7.403,3.84,14.251,10.133,18.155c3.413,2.112,7.296,3.179,11.2,3.179c3.264,0,6.528-0.747,9.536-2.24l469.333-234.667
C507.435,271.467,512,264.085,512,256S507.435,240.533,500.203,236.907z"/>
</svg>
</div>
<div class="button button-pause">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path transform='rotate(90, 256, 256)' d="M85.333,213.333h341.333C473.728,213.333,512,175.061,512,128s-38.272-85.333-85.333-85.333H85.333
C38.272,42.667,0,80.939,0,128S38.272,213.333,85.333,213.333z"/>
<path transform='rotate(90, 256, 256)' d="M426.667,298.667H85.333C38.272,298.667,0,336.939,0,384s38.272,85.333,85.333,85.333h341.333
C473.728,469.333,512,431.061,512,384S473.728,298.667,426.667,298.667z"/>
</svg>
</div>
<div class="button button-forward">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path d="M504.171,239.489l-234.667-192c-6.357-5.227-15.189-6.293-22.656-2.773c-7.424,3.541-12.181,11.051-12.181,19.285v146.987
L34.837,47.489c-6.379-5.227-15.189-6.293-22.656-2.773C4.757,48.257,0,55.767,0,64.001v384c0,8.235,4.757,15.744,12.181,19.285
c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l199.829-163.499v146.987
c0,8.235,4.757,15.744,12.181,19.285c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l234.667-192
c4.949-4.053,7.829-10.112,7.829-16.512S509.12,243.543,504.171,239.489z"/>
</svg>
</div>
</div>
</div>
</div>
<div class="container-playlist">
<div class="overlay overlay-loading">
<a>{{tr "Fetching playlist..." /}}</a>
</div>
<div class="overlay overlay-no-permissions">
<a>{{tr "You don't have permissions to see this playlist" /}}</a>
<button class="btn btn-blue button-reload-playlist">{{tr "Reload" /}}</button>
</div>
<div class="overlay overlay-error">
<a>{{tr "An error occurred while fetching the playlist" /}}</a>
<button class="btn btn-blue button-reload-playlist">{{tr "Reload" /}}</button>
</div>
<div class="overlay overlay-empty">
<a>{{tr "The playlist is currently empty." /}}</a>
<button class="btn btn-green button-song-add">{{tr "Add a song" /}}</button>
</div>
<div class="playlist">
<div class="entry">
<div class="container-thumbnail">
<!-- <img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%"> -->
<img src="https://i.ytimg.com/vi/KaXXVzGy7Y8/maxresdefault.jpg">
</div>
<div class="container-data">
<div class="row">
<div class="name">2-Hours Epic Music | THE POWER OF EPIC MUSIC - Best Of Collection - Vol.5 - 2019</div>
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
</div>
<div class="row second">
<div class="description">It is time for another 2-Hour Epic Music Mix. So far 2019 has been one amazing year for the orchestral epic music genre and i cannot wait for more. Also incl...</div>
<div class="length">00:22:01</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-thumbnail">
<img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%">
</div>
<div class="container-data">
<div class="row">
<div class="name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</div>
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
</div>
<div class="row second">
<div class="description">This is an example song description which needs some work</div>
<div class="length">00:22:01</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-thumbnail">
<img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%">
</div>
<div class="container-data">
<div class="row">
<div class="name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</div>
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
</div>
<div class="row second">
<div class="description">This is an example song description which needs some work</div>
<div class="length">00:22:01</div>
</div>
</div>
</div>
</div>
</div>
<div class="button-close"></div>
</div>
</script>
<div class="template-group-modals">
<script class="jsrender-template" id="tmpl_modal" type="text/html">
<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">

View File

@ -2,16 +2,6 @@ import {ConnectionHandler, DisconnectReason} from "./ConnectionHandler";
import {Registry} from "./events";
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {FooterRenderer} from "tc-shared/ui/frames/footer/Renderer";
import * as React from "react";
import * as ReactDOM from "react-dom";
import {SideBarController} from "tc-shared/ui/frames/SideBarController";
import {ServerEventLogController} from "tc-shared/ui/frames/log/Controller";
import {ServerLogFrame} from "tc-shared/ui/frames/log/Renderer";
import {HostBannerController} from "tc-shared/ui/frames/HostBannerController";
import {HostBanner} from "tc-shared/ui/frames/HostBannerRenderer";
import {ChannelTreeView} from "tc-shared/ui/tree/RendererView";
import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
export let server_connections: ConnectionManager;
@ -31,148 +21,6 @@ class ReplaceableContainer {
}
}
export class ConnectionManager {
private readonly event_registry: Registry<ConnectionManagerEvents>;
private connection_handlers: ConnectionHandler[] = [];
private active_handler: ConnectionHandler | undefined;
private containerChannelVideo: ReplaceableContainer;
private containerFooter: HTMLDivElement;
private containerServerLog: HTMLDivElement;
private containerHostBanner: HTMLDivElement;
private containerChannelTree: HTMLDivElement;
/* FIXME: Move these controller out! */
sideBarController: SideBarController;
serverLogController: ServerEventLogController;
hostBannerController: HostBannerController;
constructor() {
this.event_registry = new Registry<ConnectionManagerEvents>();
this.event_registry.enableDebug("connection-manager");
this.sideBarController = new SideBarController();
this.serverLogController = new ServerEventLogController();
this.hostBannerController = new HostBannerController();
this.containerChannelVideo = new ReplaceableContainer(document.getElementById("channel-video") as HTMLDivElement);
this.containerServerLog = document.getElementById("server-log") as HTMLDivElement;
this.containerFooter = document.getElementById("container-footer") as HTMLDivElement;
this.containerHostBanner = document.getElementById("hostbanner") as HTMLDivElement;
this.containerChannelTree = document.getElementById("channelTree") as HTMLDivElement;
this.sideBarController.renderInto(document.getElementById("chat") as HTMLDivElement);
this.set_active_connection(undefined);
}
initializeReactComponents() {
return;
ReactDOM.render(React.createElement(FooterRenderer), this.containerFooter);
ReactDOM.render(React.createElement(ServerLogFrame, { events: this.serverLogController.events }), this.containerServerLog);
ReactDOM.render(React.createElement(HostBanner, { events: this.hostBannerController.uiEvents }), this.containerHostBanner);
}
events() : Registry<ConnectionManagerEvents> {
return this.event_registry;
}
spawn_server_connection() : ConnectionHandler {
const handler = new ConnectionHandler();
handler.initialize_client_state(this.active_handler);
this.connection_handlers.push(handler);
this.event_registry.fire("notify_handler_created", { handler: handler, handlerId: handler.handlerId });
return handler;
}
destroy_server_connection(handler: ConnectionHandler) {
if(this.connection_handlers.length <= 1)
throw "cannot deleted the last connection handler";
if(!this.connection_handlers.remove(handler))
throw "unknown connection handler";
if(handler.serverConnection) {
const connected = handler.connected;
handler.serverConnection.disconnect("handler destroyed");
handler.handleDisconnect(DisconnectReason.HANDLER_DESTROYED, connected);
}
if(handler === this.active_handler)
this.set_active_connection_(this.connection_handlers[0]);
this.event_registry.fire("notify_handler_deleted", { handler: handler, handlerId: handler.handlerId });
/* destroy all elements */
handler.destroy();
}
set_active_connection(handler: ConnectionHandler) {
if(handler && this.connection_handlers.indexOf(handler) == -1)
throw "Handler hasn't been registered or is already obsolete!";
if(handler === this.active_handler)
return;
this.set_active_connection_(handler);
}
swapHandlerOrder(handlerA: ConnectionHandler, handlerB: ConnectionHandler) {
const indexA = this.connection_handlers.findIndex(handler => handlerA === handler);
const indexB = this.connection_handlers.findIndex(handler => handlerB === handler);
if(indexA === -1 || indexB === -1 || indexA === indexB) {
return;
}
let temp = this.connection_handlers[indexA];
this.connection_handlers[indexA] = this.connection_handlers[indexB];
this.connection_handlers[indexB] = temp;
this.events().fire("notify_handler_order_changed");
}
private set_active_connection_(handler: ConnectionHandler) {
this.sideBarController.setConnection(handler);
this.serverLogController.setConnectionHandler(handler);
this.hostBannerController.setConnectionHandler(handler);
/*
this.containerChannelVideo.replaceWith(handler?.video_frame.getContainer());
if(handler) {
ReactDOM.render(React.createElement(ChannelTreeRenderer, { handlerId: handler.handlerId, events: handler.channelTree.mainTreeUiEvents }), this.containerChannelTree);
} else {
ReactDOM.render(undefined, this.containerChannelTree);
}
*/
const old_handler = this.active_handler;
this.active_handler = handler;
this.event_registry.fire("notify_active_handler_changed", {
oldHandler: old_handler,
newHandler: handler,
oldHandlerId: old_handler?.handlerId,
newHandlerId: handler?.handlerId
});
old_handler?.events().fire("notify_visibility_changed", { visible: false });
handler?.events().fire("notify_visibility_changed", { visible: true });
}
findConnection(handlerId: string) : ConnectionHandler | undefined {
return this.connection_handlers.find(e => e.handlerId === handlerId);
}
active_connection() : ConnectionHandler | undefined {
return this.active_handler;
}
all_connections() : ConnectionHandler[] {
return this.connection_handlers;
}
getSidebarController() : SideBarController {
return this.sideBarController;
}
}
export interface ConnectionManagerEvents {
notify_handler_created: {
handlerId: string,
@ -198,11 +46,120 @@ export interface ConnectionManagerEvents {
notify_handler_order_changed: { }
}
export class ConnectionManager {
private readonly events_: Registry<ConnectionManagerEvents>;
private connectionHandlers: ConnectionHandler[] = [];
private activeConnectionHandler: ConnectionHandler | undefined;
private containerChannelVideo: ReplaceableContainer;
constructor() {
this.events_ = new Registry<ConnectionManagerEvents>();
this.events_.enableDebug("connection-manager");
/* FIXME! */
this.containerChannelVideo = new ReplaceableContainer(document.getElementById("channel-video") as HTMLDivElement);
this.set_active_connection(undefined);
}
events() : Registry<ConnectionManagerEvents> {
return this.events_;
}
spawn_server_connection() : ConnectionHandler {
const handler = new ConnectionHandler();
handler.initialize_client_state(this.activeConnectionHandler);
this.connectionHandlers.push(handler);
this.events_.fire("notify_handler_created", { handler: handler, handlerId: handler.handlerId });
return handler;
}
destroy_server_connection(handler: ConnectionHandler) {
if(this.connectionHandlers.length <= 1) {
throw "cannot deleted the last connection handler";
}
if(!this.connectionHandlers.remove(handler)) {
throw "unknown connection handler";
}
if(handler.serverConnection) {
const connected = handler.connected;
handler.serverConnection.disconnect("handler destroyed");
handler.handleDisconnect(DisconnectReason.HANDLER_DESTROYED, connected);
}
if(handler === this.activeConnectionHandler) {
this.set_active_connection_(this.connectionHandlers[0]);
}
this.events_.fire("notify_handler_deleted", { handler: handler, handlerId: handler.handlerId });
/* destroy all elements */
handler.destroy();
}
set_active_connection(handler: ConnectionHandler) {
if(handler && this.connectionHandlers.indexOf(handler) == -1) {
throw "Handler hasn't been registered or is already obsolete!";
}
if(handler === this.activeConnectionHandler) {
return;
}
this.set_active_connection_(handler);
}
swapHandlerOrder(handlerA: ConnectionHandler, handlerB: ConnectionHandler) {
const indexA = this.connectionHandlers.findIndex(handler => handlerA === handler);
const indexB = this.connectionHandlers.findIndex(handler => handlerB === handler);
if(indexA === -1 || indexB === -1 || indexA === indexB) {
return;
}
let temp = this.connectionHandlers[indexA];
this.connectionHandlers[indexA] = this.connectionHandlers[indexB];
this.connectionHandlers[indexB] = temp;
this.events().fire("notify_handler_order_changed");
}
private set_active_connection_(handler: ConnectionHandler) {
/*
this.containerChannelVideo.replaceWith(handler?.video_frame.getContainer());
*/
const oldHandler = this.activeConnectionHandler;
this.activeConnectionHandler = handler;
this.events_.fire("notify_active_handler_changed", {
oldHandler: oldHandler,
newHandler: handler,
oldHandlerId: oldHandler?.handlerId,
newHandlerId: handler?.handlerId
});
oldHandler?.events().fire("notify_visibility_changed", { visible: false });
handler?.events().fire("notify_visibility_changed", { visible: true });
}
findConnection(handlerId: string) : ConnectionHandler | undefined {
return this.connectionHandlers.find(e => e.handlerId === handlerId);
}
active_connection() : ConnectionHandler | undefined {
return this.activeConnectionHandler;
}
all_connections() : ConnectionHandler[] {
return this.connectionHandlers;
}
}
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "server manager init",
function: async () => {
server_connections = new ConnectionManager();
server_connections.initializeReactComponents();
},
priority: 80
});

View File

@ -48,7 +48,9 @@ export enum VideoBroadcastState {
}
export interface VideoClientEvents {
notify_broadcast_state_changed: { broadcastType: VideoBroadcastType, oldState: VideoBroadcastState, newState: VideoBroadcastState }
notify_broadcast_state_changed: { broadcastType: VideoBroadcastType, oldState: VideoBroadcastState, newState: VideoBroadcastState },
notify_dismissed_state_changed: { broadcastType: VideoBroadcastType, dismissed: boolean },
notify_broadcast_stream_changed: { broadcastType: VideoBroadcastType }
}
export interface VideoClient {
@ -60,6 +62,9 @@ export interface VideoClient {
joinBroadcast(broadcastType: VideoBroadcastType) : Promise<void>;
leaveBroadcast(broadcastType: VideoBroadcastType);
dismissBroadcast(broadcastType: VideoBroadcastType);
isBroadcastDismissed(broadcastType: VideoBroadcastType) : boolean;
}
export interface LocalVideoBroadcastEvents {

View File

@ -194,8 +194,9 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
return;
}
const config = Object.assign({}, this.currentConfig);
try {
await this.handle.getRTCConnection().startVideoBroadcast(this.type, this.currentConfig);
await this.handle.getRTCConnection().startVideoBroadcast(this.type, config);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
@ -211,7 +212,8 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
return;
}
this.signaledConfig = Object.assign({}, this.currentConfig);
/* TODO: Test if the config may has already be changed */
this.signaledConfig = config;
this.setState({ state: "broadcasting" });
}
@ -311,7 +313,11 @@ class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
const startId = ++this.broadcastStartId;
try {
await this.handle.getRTCConnection().startVideoBroadcast(this.type, this.currentConfig);
const config = Object.assign({}, this.currentConfig);
await this.handle.getRTCConnection().startVideoBroadcast(this.type, config);
this.setState({ state: "broadcasting" });
this.signaledConfig = config;
/* TODO: Test if the config may has already be changed */
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */

View File

@ -20,6 +20,11 @@ export class RtpVideoClient implements VideoClient {
camera: event => this.handleTrackStateChanged("camera", event.newState)
};
private dismissedStates: {[T in VideoBroadcastType]: boolean} = {
screen: false,
camera: false
};
private currentTrack: {[T in VideoBroadcastType]: RemoteRTPVideoTrack} = {
camera: undefined,
screen: undefined
@ -69,6 +74,7 @@ export class RtpVideoClient implements VideoClient {
this.joinedStates[broadcastType] = true;
this.setBroadcastState(broadcastType, VideoBroadcastState.Initializing);
this.setBroadcastDismissed(broadcastType,false);
await this.handle.getConnection().send_command("broadcastvideojoin", {
bid: this.broadcastIds[broadcastType],
bt: broadcastType === "camera" ? 0 : 1
@ -124,7 +130,9 @@ export class RtpVideoClient implements VideoClient {
if(this.currentTrack[type]) {
this.currentTrack[type].getEvents().on("notify_state_changed", this.listenerTrackStateChanged[type]);
}
this.updateBroadcastState(type);
this.events.fire("notify_broadcast_stream_changed", { broadcastType: type });
}
setBroadcastId(type: VideoBroadcastType, id: number | undefined) {
@ -140,6 +148,23 @@ export class RtpVideoClient implements VideoClient {
this.updateBroadcastState(type);
}
private setBroadcastDismissed(broadcastType: VideoBroadcastType, dismissed: boolean) {
if(this.dismissedStates[broadcastType] === dismissed) {
return;
}
this.dismissedStates[broadcastType] = dismissed;
this.events.fire("notify_dismissed_state_changed", { broadcastType: broadcastType, dismissed: dismissed });
}
dismissBroadcast(broadcastType: VideoBroadcastType) {
this.setBroadcastDismissed(broadcastType, true);
}
isBroadcastDismissed(broadcastType: VideoBroadcastType): boolean {
return this.dismissedStates[broadcastType];
}
private setBroadcastState(type: VideoBroadcastType, state: VideoBroadcastState) {
if(this.trackStates[type] === state) {
return;

View File

@ -11,6 +11,9 @@ import {Stage} from "tc-loader";
import {server_connections} from "tc-shared/ConnectionManager";
import {AppUiEvents} from "tc-shared/ui/AppDefinitions";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {SideBarController} from "tc-shared/ui/frames/SideBarController";
import {ServerEventLogController} from "tc-shared/ui/frames/log/Controller";
import {HostBannerController} from "tc-shared/ui/frames/HostBannerController";
export class AppController {
private uiEvents: Registry<AppUiEvents>;
@ -24,9 +27,14 @@ export class AppController {
private controlBarEvents: Registry<ControlBarEvents>;
private connectionListEvents: Registry<ConnectionListUIEvents>;
private sideBarController: SideBarController;
private serverLogController: ServerEventLogController;
private hostBannerController: HostBannerController;
constructor() {
this.uiEvents = new Registry<AppUiEvents>();
this.uiEvents.on("query_channel_tree", () => this.notifyChannelTree());
this.uiEvents.on("query_video_container", () => this.notifyVideoContainer());
this.listener = [];
}
@ -47,6 +55,15 @@ export class AppController {
this.connectionListEvents?.destroy();
this.connectionListEvents = undefined;
this.sideBarController?.destroy();
this.sideBarController = undefined;
this.serverLogController?.destroy();
this.serverLogController = undefined;
this.hostBannerController?.destroy();
this.hostBannerController = undefined;
this.uiEvents?.destroy();
this.uiEvents = undefined;
}
@ -66,6 +83,10 @@ export class AppController {
this.listener.push(server_connections.events().on("notify_active_handler_changed", event => this.setConnectionHandler(event.newHandler)));
this.setConnectionHandler(server_connections.active_connection());
this.sideBarController = new SideBarController();
this.serverLogController = new ServerEventLogController();
this.hostBannerController = new HostBannerController();
}
setConnectionHandler(connection: ConnectionHandler) {
@ -77,18 +98,23 @@ export class AppController {
this.listenerConnection = [];
this.currentConnection = connection;
this.sideBarController.setConnection(connection);
this.serverLogController.setConnectionHandler(connection);
this.hostBannerController.setConnectionHandler(connection);
this.notifyChannelTree();
this.notifyVideoContainer();
}
renderApp() {
ReactDOM.render(React.createElement(TeaAppMainView, {
controlBar: this.controlBarEvents,
connectionList: this.connectionListEvents,
sidebar: server_connections.getSidebarController().uiEvents,
sidebarHeader: server_connections.getSidebarController().getHeaderController().uiEvents,
log: server_connections.serverLogController.events,
sidebar: this.sideBarController.uiEvents,
sidebarHeader: this.sideBarController.getHeaderController().uiEvents,
log: this.serverLogController.events,
events: this.uiEvents,
hostBanner: server_connections.hostBannerController.uiEvents
hostBanner: this.hostBannerController.uiEvents
}), this.container);
}
@ -98,9 +124,15 @@ export class AppController {
events: this.currentConnection?.channelTree.mainTreeUiEvents
});
}
private notifyVideoContainer() {
this.uiEvents.fire_react("notify_video_container", {
container: this.currentConnection?.video_frame.getContainer()
});
}
}
let appViewController: AppController;
export let appViewController: AppController;
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "app view",
function: async () => {

View File

@ -3,9 +3,13 @@ import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
export interface AppUiEvents {
query_channel_tree: {},
query_video_container: {},
notify_channel_tree: {
events: Registry<ChannelTreeUIEvents> | undefined,
handlerId: string
},
notify_video_container: {
container: HTMLDivElement | undefined
}
}

View File

@ -29,12 +29,22 @@
width: 100%;
}
.mainContainer {
display: flex;
flex-direction: column;
justify-content: stretch;
position: relative;
height: 100%;
margin-top: 5px;
}
.channelTreeAndSidebar {
display: flex;
flex-direction: row;
justify-content: stretch;
margin-top: 5px;
min-height: 27em;
}

View File

@ -15,7 +15,7 @@ import {FooterRenderer} from "tc-shared/ui/frames/footer/Renderer";
import {HostBanner} from "tc-shared/ui/frames/HostBannerRenderer";
import {HostBannerUiEvents} from "tc-shared/ui/frames/HostBannerDefinitions";
import {AppUiEvents} from "tc-shared/ui/AppDefinitions";
import {useState} from "react";
import {useEffect, useState} from "react";
import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
@ -54,6 +54,30 @@ const cssStyle = require("./AppRenderer.scss");
</div>
*/
const VideoFrame = React.memo((props: { events: Registry<AppUiEvents> }) => {
const refElement = React.useRef<HTMLDivElement>();
const [ container, setContainer ] = useState<HTMLDivElement | undefined>(() => {
props.events.fire("query_video_container");
return undefined;
});
props.events.reactUse("notify_video_container", event => setContainer(event.container));
useEffect(() => {
if(!refElement.current || !container) {
return;
}
refElement.current.replaceWith(container);
return () => container.replaceWith(refElement.current);
});
if(!container) {
return null;
}
return <div ref={refElement} />;
});
const ChannelTree = React.memo((props: { events: Registry<AppUiEvents> }) => {
const [ data, setData ] = useState<{ events: Registry<ChannelTreeUIEvents>, handlerId: string }>(() => {
props.events.fire("query_channel_tree");
@ -88,24 +112,27 @@ export const TeaAppMainView = (props: {
<ErrorBoundary>
<ConnectionHandlerList events={props.connectionList} />
</ErrorBoundary>
{/* TODO: The video! */}
<div className={cssStyle.channelTreeAndSidebar}>
<div className={cssStyle.channelTree}>
<ErrorBoundary>
<HostBanner events={props.hostBanner} />
<ChannelTree events={props.events} />
</ErrorBoundary>
<div className={cssStyle.mainContainer}>
<VideoFrame events={props.events} />
<div className={cssStyle.channelTreeAndSidebar}>
<div className={cssStyle.channelTree}>
<ErrorBoundary>
<HostBanner events={props.hostBanner} />
<ChannelTree events={props.events} />
</ErrorBoundary>
</div>
<ContextDivider id={"channel-chat"} direction={"horizontal"} defaultValue={25} />
<SideBarRenderer events={props.sidebar} eventsHeader={props.sidebarHeader} className={cssStyle.sideBar} />
</div>
<ContextDivider id={"channel-chat"} direction={"horizontal"} defaultValue={25} />
<SideBarRenderer events={props.sidebar} eventsHeader={props.sidebarHeader} className={cssStyle.sideBar} />
<ContextDivider id={"main-log"} direction={"vertical"} defaultValue={75} />
<ErrorBoundary>
<div className={cssStyle.containerLog}>
<ServerLogFrame events={props.log} />
</div>
</ErrorBoundary>
</div>
<ContextDivider id={"main-log"} direction={"vertical"} defaultValue={75} />
<ErrorBoundary>
<div className={cssStyle.containerLog}>
<ServerLogFrame events={props.log} />
</div>
</ErrorBoundary>
<FooterRenderer />
</div>
);

View File

@ -12,6 +12,15 @@ export class HostBannerController {
this.uiEvents = new Registry<HostBannerUiEvents>();
}
destroy() {
this.currentConnection = undefined;
this.listenerConnection?.forEach(callback => callback());
this.listenerConnection = [];
this.uiEvents?.destroy();
}
setConnectionHandler(handler: ConnectionHandler) {
if(this.currentConnection === handler) {
return;

View File

@ -4,8 +4,11 @@ import {Registry} from "tc-shared/events";
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
import {LocalClientEntry, MusicClientEntry} from "tc-shared/tree/Client";
import {openMusicManage} from "tc-shared/ui/modal/ModalMusicManage";
import {createInputModal} from "tc-shared/ui/elements/Modal";
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {server_connections} from "tc-shared/ConnectionManager";
import {appViewController} from "tc-shared/ui/AppController";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {LogCategory, logError} from "tc-shared/log";
const ChannelInfoUpdateProperties: (keyof ChannelProperties)[] = [
"channel_name",
@ -74,14 +77,26 @@ export class SideHeaderController {
} catch(error) {
return false;
}
}, result => {
}, async result => {
if(!result) return;
server_connections.getSidebarController().getMusicController().getPlaylistUiEvents()
.fire("action_add_song", {
url: result as string,
mode: "last"
});
try {
const client = this.connection.channelTree.getSelectedEntry();
if(!(client instanceof MusicClientEntry)) {
throw tr("Missing music bot");
}
await this.connection.getPlaylistManager().addSong(client.properties.client_playlist_id, result as string, "any", 0);
} catch (error) {
if(error instanceof CommandResult) {
error = error.formattedMessage();
} else if(typeof error !== "string") {
logError(LogCategory.NETWORKING, tr("Failed to add song to playlist entry: %o"), error);
error = tr("Lookup the console for details");
}
createErrorModal(tr("Failed to add song song"), tra("Failed to add song:\n", error)).open();
}
}).open();
});

View File

@ -29,14 +29,17 @@ let videoIdIndex = 0;
interface ClientVideoController {
destroy();
isSubscribed(type: VideoBroadcastType);
toggleMuteState(type: VideoBroadcastType, state: boolean);
subscribeVideo(type: VideoBroadcastType);
muteVideo(type: VideoBroadcastType);
dismissVideo(type: VideoBroadcastType);
notifyVideoInfo();
notifyVideo(forceSend: boolean);
notifyVideo();
notifyVideoStream(type: VideoBroadcastType);
}
type VideoStreamStates = {[T in VideoBroadcastType]: ChannelVideoStreamState};
type SubscriptionStates = {[T in VideoBroadcastType]: boolean};
class RemoteClientVideoController implements ClientVideoController {
readonly videoId: string;
readonly client: ClientEntry;
@ -48,19 +51,15 @@ class RemoteClientVideoController implements ClientVideoController {
protected eventListenerVideoClient: (() => void)[];
private currentBroadcastState: boolean;
private currentSubscriptionState: {[T in VideoBroadcastType]: boolean} = {
private currentSubscriptionState: SubscriptionStates = {
screen: false,
camera: false
};
private dismissed: {[T in VideoBroadcastType]: boolean} = {
screen: false,
camera: false
private currentStreamStates: VideoStreamStates = {
screen: "none",
camera: "none"
};
private cachedCameraState: ChannelVideoStreamState;
private cachedScreenState: ChannelVideoStreamState;
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>, videoId?: string) {
this.client = client;
this.events = eventRegistry;
@ -79,14 +78,13 @@ class RemoteClientVideoController implements ClientVideoController {
}));
events.push(client.events.on("notify_video_handle_changed", () => {
Object.keys(this.dismissed).forEach(key => this.dismissed[key] = false);
this.updateVideoClient();
this.updateVideoClient(true);
}));
this.updateVideoClient();
this.updateVideoClient(false);
}
private updateVideoClient() {
private updateVideoClient(notifyChanged: boolean) {
this.eventListenerVideoClient?.forEach(callback => callback());
this.eventListenerVideoClient = [];
@ -94,16 +92,18 @@ class RemoteClientVideoController implements ClientVideoController {
if(videoClient) {
this.initializeVideoClient(videoClient);
}
this.updateVideoState(notifyChanged);
}
protected initializeVideoClient(videoClient: VideoClient) {
this.eventListenerVideoClient.push(videoClient.getEvents().on("notify_broadcast_state_changed", event => {
console.error("Broadcast state changed: %o - %o - %o", event.broadcastType, VideoBroadcastState[event.oldState], VideoBroadcastState[event.newState]);
if(event.newState === VideoBroadcastState.Stopped || event.oldState === VideoBroadcastState.Stopped) {
/* we've a new broadcast which hasn't been dismissed yet */
this.dismissed[event.broadcastType] = false;
}
this.notifyVideo(false);
this.updateVideoState(true);
this.notifyVideoStream(event.broadcastType);
}));
this.eventListenerVideoClient.push(videoClient.getEvents().on("notify_dismissed_state_changed", () => {
this.updateVideoState(true);
}));
this.eventListenerVideoClient.push(videoClient.getEvents().on("notify_broadcast_stream_changed", event => {
this.notifyVideoStream(event.broadcastType);
}));
}
@ -117,8 +117,7 @@ class RemoteClientVideoController implements ClientVideoController {
}
isBroadcasting() {
const videoClient = this.client.getVideoClient();
return videoClient && (videoClient.getVideoState("camera") !== VideoBroadcastState.Stopped || videoClient.getVideoState("screen") !== VideoBroadcastState.Stopped);
return this.currentBroadcastState;
}
isSubscribed(type: VideoBroadcastType) {
@ -127,44 +126,30 @@ class RemoteClientVideoController implements ClientVideoController {
return typeof videoState !== "undefined" && videoState !== VideoBroadcastState.Stopped && videoState !== VideoBroadcastState.Available;
}
toggleMuteState(type: VideoBroadcastType, muted: boolean) {
subscribeVideo(type: VideoBroadcastType) {
const videoClient = this.client.getVideoClient();
if(!videoClient) {
return;
}
const videoState = videoClient.getVideoState(type);
if(muted) {
if(videoState ===VideoBroadcastState.Stopped || videoState === VideoBroadcastState.Available) {
return;
}
this.client.getVideoClient().leaveBroadcast(type);
} else {
/* we explicitly specified that we don't want to have that */
this.dismissed[type] = true;
if(videoState !== VideoBroadcastState.Available) {
return;
}
this.client.getVideoClient().joinBroadcast(type).catch(error => {
logError(LogCategory.VIDEO, tr("Failed to join video broadcast: %o"), error);
/* TODO: Propagate error? */
});
}
this.notifyVideo(false);
videoClient.joinBroadcast(type).catch(error => {
logError(LogCategory.VIDEO, tr("Failed to join video broadcast: %o"), error);
/* TODO: Propagate error? */
});
}
dismissVideo(type: VideoBroadcastType) {
if(this.dismissed[type] === true) {
muteVideo(type: VideoBroadcastType) {
const videoClient = this.client.getVideoClient();
if(!videoClient) {
return;
}
this.dismissed[type] = true;
this.notifyVideo(false);
videoClient.leaveBroadcast(type);
videoClient.dismissBroadcast(type);
}
dismissVideo(type: VideoBroadcastType) {
this.client.getVideoClient()?.dismissBroadcast(type);
}
notifyVideoInfo() {
@ -179,63 +164,53 @@ class RemoteClientVideoController implements ClientVideoController {
});
}
notifyVideo(forceSend: boolean) {
let cameraState: ChannelVideoStreamState = "none";
let screenState: ChannelVideoStreamState = "none";
protected updateVideoState(notifyChanged: boolean) {
let subscriptionState: SubscriptionStates = {} as any;
let streamStates: VideoStreamStates = {} as any;
let broadcasting = false;
let cameraSubscribed = false, screenSubscribed = false;
if(this.hasVideoSupport()) {
const stateCamera = this.getBroadcastState("camera");
if(stateCamera === VideoBroadcastState.Available) {
cameraState = this.dismissed["camera"] ? "ignored" : "available";
} else if(stateCamera === VideoBroadcastState.Running || stateCamera === VideoBroadcastState.Initializing) {
cameraState = "streaming";
cameraSubscribed = true;
for(const videoChannel of kLocalBroadcastChannels) {
const state = this.getBroadcastState(videoChannel);
if(state === VideoBroadcastState.Available) {
streamStates[videoChannel] = this.client.getVideoClient()?.isBroadcastDismissed(videoChannel) ? "ignored" : "available";
broadcasting = true;
} else if(state === VideoBroadcastState.Running || state === VideoBroadcastState.Initializing || state === VideoBroadcastState.Buffering) {
streamStates[videoChannel] = "streaming";
subscriptionState[videoChannel] = true;
broadcasting = true;
} else {
streamStates[videoChannel] = "none";
}
const stateScreen = this.getBroadcastState("screen");
if(stateScreen === VideoBroadcastState.Available) {
screenState = this.dismissed["screen"] ? "ignored" : "available";
} else if(stateScreen === VideoBroadcastState.Running || stateScreen === VideoBroadcastState.Initializing) {
screenState = "streaming";
screenSubscribed = true;
}
broadcasting = cameraState !== "none" || screenState !== "none";
}
if(forceSend || !_.isEqual(this.cachedCameraState, cameraState) || !_.isEqual(this.cachedScreenState, screenState)) {
this.cachedCameraState = cameraState;
this.cachedScreenState = screenState;
this.events.fire_react("notify_video", {
videoId: this.videoId,
cameraStream: cameraState,
screenStream: screenState
});
if(!_.isEqual(this.currentStreamStates, streamStates)) {
this.currentStreamStates = streamStates;
this.notifyVideo();
}
if(broadcasting !== this.currentBroadcastState) {
this.currentBroadcastState = broadcasting;
if(this.callbackBroadcastStateChanged) {
if(this.callbackBroadcastStateChanged && notifyChanged) {
this.callbackBroadcastStateChanged(broadcasting);
}
}
if(this.currentSubscriptionState.camera !== cameraSubscribed || this.currentSubscriptionState.screen !== screenSubscribed) {
this.currentSubscriptionState = {
screen: screenSubscribed,
camera: cameraSubscribed
};
if(this.callbackSubscriptionStateChanged) {
if(!_.isEqual(this.currentSubscriptionState, subscriptionState)) {
this.currentSubscriptionState = subscriptionState;
if(this.callbackSubscriptionStateChanged && notifyChanged) {
this.callbackSubscriptionStateChanged();
}
}
}
notifyVideo() {
this.events.fire_react("notify_video", {
videoId: this.videoId,
cameraStream: this.currentStreamStates["camera"],
screenStream: this.currentStreamStates["screen"]
});
}
notifyVideoStream(type: VideoBroadcastType) {
let state: VideoStreamState;
@ -287,9 +262,11 @@ class LocalVideoController extends RemoteClientVideoController {
for(const broadcastType of kLocalBroadcastChannels) {
const broadcast = videoConnection.getLocalBroadcast(broadcastType);
this.eventListener.push(broadcast.getEvents().on("notify_state_changed", () => {
this.notifyVideo(false);
this.updateVideoState(true);
}));
}
/* TODO: Auto join local broadcast if one is active */
}
protected initializeVideoClient(videoClient: VideoClient) {
@ -428,11 +405,20 @@ class ChannelVideoController {
return;
}
if(event.broadcastType === undefined) {
controller.toggleMuteState("camera", event.muted);
controller.toggleMuteState("screen", event.muted);
if(event.muted) {
if(event.broadcastType === undefined) {
controller.muteVideo("camera");
controller.muteVideo("screen");
} else {
controller.muteVideo(event.broadcastType);
}
} else {
controller.toggleMuteState(event.broadcastType, event.muted);
if(event.broadcastType === undefined) {
controller.subscribeVideo("camera");
controller.subscribeVideo("screen");
} else {
controller.subscribeVideo(event.broadcastType);
}
}
});
@ -468,7 +454,7 @@ class ChannelVideoController {
return;
}
controller.notifyVideo(true);
controller.notifyVideo();
});
this.events.on("query_video_stream", event => {
@ -569,8 +555,7 @@ class ChannelVideoController {
if(localClient.currentChannel()) {
this.currentChannelId = localClient.currentChannel().channelId;
localClient.currentChannel().channelClientsOrdered().forEach(client => {
/* in some instances the server might return our own stream for debug purposes */
if(client instanceof LocalClientEntry && __build.mode !== "debug") {
if(client instanceof LocalClientEntry) {
return;
}

View File

@ -5,7 +5,7 @@ export const kLocalVideoId = "__local__video__";
export const kLocalBroadcastChannels: VideoBroadcastType[] = ["screen", "camera"];
export type ChannelVideoInfo = { clientName: string, clientUniqueId: string, clientId: number, statusIcon: ClientIcon };
export type ChannelVideoStreamState = "available" | "streaming" | "ignored" | "muted" | "none";
export type ChannelVideoStreamState = "available" | "streaming" | "ignored" | "none";
export type VideoStatistics = {
type: "sender",

View File

@ -361,20 +361,20 @@ const VideoControlButtons = React.memo((props: {
const screenShown = props.screenState !== "none" && props.videoId !== kLocalVideoId;
const cameraShown = props.cameraState !== "none" && props.videoId !== kLocalVideoId;
const screenDisabled = props.screenState === "ignored" || props.screenState === "muted" || props.screenState === "available";
const cameraDisabled = props.cameraState === "ignored" || props.cameraState === "muted" || props.cameraState === "available";
const screenDisabled = props.screenState === "ignored" || props.screenState === "available";
const cameraDisabled = props.cameraState === "ignored" || props.cameraState === "available";
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={props.screenState === "muted" ? tr("Unmute screen video") : tr("Mute screen video")}
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={props.cameraState === "muted" ? tr("Unmute camera video") : tr("Mute camera video")}
title={cameraDisabled ? tr("Unmute camera video") : tr("Mute camera video")}
>
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.VideoMuted} />
</div>