Improved the video rendering and selection

master
WolverinDEV 2020-12-12 13:19:04 +01:00 committed by WolverinDEV
parent 0457c68a04
commit d4ab372173
14 changed files with 432 additions and 204 deletions

View File

@ -24,7 +24,6 @@ export type VideoBroadcastStatistics = {
export interface VideoConnectionEvent {
notify_status_changed: { oldState: VideoConnectionStatus, newState: VideoConnectionStatus },
notify_local_broadcast_state_changed: { broadcastType: VideoBroadcastType, oldState: VideoBroadcastState, newState: VideoBroadcastState },
}
export enum VideoConnectionStatus {
@ -64,6 +63,44 @@ export interface VideoClient {
leaveBroadcast(broadcastType: VideoBroadcastType);
}
export interface LocalVideoBroadcastEvents {
notify_state_changed: { oldState: LocalVideoBroadcastState, newState: LocalVideoBroadcastState },
}
export type LocalVideoBroadcastState = {
state: "stopped",
} | {
state: "initializing"
} | {
state: "failed",
reason: string
} | {
state: "broadcasting"
}
export interface LocalVideoBroadcast {
getEvents() : Registry<LocalVideoBroadcastEvents>;
getState() : LocalVideoBroadcastState;
getSource() : VideoSource | undefined;
getStatistics() : Promise<VideoBroadcastStatistics | undefined>;
//getBandwidthLimit() : number | undefined;
//setBandwidthLimit(value: number);
/**
* @param source The source of the broadcast (No ownership will be taken. The voice connection must ref the source by itself!)
*/
startBroadcasting(source: VideoSource) : Promise<void>;
/**
* @param source The source of the broadcast (No ownership will be taken. The voice connection must ref the source by itself!)
*/
changeSource(source: VideoSource) : Promise<void>;
stopBroadcasting();
}
export interface VideoConnection {
getEvents() : Registry<VideoConnectionEvent>;
@ -73,17 +110,7 @@ export interface VideoConnection {
getConnectionStats() : Promise<ConnectionStatistics>;
isBroadcasting(type: VideoBroadcastType);
getBroadcastingSource(type: VideoBroadcastType) : VideoSource | undefined;
getBroadcastingState(type: VideoBroadcastType) : VideoBroadcastState;
getBroadcastStatistics(type: VideoBroadcastType) : Promise<VideoBroadcastStatistics | undefined>;
/**
* @param type
* @param source The source of the broadcast (No ownership will be taken. The voice connection must ref the source by itself!)
*/
startBroadcasting(type: VideoBroadcastType, source: VideoSource) : Promise<void>;
stopBroadcasting(type: VideoBroadcastType);
getLocalBroadcast(channel: VideoBroadcastType) : LocalVideoBroadcast;
registerVideoClient(clientId: number);
registeredVideoClients() : VideoClient[];

View File

@ -64,6 +64,7 @@ class RetryTimeCalculator {
}
calculateRetryTime() {
return 0;
if(this.retryCount >= 5) {
/* no more retries */
return 0;
@ -203,7 +204,7 @@ class CommandHandler extends AbstractCommandHandler {
}).then(() => {
this.handle["cachedRemoteSessionDescription"] = sdp;
this.handle["peerRemoteDescriptionReceived"] = true;
this.handle.applyCachedRemoteIceCandidates();
setTimeout(() => this.handle.applyCachedRemoteIceCandidates(), 50);
}).catch(error => {
logError(LogCategory.WEBRTC, tr("Failed to set the remote description: %o"), error);
this.handle["handleFatalError"](tr("Failed to set the remote description (answer)"), true);
@ -954,9 +955,6 @@ export class RTCConnection {
logTrace(LogCategory.WEBRTC, tr("Skipping local fqdn ICE candidate %s"), candidate.toJSON().candidate);
return;
}
if(candidate.protocol !== "tcp") {
return;
}
this.localCandidateCount++;
const json = candidate.toJSON();
@ -1009,6 +1007,7 @@ export class RTCConnection {
this.handleRemoteIceCandidate(candidate, mediaLine);
}
this.handleRemoteIceCandidate(undefined, 0);
this.cachedRemoteIceCandidates = [];
}

View File

@ -81,11 +81,22 @@ export class SdpProcessor {
sdpString = sdpString.replace(/profile-level-id=4325407/g, "profile-level-id=42e01f");
const sdp = sdpTransform.parse(sdpString);
//sdp.media.forEach(media => media.candidates = []);
//sdp.origin.address = "127.0.0.1";
this.rtpRemoteChannelMapping = SdpProcessor.generateRtpSSrcMapping(sdp);
return sdpTransform.write(sdp);
}
/*
getCandidates(sdpString: string) : string[] {
const sdp = sdpTransform.parse(sdpString);
sdp.media = [sdp.media[0]];
sdpTransform.write(sdp).split("\r\n")
.filter(line => line.startsWith("a"))
}
*/
processOutgoingSdp(sdpString: string, _mode: "offer" | "answer") : string {
const sdp = sdpTransform.parse(sdpString);

View File

@ -1,5 +1,7 @@
import {
VideoBroadcastState,
LocalVideoBroadcast,
LocalVideoBroadcastEvents,
LocalVideoBroadcastState,
VideoBroadcastStatistics,
VideoBroadcastType,
VideoClient,
@ -9,19 +11,236 @@ import {
} from "tc-shared/connection/VideoConnection";
import {Registry} from "tc-shared/events";
import {VideoSource} from "tc-shared/video/VideoSource";
import {RTCConnection, RTCConnectionEvents, RTPConnectionState} from "../Connection";
import {LogCategory, logDebug, logError, logWarn} from "tc-shared/log";
import {RTCBroadcastableTrackType, RTCConnection, RTCConnectionEvents, RTPConnectionState} from "../Connection";
import {LogCategory, logError, logWarn} from "tc-shared/log";
import {Settings, settings} from "tc-shared/settings";
import {RtpVideoClient} from "./VideoClient";
import {tr} from "tc-shared/i18n/localize";
import {ConnectionState} from "tc-shared/ConnectionHandler";
import {ConnectionStatistics} from "tc-shared/connection/ConnectionBase";
import * as _ from "lodash";
type VideoBroadcast = {
readonly source: VideoSource;
state: VideoBroadcastState,
failedReason: string | undefined,
active: boolean
class LocalRtpVideoBroadcast implements LocalVideoBroadcast {
private readonly handle: RtpVideoConnection;
private readonly type: VideoBroadcastType;
private readonly events: Registry<LocalVideoBroadcastEvents>;
private state: LocalVideoBroadcastState;
private currentSource: VideoSource;
private broadcastStartId: number;
private localStartPromise: Promise<void>;
constructor(handle: RtpVideoConnection, type: VideoBroadcastType) {
this.handle = handle;
this.type = type;
this.broadcastStartId = 0;
this.events = new Registry<LocalVideoBroadcastEvents>();
this.state = { state: "stopped" };
}
destroy() {
this.events.destroy();
}
getEvents(): Registry<LocalVideoBroadcastEvents> {
return this.events;
}
getSource(): VideoSource | undefined {
return this.currentSource;
}
getState(): LocalVideoBroadcastState {
return this.state;
}
private setState(newState: LocalVideoBroadcastState) {
if(_.isEqual(this.state, newState)) {
return;
}
const oldState = this.state;
this.state = newState;
this.events.fire("notify_state_changed", { oldState: oldState, newState: newState });
}
getStatistics(): Promise<VideoBroadcastStatistics | undefined> {
return Promise.resolve(undefined);
}
async changeSource(source: VideoSource): Promise<void> {
const videoTracks = source.getStream().getVideoTracks();
if(videoTracks.length === 0) {
throw tr("missing video stream track");
}
let sourceRef = source.ref();
while(this.localStartPromise) {
await this.localStartPromise;
}
if(this.state.state !== "broadcasting") {
sourceRef.deref();
throw tr("not broadcasting anything");
}
const startId = ++this.broadcastStartId;
let rtcBroadcastType: RTCBroadcastableTrackType = this.type === "camera" ? "video" : "video-screen";
try {
await this.handle.getRTCConnection().setTrackSource(rtcBroadcastType, videoTracks[0]);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
return;
}
sourceRef.deref();
logError(LogCategory.WEBRTC, tr("Failed to change video track for broadcast %s: %o"), this.type, error);
throw tr("failed to change video track");
}
this.setCurrentSource(sourceRef);
sourceRef.deref();
}
private setCurrentSource(source: VideoSource | undefined) {
if(this.currentSource) {
this.currentSource.deref();
}
this.currentSource = source?.ref();
}
async startBroadcasting(source: VideoSource): Promise<void> {
const sourceRef = source.ref();
while(this.localStartPromise) {
await this.localStartPromise;
}
const promise = this.doStartBroadcast(source);
this.localStartPromise = promise.catch(() => {});
this.localStartPromise.then(() => this.localStartPromise = undefined);
try {
await promise;
} finally {
sourceRef.deref();
}
}
private async doStartBroadcast(source: VideoSource) {
const videoTracks = source.getStream().getVideoTracks();
if(videoTracks.length === 0) {
throw tr("missing video stream track");
}
const startId = ++this.broadcastStartId;
this.setCurrentSource(source);
this.setState({ state: "initializing" });
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
return;
}
let rtcBroadcastType: RTCBroadcastableTrackType = this.type === "camera" ? "video" : "video-screen";
try {
await this.handle.getRTCConnection().setTrackSource(rtcBroadcastType, videoTracks[0]);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
return;
}
this.stopBroadcasting(true, { state: "failed", reason: tr("Failed to set track source") });
logError(LogCategory.WEBRTC, tr("Failed to setup video track for broadcast %s: %o"), this.type, error);
throw tr("failed to initialize video track");
}
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
return;
}
try {
await this.handle.getRTCConnection().startTrackBroadcast(rtcBroadcastType);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
return;
}
this.stopBroadcasting(true, { state: "failed", reason: error });
throw error;
}
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
return;
}
this.setState({ state: "broadcasting" });
}
stopBroadcasting(skipRtcStop?: boolean, stopState?: LocalVideoBroadcastState) {
if(this.state.state === "stopped" && (!stopState || _.isEqual(stopState, this.state))) {
return;
}
this.broadcastStartId++;
(async () => {
while(this.localStartPromise) {
await this.localStartPromise;
}
let rtcBroadcastType: RTCBroadcastableTrackType = this.type === "camera" ? "video" : "video-screen";
if(!skipRtcStop && !(this.state.state === "failed" || this.state.state === "stopped")) {
this.handle.getRTCConnection().stopTrackBroadcast(rtcBroadcastType);
}
this.setCurrentSource(undefined);
try {
await this.handle.getRTCConnection().setTrackSource(rtcBroadcastType, null);
} catch (error) {
logWarn(LogCategory.VIDEO, tr("Failed to change the RTC video track to null: %o"), error);
}
this.setState(stopState || { state: "stopped" });
})();
}
/**
* Restart the broadcast after a channel switch.
*/
restartBroadcast() {
(async () => {
while(this.localStartPromise) {
await this.localStartPromise;
}
if(this.state.state !== "broadcasting") {
return;
}
this.setState({ state: "initializing" });
let rtcBroadcastType: RTCBroadcastableTrackType = this.type === "camera" ? "video" : "video-screen";
const startId = ++this.broadcastStartId;
try {
await this.handle.getRTCConnection().startTrackBroadcast(rtcBroadcastType);
} catch (error) {
if(this.broadcastStartId !== startId) {
/* broadcast start has been canceled */
return;
}
this.stopBroadcasting(true, { state: "failed", reason: error });
throw error;
}
})();
}
}
export class RtpVideoConnection implements VideoConnection {
@ -30,9 +249,9 @@ export class RtpVideoConnection implements VideoConnection {
private readonly listener: (() => void)[];
private connectionState: VideoConnectionStatus;
private broadcasts: {[T in VideoBroadcastType]: VideoBroadcast} = {
camera: undefined,
screen: undefined
private broadcasts: {[T in VideoBroadcastType]: LocalRtpVideoBroadcast} = {
camera: new LocalRtpVideoBroadcast(this, "camera"),
screen: new LocalRtpVideoBroadcast(this, "screen")
};
private registeredClients: {[key: number]: RtpVideoClient} = {};
@ -55,12 +274,10 @@ export class RtpVideoConnection implements VideoConnection {
});
if(settings.static_global(Settings.KEY_STOP_VIDEO_ON_SWITCH)) {
this.stopBroadcasting("camera", true);
this.stopBroadcasting("screen", true);
Object.values(this.broadcasts).forEach(broadcast => broadcast.stopBroadcasting());
} else {
/* The server stops broadcasting by default, we've to reenable it */
this.restartBroadcast("screen");
this.restartBroadcast("camera");
Object.values(this.broadcasts).forEach(broadcast => broadcast.restartBroadcast());
}
} else if(parseInt("scid") === localClient.currentChannel().channelId) {
const broadcast = this.registeredClients[clientId];
@ -120,8 +337,7 @@ export class RtpVideoConnection implements VideoConnection {
this.listener.push(this.rtcConnection.getConnection().events.on("notify_connection_state_changed", event => {
if(event.newState !== ConnectionState.CONNECTED) {
this.stopBroadcasting("camera");
this.stopBroadcasting("screen");
Object.values(this.broadcasts).forEach(broadcast => broadcast.stopBroadcasting(true));
}
}));
@ -157,30 +373,6 @@ export class RtpVideoConnection implements VideoConnection {
this.events.fire("notify_status_changed", { oldState: oldState, newState: state });
}
private restartBroadcast(type: VideoBroadcastType) {
if(!this.broadcasts[type]?.active) { return; }
const broadcast = this.broadcasts[type];
if(broadcast.state !== VideoBroadcastState.Initializing) {
const oldState = broadcast.state;
broadcast.state = VideoBroadcastState.Initializing;
this.events.fire("notify_local_broadcast_state_changed", { oldState: oldState, newState: VideoBroadcastState.Initializing, broadcastType: type });
}
this.rtcConnection.startTrackBroadcast(type === "camera" ? "video" : "video-screen").then(() => {
if(!broadcast.active) { return; }
const oldState = broadcast.state;
broadcast.state = VideoBroadcastState.Running;
this.events.fire("notify_local_broadcast_state_changed", { oldState: oldState, newState: VideoBroadcastState.Initializing, broadcastType: type });
logDebug(LogCategory.VIDEO, tr("Successfully restarted video broadcast of type %s"), type);
}).catch(error => {
if(!broadcast.active) { return; }
logWarn(LogCategory.VIDEO, tr("Failed to restart video broadcast %s: %o"), type, error);
this.stopBroadcasting(type, true);
});
}
destroy() {
this.listener.forEach(callback => callback());
this.listener.splice(0, this.listener.length);
@ -188,6 +380,10 @@ export class RtpVideoConnection implements VideoConnection {
this.events.destroy();
}
getRTCConnection() : RTCConnection {
return this.rtcConnection;
}
getEvents(): Registry<VideoConnectionEvent> {
return this.events;
}
@ -204,83 +400,6 @@ export class RtpVideoConnection implements VideoConnection {
return this.rtcConnection.getFailReason();
}
getBroadcastingState(type: VideoBroadcastType): VideoBroadcastState {
return this.broadcasts[type] ? this.broadcasts[type].state : VideoBroadcastState.Stopped;
}
async getBroadcastStatistics(type: VideoBroadcastType): Promise<VideoBroadcastStatistics | undefined> {
/* TODO! */
return undefined;
}
getBroadcastingSource(type: VideoBroadcastType): VideoSource | undefined {
return this.broadcasts[type]?.source;
}
isBroadcasting(type: VideoBroadcastType) {
return typeof this.broadcasts[type] !== "undefined";
}
async startBroadcasting(type: VideoBroadcastType, source: VideoSource) : Promise<void> {
const videoTracks = source.getStream().getVideoTracks();
if(videoTracks.length === 0) {
throw tr("missing video stream track");
}
const broadcast = this.broadcasts[type] = {
source: source.ref(),
state: VideoBroadcastState.Initializing as VideoBroadcastState,
failedReason: undefined,
active: true
};
this.events.fire("notify_local_broadcast_state_changed", { oldState: this.broadcasts[type].state || VideoBroadcastState.Stopped, newState: VideoBroadcastState.Initializing, broadcastType: type });
try {
await this.rtcConnection.setTrackSource(type === "camera" ? "video" : "video-screen", videoTracks[0]);
} catch (error) {
this.stopBroadcasting(type);
logError(LogCategory.WEBRTC, tr("Failed to setup video track for broadcast %s: %o"), type, error);
throw tr("failed to initialize video track");
}
if(!broadcast.active) {
return;
}
try {
await this.rtcConnection.startTrackBroadcast(type === "camera" ? "video" : "video-screen");
} catch (error) {
this.stopBroadcasting(type);
throw error;
}
if(!broadcast.active) {
return;
}
broadcast.state = VideoBroadcastState.Running;
this.events.fire("notify_local_broadcast_state_changed", { oldState: VideoBroadcastState.Initializing, newState: VideoBroadcastState.Running, broadcastType: type });
}
stopBroadcasting(type: VideoBroadcastType, skipRtcStop?: boolean) {
const broadcast = this.broadcasts[type];
if(!broadcast) {
return;
}
if(!skipRtcStop) {
this.rtcConnection.stopTrackBroadcast(type === "camera" ? "video" : "video-screen");
}
this.rtcConnection.setTrackSource(type === "camera" ? "video" : "video-screen", null).then(undefined);
const oldState = this.broadcasts[type].state;
this.broadcasts[type].active = false;
this.broadcasts[type] = undefined;
broadcast.source.deref();
this.events.fire("notify_local_broadcast_state_changed", { oldState: oldState, newState: VideoBroadcastState.Stopped, broadcastType: type });
}
registerVideoClient(clientId: number) {
if(typeof this.registeredClients[clientId] !== "undefined") {
debugger;
@ -353,4 +472,8 @@ export class RtpVideoConnection implements VideoConnection {
bytesSend: stats.videoBytesSent
};
}
getLocalBroadcast(channel: VideoBroadcastType): LocalVideoBroadcast {
return this.broadcasts[channel];
}
}

View File

@ -197,8 +197,24 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
if(!source) { return; }
try {
connection.getServerConnection().getVideoConnection().startBroadcasting(event.broadcastType, source)
.catch(error => {
const broadcast = connection.getServerConnection().getVideoConnection().getLocalBroadcast(event.broadcastType);
if(broadcast.getState().state === "initializing" || broadcast.getState().state === "broadcasting") {
console.error("Change source");
broadcast.changeSource(source).catch(error => {
logError(LogCategory.VIDEO, tr("Failed to change broadcast source: %o"), event.broadcastType, error);
if(typeof error !== "string") {
error = tr("lookup the console for detail");
}
if(event.broadcastType === "camera") {
createErrorModal(tr("Failed to change video source"), tra("Failed to change video broadcasting source:\n{}", error)).open();
} else {
createErrorModal(tr("Failed to change screen sharing source"), tra("Failed to change screen sharing source:\n{}", error)).open();
}
});
} else {
console.error("Start broadcast");
broadcast.startBroadcasting(source).catch(error => {
logError(LogCategory.VIDEO, tr("Failed to start %s broadcasting: %o"), event.broadcastType, error);
if(typeof error !== "string") {
error = tr("lookup the console for detail");
@ -210,12 +226,15 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
createErrorModal(tr("Failed to start screen sharing"), tra("Failed to start screen sharing:\n{}", error)).open();
}
});
}
} finally {
source.deref();
}
});
} else {
event.connection.getServerConnection().getVideoConnection().stopBroadcasting(event.broadcastType);
const connection = event.connection;
const broadcast = connection.getServerConnection().getVideoConnection().getLocalBroadcast(event.broadcastType);
broadcast.stopBroadcasting();
}
});
}

View File

@ -29,13 +29,19 @@ export interface ClientGlobalControlEvents {
videoUrl: string,
handlerId: string
},
/* Start/open a new video broadcast */
action_toggle_video_broadcasting: {
connection: ConnectionHandler,
enabled: boolean,
broadcastType: VideoBroadcastType,
enabled: boolean,
quickSelect?: boolean,
defaultDevice?: string
}
},
/* Open the broadcast edit window */
action_edit_video_broadcasting: {
connection: ConnectionHandler,
broadcastType: VideoBroadcastType,
},
/* some more specific window openings */
action_open_window_connect: {

View File

@ -22,9 +22,10 @@ import {
} from "tc-shared/bookmarks";
import {LogCategory, logWarn} from "tc-shared/log";
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {VideoBroadcastState, VideoBroadcastType, VideoConnectionStatus} from "tc-shared/connection/VideoConnection";
import {VideoBroadcastType, VideoConnectionStatus} from "tc-shared/connection/VideoConnection";
import { tr } from "tc-shared/i18n/localize";
import {getVideoDriver} from "tc-shared/video/VideoSource";
import {kLocalBroadcastChannels} from "tc-shared/ui/frames/video/Definitions";
class InfoController {
private readonly mode: ControlBarMode;
@ -127,7 +128,11 @@ class InfoController {
}));
const videoConnection = handler.getServerConnection().getVideoConnection();
events.push(videoConnection.getEvents().on(["notify_local_broadcast_state_changed", "notify_status_changed"], () => {
for(const channel of kLocalBroadcastChannels) {
const broadcast = videoConnection.getLocalBroadcast(channel);
events.push(broadcast.getEvents().on("notify_state_changed", () => this.sendVideoState(channel)));
}
events.push(videoConnection.getEvents().on("notify_status_changed", () => {
this.sendVideoState("screen");
this.sendVideoState("camera");
}));
@ -253,7 +258,8 @@ class InfoController {
if(this.currentHandler?.connected) {
const videoConnection = this.currentHandler.getServerConnection().getVideoConnection();
if(videoConnection.getStatus() === VideoConnectionStatus.Connected) {
if(videoConnection.getBroadcastingState(type) === VideoBroadcastState.Running) {
const broadcast = videoConnection.getLocalBroadcast(type);
if(broadcast.getState().state === "broadcasting" || broadcast.getState().state === "initializing") {
state = "enabled";
} else {
state = "disabled";

View File

@ -247,7 +247,7 @@ const VideoButton = (props: { type: VideoBroadcastType }) => {
let tooltip = props.type === "camera" ? tr("Video broadcasting not supported") : tr("Screen sharing not supported");
let modalTitle = props.type === "camera" ? tr("Video broadcasting unsupported") : tr("Screen sharing unsupported");
let modalBody = props.type === "camera" ? tr("Video broadcasting isn't supported by the target server.") : tr("Screen sharing isn't supported by the target server.");
let dropdownText = props.type === "camera" ? tr("Start screen sharing") : tr("Start video broadcasting");
let dropdownText = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing");
return (
<Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip}
key={"unsupported"}
@ -262,7 +262,7 @@ const VideoButton = (props: { type: VideoBroadcastType }) => {
let tooltip = props.type === "camera" ? tr("Video broadcasting not available") : tr("Screen sharing not available");
let modalTitle = props.type === "camera" ? tr("Video broadcasting unavailable") : tr("Screen sharing unavailable");
let modalBody = props.type === "camera" ? tr("Video broadcasting isn't available right now.") : tr("Screen sharing isn't available right now.");
let dropdownText = props.type === "camera" ? tr("Start screen sharing") : tr("Start video broadcasting");
let dropdownText = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing");
return (
<Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip}
key={"unavailable"}
@ -275,7 +275,7 @@ const VideoButton = (props: { type: VideoBroadcastType }) => {
case "disconnected":
case "disabled": {
let tooltip = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing");
let dropdownText = props.type === "camera" ? tr("Start screen sharing") : tr("Start video broadcasting");
let dropdownText = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing");
return (
<Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon}
onToggle={() => events.fire("action_toggle_video", {enable: true, broadcastType: props.type, quickStart: true})}
@ -288,15 +288,14 @@ const VideoButton = (props: { type: VideoBroadcastType }) => {
case "enabled": {
let tooltip = props.type === "camera" ? tr("Stop video broadcasting") : tr("Stop screen sharing");
let dropdownTextManage = props.type === "camera" ? tr("Configure/change screen sharing") : tr("Configure/change video broadcasting");
let dropdownTextStop = props.type === "camera" ? tr("Stop screen sharing") : tr("Stop video broadcasting");
let dropdownTextManage = props.type === "camera" ? tr("Configure/change video broadcasting") : tr("Configure/change screen sharing");
let dropdownTextStop = props.type === "camera" ? tr("Stop video broadcasting") : tr("Stop screen sharing");
return (
<Button switched={false} colorTheme={"red"} autoSwitch={false} iconNormal={icon}
onToggle={() => events.fire("action_toggle_video", {enable: false, broadcastType: props.type})}
tooltip={tooltip} key={"disable"}>
{/* <DropdownEntry icon={icon} text={dropdownTextManage} onClick={() => events.fire("action_manage_video", { broadcastType: props.type })} /> TODO! */}
<DropdownEntry icon={icon} text={dropdownTextStop} onClick={() => events.fire("action_toggle_video", {enable: false, broadcastType: props.type})} />
<VideoDeviceList />
{props.type === "camera" ? <VideoDeviceList key={"list"} /> : null}
</Button>
);

View File

@ -3,8 +3,9 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import {ChannelVideoRenderer} from "tc-shared/ui/frames/video/Renderer";
import {Registry} from "tc-shared/events";
import {ChannelVideoEvents, kLocalVideoId} from "tc-shared/ui/frames/video/Definitions";
import {ChannelVideo, ChannelVideoEvents, kLocalVideoId} from "tc-shared/ui/frames/video/Definitions";
import {
LocalVideoBroadcastState,
VideoBroadcastState,
VideoBroadcastType,
VideoClient,
@ -14,6 +15,7 @@ import {ClientEntry, ClientType, LocalClientEntry, MusicClientEntry} from "tc-sh
import {LogCategory, logError, logWarn} from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize";
import {Settings, settings} from "tc-shared/settings";
import * as _ from "lodash";
const cssStyle = require("./Renderer.scss");
@ -24,7 +26,7 @@ interface ClientVideoController {
dismissVideo(type: VideoBroadcastType);
notifyVideoInfo();
notifyVideo();
notifyVideo(forceSend: boolean);
notifyMuteState();
}
@ -43,6 +45,8 @@ class RemoteClientVideoController implements ClientVideoController {
camera: false
};
private cachedVideoStatus: ChannelVideo;
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>, videoId?: string) {
this.client = client;
this.events = eventRegistry;
@ -70,20 +74,11 @@ class RemoteClientVideoController implements ClientVideoController {
private updateVideoClient() {
this.eventListenerVideoClient?.forEach(callback => callback());
const events = this.eventListenerVideoClient = [];
this.eventListenerVideoClient = [];
const videoClient = this.client.getVideoClient();
if(videoClient) {
this.initializeVideoClient(videoClient);
events.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();
this.notifyMuteState();
}));
}
}
@ -94,7 +89,7 @@ class RemoteClientVideoController implements ClientVideoController {
/* we've a new broadcast which hasn't been dismissed yet */
this.dismissed[event.broadcastType] = false;
}
this.notifyVideo();
this.notifyVideo(false);
this.notifyMuteState();
}));
}
@ -132,7 +127,7 @@ class RemoteClientVideoController implements ClientVideoController {
}
this.dismissed[type] = true;
this.notifyVideo();
this.notifyVideo(false);
}
notifyVideoInfo() {
@ -147,9 +142,10 @@ class RemoteClientVideoController implements ClientVideoController {
});
}
notifyVideo() {
notifyVideo(forceSend: boolean) {
let broadcasting = false;
if(this.isVideoActive()) {
let status: ChannelVideo;
if(this.hasVideoSupport()) {
let initializing = false;
let cameraStream, desktopStream;
@ -174,41 +170,34 @@ class RemoteClientVideoController implements ClientVideoController {
if(cameraStream || desktopStream) {
broadcasting = true;
this.events.fire_react("notify_video", {
videoId: this.videoId,
status: {
status: "connected",
status = {
status: "connected",
desktopStream: desktopStream,
cameraStream: cameraStream,
desktopStream: desktopStream,
cameraStream: cameraStream,
dismissed: this.dismissed
}
});
dismissed: this.dismissed
};
} else if(initializing) {
broadcasting = true;
this.events.fire_react("notify_video", {
videoId: this.videoId,
status: { status: "initializing" }
});
status = { status: "initializing" };
} else {
this.events.fire_react("notify_video", {
videoId: this.videoId,
status: {
status: "connected",
status = {
status: "connected",
cameraStream: undefined,
desktopStream: undefined,
cameraStream: undefined,
desktopStream: undefined,
dismissed: this.dismissed
}
});
dismissed: this.dismissed
};
}
} else {
this.events.fire_react("notify_video", {
videoId: this.videoId,
status: { status: "no-video" }
});
status = { status: "no-video" };
}
if(forceSend || !_.isEqual(this.cachedVideoStatus, status)) {
this.cachedVideoStatus = status;
this.events.fire_react("notify_video", { videoId: this.videoId, status: status });
}
if(broadcasting !== this.currentBroadcastState) {
@ -229,7 +218,7 @@ class RemoteClientVideoController implements ClientVideoController {
});
}
protected isVideoActive() : boolean {
protected hasVideoSupport() : boolean {
return typeof this.client.getVideoClient() !== "undefined";
}
@ -244,14 +233,19 @@ class RemoteClientVideoController implements ClientVideoController {
}
}
const kLocalBroadcastChannels: VideoBroadcastType[] = ["screen", "camera"];
class LocalVideoController extends RemoteClientVideoController {
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>) {
super(client, eventRegistry, kLocalVideoId);
const videoConnection = client.channelTree.client.serverConnection.getVideoConnection();
this.eventListener.push(videoConnection.getEvents().on("notify_local_broadcast_state_changed", () => {
this.notifyVideo();
}));
for(const broadcastType of kLocalBroadcastChannels) {
const broadcast = videoConnection.getLocalBroadcast(broadcastType);
this.eventListener.push(broadcast.getEvents().on("notify_state_changed", () => {
this.notifyVideo(false);
}));
}
}
protected initializeVideoClient(videoClient: VideoClient) {
@ -267,19 +261,52 @@ class LocalVideoController extends RemoteClientVideoController {
isBroadcasting() {
const videoConnection = this.client.channelTree.client.serverConnection.getVideoConnection();
return videoConnection.isBroadcasting("camera") || videoConnection.isBroadcasting("screen");
const isBroadcasting = (state: LocalVideoBroadcastState) => state.state === "initializing" || state.state === "broadcasting";
for(const broadcastType of kLocalBroadcastChannels) {
const broadcast = videoConnection.getLocalBroadcast(broadcastType);
if(isBroadcasting(broadcast.getState())) {
return true;
}
}
/* the super should return false as well but just in case something went wrong we want to give the user the visual feedback */
return super.isBroadcasting();
}
protected isVideoActive(): boolean {
protected hasVideoSupport(): boolean {
return true;
}
/*
protected getBroadcastState(target: VideoBroadcastType): VideoBroadcastState {
const videoConnection = this.client.channelTree.client.serverConnection.getVideoConnection();
return videoConnection.getBroadcastingState(target);
const broadcast = videoConnection.getLocalBroadcast(target);
const receivingState = super.getBroadcastState(target);
switch (broadcast.getState().state) {
case "stopped":
case "failed":
if(receivingState !== VideoBroadcastState.Stopped) {
/* this should never happen but just in case give the client a visual feedback */
return receivingState;
}
return VideoBroadcastState.Stopped;
case "initializing":
return VideoBroadcastState.Initializing;
case "broadcasting":
const state = super.getBroadcastState(target);
if(state === VideoBroadcastState.Stopped) {
/* we should receive a stream in a few seconds */
return VideoBroadcastState.Initializing;
} else {
return state;
}
}
}
/*
protected getBroadcastStream(target: VideoBroadcastType) : MediaStream | undefined {
const videoConnection = this.client.channelTree.client.serverConnection.getVideoConnection();
return videoConnection.getBroadcastingSource(target)?.getStream();
@ -391,7 +418,7 @@ class ChannelVideoController {
return;
}
controller.notifyVideo();
controller.notifyVideo(true);
});
this.events.on("query_video_mute_status", event => {

View File

@ -2,6 +2,7 @@ import {ClientIcon} from "svg-sprites/client-icons";
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
export const kLocalVideoId = "__local__video__";
export const kLocalBroadcastChannels: VideoBroadcastType[] = ["screen", "camera"];
export type ChannelVideoInfo = { clientName: string, clientUniqueId: string, clientId: number, statusIcon: ClientIcon };
export type ChannelVideoStream = "available" | MediaStream | undefined;

View File

@ -420,3 +420,9 @@ $small_height: 10em;
}
}
}
/* Opera popout button fix (we've our own?) */
html > div {
display: none;
pointer-events: none;
}

View File

@ -16,6 +16,7 @@ type SourceConstraints = { width?: number, height?: number, frameRate?: number }
export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, selectMode: "quick" | "default" | "none", defaultDeviceId?: string) : Promise<VideoSource> {
const controller = new VideoSourceController(type);
let defaultSelectSource = selectMode === "default";
if(selectMode === "quick") {
/* We need the modal itself for the native client in order to present the window selector */
if(type === "camera" || __build.target === "web") {
@ -26,6 +27,8 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele
return source;
}
} else {
defaultSelectSource = true;
}
}
@ -33,7 +36,7 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele
controller.events.on(["action_start", "action_cancel"], () => modal.destroy());
modal.show().then(() => {
if(selectMode === "default" || selectMode === "quick") {
if(defaultSelectSource) {
if(type === "screen" && getVideoDriver().screenQueryAvailable()) {
controller.events.fire_react("action_toggle_screen_capture_device_select", { shown: true });
} else {
@ -46,8 +49,8 @@ export async function spawnVideoSourceSelectModal(type: VideoBroadcastType, sele
controller.events.on("action_start", () => refSource.source = controller.getCurrentSource()?.ref());
await new Promise(resolve => {
if(type === "screen" && selectMode === "quick") {
controller.events.on("notify_video_preview", event => {
if(defaultSelectSource && selectMode === "quick") {
controller.events.one("notify_video_preview", event => {
if(event.status.status === "preview") {
/* we've successfully selected something */
modal.destroy();

View File

@ -42,7 +42,7 @@
margin-left: 1em;
width: 20em;
.body .title {
.sectionBody .title {
display: flex;
flex-direction: row;
justify-content: space-between;
@ -245,7 +245,7 @@
.bpsInfo {
margin-top: auto;
.body {
.sectionBody {
font-size: .8em;
}
}

View File

@ -27,6 +27,7 @@ import {ServerFeature} from "tc-shared/connection/ServerFeatures";
import {RTCConnection} from "tc-shared/connection/rtc/Connection";
import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection";
import { tr } from "tc-shared/i18n/localize";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
class ReturnListener<T> {
resolve: (value?: T | PromiseLike<T>) => void;