Merge pull request #1060 from thecodingmachine/media_manager_store
Switching MediaManager to using a Svelte store
This commit is contained in:
commit
796aaf82ce
3
front/dist/index.tmpl.html
vendored
3
front/dist/index.tmpl.html
vendored
@ -73,7 +73,6 @@
|
|||||||
<img id="microphone-close" src="resources/logos/microphone-close.svg">
|
<img id="microphone-close" src="resources/logos/microphone-close.svg">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cowebsite" class="cowebsite hidden">
|
<div id="cowebsite" class="cowebsite hidden">
|
||||||
@ -108,7 +107,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="audioplayer">
|
<div class="audioplayer">
|
||||||
<label id="label-audioplayer_decrease_while_talking" for="audiooplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
|
<label id="label-audioplayer_decrease_while_talking" for="audioplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
|
||||||
reduce in conversations
|
reduce in conversations
|
||||||
<input type="checkbox" id="audioplayer_decrease_while_talking" checked />
|
<input type="checkbox" id="audioplayer_decrease_while_talking" checked />
|
||||||
</label>
|
</label>
|
||||||
|
@ -92,6 +92,7 @@ import {PinchManager} from "../UserInput/PinchManager";
|
|||||||
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
|
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
|
||||||
import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes";
|
import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes";
|
||||||
import {waScaleManager} from "../Services/WaScaleManager";
|
import {waScaleManager} from "../Services/WaScaleManager";
|
||||||
|
import {peerStore} from "../../Stores/PeerStore";
|
||||||
import {EmoteManager} from "./EmoteManager";
|
import {EmoteManager} from "./EmoteManager";
|
||||||
|
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
@ -189,7 +190,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
private originalMapUrl: string|undefined;
|
private originalMapUrl: string|undefined;
|
||||||
private pinchManager: PinchManager|undefined;
|
private pinchManager: PinchManager|undefined;
|
||||||
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
||||||
private onVisibilityChangeCallback: () => void;
|
|
||||||
private emoteManager!: EmoteManager;
|
private emoteManager!: EmoteManager;
|
||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
|
||||||
@ -210,7 +210,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
|
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
|
||||||
this.connectionAnswerPromiseResolve = resolve;
|
this.connectionAnswerPromiseResolve = resolve;
|
||||||
});
|
});
|
||||||
this.onVisibilityChangeCallback = this.onVisibilityChange.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//hook preload scene
|
//hook preload scene
|
||||||
@ -515,8 +514,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', this.onVisibilityChangeCallback);
|
|
||||||
|
|
||||||
this.emoteManager = new EmoteManager(this);
|
this.emoteManager = new EmoteManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,6 +619,7 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
|
|
||||||
// When connection is performed, let's connect SimplePeer
|
// When connection is performed, let's connect SimplePeer
|
||||||
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
|
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
|
||||||
|
peerStore.connectToSimplePeer(this.simplePeer);
|
||||||
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
|
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
|
||||||
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
|
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
|
||||||
|
|
||||||
@ -639,7 +637,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
self.chatModeSprite.setVisible(false);
|
self.chatModeSprite.setVisible(false);
|
||||||
self.openChatIcon.setVisible(false);
|
self.openChatIcon.setVisible(false);
|
||||||
audioManager.restoreVolume();
|
audioManager.restoreVolume();
|
||||||
self.onVisibilityChange();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -944,8 +941,6 @@ ${escapedMessage}
|
|||||||
for(const iframeEvents of this.iframeSubscriptionList){
|
for(const iframeEvents of this.iframeSubscriptionList){
|
||||||
iframeEvents.unsubscribe();
|
iframeEvents.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.removeEventListener('visibilitychange', this.onVisibilityChangeCallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeAllRemotePlayers(): void {
|
private removeAllRemotePlayers(): void {
|
||||||
@ -1505,8 +1500,6 @@ ${escapedMessage}
|
|||||||
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
|
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
|
||||||
this.stopJitsi();
|
this.stopJitsi();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onVisibilityChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopJitsi(): void {
|
public stopJitsi(): void {
|
||||||
@ -1515,7 +1508,6 @@ ${escapedMessage}
|
|||||||
mediaManager.showGameOverlay();
|
mediaManager.showGameOverlay();
|
||||||
|
|
||||||
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
|
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
|
||||||
this.onVisibilityChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo: put this into an 'orchestrator' scene (EntryScene?)
|
//todo: put this into an 'orchestrator' scene (EntryScene?)
|
||||||
@ -1555,20 +1547,4 @@ ${escapedMessage}
|
|||||||
waScaleManager.zoomModifier *= zoomFactor;
|
waScaleManager.zoomModifier *= zoomFactor;
|
||||||
this.updateCameraOffset();
|
this.updateCameraOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onVisibilityChange(): void {
|
|
||||||
// If the overlay is not displayed, we are in Jitsi. We don't need the webcam.
|
|
||||||
if (!mediaManager.isGameOverlayVisible()) {
|
|
||||||
mediaManager.blurCamera();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
mediaManager.focusCamera();
|
|
||||||
} else {
|
|
||||||
if (this.simplePeer.getNbConnections() === 0) {
|
|
||||||
mediaManager.blurCamera();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,14 @@ import {PinchManager} from "../UserInput/PinchManager";
|
|||||||
import Zone = Phaser.GameObjects.Zone;
|
import Zone = Phaser.GameObjects.Zone;
|
||||||
import { MenuScene } from "../Menu/MenuScene";
|
import { MenuScene } from "../Menu/MenuScene";
|
||||||
import {ResizableScene} from "./ResizableScene";
|
import {ResizableScene} from "./ResizableScene";
|
||||||
|
import {
|
||||||
|
audioConstraintStore,
|
||||||
|
enableCameraSceneVisibilityStore,
|
||||||
|
localStreamStore,
|
||||||
|
mediaStreamConstraintsStore,
|
||||||
|
videoConstraintStore
|
||||||
|
} from "../../Stores/MediaStore";
|
||||||
|
import type {Unsubscriber} from "svelte/store";
|
||||||
|
|
||||||
export const EnableCameraSceneName = "EnableCameraScene";
|
export const EnableCameraSceneName = "EnableCameraScene";
|
||||||
enum LoginTextures {
|
enum LoginTextures {
|
||||||
@ -40,6 +48,7 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
private enableCameraSceneElement!: Phaser.GameObjects.DOMElement;
|
private enableCameraSceneElement!: Phaser.GameObjects.DOMElement;
|
||||||
|
|
||||||
private mobileTapZone!: Zone;
|
private mobileTapZone!: Zone;
|
||||||
|
private localStreamStoreUnsubscriber!: Unsubscriber;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
@ -119,9 +128,20 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active');
|
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active');
|
||||||
|
|
||||||
const mediaPromise = mediaManager.getCamera();
|
this.localStreamStoreUnsubscriber = localStreamStore.subscribe((result) => {
|
||||||
|
if (result.type === 'error') {
|
||||||
|
// TODO: proper handling of the error
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getDevices();
|
||||||
|
if (result.stream !== null) {
|
||||||
|
this.setupStream(result.stream);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*const mediaPromise = mediaManager.getCamera();
|
||||||
mediaPromise.then(this.getDevices.bind(this));
|
mediaPromise.then(this.getDevices.bind(this));
|
||||||
mediaPromise.then(this.setupStream.bind(this));
|
mediaPromise.then(this.setupStream.bind(this));*/
|
||||||
|
|
||||||
this.input.keyboard.on('keydown-RIGHT', this.nextCam.bind(this));
|
this.input.keyboard.on('keydown-RIGHT', this.nextCam.bind(this));
|
||||||
this.input.keyboard.on('keydown-LEFT', this.previousCam.bind(this));
|
this.input.keyboard.on('keydown-LEFT', this.previousCam.bind(this));
|
||||||
@ -133,6 +153,8 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
this.add.existing(this.soundMeterSprite);
|
this.add.existing(this.soundMeterSprite);
|
||||||
|
|
||||||
this.onResize();
|
this.onResize();
|
||||||
|
|
||||||
|
enableCameraSceneVisibilityStore.showEnableCameraScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
private previousCam(): void {
|
private previousCam(): void {
|
||||||
@ -140,7 +162,9 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cameraSelected--;
|
this.cameraSelected--;
|
||||||
mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
|
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
|
||||||
|
|
||||||
|
//mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private nextCam(): void {
|
private nextCam(): void {
|
||||||
@ -148,8 +172,10 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cameraSelected++;
|
this.cameraSelected++;
|
||||||
|
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
|
||||||
|
|
||||||
// TODO: the change of camera should be OBSERVED (reactive)
|
// TODO: the change of camera should be OBSERVED (reactive)
|
||||||
mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
|
//mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private previousMic(): void {
|
private previousMic(): void {
|
||||||
@ -157,7 +183,8 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.microphoneSelected--;
|
this.microphoneSelected--;
|
||||||
mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
|
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
|
||||||
|
//mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private nextMic(): void {
|
private nextMic(): void {
|
||||||
@ -165,8 +192,9 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.microphoneSelected++;
|
this.microphoneSelected++;
|
||||||
|
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
|
||||||
// TODO: the change of camera should be OBSERVED (reactive)
|
// TODO: the change of camera should be OBSERVED (reactive)
|
||||||
mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
|
//mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -260,15 +288,20 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
|
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
|
||||||
this.soundMeter.stop();
|
this.soundMeter.stop();
|
||||||
|
|
||||||
mediaManager.stopCamera();
|
enableCameraSceneVisibilityStore.hideEnableCameraScene();
|
||||||
mediaManager.stopMicrophone();
|
this.localStreamStoreUnsubscriber();
|
||||||
|
//mediaManager.stopCamera();
|
||||||
|
//mediaManager.stopMicrophone();
|
||||||
|
|
||||||
this.scene.sleep(EnableCameraSceneName)
|
this.scene.sleep(EnableCameraSceneName);
|
||||||
gameManager.goToStartingMap(this.scene);
|
gameManager.goToStartingMap(this.scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDevices() {
|
private async getDevices() {
|
||||||
|
// TODO: switch this in a store.
|
||||||
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
|
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
this.microphonesList = [];
|
||||||
|
this.camerasList = [];
|
||||||
for (const mediaDeviceInfo of mediaDeviceInfos) {
|
for (const mediaDeviceInfo of mediaDeviceInfos) {
|
||||||
if (mediaDeviceInfo.kind === 'audioinput') {
|
if (mediaDeviceInfo.kind === 'audioinput') {
|
||||||
this.microphonesList.push(mediaDeviceInfo);
|
this.microphonesList.push(mediaDeviceInfo);
|
||||||
|
@ -2,6 +2,8 @@ import {mediaManager} from "../../WebRtc/MediaManager";
|
|||||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||||
import {DirtyScene} from "../Game/DirtyScene";
|
import {DirtyScene} from "../Game/DirtyScene";
|
||||||
|
import {get} from "svelte/store";
|
||||||
|
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
|
||||||
|
|
||||||
export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene';
|
export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene';
|
||||||
const helpCameraSettings = 'helpCameraSettings';
|
const helpCameraSettings = 'helpCameraSettings';
|
||||||
@ -41,7 +43,7 @@ export class HelpCameraSettingsScene extends DirtyScene {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(!localUserStore.getHelpCameraSettingsShown() && (!mediaManager.constraintsMedia.audio || !mediaManager.constraintsMedia.video)){
|
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
|
||||||
this.openHelpCameraSettingsOpened();
|
this.openHelpCameraSettingsOpened();
|
||||||
localUserStore.setHelpCameraSettingsShown();
|
localUserStore.setHelpCameraSettingsShown();
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import {GameConnexionTypes} from "../../Url/UrlManager";
|
|||||||
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
|
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
|
||||||
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
|
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
|
||||||
import {menuIconVisible} from "../../Stores/MenuStore";
|
import {menuIconVisible} from "../../Stores/MenuStore";
|
||||||
|
import {videoConstraintStore} from "../../Stores/MediaStore";
|
||||||
|
|
||||||
export const MenuSceneName = 'MenuScene';
|
export const MenuSceneName = 'MenuScene';
|
||||||
const gameMenuKey = 'gameMenu';
|
const gameMenuKey = 'gameMenu';
|
||||||
@ -324,7 +325,7 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
if (valueVideo !== this.videoQualityValue) {
|
if (valueVideo !== this.videoQualityValue) {
|
||||||
this.videoQualityValue = valueVideo;
|
this.videoQualityValue = valueVideo;
|
||||||
localUserStore.setVideoQualityValue(valueVideo);
|
localUserStore.setVideoQualityValue(valueVideo);
|
||||||
mediaManager.updateCameraQuality(valueVideo);
|
videoConstraintStore.setFrameRate(valueVideo);
|
||||||
}
|
}
|
||||||
this.closeGameQualityMenu();
|
this.closeGameQualityMenu();
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import {PlayerAnimationDirections} from "./Animation";
|
|||||||
import type {GameScene} from "../Game/GameScene";
|
import type {GameScene} from "../Game/GameScene";
|
||||||
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
|
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
|
||||||
import {Character} from "../Entity/Character";
|
import {Character} from "../Entity/Character";
|
||||||
|
import {userMovingStore} from "../../Stores/GameStore";
|
||||||
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
|
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
|
||||||
|
|
||||||
export const hasMovedEventName = "hasMoved";
|
export const hasMovedEventName = "hasMoved";
|
||||||
@ -86,6 +87,7 @@ export class Player extends Character {
|
|||||||
this.previousDirection = direction;
|
this.previousDirection = direction;
|
||||||
}
|
}
|
||||||
this.wasMoving = moving;
|
this.wasMoving = moving;
|
||||||
|
userMovingStore.set(moving);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isMoving(): boolean {
|
public isMoving(): boolean {
|
||||||
|
3
front/src/Stores/GameStore.ts
Normal file
3
front/src/Stores/GameStore.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { derived, writable, Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export const userMovingStore = writable(false);
|
510
front/src/Stores/MediaStore.ts
Normal file
510
front/src/Stores/MediaStore.ts
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
||||||
|
import {peerStore} from "./PeerStore";
|
||||||
|
import {localUserStore} from "../Connexion/LocalUserStore";
|
||||||
|
import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap";
|
||||||
|
import {userMovingStore} from "./GameStore";
|
||||||
|
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the camera state requested by the user (on or off).
|
||||||
|
*/
|
||||||
|
function createRequestedCameraState() {
|
||||||
|
const { subscribe, set, update } = writable(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
enableWebcam: () => set(true),
|
||||||
|
disableWebcam: () => set(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the microphone state requested by the user (on or off).
|
||||||
|
*/
|
||||||
|
function createRequestedMicrophoneState() {
|
||||||
|
const { subscribe, set, update } = writable(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
enableMicrophone: () => set(true),
|
||||||
|
disableMicrophone: () => set(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing whether the current page is visible or not.
|
||||||
|
*/
|
||||||
|
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) {
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
set(document.visibilityState === 'visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains whether the game overlay is shown or not.
|
||||||
|
* Typically, the overlay is hidden when entering Jitsi meet.
|
||||||
|
*/
|
||||||
|
function createGameOverlayVisibilityStore() {
|
||||||
|
const { subscribe, set, update } = writable(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
showGameOverlay: () => set(true),
|
||||||
|
hideGameOverlay: () => set(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains whether the EnableCameraScene is shown or not.
|
||||||
|
*/
|
||||||
|
function createEnableCameraSceneVisibilityStore() {
|
||||||
|
const { subscribe, set, update } = writable(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
showEnableCameraScene: () => set(true),
|
||||||
|
hideEnableCameraScene: () => set(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestedCameraState = createRequestedCameraState();
|
||||||
|
export const requestedMicrophoneState = createRequestedMicrophoneState();
|
||||||
|
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();
|
||||||
|
export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
|
||||||
|
*/
|
||||||
|
function createPrivacyShutdownStore() {
|
||||||
|
let privacyEnabled = false;
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(privacyEnabled);
|
||||||
|
|
||||||
|
visibilityStore.subscribe((isVisible) => {
|
||||||
|
if (!isVisible && get(peerStore).size === 0) {
|
||||||
|
privacyEnabled = true;
|
||||||
|
set(true);
|
||||||
|
}
|
||||||
|
if (isVisible) {
|
||||||
|
privacyEnabled = false;
|
||||||
|
set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
peerStore.subscribe((peers) => {
|
||||||
|
if (peers.size === 0 && get(visibilityStore) === false) {
|
||||||
|
privacyEnabled = true;
|
||||||
|
set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const privacyShutdownStore = createPrivacyShutdownStore();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing whether the webcam was enabled in the last 10 seconds
|
||||||
|
*/
|
||||||
|
const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
||||||
|
let timeout: NodeJS.Timeout|null = null;
|
||||||
|
|
||||||
|
const unsubscribe = requestedCameraState.subscribe((enabled) => {
|
||||||
|
if (enabled === true) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
set(false);
|
||||||
|
}, 10000);
|
||||||
|
set(true);
|
||||||
|
} else {
|
||||||
|
set(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing whether the webcam was enabled in the last 5 seconds
|
||||||
|
*/
|
||||||
|
const userMoved5SecondsAgoStore = readable(false, function start(set) {
|
||||||
|
let timeout: NodeJS.Timeout|null = null;
|
||||||
|
|
||||||
|
const unsubscribe = userMovingStore.subscribe((moving) => {
|
||||||
|
if (moving === true) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
set(true);
|
||||||
|
} else {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
set(false);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing whether the mouse is getting close the bottom right corner.
|
||||||
|
*/
|
||||||
|
const mouseInBottomRight = readable(false, function start(set) {
|
||||||
|
let lastInBottomRight = false;
|
||||||
|
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game');
|
||||||
|
|
||||||
|
const detectInBottomRight = (event: MouseEvent) => {
|
||||||
|
const rect = gameDiv.getBoundingClientRect();
|
||||||
|
const inBottomRight = event.x - rect.left > rect.width * 3 / 4 && event.y - rect.top > rect.height * 3 / 4;
|
||||||
|
if (inBottomRight !== lastInBottomRight) {
|
||||||
|
lastInBottomRight = inBottomRight;
|
||||||
|
set(inBottomRight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', detectInBottomRight);
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
document.removeEventListener('mousemove', detectInBottomRight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation.
|
||||||
|
*/
|
||||||
|
export const cameraEnergySavingStore = derived([userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight], ([$userMoved5SecondsAgoStore,$peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
|
||||||
|
return !$mouseInBottomRight && !$userMoved5SecondsAgoStore && $peerStore.size === 0 && !$enabledWebCam10secondsAgoStore;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains video constraints.
|
||||||
|
*/
|
||||||
|
function createVideoConstraintStore() {
|
||||||
|
const { subscribe, set, update } = writable({
|
||||||
|
width: { min: 640, ideal: 1280, max: 1920 },
|
||||||
|
height: { min: 400, ideal: 720 },
|
||||||
|
frameRate: { ideal: localUserStore.getVideoQualityValue() },
|
||||||
|
facingMode: "user",
|
||||||
|
resizeMode: 'crop-and-scale',
|
||||||
|
aspectRatio: 1.777777778
|
||||||
|
} as MediaTrackConstraints);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
setDeviceId: (deviceId: string) => update((constraints) => {
|
||||||
|
constraints.deviceId = {
|
||||||
|
exact: deviceId
|
||||||
|
};
|
||||||
|
|
||||||
|
return constraints;
|
||||||
|
}),
|
||||||
|
setFrameRate: (frameRate: number) => update((constraints) => {
|
||||||
|
constraints.frameRate = { ideal: frameRate };
|
||||||
|
|
||||||
|
return constraints;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const videoConstraintStore = createVideoConstraintStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains video constraints.
|
||||||
|
*/
|
||||||
|
function createAudioConstraintStore() {
|
||||||
|
const { subscribe, set, update } = writable({
|
||||||
|
//TODO: make these values configurable in the game settings menu and store them in localstorage
|
||||||
|
autoGainControl: false,
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true
|
||||||
|
} as boolean|MediaTrackConstraints);
|
||||||
|
|
||||||
|
let selectedDeviceId = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
setDeviceId: (deviceId: string) => update((constraints) => {
|
||||||
|
selectedDeviceId = deviceId;
|
||||||
|
|
||||||
|
if (typeof(constraints) === 'boolean') {
|
||||||
|
constraints = {}
|
||||||
|
}
|
||||||
|
constraints.deviceId = {
|
||||||
|
exact: selectedDeviceId
|
||||||
|
};
|
||||||
|
|
||||||
|
return constraints;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const audioConstraintStore = createAudioConstraintStore();
|
||||||
|
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
|
||||||
|
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing the media constraints we want to apply.
|
||||||
|
*/
|
||||||
|
export const mediaStreamConstraintsStore = derived(
|
||||||
|
[
|
||||||
|
requestedCameraState,
|
||||||
|
requestedMicrophoneState,
|
||||||
|
gameOverlayVisibilityStore,
|
||||||
|
enableCameraSceneVisibilityStore,
|
||||||
|
videoConstraintStore,
|
||||||
|
audioConstraintStore,
|
||||||
|
privacyShutdownStore,
|
||||||
|
cameraEnergySavingStore,
|
||||||
|
], (
|
||||||
|
[
|
||||||
|
$requestedCameraState,
|
||||||
|
$requestedMicrophoneState,
|
||||||
|
$gameOverlayVisibilityStore,
|
||||||
|
$enableCameraSceneVisibilityStore,
|
||||||
|
$videoConstraintStore,
|
||||||
|
$audioConstraintStore,
|
||||||
|
$privacyShutdownStore,
|
||||||
|
$cameraEnergySavingStore,
|
||||||
|
], set
|
||||||
|
) => {
|
||||||
|
|
||||||
|
let currentVideoConstraint: boolean|MediaTrackConstraints = $videoConstraintStore;
|
||||||
|
let currentAudioConstraint: boolean|MediaTrackConstraints = $audioConstraintStore;
|
||||||
|
|
||||||
|
if ($enableCameraSceneVisibilityStore) {
|
||||||
|
set({
|
||||||
|
video: currentVideoConstraint,
|
||||||
|
audio: currentAudioConstraint,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable webcam if the user requested so
|
||||||
|
if ($requestedCameraState === false) {
|
||||||
|
currentVideoConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable microphone if the user requested so
|
||||||
|
if ($requestedMicrophoneState === false) {
|
||||||
|
currentAudioConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable webcam and microphone when in a Jitsi
|
||||||
|
if ($gameOverlayVisibilityStore === false) {
|
||||||
|
currentVideoConstraint = false;
|
||||||
|
currentAudioConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
|
||||||
|
if ($privacyShutdownStore === true) {
|
||||||
|
currentVideoConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
|
||||||
|
if ($cameraEnergySavingStore === true) {
|
||||||
|
currentVideoConstraint = false;
|
||||||
|
currentAudioConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's make the changes only if the new value is different from the old one.
|
||||||
|
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
|
||||||
|
previousComputedVideoConstraint = currentVideoConstraint;
|
||||||
|
previousComputedAudioConstraint = currentAudioConstraint;
|
||||||
|
// Let's copy the objects.
|
||||||
|
if (typeof previousComputedVideoConstraint !== 'boolean') {
|
||||||
|
previousComputedVideoConstraint = {...previousComputedVideoConstraint};
|
||||||
|
}
|
||||||
|
if (typeof previousComputedAudioConstraint !== 'boolean') {
|
||||||
|
previousComputedAudioConstraint = {...previousComputedAudioConstraint};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's wait a little bit to avoid sending too many constraint changes.
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
set({
|
||||||
|
video: currentVideoConstraint,
|
||||||
|
audio: currentAudioConstraint,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
video: false,
|
||||||
|
audio: false
|
||||||
|
} as MediaStreamConstraints);
|
||||||
|
|
||||||
|
export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
|
||||||
|
|
||||||
|
interface StreamSuccessValue {
|
||||||
|
type: "success",
|
||||||
|
stream: MediaStream|null,
|
||||||
|
// The constraints that we got (and not the one that have been requested)
|
||||||
|
constraints: MediaStreamConstraints
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamErrorValue {
|
||||||
|
type: "error",
|
||||||
|
error: Error,
|
||||||
|
constraints: MediaStreamConstraints
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentStream : MediaStream|null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the camera from filming
|
||||||
|
*/
|
||||||
|
function stopCamera(): void {
|
||||||
|
if (currentStream) {
|
||||||
|
for (const track of currentStream.getVideoTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the microphone from listening
|
||||||
|
*/
|
||||||
|
function stopMicrophone(): void {
|
||||||
|
if (currentStream) {
|
||||||
|
for (const track of currentStream.getAudioTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
|
||||||
|
*/
|
||||||
|
export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(mediaStreamConstraintsStore, ($mediaStreamConstraintsStore, set) => {
|
||||||
|
const constraints = { ...$mediaStreamConstraintsStore };
|
||||||
|
|
||||||
|
if (navigator.mediaDevices === undefined) {
|
||||||
|
if (window.location.protocol === 'http:') {
|
||||||
|
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'),
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//throw new Error('Unable to access your camera or microphone. Your browser is too old.');
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: new Error('Unable to access your camera or microphone. Your browser is too old.'),
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints.audio === false) {
|
||||||
|
stopMicrophone();
|
||||||
|
}
|
||||||
|
if (constraints.video === false) {
|
||||||
|
stopCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints.audio === false && constraints.video === false) {
|
||||||
|
currentStream = null;
|
||||||
|
set({
|
||||||
|
type: 'success',
|
||||||
|
stream: null,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
stopMicrophone();
|
||||||
|
stopCamera();
|
||||||
|
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
set({
|
||||||
|
type: 'success',
|
||||||
|
stream: currentStream,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
if (constraints.video !== false) {
|
||||||
|
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
|
||||||
|
// TODO: does it make sense to pop this error when retrying?
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: e,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
// Let's try without video constraints
|
||||||
|
requestedCameraState.disableWebcam();
|
||||||
|
} else {
|
||||||
|
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: e,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*constraints.video = false;
|
||||||
|
if (constraints.audio === false) {
|
||||||
|
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: e,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
// Let's make as if the user did not ask.
|
||||||
|
requestedCameraState.disableWebcam();
|
||||||
|
} else {
|
||||||
|
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
|
||||||
|
try {
|
||||||
|
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
set({
|
||||||
|
type: 'success',
|
||||||
|
stream: currentStream,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (e2) {
|
||||||
|
console.info("Error. Unable to get microphone fallback access.", $mediaStreamConstraintsStore, e2);
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: e,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
|
||||||
|
*/
|
||||||
|
export const obtainedMediaConstraintStore = derived(localStreamStore, ($localStreamStore) => {
|
||||||
|
return $localStreamStore.constraints;
|
||||||
|
});
|
||||||
|
|
36
front/src/Stores/PeerStore.ts
Normal file
36
front/src/Stores/PeerStore.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { derived, writable, Writable } from "svelte/store";
|
||||||
|
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
|
||||||
|
import type {SimplePeer} from "../WebRtc/SimplePeer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the camera state requested by the user (on or off).
|
||||||
|
*/
|
||||||
|
function createPeerStore() {
|
||||||
|
let users = new Map<number, UserSimplePeerInterface>();
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(users);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||||
|
users = new Map<number, UserSimplePeerInterface>();
|
||||||
|
set(users);
|
||||||
|
simplePeer.registerPeerConnectionListener({
|
||||||
|
onConnect(user: UserSimplePeerInterface) {
|
||||||
|
update(users => {
|
||||||
|
users.set(user.userId, user);
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDisconnect(userId: number) {
|
||||||
|
update(users => {
|
||||||
|
users.delete(userId);
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const peerStore = createPeerStore();
|
192
front/src/Stores/ScreenSharingStore.ts
Normal file
192
front/src/Stores/ScreenSharingStore.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
||||||
|
import {peerStore} from "./PeerStore";
|
||||||
|
import {localUserStore} from "../Connexion/LocalUserStore";
|
||||||
|
import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap";
|
||||||
|
import {userMovingStore} from "./GameStore";
|
||||||
|
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||||
|
import {
|
||||||
|
audioConstraintStore, cameraEnergySavingStore,
|
||||||
|
enableCameraSceneVisibilityStore,
|
||||||
|
gameOverlayVisibilityStore, LocalStreamStoreValue, privacyShutdownStore,
|
||||||
|
requestedCameraState,
|
||||||
|
requestedMicrophoneState, videoConstraintStore
|
||||||
|
} from "./MediaStore";
|
||||||
|
|
||||||
|
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the camera state requested by the user (on or off).
|
||||||
|
*/
|
||||||
|
function createRequestedScreenSharingState() {
|
||||||
|
const { subscribe, set, update } = writable(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
enableScreenSharing: () => set(true),
|
||||||
|
disableScreenSharing: () => set(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestedScreenSharingState = createRequestedScreenSharingState();
|
||||||
|
|
||||||
|
let currentStream : MediaStream|null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the camera from filming
|
||||||
|
*/
|
||||||
|
function stopScreenSharing(): void {
|
||||||
|
if (currentStream) {
|
||||||
|
for (const track of currentStream.getVideoTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
|
||||||
|
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing the media constraints we want to apply.
|
||||||
|
*/
|
||||||
|
export const screenSharingConstraintsStore = derived(
|
||||||
|
[
|
||||||
|
requestedScreenSharingState,
|
||||||
|
gameOverlayVisibilityStore,
|
||||||
|
peerStore,
|
||||||
|
], (
|
||||||
|
[
|
||||||
|
$requestedScreenSharingState,
|
||||||
|
$gameOverlayVisibilityStore,
|
||||||
|
$peerStore,
|
||||||
|
], set
|
||||||
|
) => {
|
||||||
|
|
||||||
|
let currentVideoConstraint: boolean|MediaTrackConstraints = true;
|
||||||
|
let currentAudioConstraint: boolean|MediaTrackConstraints = false;
|
||||||
|
|
||||||
|
// Disable screen sharing if the user requested so
|
||||||
|
if (!$requestedScreenSharingState) {
|
||||||
|
currentVideoConstraint = false;
|
||||||
|
currentAudioConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable screen sharing when in a Jitsi
|
||||||
|
if (!$gameOverlayVisibilityStore) {
|
||||||
|
currentVideoConstraint = false;
|
||||||
|
currentAudioConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable screen sharing if no peers
|
||||||
|
if ($peerStore.size === 0) {
|
||||||
|
currentVideoConstraint = false;
|
||||||
|
currentAudioConstraint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's make the changes only if the new value is different from the old one.
|
||||||
|
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
|
||||||
|
previousComputedVideoConstraint = currentVideoConstraint;
|
||||||
|
previousComputedAudioConstraint = currentAudioConstraint;
|
||||||
|
// Let's copy the objects.
|
||||||
|
/*if (typeof previousComputedVideoConstraint !== 'boolean') {
|
||||||
|
previousComputedVideoConstraint = {...previousComputedVideoConstraint};
|
||||||
|
}
|
||||||
|
if (typeof previousComputedAudioConstraint !== 'boolean') {
|
||||||
|
previousComputedAudioConstraint = {...previousComputedAudioConstraint};
|
||||||
|
}*/
|
||||||
|
|
||||||
|
set({
|
||||||
|
video: currentVideoConstraint,
|
||||||
|
audio: currentAudioConstraint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
video: false,
|
||||||
|
audio: false
|
||||||
|
} as MediaStreamConstraints);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred)
|
||||||
|
*/
|
||||||
|
export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(screenSharingConstraintsStore, ($screenSharingConstraintsStore, set) => {
|
||||||
|
const constraints = $screenSharingConstraintsStore;
|
||||||
|
|
||||||
|
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
|
||||||
|
stopScreenSharing();
|
||||||
|
requestedScreenSharingState.disableScreenSharing();
|
||||||
|
set({
|
||||||
|
type: 'success',
|
||||||
|
stream: null,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentStreamPromise: Promise<MediaStream>;
|
||||||
|
if (navigator.getDisplayMedia) {
|
||||||
|
currentStreamPromise = navigator.getDisplayMedia({constraints});
|
||||||
|
} else if (navigator.mediaDevices.getDisplayMedia) {
|
||||||
|
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints});
|
||||||
|
} else {
|
||||||
|
stopScreenSharing();
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: new Error('Your browser does not support sharing screen'),
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
stopScreenSharing();
|
||||||
|
currentStream = await currentStreamPromise;
|
||||||
|
|
||||||
|
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
|
||||||
|
for (const track of currentStream.getTracks()) {
|
||||||
|
track.onended = () => {
|
||||||
|
stopScreenSharing();
|
||||||
|
requestedScreenSharingState.disableScreenSharing();
|
||||||
|
previousComputedVideoConstraint = false;
|
||||||
|
previousComputedAudioConstraint = false;
|
||||||
|
set({
|
||||||
|
type: 'success',
|
||||||
|
stream: null,
|
||||||
|
constraints: {
|
||||||
|
video: false,
|
||||||
|
audio: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
type: 'success',
|
||||||
|
stream: currentStream,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
currentStream = null;
|
||||||
|
console.info("Error. Unable to share screen.", e);
|
||||||
|
set({
|
||||||
|
type: 'error',
|
||||||
|
error: e,
|
||||||
|
constraints
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing whether the screen sharing button should be displayed or hidden.
|
||||||
|
*/
|
||||||
|
export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) => {
|
||||||
|
if (!navigator.getDisplayMedia && !navigator.mediaDevices.getDisplayMedia) {
|
||||||
|
set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set($peerStore.size !== 0);
|
||||||
|
});
|
@ -1,6 +1,8 @@
|
|||||||
import {JITSI_URL} from "../Enum/EnvironmentVariable";
|
import {JITSI_URL} from "../Enum/EnvironmentVariable";
|
||||||
import {mediaManager} from "./MediaManager";
|
import {mediaManager} from "./MediaManager";
|
||||||
import {coWebsiteManager} from "./CoWebsiteManager";
|
import {coWebsiteManager} from "./CoWebsiteManager";
|
||||||
|
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
|
||||||
|
import {get} from "svelte/store";
|
||||||
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
interface jitsiConfigInterface {
|
interface jitsiConfigInterface {
|
||||||
@ -10,10 +12,9 @@ interface jitsiConfigInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultConfig = () : jitsiConfigInterface => {
|
const getDefaultConfig = () : jitsiConfigInterface => {
|
||||||
const constraints = mediaManager.getConstraintRequestedByUser();
|
|
||||||
return {
|
return {
|
||||||
startWithAudioMuted: !constraints.audio,
|
startWithAudioMuted: !get(requestedMicrophoneState),
|
||||||
startWithVideoMuted: constraints.video === false,
|
startWithVideoMuted: !get(requestedCameraState),
|
||||||
prejoinPageEnabled: false
|
prejoinPageEnabled: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,7 +73,6 @@ class JitsiFactory {
|
|||||||
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
private audioCallback = this.onAudioChange.bind(this);
|
private audioCallback = this.onAudioChange.bind(this);
|
||||||
private videoCallback = this.onVideoChange.bind(this);
|
private videoCallback = this.onVideoChange.bind(this);
|
||||||
private previousConfigMeet! : jitsiConfigInterface;
|
|
||||||
private jitsiScriptLoaded: boolean = false;
|
private jitsiScriptLoaded: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,9 +83,6 @@ class JitsiFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string): void {
|
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string): void {
|
||||||
//save previous config
|
|
||||||
this.previousConfigMeet = getDefaultConfig();
|
|
||||||
|
|
||||||
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
|
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
|
||||||
// Jitsi meet external API maintains some data in local storage
|
// Jitsi meet external API maintains some data in local storage
|
||||||
// which is sent via the appData URL parameter when joining a
|
// which is sent via the appData URL parameter when joining a
|
||||||
@ -134,27 +131,22 @@ class JitsiFactory {
|
|||||||
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
|
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
|
||||||
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);
|
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);
|
||||||
this.jitsiApi?.dispose();
|
this.jitsiApi?.dispose();
|
||||||
|
|
||||||
//restore previous config
|
|
||||||
if(this.previousConfigMeet?.startWithAudioMuted){
|
|
||||||
await mediaManager.disableMicrophone();
|
|
||||||
}else{
|
|
||||||
await mediaManager.enableMicrophone();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.previousConfigMeet?.startWithVideoMuted){
|
|
||||||
await mediaManager.disableCamera();
|
|
||||||
}else{
|
|
||||||
await mediaManager.enableCamera();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAudioChange({muted}: {muted: boolean}): void {
|
private onAudioChange({muted}: {muted: boolean}): void {
|
||||||
this.previousConfigMeet.startWithAudioMuted = muted;
|
if (muted) {
|
||||||
|
requestedMicrophoneState.disableMicrophone();
|
||||||
|
} else {
|
||||||
|
requestedMicrophoneState.enableMicrophone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onVideoChange({muted}: {muted: boolean}): void {
|
private onVideoChange({muted}: {muted: boolean}): void {
|
||||||
this.previousConfigMeet.startWithVideoMuted = muted;
|
if (muted) {
|
||||||
|
requestedCameraState.disableWebcam();
|
||||||
|
} else {
|
||||||
|
requestedCameraState.enableWebcam();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadJitsiScript(domain: string): Promise<void> {
|
private async loadJitsiScript(domain: string): Promise<void> {
|
||||||
|
@ -6,10 +6,21 @@ import {localUserStore} from "../Connexion/LocalUserStore";
|
|||||||
import type {UserSimplePeerInterface} from "./SimplePeer";
|
import type {UserSimplePeerInterface} from "./SimplePeer";
|
||||||
import {SoundMeter} from "../Phaser/Components/SoundMeter";
|
import {SoundMeter} from "../Phaser/Components/SoundMeter";
|
||||||
import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable";
|
import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable";
|
||||||
|
import {
|
||||||
|
gameOverlayVisibilityStore, localStreamStore,
|
||||||
|
mediaStreamConstraintsStore,
|
||||||
|
requestedCameraState,
|
||||||
|
requestedMicrophoneState
|
||||||
|
} from "../Stores/MediaStore";
|
||||||
|
import {
|
||||||
|
requestedScreenSharingState,
|
||||||
|
screenSharingAvailableStore,
|
||||||
|
screenSharingLocalStreamStore
|
||||||
|
} from "../Stores/ScreenSharingStore";
|
||||||
|
|
||||||
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
let videoConstraint: boolean|MediaTrackConstraints = {
|
const videoConstraint: boolean|MediaTrackConstraints = {
|
||||||
width: { min: 640, ideal: 1280, max: 1920 },
|
width: { min: 640, ideal: 1280, max: 1920 },
|
||||||
height: { min: 400, ideal: 720 },
|
height: { min: 400, ideal: 720 },
|
||||||
frameRate: { ideal: localUserStore.getVideoQualityValue() },
|
frameRate: { ideal: localUserStore.getVideoQualityValue() },
|
||||||
@ -31,7 +42,6 @@ export type ReportCallback = (message: string) => void;
|
|||||||
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void;
|
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void;
|
||||||
export type HelpCameraSettingsCallBack = () => void;
|
export type HelpCameraSettingsCallBack = () => void;
|
||||||
|
|
||||||
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
|
|
||||||
export class MediaManager {
|
export class MediaManager {
|
||||||
localStream: MediaStream|null = null;
|
localStream: MediaStream|null = null;
|
||||||
localScreenCapture: MediaStream|null = null;
|
localScreenCapture: MediaStream|null = null;
|
||||||
@ -47,10 +57,6 @@ export class MediaManager {
|
|||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
//mySoundMeterElement: HTMLDivElement;
|
//mySoundMeterElement: HTMLDivElement;
|
||||||
private webrtcOutAudio: HTMLAudioElement;
|
private webrtcOutAudio: HTMLAudioElement;
|
||||||
constraintsMedia : MediaStreamConstraints = {
|
|
||||||
audio: audioConstraint,
|
|
||||||
video: videoConstraint
|
|
||||||
};
|
|
||||||
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
|
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
|
||||||
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||||
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||||
@ -61,11 +67,8 @@ export class MediaManager {
|
|||||||
private cinemaBtn: HTMLDivElement;
|
private cinemaBtn: HTMLDivElement;
|
||||||
private monitorBtn: HTMLDivElement;
|
private monitorBtn: HTMLDivElement;
|
||||||
|
|
||||||
private previousConstraint : MediaStreamConstraints;
|
|
||||||
private focused : boolean = true;
|
private focused : boolean = true;
|
||||||
|
|
||||||
private hasCamera = true;
|
|
||||||
|
|
||||||
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
|
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
|
||||||
|
|
||||||
private userInputManager?: UserInputManager;
|
private userInputManager?: UserInputManager;
|
||||||
@ -88,14 +91,12 @@ export class MediaManager {
|
|||||||
this.microphoneClose.style.display = "none";
|
this.microphoneClose.style.display = "none";
|
||||||
this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
|
this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.enableMicrophone();
|
requestedMicrophoneState.enableMicrophone();
|
||||||
//update tracking
|
|
||||||
});
|
});
|
||||||
this.microphone = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone');
|
this.microphone = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone');
|
||||||
this.microphone.addEventListener('click', (e: MouseEvent) => {
|
this.microphone.addEventListener('click', (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.disableMicrophone();
|
requestedMicrophoneState.disableMicrophone();
|
||||||
//update tracking
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.cinemaBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-video');
|
this.cinemaBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-video');
|
||||||
@ -103,14 +104,12 @@ export class MediaManager {
|
|||||||
this.cinemaClose.style.display = "none";
|
this.cinemaClose.style.display = "none";
|
||||||
this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
|
this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.enableCamera();
|
requestedCameraState.enableWebcam();
|
||||||
//update tracking
|
|
||||||
});
|
});
|
||||||
this.cinema = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema');
|
this.cinema = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema');
|
||||||
this.cinema.addEventListener('click', (e: MouseEvent) => {
|
this.cinema.addEventListener('click', (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.disableCamera();
|
requestedCameraState.disableWebcam();
|
||||||
//update tracking
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.monitorBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
|
this.monitorBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
|
||||||
@ -118,21 +117,20 @@ export class MediaManager {
|
|||||||
this.monitorClose.style.display = "block";
|
this.monitorClose.style.display = "block";
|
||||||
this.monitorClose.addEventListener('click', (e: MouseEvent) => {
|
this.monitorClose.addEventListener('click', (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.enableScreenSharing();
|
//this.enableScreenSharing();
|
||||||
//update tracking
|
requestedScreenSharingState.enableScreenSharing();
|
||||||
});
|
});
|
||||||
this.monitor = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor');
|
this.monitor = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor');
|
||||||
this.monitor.style.display = "none";
|
this.monitor.style.display = "none";
|
||||||
this.monitor.addEventListener('click', (e: MouseEvent) => {
|
this.monitor.addEventListener('click', (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.disableScreenSharing();
|
//this.disableScreenSharing();
|
||||||
//update tracking
|
requestedScreenSharingState.disableScreenSharing();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
|
|
||||||
this.pingCameraStatus();
|
this.pingCameraStatus();
|
||||||
|
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
//FIX ME SOUNDMETER: check stability of sound meter calculation
|
||||||
/*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter'));
|
/*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter'));
|
||||||
this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => {
|
this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => {
|
||||||
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
|
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
|
||||||
@ -140,37 +138,98 @@ export class MediaManager {
|
|||||||
|
|
||||||
//Check of ask notification navigator permission
|
//Check of ask notification navigator permission
|
||||||
this.getNotification();
|
this.getNotification();
|
||||||
|
|
||||||
|
localStreamStore.subscribe((result) => {
|
||||||
|
if (result.type === 'error') {
|
||||||
|
console.error(result.error);
|
||||||
|
layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => {
|
||||||
|
this.showHelpCameraSettingsCallBack();
|
||||||
|
}, this.userInputManager);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.constraints.video !== false) {
|
||||||
|
HtmlUtils.getElementByIdOrFail('div-myCamVideo').classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
HtmlUtils.getElementByIdOrFail('div-myCamVideo').classList.add('hide');
|
||||||
|
}/*
|
||||||
|
if (result.constraints.audio !== false) {
|
||||||
|
this.enableMicrophoneStyle();
|
||||||
|
} else {
|
||||||
|
this.disableMicrophoneStyle();
|
||||||
|
}*/
|
||||||
|
|
||||||
|
this.localStream = result.stream;
|
||||||
|
this.myCamVideo.srcObject = this.localStream;
|
||||||
|
|
||||||
|
// TODO: migrate all listeners to the store directly.
|
||||||
|
this.triggerUpdatedLocalStreamCallbacks(result.stream);
|
||||||
|
});
|
||||||
|
|
||||||
|
requestedCameraState.subscribe((enabled) => {
|
||||||
|
if (enabled) {
|
||||||
|
this.enableCameraStyle();
|
||||||
|
} else {
|
||||||
|
this.disableCameraStyle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
requestedMicrophoneState.subscribe((enabled) => {
|
||||||
|
if (enabled) {
|
||||||
|
this.enableMicrophoneStyle();
|
||||||
|
} else {
|
||||||
|
this.disableMicrophoneStyle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//let screenSharingStream : MediaStream|null;
|
||||||
|
screenSharingLocalStreamStore.subscribe((result) => {
|
||||||
|
if (result.type === 'error') {
|
||||||
|
console.error(result.error);
|
||||||
|
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check navigators permissions.', () => {
|
||||||
|
this.showHelpCameraSettingsCallBack();
|
||||||
|
}, this.userInputManager);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.stream !== null) {
|
||||||
|
this.enableScreenSharingStyle();
|
||||||
|
mediaManager.localScreenCapture = result.stream;
|
||||||
|
|
||||||
|
// TODO: migrate this out of MediaManager
|
||||||
|
this.triggerStartedScreenSharingCallbacks(result.stream);
|
||||||
|
|
||||||
|
//screenSharingStream = result.stream;
|
||||||
|
|
||||||
|
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
|
||||||
|
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = result.stream;
|
||||||
|
} else {
|
||||||
|
this.disableScreenSharingStyle();
|
||||||
|
this.removeActiveScreenSharingVideo('me');
|
||||||
|
|
||||||
|
// FIXME: we need the old stream that is being stopped!
|
||||||
|
if (this.localScreenCapture) {
|
||||||
|
this.triggerStoppedScreenSharingCallbacks(this.localScreenCapture);
|
||||||
|
this.localScreenCapture = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//screenSharingStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
screenSharingAvailableStore.subscribe((available) => {
|
||||||
|
if (available) {
|
||||||
|
document.querySelector('.btn-monitor')?.classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
document.querySelector('.btn-monitor')?.classList.add('hide');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateScene(){
|
public updateScene(){
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
//FIX ME SOUNDMETER: check stability of sound meter calculation
|
||||||
//this.updateSoudMeter();
|
//this.updateSoudMeter();
|
||||||
}
|
}
|
||||||
|
|
||||||
public blurCamera() {
|
|
||||||
if(!this.focused){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.focused = false;
|
|
||||||
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
|
|
||||||
this.disableCamera();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the constraint that the user wants (independently of the visibility / jitsi state...)
|
|
||||||
*/
|
|
||||||
public getConstraintRequestedByUser(): MediaStreamConstraints {
|
|
||||||
return this.previousConstraint ?? this.constraintsMedia;
|
|
||||||
}
|
|
||||||
|
|
||||||
public focusCamera() {
|
|
||||||
if(this.focused){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.focused = true;
|
|
||||||
this.applyPreviousConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
|
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
|
||||||
this.updatedLocalStreamCallBacks.add(callback);
|
this.updatedLocalStreamCallBacks.add(callback);
|
||||||
}
|
}
|
||||||
@ -214,6 +273,8 @@ export class MediaManager {
|
|||||||
this.triggerCloseJitsiFrameButton();
|
this.triggerCloseJitsiFrameButton();
|
||||||
}
|
}
|
||||||
buttonCloseFrame.removeEventListener('click', functionTrigger);
|
buttonCloseFrame.removeEventListener('click', functionTrigger);
|
||||||
|
|
||||||
|
gameOverlayVisibilityStore.showGameOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
public hideGameOverlay(): void {
|
public hideGameOverlay(): void {
|
||||||
@ -225,110 +286,8 @@ export class MediaManager {
|
|||||||
this.triggerCloseJitsiFrameButton();
|
this.triggerCloseJitsiFrameButton();
|
||||||
}
|
}
|
||||||
buttonCloseFrame.addEventListener('click', functionTrigger);
|
buttonCloseFrame.addEventListener('click', functionTrigger);
|
||||||
}
|
|
||||||
|
|
||||||
public isGameOverlayVisible(): boolean {
|
gameOverlayVisibilityStore.hideGameOverlay();
|
||||||
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
|
||||||
return gameOverlay.classList.contains('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateCameraQuality(value: number) {
|
|
||||||
this.enableCameraStyle();
|
|
||||||
const newVideoConstraint = JSON.parse(JSON.stringify(videoConstraint));
|
|
||||||
newVideoConstraint.frameRate = {exact: value, ideal: value};
|
|
||||||
videoConstraint = newVideoConstraint;
|
|
||||||
this.constraintsMedia.video = videoConstraint;
|
|
||||||
this.getCamera().then((stream: MediaStream) => {
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async enableCamera() {
|
|
||||||
this.constraintsMedia.video = videoConstraint;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stream = await this.getCamera()
|
|
||||||
//TODO show error message tooltip upper of camera button
|
|
||||||
//TODO message : please check camera permission of your navigator
|
|
||||||
if(stream.getVideoTracks().length === 0) {
|
|
||||||
throw new Error('Video track is empty, please check camera permission of your navigator')
|
|
||||||
}
|
|
||||||
this.enableCameraStyle();
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
this.disableCameraStyle();
|
|
||||||
this.stopCamera();
|
|
||||||
|
|
||||||
layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => {
|
|
||||||
this.showHelpCameraSettingsCallBack();
|
|
||||||
}, this.userInputManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async disableCamera() {
|
|
||||||
this.disableCameraStyle();
|
|
||||||
this.stopCamera();
|
|
||||||
|
|
||||||
if (this.constraintsMedia.audio !== false) {
|
|
||||||
const stream = await this.getCamera();
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
|
||||||
} else {
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async enableMicrophone() {
|
|
||||||
this.constraintsMedia.audio = audioConstraint;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stream = await this.getCamera();
|
|
||||||
|
|
||||||
//TODO show error message tooltip upper of camera button
|
|
||||||
//TODO message : please check microphone permission of your navigator
|
|
||||||
if (stream.getAudioTracks().length === 0) {
|
|
||||||
throw Error('Audio track is empty, please check microphone permission of your navigator')
|
|
||||||
}
|
|
||||||
this.enableMicrophoneStyle();
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
this.disableMicrophoneStyle();
|
|
||||||
|
|
||||||
layoutManager.addInformation('warning', 'Microphone access denied. Click here and check navigators permissions.', () => {
|
|
||||||
this.showHelpCameraSettingsCallBack();
|
|
||||||
}, this.userInputManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async disableMicrophone() {
|
|
||||||
this.disableMicrophoneStyle();
|
|
||||||
this.stopMicrophone();
|
|
||||||
|
|
||||||
if (this.constraintsMedia.video !== false) {
|
|
||||||
const stream = await this.getCamera();
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
|
||||||
} else {
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyPreviousConfig() {
|
|
||||||
this.constraintsMedia = this.previousConstraint;
|
|
||||||
if(!this.constraintsMedia.video){
|
|
||||||
this.disableCameraStyle();
|
|
||||||
}else{
|
|
||||||
this.enableCameraStyle();
|
|
||||||
}
|
|
||||||
if(!this.constraintsMedia.audio){
|
|
||||||
this.disableMicrophoneStyle()
|
|
||||||
}else{
|
|
||||||
this.enableMicrophoneStyle()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getCamera().then((stream: MediaStream) => {
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableCameraStyle(){
|
private enableCameraStyle(){
|
||||||
@ -341,8 +300,6 @@ export class MediaManager {
|
|||||||
this.cinemaClose.style.display = "block";
|
this.cinemaClose.style.display = "block";
|
||||||
this.cinema.style.display = "none";
|
this.cinema.style.display = "none";
|
||||||
this.cinemaBtn.classList.add("disabled");
|
this.cinemaBtn.classList.add("disabled");
|
||||||
this.constraintsMedia.video = false;
|
|
||||||
this.myCamVideo.srcObject = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableMicrophoneStyle(){
|
private enableMicrophoneStyle(){
|
||||||
@ -355,185 +312,18 @@ export class MediaManager {
|
|||||||
this.microphoneClose.style.display = "block";
|
this.microphoneClose.style.display = "block";
|
||||||
this.microphone.style.display = "none";
|
this.microphone.style.display = "none";
|
||||||
this.microphoneBtn.classList.add("disabled");
|
this.microphoneBtn.classList.add("disabled");
|
||||||
this.constraintsMedia.audio = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableScreenSharing() {
|
private enableScreenSharingStyle(){
|
||||||
this.getScreenMedia().then((stream) => {
|
|
||||||
this.triggerStartedScreenSharingCallbacks(stream);
|
|
||||||
this.monitorClose.style.display = "none";
|
this.monitorClose.style.display = "none";
|
||||||
this.monitor.style.display = "block";
|
this.monitor.style.display = "block";
|
||||||
this.monitorBtn.classList.add("enabled");
|
this.monitorBtn.classList.add("enabled");
|
||||||
}, () => {
|
}
|
||||||
|
|
||||||
|
private disableScreenSharingStyle(){
|
||||||
this.monitorClose.style.display = "block";
|
this.monitorClose.style.display = "block";
|
||||||
this.monitor.style.display = "none";
|
this.monitor.style.display = "none";
|
||||||
this.monitorBtn.classList.remove("enabled");
|
this.monitorBtn.classList.remove("enabled");
|
||||||
|
|
||||||
layoutManager.addInformation('warning', 'Screen sharing access denied. Click here and check navigators permissions.', () => {
|
|
||||||
this.showHelpCameraSettingsCallBack();
|
|
||||||
}, this.userInputManager);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private disableScreenSharing() {
|
|
||||||
this.monitorClose.style.display = "block";
|
|
||||||
this.monitor.style.display = "none";
|
|
||||||
this.monitorBtn.classList.remove("enabled");
|
|
||||||
this.removeActiveScreenSharingVideo('me');
|
|
||||||
this.localScreenCapture?.getTracks().forEach((track: MediaStreamTrack) => {
|
|
||||||
track.stop();
|
|
||||||
});
|
|
||||||
if (this.localScreenCapture === null) {
|
|
||||||
console.warn('Weird: trying to remove a screen sharing that is not enabled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const localScreenCapture = this.localScreenCapture;
|
|
||||||
this.getCamera().then((stream) => {
|
|
||||||
this.triggerStoppedScreenSharingCallbacks(localScreenCapture);
|
|
||||||
}).catch((err) => { //catch error get camera
|
|
||||||
console.error(err);
|
|
||||||
this.triggerStoppedScreenSharingCallbacks(localScreenCapture);
|
|
||||||
});
|
|
||||||
this.localScreenCapture = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get screen
|
|
||||||
getScreenMedia() : Promise<MediaStream>{
|
|
||||||
try {
|
|
||||||
return this._startScreenCapture()
|
|
||||||
.then((stream: MediaStream) => {
|
|
||||||
this.localScreenCapture = stream;
|
|
||||||
|
|
||||||
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
|
|
||||||
for (const track of stream.getTracks()) {
|
|
||||||
track.onended = () => {
|
|
||||||
this.disableScreenSharing();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = stream;
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
console.error("Error => getScreenMedia => ", err);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}catch (err) {
|
|
||||||
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _startScreenCapture() {
|
|
||||||
if (navigator.getDisplayMedia) {
|
|
||||||
return navigator.getDisplayMedia({video: true});
|
|
||||||
} else if (navigator.mediaDevices.getDisplayMedia) {
|
|
||||||
return navigator.mediaDevices.getDisplayMedia({video: true});
|
|
||||||
} else {
|
|
||||||
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
|
|
||||||
reject("error sharing screen");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//get camera
|
|
||||||
async getCamera(): Promise<MediaStream> {
|
|
||||||
if (navigator.mediaDevices === undefined) {
|
|
||||||
if (window.location.protocol === 'http:') {
|
|
||||||
throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
|
|
||||||
} else {
|
|
||||||
throw new Error('Unable to access your camera or microphone. Your browser is too old.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getLocalStream().catch((err) => {
|
|
||||||
console.info('Error get camera, trying with video option at null =>', err);
|
|
||||||
this.disableCameraStyle();
|
|
||||||
this.stopCamera();
|
|
||||||
|
|
||||||
return this.getLocalStream().then((stream : MediaStream) => {
|
|
||||||
this.hasCamera = false;
|
|
||||||
return stream;
|
|
||||||
}).catch((err) => {
|
|
||||||
this.disableMicrophoneStyle();
|
|
||||||
console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//TODO resize remote cam
|
|
||||||
/*console.log(this.localStream.getTracks());
|
|
||||||
let videoMediaStreamTrack = this.localStream.getTracks().find((media : MediaStreamTrack) => media.kind === "video");
|
|
||||||
let {width, height} = videoMediaStreamTrack.getSettings();
|
|
||||||
console.info(`${width}x${height}`); // 6*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLocalStream() : Promise<MediaStream> {
|
|
||||||
return navigator.mediaDevices.getUserMedia(this.constraintsMedia).then((stream : MediaStream) => {
|
|
||||||
this.localStream = stream;
|
|
||||||
this.myCamVideo.srcObject = this.localStream;
|
|
||||||
|
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
|
||||||
/*this.mySoundMeter = null;
|
|
||||||
if(this.constraintsMedia.audio){
|
|
||||||
this.mySoundMeter = new SoundMeter();
|
|
||||||
this.mySoundMeter.connectToSource(stream, new AudioContext());
|
|
||||||
}*/
|
|
||||||
return stream;
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the camera from filming
|
|
||||||
*/
|
|
||||||
public stopCamera(): void {
|
|
||||||
if (this.localStream) {
|
|
||||||
for (const track of this.localStream.getVideoTracks()) {
|
|
||||||
track.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the microphone from listening
|
|
||||||
*/
|
|
||||||
public stopMicrophone(): void {
|
|
||||||
if (this.localStream) {
|
|
||||||
for (const track of this.localStream.getAudioTracks()) {
|
|
||||||
track.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//this.mySoundMeter?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCamera(id: string): Promise<MediaStream> {
|
|
||||||
let video = this.constraintsMedia.video;
|
|
||||||
if (typeof(video) === 'boolean' || video === undefined) {
|
|
||||||
video = {}
|
|
||||||
}
|
|
||||||
video.deviceId = {
|
|
||||||
exact: id
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getCamera();
|
|
||||||
}
|
|
||||||
|
|
||||||
setMicrophone(id: string): Promise<MediaStream> {
|
|
||||||
let audio = this.constraintsMedia.audio;
|
|
||||||
if (typeof(audio) === 'boolean' || audio === undefined) {
|
|
||||||
audio = {}
|
|
||||||
}
|
|
||||||
audio.deviceId = {
|
|
||||||
exact: id
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getCamera();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
|
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
|
||||||
|
@ -14,6 +14,8 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
|
|||||||
import {connectionManager} from "../Connexion/ConnectionManager";
|
import {connectionManager} from "../Connexion/ConnectionManager";
|
||||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
import {GameConnexionTypes} from "../Url/UrlManager";
|
||||||
import {blackListManager} from "./BlackListManager";
|
import {blackListManager} from "./BlackListManager";
|
||||||
|
import {get} from "svelte/store";
|
||||||
|
import {localStreamStore, obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||||
|
|
||||||
export interface UserSimplePeerInterface{
|
export interface UserSimplePeerInterface{
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -82,12 +84,11 @@ export class SimplePeer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mediaManager.showGameOverlay();
|
mediaManager.showGameOverlay();
|
||||||
mediaManager.getCamera().finally(() => {
|
|
||||||
//receive message start
|
//receive message start
|
||||||
this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
|
this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
|
||||||
this.receiveWebrtcStart(message);
|
this.receiveWebrtcStart(message);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => {
|
this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => {
|
||||||
this.closeConnection(data.userId);
|
this.closeConnection(data.userId);
|
||||||
@ -344,8 +345,15 @@ export class SimplePeer {
|
|||||||
if (!PeerConnection) {
|
if (!PeerConnection) {
|
||||||
throw new Error('While adding media, cannot find user with ID ' + userId);
|
throw new Error('While adding media, cannot find user with ID ' + userId);
|
||||||
}
|
}
|
||||||
const localStream: MediaStream | null = mediaManager.localStream;
|
|
||||||
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia})));
|
const result = get(localStreamStore);
|
||||||
|
|
||||||
|
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...result.constraints})));
|
||||||
|
|
||||||
|
if (result.type === 'error') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const localStream: MediaStream | null = result.stream;
|
||||||
|
|
||||||
if(!localStream){
|
if(!localStream){
|
||||||
return;
|
return;
|
||||||
|
@ -5,6 +5,8 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
|
|||||||
import {blackListManager} from "./BlackListManager";
|
import {blackListManager} from "./BlackListManager";
|
||||||
import type {Subscription} from "rxjs";
|
import type {Subscription} from "rxjs";
|
||||||
import type {UserSimplePeerInterface} from "./SimplePeer";
|
import type {UserSimplePeerInterface} from "./SimplePeer";
|
||||||
|
import {get} from "svelte/store";
|
||||||
|
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||||
|
|
||||||
@ -191,7 +193,7 @@ export class VideoPeer extends Peer {
|
|||||||
private pushVideoToRemoteUser() {
|
private pushVideoToRemoteUser() {
|
||||||
try {
|
try {
|
||||||
const localStream: MediaStream | null = mediaManager.localStream;
|
const localStream: MediaStream | null = mediaManager.localStream;
|
||||||
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia})));
|
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore)})));
|
||||||
|
|
||||||
if(!localStream){
|
if(!localStream){
|
||||||
return;
|
return;
|
||||||
|
@ -143,6 +143,11 @@ body .message-info.warning{
|
|||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
border-radius: 15px 15px 15px 15px;
|
border-radius: 15px 15px 15px 15px;
|
||||||
max-height: 20%;
|
max-height: 20%;
|
||||||
|
transition: right 350ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#div-myCamVideo.hide {
|
||||||
|
right: -20vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
video#myCamVideo{
|
video#myCamVideo{
|
||||||
@ -196,12 +201,12 @@ video#myCamVideo{
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
width: 15vw;
|
width: 180px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
}
|
}
|
||||||
/*btn animation*/
|
/*btn animation*/
|
||||||
@ -216,7 +221,6 @@ video#myCamVideo{
|
|||||||
border-radius: 48px;
|
border-radius: 48px;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
margin-bottom: 20px;
|
|
||||||
margin: 0 4%;
|
margin: 0 4%;
|
||||||
}
|
}
|
||||||
.btn-cam-action div.disabled {
|
.btn-cam-action div.disabled {
|
||||||
@ -248,6 +252,12 @@ video#myCamVideo{
|
|||||||
transition: all .2s;
|
transition: all .2s;
|
||||||
/*right: 224px;*/
|
/*right: 224px;*/
|
||||||
}
|
}
|
||||||
|
.btn-monitor.hide {
|
||||||
|
transform: translateY(60px);
|
||||||
|
}
|
||||||
|
.btn-cam-action:hover .btn-monitor.hide{
|
||||||
|
transform: translateY(60px);
|
||||||
|
}
|
||||||
.btn-copy{
|
.btn-copy{
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transition: all .3s;
|
transition: all .3s;
|
||||||
@ -346,6 +356,8 @@ video#myCamVideo{
|
|||||||
#myCamVideoSetup {
|
#myCamVideoSetup {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
-webkit-transform: scaleX(-1);
|
||||||
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
.webrtcsetup.active{
|
.webrtcsetup.active{
|
||||||
display: block;
|
display: block;
|
||||||
|
Loading…
Reference in New Issue
Block a user