Added a connection status indicator and broke Firefox audio playback (not sure why).
parent
10ed00dd19
commit
ae83459c30
|
@ -31,15 +31,7 @@
|
|||
<div class="container-seperator horizontal" seperator-id="seperator-main-log"></div>
|
||||
<div class="container-bottom">
|
||||
<div class="container-server-log" id="server-log"></div>
|
||||
<div class="container-footer">
|
||||
<span>
|
||||
<a>{{tr "Version:" /}} {{>app_version}}</a>
|
||||
<div class="hide-small">
|
||||
(Open source on
|
||||
<a target="_blank" href="https://github.com/TeaSpeak/TeaSpeak-Web"
|
||||
style="display: inline-block; position: relative">github.com</a>)
|
||||
</div>
|
||||
</span>
|
||||
<div class="container-footer" id="container-footer">
|
||||
</div>
|
||||
</div> <!-- Selection info -->
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,9 @@ 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";
|
||||
|
||||
export let server_connections: ConnectionManager;
|
||||
|
||||
|
@ -31,6 +34,7 @@ export class ConnectionManager {
|
|||
private _container_hostbanner: JQuery;
|
||||
private _container_chat: JQuery;
|
||||
private containerChannelVideo: ReplaceableContainer;
|
||||
private containerFooter: HTMLDivElement;
|
||||
|
||||
constructor() {
|
||||
this.event_registry = new Registry<ConnectionManagerEvents>();
|
||||
|
@ -41,10 +45,15 @@ export class ConnectionManager {
|
|||
this._container_channel_tree = $("#channelTree");
|
||||
this._container_hostbanner = $("#hostbanner");
|
||||
this._container_chat = $("#chat");
|
||||
this.containerFooter = document.getElementById("container-footer") as HTMLDivElement;
|
||||
|
||||
this.set_active_connection(undefined);
|
||||
}
|
||||
|
||||
initializeFooter() {
|
||||
ReactDOM.render(React.createElement(FooterRenderer), this.containerFooter);
|
||||
}
|
||||
|
||||
events() : Registry<ConnectionManagerEvents> {
|
||||
return this.event_registry;
|
||||
}
|
||||
|
@ -169,6 +178,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
name: "server manager init",
|
||||
function: async () => {
|
||||
server_connections = new ConnectionManager();
|
||||
server_connections.initializeFooter();
|
||||
},
|
||||
priority: 80
|
||||
});
|
||||
|
|
|
@ -28,6 +28,8 @@ export interface ServerConnectionEvents {
|
|||
}
|
||||
|
||||
export type ConnectionStateListener = (old_state: ConnectionState, new_state: ConnectionState) => any;
|
||||
export type ConnectionStatistics = { bytesReceived: number, bytesSend: number };
|
||||
|
||||
export abstract class AbstractServerConnection {
|
||||
readonly events: Registry<ServerConnectionEvents>;
|
||||
|
||||
|
@ -58,6 +60,7 @@ export abstract class AbstractServerConnection {
|
|||
connectionProxyAddress() : ServerAddress | undefined { return undefined; };
|
||||
|
||||
abstract handshake_handler() : HandshakeHandler; /* only valid when connected */
|
||||
abstract getControlStatistics() : ConnectionStatistics;
|
||||
|
||||
//FIXME: Remove this this is currently only some kind of hack
|
||||
updateConnectionState(state: ConnectionState) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import {VideoSource} from "tc-shared/video/VideoSource";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ConnectionStatus} from "tc-shared/ui/frames/footer/StatusDefinitions";
|
||||
import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
||||
|
||||
export type VideoBroadcastType = "camera" | "screen";
|
||||
|
||||
|
@ -43,6 +45,7 @@ export interface VideoConnection {
|
|||
getEvents() : Registry<VideoConnectionEvent>;
|
||||
|
||||
getStatus() : VideoConnectionStatus;
|
||||
getConnectionStats() : Promise<ConnectionStatistics>;
|
||||
|
||||
isBroadcasting(type: VideoBroadcastType);
|
||||
getBroadcastingSource(type: VideoBroadcastType) : VideoSource | undefined;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {RecorderProfile} from "../voice/RecorderProfile";
|
||||
import {AbstractServerConnection} from "../connection/ConnectionBase";
|
||||
import {AbstractServerConnection, ConnectionStatistics} from "../connection/ConnectionBase";
|
||||
import {Registry} from "../events";
|
||||
import {VoiceClient} from "../voice/VoiceClient";
|
||||
import {WhisperSession, WhisperTarget} from "../voice/VoiceWhisper";
|
||||
|
@ -62,6 +62,7 @@ export abstract class AbstractVoiceConnection {
|
|||
|
||||
abstract getConnectionState() : VoiceConnectionStatus;
|
||||
abstract getFailedMessage() : string;
|
||||
abstract getConnectionStats() : Promise<ConnectionStatistics>;
|
||||
|
||||
abstract encodingSupported(codec: number) : boolean;
|
||||
abstract decodingSupported(codec: number) : boolean;
|
||||
|
|
|
@ -159,6 +159,27 @@ export namespace network {
|
|||
export const GB = 1024 * MB;
|
||||
export const TB = 1024 * GB;
|
||||
|
||||
export function byteSizeToString(value: number) {
|
||||
let v: number, unit;
|
||||
if(value > 5 * TB) {
|
||||
unit = "tb";
|
||||
v = value / TB;
|
||||
} else if(value > 5 * GB) {
|
||||
unit = "gb";
|
||||
v = value / GB;
|
||||
} else if(value > 5 * MB) {
|
||||
unit = "mb";
|
||||
v = value / MB;
|
||||
} else if(value > 5 * KB) {
|
||||
unit = "kb";
|
||||
v = value / KB;
|
||||
} else {
|
||||
return value + "b";
|
||||
}
|
||||
|
||||
return v.toFixed(2) + unit;
|
||||
}
|
||||
|
||||
export function format_bytes(value: number, options?: {
|
||||
time?: string,
|
||||
unit?: string,
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
@include user-select(none);
|
||||
width: 100%;
|
||||
|
||||
.version {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.source {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.rtcStatus {
|
||||
margin-left: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
|
||||
.status {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
min-width: 7.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
flex-grow: 1;
|
||||
margin-left: .5em;
|
||||
|
||||
&.healthy {
|
||||
color: #247524;
|
||||
}
|
||||
|
||||
&.unhealthy {
|
||||
color: #a63030;
|
||||
}
|
||||
|
||||
&.connecting {
|
||||
color: #406d96;
|
||||
}
|
||||
|
||||
&.disconnected {}
|
||||
}
|
||||
|
||||
.rtcStatusDetail {
|
||||
position: absolute;
|
||||
|
||||
bottom: 100%;
|
||||
right: -.4em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 14em;
|
||||
padding-bottom: .5em;
|
||||
|
||||
border-radius: .1em;
|
||||
|
||||
background: #19191b;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.24);
|
||||
|
||||
color: #999;
|
||||
text-align: center;
|
||||
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
|
||||
@include transition(all $button_hover_animation_time ease-in-out);
|
||||
|
||||
&.shown {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: .25em;
|
||||
background-color: #222224;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
|
||||
height: 1.4em;
|
||||
|
||||
.key { }
|
||||
.value { }
|
||||
|
||||
.text {
|
||||
font-size: .8em;
|
||||
text-align: left;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
&.title {
|
||||
margin-top: .5em;
|
||||
|
||||
.key {
|
||||
color: #557edc;
|
||||
}
|
||||
}
|
||||
|
||||
&.doubleSize {
|
||||
height: 2.8em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import * as React from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {StatusController} from "tc-shared/ui/frames/footer/StatusController";
|
||||
import {ConnectionStatusEvents} from "tc-shared/ui/frames/footer/StatusDefinitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {StatusDetailRenderer, StatusEvents, StatusTextRenderer} from "tc-shared/ui/frames/footer/StatusRenderer";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
const VersionsRenderer = () => (
|
||||
<React.Fragment>
|
||||
<a className={cssStyle.version} key={"version"}>
|
||||
<Translatable>Version:</Translatable> {__build.version}
|
||||
</a>
|
||||
<div className={cssStyle.source} key={"link"}>
|
||||
<VariadicTranslatable text={"(Open source on {})"}>
|
||||
<a target="_blank" href="https://github.com/TeaSpeak/TeaSpeak-Web">github.com</a>
|
||||
</VariadicTranslatable>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const RtcStatus = () => {
|
||||
const statusController = useMemo(() => new StatusController(new Registry<ConnectionStatusEvents>()), []);
|
||||
statusController.setConnectionHandler(server_connections.active_connection());
|
||||
|
||||
server_connections.events().reactUse("notify_active_handler_changed", event => {
|
||||
statusController.setConnectionHandler(event.newHandler);
|
||||
}, undefined, []);
|
||||
|
||||
return (
|
||||
<StatusEvents.Provider value={statusController.getEvents()}>
|
||||
<StatusTextRenderer />
|
||||
<StatusDetailRenderer />
|
||||
</StatusEvents.Provider>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
export const FooterRenderer = () => {
|
||||
return (
|
||||
<div className={cssStyle.container}>
|
||||
<VersionsRenderer />
|
||||
<RtcStatus />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,223 @@
|
|||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {
|
||||
ConnectionComponent,
|
||||
ConnectionStatus,
|
||||
ConnectionStatusEvents
|
||||
} from "./StatusDefinitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {VoiceConnectionStatus} from "tc-shared/connection/VoiceConnection";
|
||||
import {VideoConnectionStatus} from "tc-shared/connection/VideoConnection";
|
||||
|
||||
export class StatusController {
|
||||
private readonly events: Registry<ConnectionStatusEvents>;
|
||||
|
||||
private currentConnectionHandler: ConnectionHandler;
|
||||
private listenerHandler: (() => void)[];
|
||||
|
||||
private detailedInfoOpen = false;
|
||||
private detailUpdateTimer: number;
|
||||
|
||||
constructor(events: Registry<ConnectionStatusEvents>) {
|
||||
this.events = events;
|
||||
|
||||
this.events.on("query_component_detail_state", () => this.events.fire_react("notify_component_detail_state", { shown: this.detailedInfoOpen }));
|
||||
this.events.on("query_component_status", event => this.notifyComponentStatus(event.component));
|
||||
this.events.on("query_connection_status", () => this.notifyConnectionStatus());
|
||||
|
||||
this.events.on("action_toggle_component_detail", event => this.setDetailsShown(typeof event.shown === "boolean" ? event.shown : !this.detailedInfoOpen));
|
||||
}
|
||||
|
||||
getEvents() : Registry<ConnectionStatusEvents> {
|
||||
return this.events;
|
||||
}
|
||||
|
||||
setConnectionHandler(handler: ConnectionHandler | undefined) {
|
||||
if(this.currentConnectionHandler === handler) { return; }
|
||||
|
||||
this.unregisterHandlerEvents();
|
||||
this.currentConnectionHandler = handler;
|
||||
this.registerHandlerEvents(handler);
|
||||
|
||||
this.notifyConnectionStatus();
|
||||
}
|
||||
|
||||
setDetailsShown(flag: boolean) {
|
||||
if(this.detailedInfoOpen === flag) { return; }
|
||||
|
||||
this.detailedInfoOpen = flag;
|
||||
this.events.fire_react("notify_component_detail_state", { shown: this.detailedInfoOpen });
|
||||
|
||||
if(this.detailedInfoOpen) {
|
||||
this.detailUpdateTimer = setInterval(() => {
|
||||
this.notifyComponentStatus("video");
|
||||
this.notifyComponentStatus("signaling");
|
||||
this.notifyComponentStatus("voice");
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(this.detailUpdateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
private registerHandlerEvents(handler: ConnectionHandler) {
|
||||
this.unregisterHandlerEvents();
|
||||
|
||||
const events = this.listenerHandler = [];
|
||||
events.push(handler.events().on("notify_connection_state_changed", () => {
|
||||
this.notifyConnectionStatus();
|
||||
if(this.detailedInfoOpen) {
|
||||
this.notifyComponentStatus("signaling");
|
||||
}
|
||||
}));
|
||||
|
||||
events.push(handler.getServerConnection().getVoiceConnection().events.on("notify_connection_status_changed", () => {
|
||||
this.notifyConnectionStatus();
|
||||
if(this.detailedInfoOpen) {
|
||||
this.notifyComponentStatus("voice");
|
||||
}
|
||||
}));
|
||||
|
||||
events.push(handler.getServerConnection().getVideoConnection().getEvents().on("notify_status_changed", () => {
|
||||
this.notifyConnectionStatus();
|
||||
if(this.detailedInfoOpen) {
|
||||
this.notifyComponentStatus("video");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private unregisterHandlerEvents() {
|
||||
this.listenerHandler?.forEach(callback => callback());
|
||||
}
|
||||
|
||||
private async getComponentStatus(component: ConnectionComponent, detailed: boolean) : Promise<ConnectionStatus> {
|
||||
if(!this.currentConnectionHandler) {
|
||||
return { type: "disconnected" };
|
||||
}
|
||||
switch (component) {
|
||||
case "video":
|
||||
const videoConnection = this.currentConnectionHandler.getServerConnection().getVideoConnection();
|
||||
switch (videoConnection.getStatus()) {
|
||||
case VideoConnectionStatus.Failed:
|
||||
/* FIXME: Reason! */
|
||||
return { type: "unhealthy", reason: tr("Unknown") };
|
||||
case VideoConnectionStatus.Connected:
|
||||
if(detailed) {
|
||||
const statistics = await videoConnection.getConnectionStats();
|
||||
return {
|
||||
type: "healthy",
|
||||
bytesSend: statistics.bytesSend,
|
||||
bytesReceived: statistics.bytesReceived
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "healthy"
|
||||
};
|
||||
}
|
||||
|
||||
case VideoConnectionStatus.Disconnected:
|
||||
return {
|
||||
type: "disconnected"
|
||||
};
|
||||
|
||||
case VideoConnectionStatus.Connecting:
|
||||
return { type: "connecting-video" };
|
||||
|
||||
case VideoConnectionStatus.Unsupported:
|
||||
return { type: "unsupported", side: "server" };
|
||||
}
|
||||
break;
|
||||
|
||||
case "voice":
|
||||
const voiceConnection = this.currentConnectionHandler.getServerConnection().getVoiceConnection();
|
||||
switch (voiceConnection.getConnectionState()) {
|
||||
case VoiceConnectionStatus.Failed:
|
||||
return { type: "unhealthy", reason: voiceConnection.getFailedMessage() };
|
||||
|
||||
case VoiceConnectionStatus.Connected:
|
||||
if(detailed) {
|
||||
const statistics = await voiceConnection.getConnectionStats();
|
||||
return {
|
||||
type: "healthy",
|
||||
bytesSend: statistics.bytesSend,
|
||||
bytesReceived: statistics.bytesReceived
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "healthy"
|
||||
};
|
||||
}
|
||||
|
||||
case VoiceConnectionStatus.Disconnecting:
|
||||
case VoiceConnectionStatus.Disconnected:
|
||||
return { type: "disconnected" };
|
||||
|
||||
case VoiceConnectionStatus.Connecting:
|
||||
return { type: "connecting-voice" };
|
||||
|
||||
case VoiceConnectionStatus.ServerUnsupported:
|
||||
return { type: "unsupported", side: "server" };
|
||||
|
||||
case VoiceConnectionStatus.ClientUnsupported:
|
||||
return { type: "unsupported", side: "client" };
|
||||
}
|
||||
break;
|
||||
|
||||
case "signaling":
|
||||
switch (this.currentConnectionHandler.connection_state) {
|
||||
case ConnectionState.INITIALISING:
|
||||
return { type: "connecting-signalling", state: "initializing" };
|
||||
case ConnectionState.CONNECTING:
|
||||
return { type: "connecting-signalling", state: "connecting" };
|
||||
case ConnectionState.AUTHENTICATING:
|
||||
return { type: "connecting-signalling", state: "authentication" };
|
||||
case ConnectionState.CONNECTED:
|
||||
if(detailed) {
|
||||
const statistics = this.currentConnectionHandler.getServerConnection().getControlStatistics();
|
||||
return {
|
||||
type: "healthy",
|
||||
bytesSend: statistics.bytesSend,
|
||||
bytesReceived: statistics.bytesReceived
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "healthy"
|
||||
};
|
||||
}
|
||||
case ConnectionState.UNCONNECTED:
|
||||
case ConnectionState.DISCONNECTING:
|
||||
return { type: "disconnected" };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async notifyComponentStatus(component: ConnectionComponent) {
|
||||
this.events.fire_react("notify_component_status", { component: component, status: await this.getComponentStatus(component, true) });
|
||||
}
|
||||
|
||||
async notifyConnectionStatus() {
|
||||
let status: ConnectionStatus = { type: "healthy" };
|
||||
|
||||
for(const component of ["signaling", "voice", "video"] as ConnectionComponent[]) {
|
||||
let componentState = await this.getComponentStatus(component, false);
|
||||
if(componentState.type === "healthy" || componentState.type === "unsupported") {
|
||||
continue;
|
||||
} else if(componentState.type === "disconnected" && component !== "signaling") {
|
||||
switch (component) {
|
||||
case "voice":
|
||||
componentState = { type: "unhealthy", reason: tr("No voice connection") };
|
||||
break;
|
||||
|
||||
case "video":
|
||||
componentState = { type: "unhealthy", reason: tr("No video connection") };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
status = componentState;
|
||||
break;
|
||||
}
|
||||
|
||||
this.events.fire_react("notify_connection_status", { status: status });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
export type ConnectionStatus = {
|
||||
type: "healthy",
|
||||
/* only for component stati */
|
||||
bytesReceived?: number,
|
||||
bytesSend?: number
|
||||
} | {
|
||||
type: "unhealthy",
|
||||
reason: string,
|
||||
/* try reconnect attribute */
|
||||
} | {
|
||||
type: "connecting-signalling",
|
||||
state: "initializing" | "connecting" | "authentication"
|
||||
} | {
|
||||
type: "connecting-voice"
|
||||
} | {
|
||||
type: "connecting-video"
|
||||
} | {
|
||||
type: "disconnected"
|
||||
} | {
|
||||
type: "unsupported",
|
||||
side: "server" | "client"
|
||||
}
|
||||
|
||||
export type ConnectionComponent = "signaling" | "video" | "voice";
|
||||
|
||||
export interface ConnectionStatusEvents {
|
||||
action_toggle_component_detail: { shown: boolean | undefined },
|
||||
|
||||
query_component_detail_state: {},
|
||||
query_component_status: { component: ConnectionComponent },
|
||||
query_connection_status: {},
|
||||
|
||||
notify_component_detail_state: {
|
||||
shown: boolean
|
||||
},
|
||||
notify_component_status: {
|
||||
component: ConnectionComponent,
|
||||
status: ConnectionStatus
|
||||
},
|
||||
notify_connection_status: {
|
||||
status: ConnectionStatus,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {
|
||||
ConnectionComponent,
|
||||
ConnectionStatus,
|
||||
ConnectionStatusEvents
|
||||
} from "tc-shared/ui/frames/footer/StatusDefinitions";
|
||||
import * as React from "react";
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {network} from "tc-shared/ui/frames/chat";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
export const StatusEvents = React.createContext<Registry<ConnectionStatusEvents>>(undefined);
|
||||
|
||||
const ConnectionStateRenderer = React.memo((props: { state: ConnectionStatus, isGeneral: boolean, onClick?: () => void }) => {
|
||||
let statusClass;
|
||||
let statusBody;
|
||||
let title;
|
||||
switch (props.state.type) {
|
||||
case "disconnected":
|
||||
title = tr("Not connected");
|
||||
statusClass = cssStyle.disconnected;
|
||||
statusBody = <Translatable key={"disconnected"}>Disconnected</Translatable>;
|
||||
break;
|
||||
|
||||
case "connecting-signalling":
|
||||
statusClass = cssStyle.connecting;
|
||||
|
||||
switch (props.state.state) {
|
||||
case "initializing":
|
||||
title = tr("Initializing connection");
|
||||
statusBody = <Translatable key={"connecting-signalling-initializing"}>Initializing</Translatable>;
|
||||
break;
|
||||
|
||||
case "connecting":
|
||||
title = tr("Connecting to target");
|
||||
statusBody = <Translatable key={"connecting-signalling-connecting"}>Connecting</Translatable>;
|
||||
break;
|
||||
|
||||
case "authentication":
|
||||
title = tr("Authenticating");
|
||||
statusBody = <Translatable key={"connecting-signalling-authenticating"}>Authenticating</Translatable>;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "connecting-voice":
|
||||
title = tr("Establishing audio connection");
|
||||
statusClass = cssStyle.connecting;
|
||||
if(props.isGeneral) {
|
||||
statusBody = <Translatable key={"connecting-voice-general"}>Audio connecting</Translatable>;
|
||||
} else {
|
||||
statusBody = <Translatable key={"connecting-voice-general"}>Connecting</Translatable>;
|
||||
}
|
||||
break;
|
||||
|
||||
case "connecting-video":
|
||||
title = tr("Establishing video connection");
|
||||
statusClass = cssStyle.connecting;
|
||||
if(props.isGeneral) {
|
||||
statusBody = <Translatable key={"connecting-video-general"}>Video connecting</Translatable>;
|
||||
} else {
|
||||
statusBody = <Translatable key={"connecting-video-general"}>Connecting</Translatable>;
|
||||
}
|
||||
break;
|
||||
|
||||
case "unhealthy":
|
||||
title = props.state.reason;
|
||||
statusClass = cssStyle.unhealthy;
|
||||
statusBody = <Translatable key={"unhealthy"}>Unhealthy</Translatable>;
|
||||
break;
|
||||
|
||||
case "healthy":
|
||||
title = tr("Connection is healthy");
|
||||
statusClass = cssStyle.healthy;
|
||||
statusBody = <Translatable key={"healthy"}>Healthy</Translatable>;
|
||||
break;
|
||||
|
||||
default:
|
||||
statusClass = cssStyle.unhealthy;
|
||||
statusBody = <Translatable key={"invalid-state"}>Invalid state</Translatable>;
|
||||
break;
|
||||
}
|
||||
|
||||
return <div className={cssStyle.status + " " + statusClass} title={title} onClick={props.onClick}>{statusBody}</div>;
|
||||
})
|
||||
|
||||
export const StatusTextRenderer = React.memo(() => {
|
||||
const events = useContext(StatusEvents);
|
||||
|
||||
const [ status, setStatus ] = useState<ConnectionStatus>(() => {
|
||||
events.fire("query_connection_status");
|
||||
return { type: "disconnected" };
|
||||
});
|
||||
events.reactUse("notify_connection_status", event => setStatus(event.status), undefined, []);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.rtcStatus}>
|
||||
<div><Translatable>Connection status:</Translatable></div>
|
||||
<ConnectionStateRenderer
|
||||
state={status}
|
||||
isGeneral={true}
|
||||
onClick={() => events.fire("action_toggle_component_detail")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ComponentStatusRenderer = React.memo((props: { component: ConnectionComponent }) => {
|
||||
const events = useContext(StatusEvents);
|
||||
const [ status, setStatus ] = useState<ConnectionStatus>(() => {
|
||||
events.fire("query_component_status", { component: props.component });
|
||||
return { type: "disconnected" };
|
||||
});
|
||||
events.reactUse("notify_component_status", event => event.component === props.component && setStatus(event.status),
|
||||
undefined, []);
|
||||
|
||||
let title;
|
||||
switch (props.component) {
|
||||
case "signaling":
|
||||
title = <Translatable key={"control"}>Control</Translatable>;
|
||||
break;
|
||||
case "video":
|
||||
title = <Translatable key={"video"}>Video</Translatable>;
|
||||
break;
|
||||
case "voice":
|
||||
title = <Translatable key={"voice"}>Voice</Translatable>;
|
||||
break;
|
||||
}
|
||||
|
||||
let body;
|
||||
switch (status.type) {
|
||||
case "healthy":
|
||||
body = (
|
||||
<React.Fragment key={"healthy"}>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}><Translatable>Incoming:</Translatable></div>
|
||||
<div className={cssStyle.value}>{network.byteSizeToString(status.bytesReceived)}</div>
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}><Translatable>Outgoing:</Translatable></div>
|
||||
<div className={cssStyle.value}>{network.byteSizeToString(status.bytesSend)}</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
break;
|
||||
|
||||
case "connecting-signalling":
|
||||
case "connecting-voice":
|
||||
case "connecting-video":
|
||||
case "disconnected":
|
||||
case "unsupported":
|
||||
let text;
|
||||
switch (props.component) {
|
||||
case "signaling":
|
||||
text = <Translatable key={"signalling"}>The control component is the main server connection. All actions are transceived by this connection.</Translatable>;
|
||||
break;
|
||||
case "video":
|
||||
text = <Translatable key={"video"}>The video component transmits and receives video data. It's used to transmit camara and screen data.</Translatable>;
|
||||
break;
|
||||
case "voice":
|
||||
text = <Translatable key={"voice"}>The voice component transmits and receives audio data.</Translatable>;
|
||||
break;
|
||||
}
|
||||
body = (
|
||||
<div className={cssStyle.row + " " + cssStyle.doubleSize} key={"description"}>
|
||||
<div className={cssStyle.text}>{text}</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "unhealthy":
|
||||
body = (
|
||||
<div className={cssStyle.row + " " + cssStyle.doubleSize} key={"error"}>
|
||||
<div className={cssStyle.text}><Translatable>Some error occured</Translatable></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (<>
|
||||
<div className={cssStyle.row + " " + cssStyle.title}>
|
||||
<div className={cssStyle.key}>{title}</div>
|
||||
<div className={cssStyle.value}>
|
||||
<ConnectionStateRenderer state={status} isGeneral={false} />
|
||||
</div>
|
||||
</div>
|
||||
{body}
|
||||
</>);
|
||||
})
|
||||
|
||||
export const StatusDetailRenderer = () => {
|
||||
const events = useContext(StatusEvents);
|
||||
|
||||
const refContainer = useRef<HTMLDivElement>();
|
||||
const refShowId = useRef(0);
|
||||
|
||||
const [ shown, setShown ] = useState(() => {
|
||||
events.fire("query_component_detail_state");
|
||||
return false;
|
||||
});
|
||||
events.reactUse("notify_component_detail_state", event => {
|
||||
if(event.shown) {
|
||||
refShowId.current++;
|
||||
}
|
||||
setShown(event.shown);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if(!shown) { return; }
|
||||
|
||||
const listener = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLDivElement;
|
||||
if(!refContainer.current?.contains(target)) {
|
||||
events.fire("action_toggle_component_detail", { shown: false });
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", listener);
|
||||
return () => document.removeEventListener("click", listener);
|
||||
}, [shown]);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.rtcStatusDetail + " " + (shown ? cssStyle.shown : "")} ref={refContainer}>
|
||||
<div className={cssStyle.header}><Translatable>Connection Details</Translatable></div>
|
||||
<ComponentStatusRenderer component={"signaling"} key={"rev-0-" + refShowId.current} />
|
||||
<ComponentStatusRenderer component={"voice"} key={"rev-1-" + refShowId.current} />
|
||||
<ComponentStatusRenderer component={"video"} key={"rev-2-" + refShowId.current} />
|
||||
</div>
|
||||
)
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
AbstractServerConnection,
|
||||
CommandOptionDefaults,
|
||||
CommandOptions,
|
||||
CommandOptions, ConnectionStatistics,
|
||||
ConnectionStateListener,
|
||||
} from "tc-shared/connection/ConnectionBase";
|
||||
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
|
||||
|
@ -383,7 +383,7 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
return;
|
||||
}
|
||||
|
||||
this.socket.socket.send(data);
|
||||
this.socket.sendMessage(data);
|
||||
}
|
||||
|
||||
private static commandDataToJson(input: any) : string {
|
||||
|
@ -495,4 +495,8 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
native: this.pingStatistics.currentNativeValue
|
||||
};
|
||||
}
|
||||
|
||||
getControlStatistics(): ConnectionStatistics {
|
||||
return this.socket?.getControlStatistics() || { bytesSend: 0, bytesReceived: 0 };
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
||||
|
||||
const kPreventOpeningWebSocketClosing = false;
|
||||
|
||||
|
@ -13,9 +14,10 @@ export type WebSocketUrl = {
|
|||
};
|
||||
export class WrappedWebSocket {
|
||||
public readonly address: WebSocketUrl;
|
||||
public socket: WebSocket;
|
||||
public state: "unconnected" | "connecting" | "connected" | "errored";
|
||||
|
||||
private socket: WebSocket;
|
||||
|
||||
/* callbacks for events after the socket has successfully connected! */
|
||||
public callbackMessage: (message) => void;
|
||||
public callbackDisconnect: (code: number, reason?: string) => void;
|
||||
|
@ -24,11 +26,21 @@ export class WrappedWebSocket {
|
|||
private errorQueue = [];
|
||||
private connectResultListener = [];
|
||||
|
||||
private bytesReceived;
|
||||
private bytesSend;
|
||||
|
||||
constructor(addr: WebSocketUrl) {
|
||||
this.address = addr;
|
||||
this.state = "unconnected";
|
||||
}
|
||||
|
||||
getControlStatistics() : ConnectionStatistics {
|
||||
return {
|
||||
bytesReceived: this.bytesReceived,
|
||||
bytesSend: this.bytesSend
|
||||
};
|
||||
}
|
||||
|
||||
socketUrl() : string {
|
||||
let result = "";
|
||||
result += this.address.secure ? "wss://" : "ws://";
|
||||
|
@ -64,17 +76,24 @@ export class WrappedWebSocket {
|
|||
};
|
||||
|
||||
this.socket.onmessage = event => {
|
||||
if(this.callbackMessage)
|
||||
if(typeof event.data === "string") {
|
||||
this.bytesReceived += event.data.length;
|
||||
} else if(event.data instanceof ArrayBuffer) {
|
||||
this.bytesReceived += event.data.byteLength;
|
||||
}
|
||||
|
||||
if(this.callbackMessage) {
|
||||
this.callbackMessage(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onerror = () => {
|
||||
if(this.state === "connected") {
|
||||
this.state = "errored";
|
||||
|
||||
if(this.callbackErrored)
|
||||
if(this.callbackErrored) {
|
||||
this.callbackErrored();
|
||||
|
||||
}
|
||||
} else if(this.state === "connecting") {
|
||||
this.state = "errored";
|
||||
this.fireConnectResult();
|
||||
|
@ -88,8 +107,9 @@ export class WrappedWebSocket {
|
|||
}
|
||||
|
||||
async awaitConnectResult() {
|
||||
while (this.state === "connecting")
|
||||
while (this.state === "connecting") {
|
||||
await new Promise<void>(resolve => this.connectResultListener.push(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
closeConnection() {
|
||||
|
@ -134,6 +154,9 @@ export class WrappedWebSocket {
|
|||
this.socket = undefined;
|
||||
}
|
||||
|
||||
this.bytesReceived = 0;
|
||||
this.bytesSend = 0;
|
||||
|
||||
this.errorQueue = [];
|
||||
this.fireConnectResult();
|
||||
}
|
||||
|
@ -150,4 +173,14 @@ export class WrappedWebSocket {
|
|||
popError() {
|
||||
return this.errorQueue.pop_front();
|
||||
}
|
||||
|
||||
sendMessage(message: string | ArrayBufferLike | Blob | ArrayBufferView) {
|
||||
if(typeof message === "string") {
|
||||
this.bytesSend += message.length;
|
||||
} else if(message instanceof ArrayBuffer) {
|
||||
this.bytesSend += message.byteLength;
|
||||
}
|
||||
|
||||
this.socket.send(message);
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ let dummyAudioTrack: MediaStreamTrack | undefined;
|
|||
* So we've to keep it alive with a dummy track.
|
||||
*/
|
||||
function getIdleTrack(kind: "video" | "audio") : MediaStreamTrack | null {
|
||||
if(window.detectedBrowser?.name === "firefox" || true) {
|
||||
if(window.detectedBrowser?.name === "firefox") {
|
||||
if(kind === "video") {
|
||||
if(!dummyVideoTrack) {
|
||||
const canvas = document.createElement("canvas");
|
||||
|
@ -343,6 +343,14 @@ type TemporaryRtpStream = {
|
|||
info: TrackClientInfo | undefined
|
||||
}
|
||||
|
||||
export type RTCConnectionStatistics = {
|
||||
videoBytesReceived: number,
|
||||
videoBytesSent: number,
|
||||
|
||||
voiceBytesReceived: number,
|
||||
voiceBytesSent
|
||||
}
|
||||
|
||||
export interface RTCConnectionEvents {
|
||||
notify_state_changed: { oldState: RTPConnectionState, newState: RTPConnectionState },
|
||||
notify_audio_assignment_changed: { track: RemoteRTPAudioTrack, info: TrackClientInfo | undefined },
|
||||
|
@ -730,6 +738,11 @@ export class RTCConnection {
|
|||
break;
|
||||
|
||||
case "failed":
|
||||
if(this.connectionState !== RTPConnectionState.FAILED) {
|
||||
this.handleFatalError(tr("peer connection failed"), 5000);
|
||||
}
|
||||
break;
|
||||
|
||||
case "closed":
|
||||
case "disconnected":
|
||||
case "new":
|
||||
|
@ -851,4 +864,34 @@ export class RTCConnection {
|
|||
tempStream.status = state;
|
||||
}
|
||||
}
|
||||
|
||||
async getConnectionStatistics() : Promise<RTCConnectionStatistics> {
|
||||
try {
|
||||
if(!this.peer) {
|
||||
throw "missing peer";
|
||||
}
|
||||
|
||||
const statisticsInfo = await this.peer.getStats();
|
||||
const statistics = [...statisticsInfo.entries()].map(e => e[1]) as RTCStats[];
|
||||
const inboundStreams = statistics.filter(e => e.type.replace(/-/, "") === "inboundrtp" && 'bytesReceived' in e) as any[];
|
||||
const outboundStreams = statistics.filter(e => e.type.replace(/-/, "") === "outboundrtp" && 'bytesSent' in e) as any[];
|
||||
|
||||
return {
|
||||
voiceBytesSent: outboundStreams.filter(e => e.mediaType === "audio").reduce((a, b) => a + b.bytesSent, 0),
|
||||
voiceBytesReceived: inboundStreams.filter(e => e.mediaType === "audio").reduce((a, b) => a + b.bytesReceived, 0),
|
||||
|
||||
videoBytesSent: outboundStreams.filter(e => e.mediaType === "video").reduce((a, b) => a + b.bytesSent, 0),
|
||||
videoBytesReceived: inboundStreams.filter(e => e.mediaType === "video").reduce((a, b) => a + b.bytesReceived, 0)
|
||||
}
|
||||
} catch (error) {
|
||||
logWarn(LogCategory.WEBRTC, tr("Failed to calculate connection statistics: %o"), error);
|
||||
return {
|
||||
videoBytesReceived: 0,
|
||||
videoBytesSent: 0,
|
||||
|
||||
voiceBytesReceived: 0,
|
||||
voiceBytesSent: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
import {MediaDescription, SessionDescription} from "sdp-transform";
|
||||
import * as sdpTransform from "sdp-transform";
|
||||
|
||||
export interface RTCNegotiationMediaMapping {
|
||||
direction: "sendrecv" | "recvonly" | "sendonly" | "inactive",
|
||||
ssrc: number
|
||||
}
|
||||
|
||||
export interface RTCNegotiationIceConfig {
|
||||
username: string,
|
||||
password: string,
|
||||
fingerprint: string,
|
||||
fingerprint_type: string,
|
||||
setup: "active" | "passive" | "actpass",
|
||||
candidates: string[]
|
||||
}
|
||||
|
||||
export interface RTCNegotiationExtension {
|
||||
id: number;
|
||||
uri: string;
|
||||
|
||||
media?: "audio" | "video";
|
||||
|
||||
direction?: "recvonly" | "sendonly";
|
||||
config?: string;
|
||||
}
|
||||
|
||||
export interface RTCNegotiationCodec {
|
||||
payload: number,
|
||||
name: string,
|
||||
|
||||
channels?: number,
|
||||
rate?: number,
|
||||
|
||||
fmtp?: string,
|
||||
feedback?: string[]
|
||||
}
|
||||
|
||||
/** The offer send by the client to the server */
|
||||
export interface RTCNegotiationOffer {
|
||||
type: "initial-offer" | "negotiation-offer",
|
||||
sessionId: number,
|
||||
ssrcs: number[],
|
||||
ssrc_types: number[],
|
||||
ice: RTCNegotiationIceConfig,
|
||||
/* Only present in initial response */
|
||||
extension: RTCNegotiationExtension | undefined
|
||||
}
|
||||
|
||||
/** The offer send by the server to the client */
|
||||
export interface RTCNegotiationMessage {
|
||||
type: "initial-offer" | "negotiation-offer" | "initial-answer" | "negotiation-answer",
|
||||
sessionId: number,
|
||||
sessionUsername: string,
|
||||
|
||||
ssrc: number[],
|
||||
ssrc_flags: number[],
|
||||
|
||||
ice: RTCNegotiationIceConfig,
|
||||
|
||||
/* Only present in initial answer */
|
||||
extension: RTCNegotiationExtension[] | undefined,
|
||||
|
||||
/* Only present in initial answer */
|
||||
audio_codecs: RTCNegotiationCodec[] | undefined,
|
||||
|
||||
/* Only present in initial answer */
|
||||
video_codecs: RTCNegotiationCodec[] | undefined
|
||||
}
|
||||
|
||||
type RTCDirection = "sendonly" | "recvonly" | "sendrecv" | "inactive";
|
||||
|
||||
type SsrcFlags = {
|
||||
type: "audio",
|
||||
mode: RTCDirection
|
||||
} | {
|
||||
type: "video",
|
||||
mode: RTCDirection
|
||||
}
|
||||
|
||||
namespace SsrcFlags {
|
||||
function parseRtcDirection(direction: number) : RTCDirection {
|
||||
switch (direction) {
|
||||
case 0: return "sendrecv";
|
||||
case 1: return "sendonly";
|
||||
case 2: return "recvonly";
|
||||
case 3: return "inactive";
|
||||
default: throw tra("invalid rtc direction type {}", direction);
|
||||
}
|
||||
}
|
||||
|
||||
export function parse(flags: number) : SsrcFlags {
|
||||
switch (flags & 0x7) {
|
||||
case 0:
|
||||
return {
|
||||
type: "audio",
|
||||
mode: parseRtcDirection((flags >> 3) & 0x3)
|
||||
};
|
||||
|
||||
case 1:
|
||||
return {
|
||||
type: "video",
|
||||
mode: parseRtcDirection((flags >> 3) & 0x3)
|
||||
};
|
||||
|
||||
default:
|
||||
throw tr("invalid ssrc flags");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateSdp(session: RTCNegotiationMessage) {
|
||||
const sdp = {} as SessionDescription;
|
||||
sdp.version = 0;
|
||||
sdp.origin = {
|
||||
username: session.sessionUsername,
|
||||
sessionId: session.sessionId,
|
||||
ipVer: 4,
|
||||
netType: "IN",
|
||||
address: "127.0.0.1",
|
||||
sessionVersion: 2
|
||||
};
|
||||
sdp.name = "-";
|
||||
sdp.timing = { start: 0, stop: 0 };
|
||||
sdp.groups = [{
|
||||
type: "BUNDLE",
|
||||
mids: [...session.ssrc].map((_, idx) => idx).join(" ")
|
||||
}];
|
||||
|
||||
sdp.media = [];
|
||||
const generateMedia = (ssrc: number, flags: SsrcFlags) => {
|
||||
let formats: RTCNegotiationCodec[];
|
||||
if(flags.type === "audio") {
|
||||
formats = session.audio_codecs;
|
||||
} else if(flags.type === "video") {
|
||||
formats = session.video_codecs;
|
||||
}
|
||||
const index = sdp.media.push({
|
||||
type: flags.type,
|
||||
port: 9,
|
||||
protocol: "UDP/TLS/RTP/SAVPF",
|
||||
payloads: formats.map(e => e.payload).join(" "),
|
||||
|
||||
fmtp: [],
|
||||
rtp: [],
|
||||
rtcpFb: [],
|
||||
ext: [],
|
||||
ssrcs: [],
|
||||
invalid: []
|
||||
});
|
||||
|
||||
const tMedia = sdp.media[index - 1];
|
||||
|
||||
/* basic properties */
|
||||
tMedia.mid = (index - 1).toString();
|
||||
tMedia.direction = flags.mode;
|
||||
tMedia.rtcpMux = "rtcp-mux";
|
||||
|
||||
/* ice */
|
||||
tMedia.iceUfrag = session.ice.username;
|
||||
tMedia.icePwd = session.ice.password;
|
||||
tMedia.invalid.push({ value: "ice-options:trickle" });
|
||||
tMedia.fingerprint = {
|
||||
hash: session.ice.fingerprint,
|
||||
type: session.ice.fingerprint_type,
|
||||
};
|
||||
tMedia.setup = session.ice.setup;
|
||||
|
||||
/* codecs */
|
||||
for(const codec of formats) {
|
||||
tMedia.rtp.push({
|
||||
codec: codec.name,
|
||||
payload: codec.payload,
|
||||
rate: codec.rate,
|
||||
encoding: codec.channels
|
||||
});
|
||||
|
||||
for(const feedback of codec.feedback || []) {
|
||||
tMedia.rtcpFb.push({
|
||||
payload: codec.payload,
|
||||
type: feedback
|
||||
});
|
||||
}
|
||||
|
||||
if(codec.fmtp) {
|
||||
tMedia.fmtp.push({
|
||||
payload: codec.payload,
|
||||
config: codec.fmtp
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* extensions */
|
||||
for(const extension of session.extension || []) {
|
||||
if(extension.media && extension.media !== tMedia.type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tMedia.ext.push({
|
||||
value: extension.id,
|
||||
config: extension.config,
|
||||
direction: extension.direction,
|
||||
uri: extension.uri
|
||||
});
|
||||
}
|
||||
|
||||
/* ssrc */
|
||||
tMedia.ssrcs.push({
|
||||
id: ssrc >>> 0,
|
||||
attribute: "cname",
|
||||
value: (ssrc >>> 0).toString(16)
|
||||
});
|
||||
};
|
||||
|
||||
for(let index = 0; index < session.ssrc.length; index++) {
|
||||
const flags = SsrcFlags.parse(session.ssrc_flags[index]);
|
||||
generateMedia(session.ssrc[index], flags);
|
||||
}
|
||||
|
||||
console.error(JSON.stringify(session));
|
||||
return sdpTransform.write(sdp);
|
||||
}
|
||||
|
||||
export class RTCNegotiator {
|
||||
private readonly peer: RTCPeerConnection;
|
||||
|
||||
public callbackData: (data: string) => void;
|
||||
public callbackFailed: (reason: string) => void;
|
||||
|
||||
private sessionCodecs: RTCNegotiationCodec | undefined;
|
||||
private sessionExtensions: RTCNegotiationExtension | undefined;
|
||||
|
||||
constructor(peer: RTCPeerConnection) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
doInitialNegotiation() {
|
||||
|
||||
}
|
||||
|
||||
handleRemoteData(dataString: string) {
|
||||
|
||||
}
|
||||
}
|
||||
/* FIXME: mozilla...THIS_IS_SDPARTA-82.0.3 (Needs to be parsed from the offer) */
|
||||
/*
|
||||
console.error("XYX\n%s",
|
||||
generateSdp({
|
||||
sessionId: "1234",
|
||||
audio_media: [
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
},
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
},
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
},
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
}
|
||||
],
|
||||
video_media: [
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
},
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
},
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
},
|
||||
{
|
||||
direction: "sendrecv",
|
||||
ssrc: 123885,
|
||||
}
|
||||
],
|
||||
audio_codecs: [{
|
||||
payload: 88,
|
||||
channels: 2,
|
||||
feedback: ["nack"],
|
||||
fmtp: "minptime=10;useinbandfec=1",
|
||||
name: "opus",
|
||||
rate: 48000
|
||||
}],
|
||||
video_codecs: [{
|
||||
payload: 89,
|
||||
name: "VP8",
|
||||
feedback: ["nack", "nack pli"]
|
||||
}],
|
||||
extension: [{
|
||||
id: 1,
|
||||
uri: "urn:ietf:params:rtp-hdrext:ssrc-audio-level"
|
||||
}],
|
||||
ice: {
|
||||
candidates: [],
|
||||
fingerprint: "E6:C3:F3:17:71:11:4B:E5:1A:DD:EC:3C:AA:F2:BB:48:08:3B:A5:69:18:44:4A:97:59:62:BF:B4:43:F1:5D:00",
|
||||
fingerprint_type: "sha-256",
|
||||
password: "passwd",
|
||||
username: "uname",
|
||||
setup: "actpass"
|
||||
}
|
||||
}, "-")
|
||||
);
|
||||
throw "dummy load error";
|
||||
*/
|
|
@ -14,6 +14,7 @@ import {Settings, settings} from "tc-shared/settings";
|
|||
import {RtpVideoClient} from "tc-backend/web/rtc/video/VideoClient";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import {ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
||||
|
||||
type VideoBroadcast = {
|
||||
readonly source: VideoSource;
|
||||
|
@ -249,4 +250,13 @@ export class RtpVideoConnection implements VideoConnection {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getConnectionStats(): Promise<ConnectionStatistics> {
|
||||
const stats = await this.rtcConnection.getConnectionStatistics();
|
||||
|
||||
return {
|
||||
bytesReceived: stats.videoBytesReceived,
|
||||
bytesSend: stats.videoBytesSent
|
||||
};
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
|||
import {VoiceClient} from "tc-shared/voice/VoiceClient";
|
||||
import {WhisperSession, WhisperSessionState, WhisperTarget} from "tc-shared/voice/VoiceWhisper";
|
||||
import {RTCConnection, RTCConnectionEvents, RTPConnectionState} from "tc-backend/web/rtc/Connection";
|
||||
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
|
||||
import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
|
||||
import {VoicePlayerState} from "tc-shared/voice/VoicePlayer";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
|
||||
|
@ -361,4 +361,13 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getConnectionStats(): Promise<ConnectionStatistics> {
|
||||
const stats = await this.rtcConnection.getConnectionStatistics();
|
||||
|
||||
return {
|
||||
bytesReceived: stats.voiceBytesReceived,
|
||||
bytesSend: stats.voiceBytesSent
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue