Outsourced PIP into the video controller itself
This commit is contained in:
parent
7dcf13ea5f
commit
26505f2aeb
6 changed files with 257 additions and 305 deletions
|
@ -65,6 +65,8 @@ export interface VideoClient {
|
||||||
|
|
||||||
dismissBroadcast(broadcastType: VideoBroadcastType);
|
dismissBroadcast(broadcastType: VideoBroadcastType);
|
||||||
isBroadcastDismissed(broadcastType: VideoBroadcastType) : boolean;
|
isBroadcastDismissed(broadcastType: VideoBroadcastType) : boolean;
|
||||||
|
|
||||||
|
showPip(broadcastType: VideoBroadcastType) : Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalVideoBroadcastEvents {
|
export interface LocalVideoBroadcastEvents {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {RemoteRTPTrackState, RemoteRTPVideoTrack} from "../RemoteTrack";
|
||||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
import {RTCConnection} from "tc-shared/connection/rtc/Connection";
|
import {RTCConnection} from "tc-shared/connection/rtc/Connection";
|
||||||
|
import {makeVideoAutoplay} from "tc-shared/ui/frames/video/Definitions";
|
||||||
|
|
||||||
export class RtpVideoClient implements VideoClient {
|
export class RtpVideoClient implements VideoClient {
|
||||||
private readonly handle: RTCConnection;
|
private readonly handle: RTCConnection;
|
||||||
|
@ -45,6 +46,9 @@ export class RtpVideoClient implements VideoClient {
|
||||||
screen: false
|
screen: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private pipElement: HTMLVideoElement | undefined;
|
||||||
|
private pipBroadcastType: VideoBroadcastType | undefined;
|
||||||
|
|
||||||
constructor(handle: RTCConnection, clientId: number) {
|
constructor(handle: RTCConnection, clientId: number) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
|
@ -113,6 +117,7 @@ export class RtpVideoClient implements VideoClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.stopPip();
|
||||||
this.setRtpTrack("camera", undefined);
|
this.setRtpTrack("camera", undefined);
|
||||||
this.setRtpTrack("screen", undefined);
|
this.setRtpTrack("screen", undefined);
|
||||||
}
|
}
|
||||||
|
@ -133,6 +138,13 @@ export class RtpVideoClient implements VideoClient {
|
||||||
|
|
||||||
this.updateBroadcastState(type);
|
this.updateBroadcastState(type);
|
||||||
this.events.fire("notify_broadcast_stream_changed", { broadcastType: type });
|
this.events.fire("notify_broadcast_stream_changed", { broadcastType: type });
|
||||||
|
if(type === this.pipBroadcastType && this.pipElement) {
|
||||||
|
if(track) {
|
||||||
|
this.pipElement.srcObject = track.getMediaStream();
|
||||||
|
} else {
|
||||||
|
this.stopPip();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setBroadcastId(type: VideoBroadcastType, id: number | undefined) {
|
setBroadcastId(type: VideoBroadcastType, id: number | undefined) {
|
||||||
|
@ -165,6 +177,86 @@ export class RtpVideoClient implements VideoClient {
|
||||||
return this.dismissedStates[broadcastType];
|
return this.dismissedStates[broadcastType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showPip(broadcastType: VideoBroadcastType): Promise<void> {
|
||||||
|
if(this.trackStates[broadcastType] !== VideoBroadcastState.Running && this.trackStates[broadcastType] !== VideoBroadcastState.Buffering) {
|
||||||
|
throw tr("Target broadcast isn't running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.pipBroadcastType === broadcastType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pipBroadcastType = broadcastType;
|
||||||
|
|
||||||
|
if(!("requestPictureInPicture" in HTMLVideoElement.prototype)) {
|
||||||
|
throw tr("Picture in picture isn't supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = this.getVideoStream(broadcastType);
|
||||||
|
if(!stream) {
|
||||||
|
throw tr("Missing video stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = document.createElement("video");
|
||||||
|
element.srcObject = stream;
|
||||||
|
element.muted = true;
|
||||||
|
element.style.position = "absolute";
|
||||||
|
element.style.top = "-1000000px";
|
||||||
|
|
||||||
|
this.pipElement?.remove();
|
||||||
|
this.pipElement = element;
|
||||||
|
this.pipBroadcastType = broadcastType;
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
element.onloadedmetadata = resolve;
|
||||||
|
element.onerror = reject;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw tr("Failed to load video meta data");
|
||||||
|
} finally {
|
||||||
|
element.onloadedmetadata = undefined;
|
||||||
|
element.onerror = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (element as any).requestPictureInPicture();
|
||||||
|
} catch(error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelAutoplay = makeVideoAutoplay(element);
|
||||||
|
element.addEventListener('leavepictureinpicture', () => {
|
||||||
|
cancelAutoplay();
|
||||||
|
element.remove();
|
||||||
|
if(this.pipElement === element) {
|
||||||
|
this.pipElement = undefined;
|
||||||
|
this.pipBroadcastType = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(error) {
|
||||||
|
element.remove();
|
||||||
|
if(this.pipElement === element) {
|
||||||
|
this.pipElement = undefined;
|
||||||
|
this.pipBroadcastType = undefined;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopPip() {
|
||||||
|
if((document as any).pictureInPictureElement === this.pipElement && "exitPictureInPicture" in document) {
|
||||||
|
(document as any).exitPictureInPicture();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pipElement?.remove();
|
||||||
|
this.pipElement = undefined;
|
||||||
|
this.pipBroadcastType = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private setBroadcastState(type: VideoBroadcastType, state: VideoBroadcastState) {
|
private setBroadcastState(type: VideoBroadcastType, state: VideoBroadcastState) {
|
||||||
if(this.trackStates[type] === state) {
|
if(this.trackStates[type] === state) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {ClientEntry, LocalClientEntry, MusicClientEntry} from "./Client";
|
||||||
import {ChannelTreeEntry} from "./ChannelTreeEntry";
|
import {ChannelTreeEntry} from "./ChannelTreeEntry";
|
||||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
|
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
|
@ -20,7 +19,7 @@ import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import {tr, tra} from "tc-shared/i18n/localize";
|
import {tr, tra} from "tc-shared/i18n/localize";
|
||||||
import {initializeChannelTreeUiEvents, renderChannelTree} from "tc-shared/ui/tree/Controller";
|
import {initializeChannelTreeUiEvents} from "tc-shared/ui/tree/Controller";
|
||||||
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
|
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {Registry} from "tc-shared/events";
|
||||||
import {
|
import {
|
||||||
ChannelVideoEvents,
|
ChannelVideoEvents,
|
||||||
ChannelVideoStreamState,
|
ChannelVideoStreamState,
|
||||||
kLocalVideoId, makeVideoAutoplay,
|
kLocalVideoId,
|
||||||
VideoStreamState
|
VideoStreamState
|
||||||
} from "tc-shared/ui/frames/video/Definitions";
|
} from "tc-shared/ui/frames/video/Definitions";
|
||||||
import {
|
import {
|
||||||
|
@ -62,9 +62,6 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
camera: "none"
|
camera: "none"
|
||||||
};
|
};
|
||||||
|
|
||||||
private pipElement: HTMLVideoElement | undefined;
|
|
||||||
private pipBroadcastType: VideoBroadcastType | undefined;
|
|
||||||
|
|
||||||
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>, videoId?: string) {
|
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>, videoId?: string) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.events = eventRegistry;
|
this.events = eventRegistry;
|
||||||
|
@ -119,9 +116,6 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
|
|
||||||
this.eventListener?.forEach(callback => callback());
|
this.eventListener?.forEach(callback => callback());
|
||||||
this.eventListener = undefined;
|
this.eventListener = undefined;
|
||||||
|
|
||||||
this.pipElement?.remove();
|
|
||||||
this.pipElement = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isBroadcasting() {
|
isBroadcasting() {
|
||||||
|
@ -209,10 +203,6 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
this.callbackSubscriptionStateChanged();
|
this.callbackSubscriptionStateChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.pipBroadcastType && this.currentStreamStates[this.pipBroadcastType] !== "streaming") {
|
|
||||||
this.stopPip();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyVideo() {
|
notifyVideo() {
|
||||||
|
@ -242,14 +232,6 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.pipBroadcastType === type && this.pipElement) {
|
|
||||||
if(state.state === "connected") {
|
|
||||||
this.pipElement.srcObject = state.stream;
|
|
||||||
} else {
|
|
||||||
this.stopPip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.events.fire_react("notify_video_stream", {
|
this.events.fire_react("notify_video_stream", {
|
||||||
videoId: this.videoId,
|
videoId: this.videoId,
|
||||||
broadcastType: type,
|
broadcastType: type,
|
||||||
|
@ -271,79 +253,13 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
return videoClient ? videoClient.getVideoStream(target) : undefined;
|
return videoClient ? videoClient.getVideoStream(target) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopPip() {
|
|
||||||
if((document as any).pictureInPictureElement === this.pipElement && "exitPictureInPicture" in document) {
|
|
||||||
(document as any).exitPictureInPicture();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pipElement?.remove();
|
|
||||||
this.pipElement = undefined;
|
|
||||||
this.pipBroadcastType = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async showPip(type: VideoBroadcastType) {
|
async showPip(type: VideoBroadcastType) {
|
||||||
if(this.pipBroadcastType === type) {
|
const client = this.client.getVideoClient();
|
||||||
|
if(!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pipBroadcastType = type;
|
|
||||||
|
|
||||||
if(!("requestPictureInPicture" in HTMLVideoElement.prototype)) {
|
await client.showPip(type);
|
||||||
throw tr("Picture in picture isn't supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = this.getBroadcastStream(type);
|
|
||||||
if(!stream) {
|
|
||||||
throw tr("Missing video stream");
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = document.createElement("video");
|
|
||||||
element.srcObject = stream;
|
|
||||||
element.muted = true;
|
|
||||||
element.style.position = "absolute";
|
|
||||||
element.style.top = "-1000000px";
|
|
||||||
|
|
||||||
this.pipElement?.remove();
|
|
||||||
this.pipElement = element;
|
|
||||||
this.pipBroadcastType = type;
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.body.appendChild(element);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
element.onloadedmetadata = resolve;
|
|
||||||
element.onerror = reject;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw tr("Failed to load video meta data");
|
|
||||||
} finally {
|
|
||||||
element.onloadedmetadata = undefined;
|
|
||||||
element.onerror = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await (element as any).requestPictureInPicture();
|
|
||||||
} catch(error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelAutoplay = makeVideoAutoplay(element);
|
|
||||||
element.addEventListener('leavepictureinpicture', () => {
|
|
||||||
cancelAutoplay();
|
|
||||||
element.remove();
|
|
||||||
if(this.pipElement === element) {
|
|
||||||
this.pipElement = undefined;
|
|
||||||
this.pipBroadcastType = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch(error) {
|
|
||||||
element.remove();
|
|
||||||
if(this.pipElement === element) {
|
|
||||||
this.pipElement = undefined;
|
|
||||||
this.pipBroadcastType = undefined;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -190,53 +190,3 @@ export namespace callbacks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window[callback_object_id] = callbacks;
|
window[callback_object_id] = callbacks;
|
||||||
|
|
||||||
namespace bbcodes {
|
|
||||||
/* the = because we sometimes get that */
|
|
||||||
//const url_client_regex = /?client:\/\/(?<client_id>[0-9]+)\/(?<client_unique_id>[a-zA-Z0-9+=#]+)~(?<client_name>(?:[^%]|%[0-9A-Fa-f]{2})+)$/g;
|
|
||||||
const url_client_regex = /client:\/\/([0-9]+)\/([a-zA-Z0-9+=/#]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g; /* IDK which browsers already support group naming */
|
|
||||||
const url_channel_regex = /channel:\/\/([0-9]+)~((?:[^%]|%[0-9A-Fa-f]{2})+)$/g;
|
|
||||||
|
|
||||||
function initialize() {
|
|
||||||
/* FIXME: Reimplement client BB codes */
|
|
||||||
/*
|
|
||||||
const origin_url = xbbcode.register.find_parser('url');
|
|
||||||
xbbcode.register.register_parser({
|
|
||||||
tag: 'url',
|
|
||||||
build_html_tag_open(layer): string {
|
|
||||||
if(layer.options) {
|
|
||||||
if(layer.options.match(url_channel_regex)) {
|
|
||||||
const groups = url_channel_regex.exec(layer.options);
|
|
||||||
|
|
||||||
return generate_channel_open({
|
|
||||||
add_braces: false,
|
|
||||||
channel_id: parseInt(groups[1]),
|
|
||||||
channel_name: decodeURIComponent(groups[2])
|
|
||||||
});
|
|
||||||
} else if(layer.options.match(url_client_regex)) {
|
|
||||||
const groups = url_client_regex.exec(layer.options);
|
|
||||||
|
|
||||||
return generate_client_open({
|
|
||||||
add_braces: false,
|
|
||||||
client_id: parseInt(groups[1]),
|
|
||||||
client_unique_id: groups[2],
|
|
||||||
client_name: decodeURIComponent(groups[3])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return origin_url.build_html_tag_open(layer);
|
|
||||||
},
|
|
||||||
build_html_tag_close(layer): string {
|
|
||||||
if(layer.options) {
|
|
||||||
if(layer.options.match(url_client_regex))
|
|
||||||
return "</div>";
|
|
||||||
if(layer.options.match(url_channel_regex))
|
|
||||||
return "</div>";
|
|
||||||
}
|
|
||||||
return origin_url.build_html_tag_close(layer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
initialize();
|
|
||||||
}
|
|
|
@ -8,9 +8,7 @@ import {
|
||||||
ClientTalkIconState, FullChannelTreeEntry,
|
ClientTalkIconState, FullChannelTreeEntry,
|
||||||
ServerState
|
ServerState
|
||||||
} from "tc-shared/ui/tree/Definitions";
|
} from "tc-shared/ui/tree/Definitions";
|
||||||
import {ChannelTreeRenderer} from "./Renderer";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import {LogCategory, logWarn} from "tc-shared/log";
|
import {LogCategory, logWarn} from "tc-shared/log";
|
||||||
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
|
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
|
||||||
import {ClientEntry, ClientProperties, ClientType, LocalClientEntry, MusicClientEntry} from "tc-shared/tree/Client";
|
import {ClientEntry, ClientProperties, ClientType, LocalClientEntry, MusicClientEntry} from "tc-shared/tree/Client";
|
||||||
|
@ -19,7 +17,6 @@ import {VoiceConnectionEvents, VoiceConnectionStatus} from "tc-shared/connection
|
||||||
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||||
import {GroupManager, GroupManagerEvents} from "tc-shared/permission/GroupManager";
|
import {GroupManager, GroupManagerEvents} from "tc-shared/permission/GroupManager";
|
||||||
import {ServerEntry} from "tc-shared/tree/Server";
|
import {ServerEntry} from "tc-shared/tree/Server";
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
|
||||||
|
|
||||||
export interface ChannelTreeRendererOptions {
|
export interface ChannelTreeRendererOptions {
|
||||||
popoutButton: boolean;
|
popoutButton: boolean;
|
||||||
|
@ -32,25 +29,154 @@ export function initializeChannelTreeUiEvents(channelTree: ChannelTree, options:
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement, options: ChannelTreeRendererOptions) {
|
function generateServerStatus(serverEntry: ServerEntry) : ServerState {
|
||||||
const events = initializeChannelTreeUiEvents(channelTree, options);
|
switch (serverEntry.channelTree.client.connection_state) {
|
||||||
|
case ConnectionState.AUTHENTICATING:
|
||||||
|
case ConnectionState.CONNECTING:
|
||||||
|
case ConnectionState.INITIALISING:
|
||||||
|
return {
|
||||||
|
state: "connecting",
|
||||||
|
targetAddress: serverEntry.remote_address.host + (serverEntry.remote_address.port === 9987 ? "" : `:${serverEntry.remote_address.port}`)
|
||||||
|
};
|
||||||
|
|
||||||
ReactDOM.render(<ChannelTreeRenderer handlerId={channelTree.client.handlerId} events={events} />, target);
|
case ConnectionState.DISCONNECTING:
|
||||||
|
case ConnectionState.UNCONNECTED:
|
||||||
|
return { state: "disconnected" };
|
||||||
|
|
||||||
let handlerDestroyListener;
|
case ConnectionState.CONNECTED:
|
||||||
server_connections.events().on("notify_handler_deleted", handlerDestroyListener = event => {
|
return {
|
||||||
if(event.handler !== channelTree.client) {
|
state: "connected",
|
||||||
return;
|
name: serverEntry.properties.virtualserver_name,
|
||||||
|
icon: { iconId: serverEntry.properties.virtualserver_icon_id, serverUniqueId: serverEntry.properties.virtualserver_unique_identifier }
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.unmountComponentAtNode(target);
|
function generateClientTalkStatus(client: ClientEntry) : { status: ClientTalkIconState, requestMessage?: string } {
|
||||||
server_connections.events().off("notify_handler_deleted", handlerDestroyListener);
|
let status: ClientTalkIconState = "unset";
|
||||||
events.fire("notify_destroy");
|
|
||||||
events.destroy();
|
if(client.properties.client_is_talker) {
|
||||||
|
status = "granted";
|
||||||
|
} else if(client.properties.client_talk_power < client.currentChannel().properties.channel_needed_talk_power) {
|
||||||
|
status = "prohibited";
|
||||||
|
|
||||||
|
if(client.properties.client_talk_request !== 0) {
|
||||||
|
status = "requested";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestMessage: client.properties.client_talk_request_msg,
|
||||||
|
status: status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateClientIcons(client: ClientEntry) : ClientIcons {
|
||||||
|
const uniqueServerId = client.channelTree.client.getCurrentServerUniqueId();
|
||||||
|
|
||||||
|
const serverGroupIcons = client.assignedServerGroupIds()
|
||||||
|
.map(groupId => client.channelTree.client.groups.findServerGroup(groupId))
|
||||||
|
.filter(group => !!group && group.properties.iconid !== 0)
|
||||||
|
.sort(GroupManager.sorter())
|
||||||
|
.map(group => {
|
||||||
|
return {
|
||||||
|
iconId: group.properties.iconid,
|
||||||
|
groupName: group.name,
|
||||||
|
groupId: group.id,
|
||||||
|
serverUniqueId: uniqueServerId
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const channelGroupIcon = [client.assignedChannelGroup()]
|
||||||
|
.map(groupId => client.channelTree.client.groups.findChannelGroup(groupId))
|
||||||
|
.filter(group => !!group && group.properties.iconid !== 0)
|
||||||
|
.map(group => {
|
||||||
|
return {
|
||||||
|
iconId: group.properties.iconid,
|
||||||
|
groupName: group.name,
|
||||||
|
groupId: group.id,
|
||||||
|
serverUniqueId: uniqueServerId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientIcon = client.properties.client_icon_id === 0 ? [] : [client.properties.client_icon_id];
|
||||||
|
return {
|
||||||
|
serverGroupIcons: serverGroupIcons,
|
||||||
|
channelGroupIcon: channelGroupIcon[0],
|
||||||
|
clientIcon: clientIcon.length > 0 ? { iconId: clientIcon[0], serverUniqueId: uniqueServerId } : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateClientNameInfo(client: ClientEntry) : ClientNameInfo {
|
||||||
|
let prefix = [];
|
||||||
|
let suffix = [];
|
||||||
|
for(const groupId of client.assignedServerGroupIds()) {
|
||||||
|
const group = client.channelTree.client.groups.findServerGroup(groupId);
|
||||||
|
if(!group) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(group.properties.namemode === 1) {
|
||||||
|
prefix.push(group.name);
|
||||||
|
} else if(group.properties.namemode === 2) {
|
||||||
|
suffix.push(group.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelGroup = client.channelTree.client.groups.findChannelGroup(client.assignedChannelGroup());
|
||||||
|
if(channelGroup) {
|
||||||
|
if(channelGroup.properties.namemode === 1) {
|
||||||
|
prefix.push(channelGroup.name);
|
||||||
|
} else if(channelGroup.properties.namemode === 2) {
|
||||||
|
suffix.push(channelGroup.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const afkMessage = client.properties.client_away ? client.properties.client_away_message : undefined;
|
||||||
|
return {
|
||||||
|
name: client.clientNickName(),
|
||||||
|
awayMessage: afkMessage,
|
||||||
|
prefix: prefix,
|
||||||
|
suffix: suffix
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChannelIcons(channel: ChannelEntry) : ChannelIcons {
|
||||||
|
let icons: ChannelIcons = {
|
||||||
|
musicQuality: channel.properties.channel_codec === 3 || channel.properties.channel_codec === 5,
|
||||||
|
codecUnsupported: true,
|
||||||
|
default: channel.properties.channel_flag_default,
|
||||||
|
moderated: channel.properties.channel_needed_talk_power !== 0,
|
||||||
|
passwordProtected: channel.properties.channel_flag_password,
|
||||||
|
channelIcon: {
|
||||||
|
iconId: channel.properties.channel_icon_id,
|
||||||
|
serverUniqueId: channel.channelTree.client.getCurrentServerUniqueId()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const voiceConnection = channel.channelTree.client.serverConnection.getVoiceConnection();
|
||||||
|
const voiceState = voiceConnection.getConnectionState();
|
||||||
|
|
||||||
|
switch (voiceState) {
|
||||||
|
case VoiceConnectionStatus.Connected:
|
||||||
|
icons.codecUnsupported = !voiceConnection.decodingSupported(channel.properties.channel_codec);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
icons.codecUnsupported = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChannelInfo(channel: ChannelEntry) : ChannelEntryInfo {
|
||||||
|
return {
|
||||||
|
collapsedState: channel.child_channel_head || channel.channelClientsOrdered().length > 0 ? channel.isCollapsed() ? "collapsed" : "expended" : "unset",
|
||||||
|
name: channel.parsed_channel_name.text,
|
||||||
|
nameStyle: channel.parsed_channel_name.alignment
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FIXME: Client move is not a part of the channel tree, it's part of our own controller here */
|
|
||||||
const ChannelIconUpdateKeys: (keyof ChannelProperties)[] = [
|
const ChannelIconUpdateKeys: (keyof ChannelProperties)[] = [
|
||||||
"channel_name",
|
"channel_name",
|
||||||
"channel_flag_password",
|
"channel_flag_password",
|
||||||
|
@ -425,19 +551,19 @@ class ChannelTreeController {
|
||||||
entryId: entry.entry.uniqueEntryId,
|
entryId: entry.entry.uniqueEntryId,
|
||||||
depth: entry.depth,
|
depth: entry.depth,
|
||||||
fullInfo: true,
|
fullInfo: true,
|
||||||
state: this.generateServerStatus(entry.entry),
|
state: generateServerStatus(entry.entry),
|
||||||
unread: entry.entry.isUnread()
|
unread: entry.entry.isUnread()
|
||||||
});
|
});
|
||||||
} else if(entry.entry instanceof ClientEntry) {
|
} else if(entry.entry instanceof ClientEntry) {
|
||||||
const talkStatus = this.generateClientTalkStatus(entry.entry);
|
const talkStatus = generateClientTalkStatus(entry.entry);
|
||||||
entries.push({
|
entries.push({
|
||||||
type: entry.entry instanceof LocalClientEntry ? "client-local" : "client",
|
type: entry.entry instanceof LocalClientEntry ? "client-local" : "client",
|
||||||
entryId: entry.entry.uniqueEntryId,
|
entryId: entry.entry.uniqueEntryId,
|
||||||
depth: entry.depth,
|
depth: entry.depth,
|
||||||
fullInfo: true,
|
fullInfo: true,
|
||||||
unread: entry.entry.isUnread(),
|
unread: entry.entry.isUnread(),
|
||||||
name: this.generateClientNameInfo(entry.entry),
|
name: generateClientNameInfo(entry.entry),
|
||||||
icons: this.generateClientIcons(entry.entry),
|
icons: generateClientIcons(entry.entry),
|
||||||
status: entry.entry.getStatusIcon(),
|
status: entry.entry.getStatusIcon(),
|
||||||
talkStatus: talkStatus.status,
|
talkStatus: talkStatus.status,
|
||||||
talkRequestMessage: talkStatus.requestMessage
|
talkRequestMessage: talkStatus.requestMessage
|
||||||
|
@ -449,8 +575,8 @@ class ChannelTreeController {
|
||||||
depth: entry.depth,
|
depth: entry.depth,
|
||||||
fullInfo: true,
|
fullInfo: true,
|
||||||
unread: entry.entry.isUnread(),
|
unread: entry.entry.isUnread(),
|
||||||
icons: this.generateChannelIcons(entry.entry),
|
icons: generateChannelIcons(entry.entry),
|
||||||
info: this.generateChannelInfo(entry.entry),
|
info: generateChannelInfo(entry.entry),
|
||||||
icon: entry.entry.getStatusIcon()
|
icon: entry.entry.getStatusIcon()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -492,18 +618,10 @@ class ChannelTreeController {
|
||||||
this.events.fire_react("notify_selected_entry", { treeEntryId: selectedEntry ? selectedEntry.uniqueEntryId : 0 });
|
this.events.fire_react("notify_selected_entry", { treeEntryId: selectedEntry ? selectedEntry.uniqueEntryId : 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateChannelInfo(channel: ChannelEntry) : ChannelEntryInfo {
|
|
||||||
return {
|
|
||||||
collapsedState: channel.child_channel_head || channel.channelClientsOrdered().length > 0 ? channel.isCollapsed() ? "collapsed" : "expended" : "unset",
|
|
||||||
name: channel.parsed_channel_name.text,
|
|
||||||
nameStyle: channel.parsed_channel_name.alignment
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendChannelInfo(channel: ChannelEntry) {
|
public sendChannelInfo(channel: ChannelEntry) {
|
||||||
this.events.fire_react("notify_channel_info", {
|
this.events.fire_react("notify_channel_info", {
|
||||||
treeEntryId: channel.uniqueEntryId,
|
treeEntryId: channel.uniqueEntryId,
|
||||||
info: this.generateChannelInfo(channel)
|
info: generateChannelInfo(channel)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,157 +629,31 @@ class ChannelTreeController {
|
||||||
this.events.fire_react("notify_channel_icon", { icon: channel.getStatusIcon(), treeEntryId: channel.uniqueEntryId });
|
this.events.fire_react("notify_channel_icon", { icon: channel.getStatusIcon(), treeEntryId: channel.uniqueEntryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateChannelIcons(channel: ChannelEntry) : ChannelIcons {
|
|
||||||
let icons: ChannelIcons = {
|
|
||||||
musicQuality: channel.properties.channel_codec === 3 || channel.properties.channel_codec === 5,
|
|
||||||
codecUnsupported: true,
|
|
||||||
default: channel.properties.channel_flag_default,
|
|
||||||
moderated: channel.properties.channel_needed_talk_power !== 0,
|
|
||||||
passwordProtected: channel.properties.channel_flag_password,
|
|
||||||
channelIcon: {
|
|
||||||
iconId: channel.properties.channel_icon_id,
|
|
||||||
serverUniqueId: this.channelTree.client.getCurrentServerUniqueId()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const voiceConnection = this.channelTree.client.serverConnection.getVoiceConnection();
|
|
||||||
const voiceState = voiceConnection.getConnectionState();
|
|
||||||
|
|
||||||
switch (voiceState) {
|
|
||||||
case VoiceConnectionStatus.Connected:
|
|
||||||
icons.codecUnsupported = !voiceConnection.decodingSupported(channel.properties.channel_codec);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
icons.codecUnsupported = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendChannelIcons(channel: ChannelEntry) {
|
public sendChannelIcons(channel: ChannelEntry) {
|
||||||
this.events.fire_react("notify_channel_icons", { icons: this.generateChannelIcons(channel), treeEntryId: channel.uniqueEntryId });
|
this.events.fire_react("notify_channel_icons", { icons: generateChannelIcons(channel), treeEntryId: channel.uniqueEntryId });
|
||||||
}
|
|
||||||
|
|
||||||
private generateClientNameInfo(client: ClientEntry) : ClientNameInfo {
|
|
||||||
let prefix = [];
|
|
||||||
let suffix = [];
|
|
||||||
for(const groupId of client.assignedServerGroupIds()) {
|
|
||||||
const group = this.channelTree.client.groups.findServerGroup(groupId);
|
|
||||||
if(!group) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(group.properties.namemode === 1) {
|
|
||||||
prefix.push(group.name);
|
|
||||||
} else if(group.properties.namemode === 2) {
|
|
||||||
suffix.push(group.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelGroup = this.channelTree.client.groups.findChannelGroup(client.assignedChannelGroup());
|
|
||||||
if(channelGroup) {
|
|
||||||
if(channelGroup.properties.namemode === 1) {
|
|
||||||
prefix.push(channelGroup.name);
|
|
||||||
} else if(channelGroup.properties.namemode === 2) {
|
|
||||||
suffix.push(channelGroup.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const afkMessage = client.properties.client_away ? client.properties.client_away_message : undefined;
|
|
||||||
return {
|
|
||||||
name: client.clientNickName(),
|
|
||||||
awayMessage: afkMessage,
|
|
||||||
prefix: prefix,
|
|
||||||
suffix: suffix
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendClientNameInfo(client: ClientEntry) {
|
public sendClientNameInfo(client: ClientEntry) {
|
||||||
this.events.fire_react("notify_client_name", {
|
this.events.fire_react("notify_client_name", {
|
||||||
info: this.generateClientNameInfo(client),
|
info: generateClientNameInfo(client),
|
||||||
treeEntryId: client.uniqueEntryId
|
treeEntryId: client.uniqueEntryId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateClientIcons(client: ClientEntry) : ClientIcons {
|
|
||||||
const uniqueServerId = this.channelTree.client.getCurrentServerUniqueId();
|
|
||||||
|
|
||||||
const serverGroupIcons = client.assignedServerGroupIds()
|
|
||||||
.map(groupId => this.channelTree.client.groups.findServerGroup(groupId))
|
|
||||||
.filter(group => !!group && group.properties.iconid !== 0)
|
|
||||||
.sort(GroupManager.sorter())
|
|
||||||
.map(group => { return { iconId: group.properties.iconid, groupName: group.name, groupId: group.id, serverUniqueId: uniqueServerId }; });
|
|
||||||
|
|
||||||
const channelGroupIcon = [client.assignedChannelGroup()]
|
|
||||||
.map(groupId => this.channelTree.client.groups.findChannelGroup(groupId))
|
|
||||||
.filter(group => !!group && group.properties.iconid !== 0)
|
|
||||||
.map(group => { return { iconId: group.properties.iconid, groupName: group.name, groupId: group.id, serverUniqueId: uniqueServerId }; });
|
|
||||||
|
|
||||||
const clientIcon = client.properties.client_icon_id === 0 ? [] : [client.properties.client_icon_id];
|
|
||||||
return {
|
|
||||||
serverGroupIcons: serverGroupIcons,
|
|
||||||
channelGroupIcon: channelGroupIcon[0],
|
|
||||||
clientIcon: clientIcon.length > 0 ? { iconId: clientIcon[0], serverUniqueId: uniqueServerId } : undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendClientIcons(client: ClientEntry) {
|
public sendClientIcons(client: ClientEntry) {
|
||||||
this.events.fire_react("notify_client_icons", {
|
this.events.fire_react("notify_client_icons", {
|
||||||
icons: this.generateClientIcons(client),
|
icons: generateClientIcons(client),
|
||||||
treeEntryId: client.uniqueEntryId
|
treeEntryId: client.uniqueEntryId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateClientTalkStatus(client: ClientEntry) : { status: ClientTalkIconState, requestMessage?: string } {
|
|
||||||
let status: ClientTalkIconState = "unset";
|
|
||||||
|
|
||||||
if(client.properties.client_is_talker) {
|
|
||||||
status = "granted";
|
|
||||||
} else if(client.properties.client_talk_power < client.currentChannel().properties.channel_needed_talk_power) {
|
|
||||||
status = "prohibited";
|
|
||||||
|
|
||||||
if(client.properties.client_talk_request !== 0) {
|
|
||||||
status = "requested";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
requestMessage: client.properties.client_talk_request_msg,
|
|
||||||
status: status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendClientTalkStatus(client: ClientEntry) {
|
public sendClientTalkStatus(client: ClientEntry) {
|
||||||
const status = this.generateClientTalkStatus(client);
|
const status = generateClientTalkStatus(client);
|
||||||
this.events.fire_react("notify_client_talk_status", { treeEntryId: client.uniqueEntryId, requestMessage: status.requestMessage, status: status.status });
|
this.events.fire_react("notify_client_talk_status", { treeEntryId: client.uniqueEntryId, requestMessage: status.requestMessage, status: status.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateServerStatus(serverEntry: ServerEntry) : ServerState {
|
|
||||||
switch (this.channelTree.client.connection_state) {
|
|
||||||
case ConnectionState.AUTHENTICATING:
|
|
||||||
case ConnectionState.CONNECTING:
|
|
||||||
case ConnectionState.INITIALISING:
|
|
||||||
return {
|
|
||||||
state: "connecting",
|
|
||||||
targetAddress: serverEntry.remote_address.host + (serverEntry.remote_address.port === 9987 ? "" : `:${serverEntry.remote_address.port}`)
|
|
||||||
};
|
|
||||||
|
|
||||||
case ConnectionState.DISCONNECTING:
|
|
||||||
case ConnectionState.UNCONNECTED:
|
|
||||||
return { state: "disconnected" };
|
|
||||||
|
|
||||||
case ConnectionState.CONNECTED:
|
|
||||||
return {
|
|
||||||
state: "connected",
|
|
||||||
name: serverEntry.properties.virtualserver_name,
|
|
||||||
icon: { iconId: serverEntry.properties.virtualserver_icon_id, serverUniqueId: serverEntry.properties.virtualserver_unique_identifier }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendServerStatus(serverEntry: ServerEntry) {
|
public sendServerStatus(serverEntry: ServerEntry) {
|
||||||
this.events.fire_react("notify_server_state", { treeEntryId: serverEntry.uniqueEntryId, state: this.generateServerStatus(serverEntry) });
|
this.events.fire_react("notify_server_state", { treeEntryId: serverEntry.uniqueEntryId, state: generateServerStatus(serverEntry) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,6 +679,7 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
||||||
events.on("notify_destroy", channelTree.client.events().on("notify_visibility_changed", event => events.fire("notify_visibility_changed", event)));
|
events.on("notify_destroy", channelTree.client.events().on("notify_visibility_changed", event => events.fire("notify_visibility_changed", event)));
|
||||||
|
|
||||||
events.on("query_tree_entries", event => controller.sendChannelTreeEntriesFull(event.fullInfo ? undefined : []));
|
events.on("query_tree_entries", event => controller.sendChannelTreeEntriesFull(event.fullInfo ? undefined : []));
|
||||||
|
events.on("query_selected_entry", () => controller.sendSelectedEntry());
|
||||||
events.on("query_channel_info", event => {
|
events.on("query_channel_info", event => {
|
||||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||||
if(!entry || !(entry instanceof ChannelEntry)) {
|
if(!entry || !(entry instanceof ChannelEntry)) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue