Fixed the video bar using the new full react app layout
parent
2211da243d
commit
4d9df41c35
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue