TeaWeb/shared/js/ui/modal/video-viewers/Controller.ts

268 lines
No EOL
9.4 KiB
TypeScript

import {Registry} from "tc-events";
import {
ModalVideoViewersEvents,
ModalVideoViewersVariables,
VideoViewerInfo, VideoViewerList
} from "tc-shared/ui/modal/video-viewers/Definitions";
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
import {ClientEntry} from "tc-shared/tree/Client";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {CallOnce, ignorePromise} from "tc-shared/proto";
import {VideoBroadcastType, VideoBroadcastViewer} from "tc-shared/connection/VideoConnection";
import {spawnModal} from "tc-shared/ui/react-elements/modal";
class Controller {
readonly handler: ConnectionHandler;
readonly events: Registry<ModalVideoViewersEvents>;
readonly variables: IpcUiVariableProvider<ModalVideoViewersVariables>;
private registeredEvents: (() => void)[];
private registeredClientEvents: { [key: number]: (() => void)[] } = {};
/* Active video viewers */
private videoViewerInfo: { [key: number]: VideoViewerInfo & { talkPower: number } };
private videoViewer: VideoViewerList;
constructor(handler: ConnectionHandler) {
this.handler = handler;
this.events = new Registry<ModalVideoViewersEvents>();
this.variables = new IpcUiVariableProvider<ModalVideoViewersVariables>();
this.videoViewerInfo = {};
this.videoViewer = { __internal_client_order: [] };
}
@CallOnce
destroy() {
this.videoViewerInfo = {};
this.videoViewer = { __internal_client_order: [] };
this.registeredEvents?.forEach(callback => callback());
this.registeredEvents = undefined;
Object.keys(this.registeredClientEvents).forEach(clientId => this.unregisterClientEvents(parseInt(clientId)));
this.events.destroy();
this.variables.destroy();
}
@CallOnce
initialize() {
this.variables.setVariableProvider("viewerInfo", clientId => this.videoViewerInfo[clientId]);
this.variables.setVariableProvider("videoViewers", () => this.videoViewer);
this.registeredEvents = [];
this.registeredEvents.push(this.handler.channelTree.events.on("notify_client_leave_view", event => {
this.unregisterClientEvents(event.client.clientId());
}));
this.registeredEvents.push(this.handler.channelTree.events.on("notify_client_enter_view", event => {
if(this.videoViewerInfo[event.client.clientId()]) {
this.registerClientEvents(event.client);
this.updateViewerInfo(event.client);
}
}));
this.registerBroadcastListener("camera");
this.registerBroadcastListener("screen");
this.updateViewerList();
}
private registerBroadcastListener(broadcastType: VideoBroadcastType) {
const videoConnection = this.handler.getServerConnection().getVideoConnection();
const broadcast = videoConnection.getLocalBroadcast(broadcastType);
this.registeredEvents.push(broadcast.getEvents().on("notify_clients_joined", event => {
const viewers = this.videoViewer[broadcastType] = this.videoViewer[broadcastType] || [];
for(const viewer of event.clients) {
viewers.push(viewer.clientId);
this.registerNewViewer(viewer);
}
this.updateViewerList();
}));
this.registeredEvents.push(broadcast.getEvents().on("notify_clients_left", event => {
/* We can't remove the client listeners here since the client might also be in other broadcasts */
for(const clientId of event.clientIds) {
this.videoViewer[broadcastType]?.remove(clientId);
}
this.updateViewerList();
}));
this.registeredEvents.push(broadcast.getEvents().on("notify_state_changed", event => {
switch (event.newState.state) {
case "broadcasting":
case "initializing":
this.videoViewer[broadcastType] = this.videoViewer[broadcastType] || [];
break;
case "failed":
case "stopped":
default:
delete this.videoViewer[broadcastType];
break;
}
this.updateViewerList();
}));
switch (broadcast.getState().state) {
case "initializing":
case "broadcasting":
const viewers = this.videoViewer[broadcastType] = [];
for(const viewer of broadcast.getViewer()) {
this.registerNewViewer(viewer);
viewers.push(viewer.clientId);
}
break;
case "failed":
case "stopped":
default:
delete this.videoViewer[broadcastType];
break;
}
}
private registerNewViewer(viewer: VideoBroadcastViewer) {
if(typeof this.registeredClientEvents[viewer.clientId] !== "undefined") {
/* We've up2date data for that viewer */
return;
}
/* Store newest viewer data */
this.videoViewerInfo[viewer.clientId] = {
handlerId: this.handler.handlerId,
clientName: viewer.clientName,
clientUniqueId: viewer.clientUniqueId,
clientStatus: undefined,
talkPower: 0
};
const client = this.handler.channelTree.findClient(viewer.clientId);
if(!client) {
/* Target client isn't in our view */
return;
}
this.registerClientEvents(client);
this.updateViewerInfo(client);
}
private unregisterClientEvents(clientId: number) {
this.registeredClientEvents[clientId]?.forEach(callback => callback());
delete this.registeredClientEvents[clientId];
}
private registerClientEvents(client: ClientEntry) {
const clientId = client.clientId();
const events = this.registeredClientEvents[clientId] = [];
events.push(client.events.on("notify_status_icon_changed", () => this.updateViewerInfo(client)));
events.push(client.events.on("notify_properties_updated", event => {
if("client_talk_power" in event.updated_properties || "client_nickname" in event.updated_properties) {
/* Fill update incl. resort since the client order might has changed */
this.updateViewerInfo(client);
this.updateViewerList();
}
}));
}
/**
* Update the viewer cache for the target client.
* @param client
* @private
*/
private updateViewerInfo(client: ClientEntry) {
this.videoViewerInfo[client.clientId()] = {
handlerId: this.handler.handlerId,
clientName: client.clientNickName(),
clientStatus: client.getStatusIcon(),
clientUniqueId: client.properties.client_unique_identifier,
talkPower: client.properties.client_talk_power
};
/* The variables handler will only send an update if the variable really has changed. */
this.variables.sendVariable("viewerInfo", client.clientId());
}
private updateViewerList() {
const uniqueTotalViewers = new Set<number>();
for(const key of Object.keys(this.videoViewer)) {
if(key === "__internal_client_order") {
continue;
}
for(const clientId of this.videoViewer[key]) {
uniqueTotalViewers.add(clientId);
}
}
Object.keys(this.registeredClientEvents).forEach(clientIdString => {
const clientId = parseInt(clientIdString);
if(uniqueTotalViewers.has(clientId)) {
return;
}
/* We're not any more subscribed */
this.unregisterClientEvents(clientId);
});
Object.keys(this.videoViewerInfo).forEach(clientIdString => {
const clientId = parseInt(clientIdString);
if(uniqueTotalViewers.has(clientId)) {
return;
}
delete this.videoViewerInfo[clientIdString];
});
this.videoViewer["__internal_client_order"] = [...uniqueTotalViewers];
this.videoViewer["__internal_client_order"].sort((clientAId, clientBId) => {
const clientA = this.videoViewerInfo[clientAId];
const clientB = this.videoViewerInfo[clientBId];
if(!clientA) {
return -1;
} else if(!clientB) {
return 1;
}
if(clientA.talkPower < clientB.talkPower) {
return 1;
}
if(clientA.talkPower > clientB.talkPower) {
return -1;
}
if(clientA.clientName > clientB.clientName) {
return 1;
}
if(clientA.clientName < clientB.clientName) {
return -1;
}
return 0;
});
this.variables.sendVariable("videoViewers");
}
}
export function spawnVideoViewerInfo(handler: ConnectionHandler) {
const controller = new Controller(handler);
controller.initialize();
const modal = spawnModal("modal-video-viewers", [
controller.events.generateIpcDescription(),
controller.variables.generateConsumerDescription()
], {
popoutable: true
});
modal.getEvents().on("destroy", () => controller.destroy());
ignorePromise(modal.show());
}