Merge branch 'develop' of github.com:thecodingmachine/workadventure into metadataScriptingApi
This commit is contained in:
commit
5565ddd3f4
13
CHANGELOG.md
13
CHANGELOG.md
@ -8,16 +8,17 @@
|
||||
|
||||
### Updates
|
||||
|
||||
- Added the emote feature to Workadventure. (@Kharhamel, @Tabascoeye)
|
||||
- Added the emote feature to WorkAdventure. (@Kharhamel, @Tabascoeye)
|
||||
- The emote menu can be opened by clicking on your character.
|
||||
- Clicking on one of its element will close the menu and play an emote above your character.
|
||||
- This emote can be seen by other players.
|
||||
- Mobile support has been improved
|
||||
- WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
|
||||
- Mouse wheel support to zoom in / out
|
||||
- Pinch support on mobile to zoom in / out
|
||||
- Improved virtual joystick size (adapts to the zoom level)
|
||||
|
||||
- WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
|
||||
- Mouse wheel support to zoom in / out
|
||||
- Pinch support on mobile to zoom in / out
|
||||
- Improved virtual joystick size (adapts to the zoom level)
|
||||
- New scripting API features:
|
||||
- Use `WA.loadSound(): Sound` to load / play / stop a sound
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
@ -1251,9 +1251,9 @@ has-values@^1.0.0:
|
||||
kind-of "^4.0.0"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
http-errors@1.7.2:
|
||||
version "1.7.2"
|
||||
|
6
benchmark/package-lock.json
generated
6
benchmark/package-lock.json
generated
@ -230,9 +230,9 @@
|
||||
}
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "2.1.0",
|
||||
|
@ -169,8 +169,8 @@ graceful-fs@^4.1.2:
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
|
||||
indent-string@^2.1.0:
|
||||
version "2.1.0"
|
||||
|
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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="cowebsite" class="cowebsite hidden">
|
||||
@ -108,7 +107,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
<input type="checkbox" id="audioplayer_decrease_while_talking" checked />
|
||||
</label>
|
||||
|
@ -15,6 +15,9 @@ import type { UserInputChatEvent } from './UserInputChatEvent';
|
||||
import type { DataLayerEvent } from "./DataLayerEvent";
|
||||
import type { LayerEvent } from './LayerEvent';
|
||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||
import type {LoadSoundEvent} from "./LoadSoundEvent";
|
||||
import type {PlaySoundEvent} from "./PlaySoundEvent";
|
||||
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T
|
||||
@ -40,6 +43,9 @@ export type IframeEventMap = {
|
||||
setProperty: SetPropertyEvent
|
||||
getDataLayer: undefined
|
||||
//tilsetEvent: TilesetEvent
|
||||
loadSound: LoadSoundEvent
|
||||
playSound: PlaySoundEvent
|
||||
stopSound: null
|
||||
}
|
||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||
type: T;
|
||||
|
11
front/src/Api/Events/LoadSoundEvent.ts
Normal file
11
front/src/Api/Events/LoadSoundEvent.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isLoadSoundEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type LoadSoundEvent = tg.GuardedType<typeof isLoadSoundEvent>;
|
24
front/src/Api/Events/PlaySoundEvent.ts
Normal file
24
front/src/Api/Events/PlaySoundEvent.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
const isSoundConfig =
|
||||
new tg.IsInterface().withProperties({
|
||||
volume: tg.isOptional(tg.isNumber),
|
||||
loop: tg.isOptional(tg.isBoolean),
|
||||
mute: tg.isOptional(tg.isBoolean),
|
||||
rate: tg.isOptional(tg.isNumber),
|
||||
detune: tg.isOptional(tg.isNumber),
|
||||
seek: tg.isOptional(tg.isNumber),
|
||||
delay: tg.isOptional(tg.isNumber)
|
||||
}).get();
|
||||
|
||||
export const isPlaySoundEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
config : tg.isOptional(isSoundConfig),
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type PlaySoundEvent = tg.GuardedType<typeof isPlaySoundEvent>;
|
11
front/src/Api/Events/StopSoundEvent.ts
Normal file
11
front/src/Api/Events/StopSoundEvent.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isStopSoundEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type StopSoundEvent = tg.GuardedType<typeof isStopSoundEvent>;
|
@ -1,3 +1,4 @@
|
||||
|
||||
import { Subject } from "rxjs";
|
||||
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
|
||||
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||
@ -20,8 +21,9 @@ import type { DataLayerEvent } from "./Events/DataLayerEvent";
|
||||
import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent';
|
||||
import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent';
|
||||
//import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent";
|
||||
|
||||
|
||||
import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
|
||||
import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
|
||||
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
|
||||
/**
|
||||
* Listens to messages from iframes and turn those messages into easy to use observables.
|
||||
* Also allows to send messages to those iframes.
|
||||
@ -82,6 +84,15 @@ class IframeListener {
|
||||
/* private readonly _tilesetLoaderStream: Subject<TilesetEvent> = new Subject();
|
||||
public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable();*/
|
||||
|
||||
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
|
||||
public readonly playSoundStream = this._playSoundStream.asObservable();
|
||||
|
||||
private readonly _stopSoundStream: Subject<StopSoundEvent> = new Subject();
|
||||
public readonly stopSoundStream = this._stopSoundStream.asObservable();
|
||||
|
||||
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
|
||||
public readonly loadSoundStream = this._loadSoundStream.asObservable();
|
||||
|
||||
private readonly iframes = new Set<HTMLIFrameElement>();
|
||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
private sendPlayerMove: boolean = false;
|
||||
@ -123,6 +134,15 @@ class IframeListener {
|
||||
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
|
||||
scriptUtils.goToPage(payload.data.url);
|
||||
}
|
||||
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
|
||||
this._playSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
|
||||
this._stopSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
|
||||
this._loadSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
|
||||
const scriptUrl = [...this.scripts.keys()].find(key => {
|
||||
return this.scripts.get(key)?.contentWindow == message.source
|
||||
@ -130,9 +150,11 @@ class IframeListener {
|
||||
|
||||
scriptUtils.openCoWebsite(payload.data.url, scriptUrl || foundSrc);
|
||||
}
|
||||
|
||||
else if (payload.type === 'closeCoWebSite') {
|
||||
scriptUtils.closeCoWebSite();
|
||||
}
|
||||
|
||||
else if (payload.type === 'disablePlayerControls') {
|
||||
this._disablePlayerControlStream.next();
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export abstract class DirtyScene extends ResizableScene {
|
||||
private isAlreadyTracking: boolean = false;
|
||||
protected dirty:boolean = true;
|
||||
private objectListChanged:boolean = true;
|
||||
private physicsEnabled: boolean = false;
|
||||
|
||||
/**
|
||||
* Track all objects added to the scene and adds a callback each time an animation is added.
|
||||
@ -38,6 +39,27 @@ export abstract class DirtyScene extends ResizableScene {
|
||||
this.objectListChanged = false;
|
||||
this.dirty = false;
|
||||
});
|
||||
|
||||
this.physics.disableUpdate();
|
||||
this.events.on(Events.POST_UPDATE, () => {
|
||||
let objectMoving = false;
|
||||
for (const body of this.physics.world.bodies.entries) {
|
||||
if (body.velocity.x !== 0 || body.velocity.y !== 0) {
|
||||
this.objectListChanged = true;
|
||||
objectMoving = true;
|
||||
if (!this.physicsEnabled) {
|
||||
this.physics.enableUpdate();
|
||||
this.physicsEnabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!objectMoving && this.physicsEnabled) {
|
||||
this.physics.disableUpdate();
|
||||
this.physicsEnabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private trackAnimation(): void {
|
||||
|
@ -52,6 +52,7 @@ import { mediaManager } from "../../WebRtc/MediaManager";
|
||||
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
|
||||
import type { ActionableItem } from "../Items/ActionableItem";
|
||||
import { UserInputManager } from "../UserInput/UserInputManager";
|
||||
import {soundManager} from "./SoundManager";
|
||||
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
|
||||
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
@ -92,7 +93,8 @@ import { PinchManager } from "../UserInput/PinchManager";
|
||||
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
|
||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
import { EmoteManager } from "./EmoteManager";
|
||||
import { peerStore} from "../../Stores/PeerStore";
|
||||
import {EmoteManager } from "./EmoteManager";
|
||||
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
|
||||
import { MenuScene, MenuSceneName } from '../Menu/MenuScene';
|
||||
|
||||
@ -189,9 +191,7 @@ export class GameScene extends DirtyScene implements CenterListener {
|
||||
private popUpElements : Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
|
||||
private originalMapUrl: string | undefined;
|
||||
private pinchManager: PinchManager | undefined;
|
||||
private physicsEnabled: boolean = true;
|
||||
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
||||
private onVisibilityChangeCallback: () => void;
|
||||
private emoteManager!: EmoteManager;
|
||||
|
||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||
@ -212,7 +212,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
||||
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
|
||||
this.connectionAnswerPromiseResolve = resolve;
|
||||
});
|
||||
this.onVisibilityChangeCallback = this.onVisibilityChange.bind(this);
|
||||
}
|
||||
|
||||
//hook preload scene
|
||||
@ -507,8 +506,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
||||
if (!this.room.isDisconnected()) {
|
||||
this.connect();
|
||||
}
|
||||
console.log('display');
|
||||
document.addEventListener('visibilitychange', this.onVisibilityChangeCallback);
|
||||
|
||||
this.emoteManager = new EmoteManager(this);
|
||||
}
|
||||
@ -615,6 +612,7 @@ export class GameScene extends DirtyScene implements CenterListener {
|
||||
|
||||
// When connection is performed, let's connect SimplePeer
|
||||
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
|
||||
peerStore.connectToSimplePeer(this.simplePeer);
|
||||
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
|
||||
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
|
||||
|
||||
@ -632,7 +630,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
||||
self.chatModeSprite.setVisible(false);
|
||||
self.openChatIcon.setVisible(false);
|
||||
audioManager.restoreVolume();
|
||||
self.onVisibilityChange();
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -870,6 +867,24 @@ ${escapedMessage}
|
||||
this.userInputManager.disableControls();
|
||||
}));
|
||||
|
||||
this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent)=>
|
||||
{
|
||||
const url = new URL(playSoundEvent.url, this.MapUrlFile);
|
||||
soundManager.playSound(this.load,this.sound,url.toString(),playSoundEvent.config);
|
||||
}))
|
||||
|
||||
this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent)=>
|
||||
{
|
||||
const url = new URL(stopSoundEvent.url, this.MapUrlFile);
|
||||
soundManager.stopSound(this.sound,url.toString());
|
||||
}))
|
||||
|
||||
this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent)=>
|
||||
{
|
||||
const url = new URL(loadSoundEvent.url, this.MapUrlFile);
|
||||
soundManager.loadSound(this.load,this.sound,url.toString());
|
||||
}))
|
||||
|
||||
this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(() => {
|
||||
this.userInputManager.restoreControls();
|
||||
}));
|
||||
@ -1000,8 +1015,6 @@ ${escapedMessage}
|
||||
for (const iframeEvents of this.iframeSubscriptionList) {
|
||||
iframeEvents.unsubscribe();
|
||||
}
|
||||
|
||||
document.removeEventListener('visibilitychange', this.onVisibilityChangeCallback);
|
||||
}
|
||||
|
||||
private removeAllRemotePlayers(): void {
|
||||
@ -1150,8 +1163,6 @@ ${escapedMessage}
|
||||
}
|
||||
|
||||
createCollisionWithPlayer() {
|
||||
this.physics.disableUpdate();
|
||||
this.physicsEnabled = false;
|
||||
//add collision layer
|
||||
for (const phaserLayer of this.gameMap.phaserLayers) {
|
||||
if (phaserLayer.type == "tilelayer") {
|
||||
@ -1294,20 +1305,7 @@ ${escapedMessage}
|
||||
update(time: number, delta: number): void {
|
||||
mediaManager.updateScene();
|
||||
this.currentTick = time;
|
||||
if (this.CurrentPlayer.isMoving()) {
|
||||
this.dirty = true;
|
||||
}
|
||||
this.CurrentPlayer.moveUser(delta);
|
||||
if (this.CurrentPlayer.isMoving()) {
|
||||
this.dirty = true;
|
||||
if (!this.physicsEnabled) {
|
||||
this.physics.enableUpdate();
|
||||
this.physicsEnabled = true;
|
||||
}
|
||||
} else if (this.physicsEnabled) {
|
||||
this.physics.disableUpdate();
|
||||
this.physicsEnabled = false;
|
||||
}
|
||||
|
||||
// Let's handle all events
|
||||
while (this.pendingEvents.length !== 0) {
|
||||
@ -1575,8 +1573,6 @@ ${escapedMessage}
|
||||
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi', () => {
|
||||
this.stopJitsi();
|
||||
});
|
||||
|
||||
this.onVisibilityChange();
|
||||
}
|
||||
|
||||
public stopJitsi(): void {
|
||||
@ -1585,7 +1581,6 @@ ${escapedMessage}
|
||||
mediaManager.showGameOverlay();
|
||||
|
||||
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
|
||||
this.onVisibilityChange();
|
||||
}
|
||||
|
||||
//todo: put this into an 'orchestrator' scene (EntryScene?)
|
||||
@ -1625,20 +1620,4 @@ ${escapedMessage}
|
||||
waScaleManager.zoomModifier *= zoomFactor;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
front/src/Phaser/Game/SoundManager.ts
Normal file
37
front/src/Phaser/Game/SoundManager.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
||||
import BaseSoundManager = Phaser.Sound.BaseSoundManager;
|
||||
import BaseSound = Phaser.Sound.BaseSound;
|
||||
import SoundConfig = Phaser.Types.Sound.SoundConfig;
|
||||
|
||||
class SoundManager {
|
||||
private soundPromises : Map<string,Promise<BaseSound>> = new Map<string, Promise<Phaser.Sound.BaseSound>>();
|
||||
public loadSound (loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string) : Promise<BaseSound> {
|
||||
let soundPromise = this.soundPromises.get(soundUrl);
|
||||
if (soundPromise !== undefined) {
|
||||
return soundPromise;
|
||||
}
|
||||
soundPromise = new Promise<BaseSound>((res) => {
|
||||
|
||||
const sound = soundManager.get(soundUrl);
|
||||
if (sound !== null) {
|
||||
return res(sound);
|
||||
}
|
||||
loadPlugin.audio(soundUrl, soundUrl);
|
||||
loadPlugin.once('filecomplete-audio-' + soundUrl, () => res(soundManager.add(soundUrl)));
|
||||
loadPlugin.start();
|
||||
});
|
||||
this.soundPromises.set(soundUrl,soundPromise);
|
||||
return soundPromise;
|
||||
}
|
||||
|
||||
public async playSound(loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string, config: SoundConfig|undefined) : Promise<void> {
|
||||
const sound = await this.loadSound(loadPlugin,soundManager,soundUrl);
|
||||
if (config === undefined) sound.play();
|
||||
else sound.play(config);
|
||||
}
|
||||
|
||||
public stopSound(soundManager : BaseSoundManager,soundUrl : string){
|
||||
soundManager.get(soundUrl).stop();
|
||||
}
|
||||
}
|
||||
export const soundManager = new SoundManager();
|
@ -10,6 +10,14 @@ import {PinchManager} from "../UserInput/PinchManager";
|
||||
import Zone = Phaser.GameObjects.Zone;
|
||||
import { MenuScene } from "../Menu/MenuScene";
|
||||
import {ResizableScene} from "./ResizableScene";
|
||||
import {
|
||||
audioConstraintStore,
|
||||
enableCameraSceneVisibilityStore,
|
||||
localStreamStore,
|
||||
mediaStreamConstraintsStore,
|
||||
videoConstraintStore
|
||||
} from "../../Stores/MediaStore";
|
||||
import type {Unsubscriber} from "svelte/store";
|
||||
|
||||
export const EnableCameraSceneName = "EnableCameraScene";
|
||||
enum LoginTextures {
|
||||
@ -40,6 +48,7 @@ export class EnableCameraScene extends ResizableScene {
|
||||
private enableCameraSceneElement!: Phaser.GameObjects.DOMElement;
|
||||
|
||||
private mobileTapZone!: Zone;
|
||||
private localStreamStoreUnsubscriber!: Unsubscriber;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@ -119,9 +128,20 @@ export class EnableCameraScene extends ResizableScene {
|
||||
|
||||
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.setupStream.bind(this));
|
||||
mediaPromise.then(this.setupStream.bind(this));*/
|
||||
|
||||
this.input.keyboard.on('keydown-RIGHT', this.nextCam.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.onResize();
|
||||
|
||||
enableCameraSceneVisibilityStore.showEnableCameraScene();
|
||||
}
|
||||
|
||||
private previousCam(): void {
|
||||
@ -140,7 +162,9 @@ export class EnableCameraScene extends ResizableScene {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
@ -148,8 +172,10 @@ export class EnableCameraScene extends ResizableScene {
|
||||
return;
|
||||
}
|
||||
this.cameraSelected++;
|
||||
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
|
||||
|
||||
// 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 {
|
||||
@ -157,7 +183,8 @@ export class EnableCameraScene extends ResizableScene {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
@ -165,8 +192,9 @@ export class EnableCameraScene extends ResizableScene {
|
||||
return;
|
||||
}
|
||||
this.microphoneSelected++;
|
||||
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
|
||||
// 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';
|
||||
this.soundMeter.stop();
|
||||
|
||||
mediaManager.stopCamera();
|
||||
mediaManager.stopMicrophone();
|
||||
enableCameraSceneVisibilityStore.hideEnableCameraScene();
|
||||
this.localStreamStoreUnsubscriber();
|
||||
//mediaManager.stopCamera();
|
||||
//mediaManager.stopMicrophone();
|
||||
|
||||
this.scene.sleep(EnableCameraSceneName)
|
||||
this.scene.sleep(EnableCameraSceneName);
|
||||
gameManager.goToStartingMap(this.scene);
|
||||
}
|
||||
|
||||
private async getDevices() {
|
||||
// TODO: switch this in a store.
|
||||
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
|
||||
this.microphonesList = [];
|
||||
this.camerasList = [];
|
||||
for (const mediaDeviceInfo of mediaDeviceInfos) {
|
||||
if (mediaDeviceInfo.kind === 'audioinput') {
|
||||
this.microphonesList.push(mediaDeviceInfo);
|
||||
|
@ -2,6 +2,8 @@ import {mediaManager} from "../../WebRtc/MediaManager";
|
||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {DirtyScene} from "../Game/DirtyScene";
|
||||
import {get} from "svelte/store";
|
||||
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
|
||||
|
||||
export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene';
|
||||
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();
|
||||
localUserStore.setHelpCameraSettingsShown();
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import {menuIconVisible} from "../../Stores/MenuStore";
|
||||
import { HtmlUtils } from '../../WebRtc/HtmlUtils';
|
||||
import { iframeListener } from '../../Api/IframeListener';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { videoConstraintStore } from "../../Stores/MediaStore";
|
||||
|
||||
export const MenuSceneName = 'MenuScene';
|
||||
const gameMenuKey = 'gameMenu';
|
||||
@ -356,7 +357,7 @@ export class MenuScene extends Phaser.Scene {
|
||||
if (valueVideo !== this.videoQualityValue) {
|
||||
this.videoQualityValue = valueVideo;
|
||||
localUserStore.setVideoQualityValue(valueVideo);
|
||||
mediaManager.updateCameraQuality(valueVideo);
|
||||
videoConstraintStore.setFrameRate(valueVideo);
|
||||
}
|
||||
this.closeGameQualityMenu();
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {PlayerAnimationDirections} from "./Animation";
|
||||
import type {GameScene} from "../Game/GameScene";
|
||||
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
|
||||
import {Character} from "../Entity/Character";
|
||||
import {userMovingStore} from "../../Stores/GameStore";
|
||||
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
|
||||
|
||||
export const hasMovedEventName = "hasMoved";
|
||||
@ -86,6 +87,7 @@ export class Player extends Character {
|
||||
this.previousDirection = direction;
|
||||
}
|
||||
this.wasMoving = moving;
|
||||
userMovingStore.set(moving);
|
||||
}
|
||||
|
||||
public isMoving(): boolean {
|
||||
@ -99,7 +101,7 @@ export class Player extends Character {
|
||||
this.openEmoteMenu(emotes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
isClickable(): boolean {
|
||||
return true;
|
||||
}
|
||||
@ -113,13 +115,13 @@ export class Player extends Character {
|
||||
this.playEmote(item.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
closeEmoteMenu(): void {
|
||||
if (!this.emoteMenu) return;
|
||||
this.emoteMenu.destroy();
|
||||
this.emoteMenu = null;
|
||||
}
|
||||
|
||||
|
||||
destroy() {
|
||||
this.scene.events.removeListener('postupdate', this.updateListener);
|
||||
super.destroy();
|
||||
|
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 {mediaManager} from "./MediaManager";
|
||||
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
|
||||
|
||||
interface jitsiConfigInterface {
|
||||
@ -10,10 +12,9 @@ interface jitsiConfigInterface {
|
||||
}
|
||||
|
||||
const getDefaultConfig = () : jitsiConfigInterface => {
|
||||
const constraints = mediaManager.getConstraintRequestedByUser();
|
||||
return {
|
||||
startWithAudioMuted: !constraints.audio,
|
||||
startWithVideoMuted: constraints.video === false,
|
||||
startWithAudioMuted: !get(requestedMicrophoneState),
|
||||
startWithVideoMuted: !get(requestedCameraState),
|
||||
prejoinPageEnabled: false
|
||||
}
|
||||
}
|
||||
@ -72,7 +73,6 @@ class JitsiFactory {
|
||||
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
private audioCallback = this.onAudioChange.bind(this);
|
||||
private videoCallback = this.onVideoChange.bind(this);
|
||||
private previousConfigMeet! : jitsiConfigInterface;
|
||||
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 {
|
||||
//save previous config
|
||||
this.previousConfigMeet = getDefaultConfig();
|
||||
|
||||
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
|
||||
// Jitsi meet external API maintains some data in local storage
|
||||
// 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('videoMuteStatusChanged', this.videoCallback);
|
||||
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 {
|
||||
this.previousConfigMeet.startWithAudioMuted = muted;
|
||||
if (muted) {
|
||||
requestedMicrophoneState.disableMicrophone();
|
||||
} else {
|
||||
requestedMicrophoneState.enableMicrophone();
|
||||
}
|
||||
}
|
||||
|
||||
private onVideoChange({muted}: {muted: boolean}): void {
|
||||
this.previousConfigMeet.startWithVideoMuted = muted;
|
||||
if (muted) {
|
||||
requestedCameraState.disableWebcam();
|
||||
} else {
|
||||
requestedCameraState.enableWebcam();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadJitsiScript(domain: string): Promise<void> {
|
||||
|
@ -6,10 +6,21 @@ import { localUserStore } from "../Connexion/LocalUserStore";
|
||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
||||
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
|
||||
|
||||
let videoConstraint: boolean | MediaTrackConstraints = {
|
||||
const videoConstraint: boolean | MediaTrackConstraints = {
|
||||
width: { min: 640, ideal: 1280, max: 1920 },
|
||||
height: { min: 400, ideal: 720 },
|
||||
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 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 {
|
||||
localStream: MediaStream | null = null;
|
||||
localScreenCapture: MediaStream | null = null;
|
||||
@ -47,10 +57,7 @@ export class MediaManager {
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
//mySoundMeterElement: HTMLDivElement;
|
||||
private webrtcOutAudio: HTMLAudioElement;
|
||||
constraintsMedia: MediaStreamConstraints = {
|
||||
audio: audioConstraint,
|
||||
video: videoConstraint
|
||||
};
|
||||
|
||||
updatedLocalStreamCallBacks: Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
|
||||
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||
@ -61,10 +68,8 @@ export class MediaManager {
|
||||
private cinemaBtn: HTMLDivElement;
|
||||
private monitorBtn: HTMLDivElement;
|
||||
|
||||
private previousConstraint: MediaStreamConstraints;
|
||||
private focused: boolean = true;
|
||||
|
||||
private hasCamera = true;
|
||||
private focused: boolean = true;
|
||||
|
||||
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
|
||||
|
||||
@ -88,14 +93,12 @@ export class MediaManager {
|
||||
this.microphoneClose.style.display = "none";
|
||||
this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.enableMicrophone();
|
||||
//update tracking
|
||||
requestedMicrophoneState.enableMicrophone();
|
||||
});
|
||||
this.microphone = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone');
|
||||
this.microphone.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.disableMicrophone();
|
||||
//update tracking
|
||||
requestedMicrophoneState.disableMicrophone();
|
||||
});
|
||||
|
||||
this.cinemaBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-video');
|
||||
@ -103,14 +106,12 @@ export class MediaManager {
|
||||
this.cinemaClose.style.display = "none";
|
||||
this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.enableCamera();
|
||||
//update tracking
|
||||
requestedCameraState.enableWebcam();
|
||||
});
|
||||
this.cinema = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema');
|
||||
this.cinema.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.disableCamera();
|
||||
//update tracking
|
||||
requestedCameraState.disableWebcam();
|
||||
});
|
||||
|
||||
this.monitorBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
|
||||
@ -118,21 +119,20 @@ export class MediaManager {
|
||||
this.monitorClose.style.display = "block";
|
||||
this.monitorClose.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.enableScreenSharing();
|
||||
//update tracking
|
||||
//this.enableScreenSharing();
|
||||
requestedScreenSharingState.enableScreenSharing();
|
||||
});
|
||||
this.monitor = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor');
|
||||
this.monitor.style.display = "none";
|
||||
this.monitor.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.disableScreenSharing();
|
||||
//update tracking
|
||||
//this.disableScreenSharing();
|
||||
requestedScreenSharingState.disableScreenSharing();
|
||||
});
|
||||
|
||||
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
|
||||
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.childNodes.forEach((value: ChildNode, index) => {
|
||||
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
|
||||
@ -140,37 +140,98 @@ export class MediaManager {
|
||||
|
||||
//Check of ask notification navigator permission
|
||||
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(){
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
//FIX ME SOUNDMETER: check stability of sound meter calculation
|
||||
//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 {
|
||||
this.updatedLocalStreamCallBacks.add(callback);
|
||||
}
|
||||
@ -214,6 +275,8 @@ export class MediaManager {
|
||||
this.triggerCloseJitsiFrameButton();
|
||||
}
|
||||
buttonCloseFrame.removeEventListener('click', functionTrigger);
|
||||
|
||||
gameOverlayVisibilityStore.showGameOverlay();
|
||||
}
|
||||
|
||||
public hideGameOverlay(): void {
|
||||
@ -225,110 +288,8 @@ export class MediaManager {
|
||||
this.triggerCloseJitsiFrameButton();
|
||||
}
|
||||
buttonCloseFrame.addEventListener('click', functionTrigger);
|
||||
}
|
||||
|
||||
public isGameOverlayVisible(): boolean {
|
||||
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);
|
||||
});
|
||||
gameOverlayVisibilityStore.hideGameOverlay();
|
||||
}
|
||||
|
||||
private enableCameraStyle() {
|
||||
@ -341,8 +302,6 @@ export class MediaManager {
|
||||
this.cinemaClose.style.display = "block";
|
||||
this.cinema.style.display = "none";
|
||||
this.cinemaBtn.classList.add("disabled");
|
||||
this.constraintsMedia.video = false;
|
||||
this.myCamVideo.srcObject = null;
|
||||
}
|
||||
|
||||
private enableMicrophoneStyle() {
|
||||
@ -355,185 +314,18 @@ export class MediaManager {
|
||||
this.microphoneClose.style.display = "block";
|
||||
this.microphone.style.display = "none";
|
||||
this.microphoneBtn.classList.add("disabled");
|
||||
this.constraintsMedia.audio = false;
|
||||
}
|
||||
|
||||
private enableScreenSharing() {
|
||||
this.getScreenMedia().then((stream) => {
|
||||
this.triggerStartedScreenSharingCallbacks(stream);
|
||||
this.monitorClose.style.display = "none";
|
||||
this.monitor.style.display = "block";
|
||||
this.monitorBtn.classList.add("enabled");
|
||||
}, () => {
|
||||
this.monitorClose.style.display = "block";
|
||||
this.monitor.style.display = "none";
|
||||
this.monitorBtn.classList.remove("enabled");
|
||||
|
||||
layoutManager.addInformation('warning', 'Screen sharing access denied. Click here and check navigators permissions.', () => {
|
||||
this.showHelpCameraSettingsCallBack();
|
||||
}, this.userInputManager);
|
||||
});
|
||||
|
||||
private enableScreenSharingStyle(){
|
||||
this.monitorClose.style.display = "none";
|
||||
this.monitor.style.display = "block";
|
||||
this.monitorBtn.classList.add("enabled");
|
||||
}
|
||||
|
||||
private disableScreenSharing() {
|
||||
private disableScreenSharingStyle(){
|
||||
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 = "") {
|
||||
|
@ -14,6 +14,8 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import {connectionManager} from "../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
||||
import {blackListManager} from "./BlackListManager";
|
||||
import {get} from "svelte/store";
|
||||
import {localStreamStore, obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||
|
||||
export interface UserSimplePeerInterface{
|
||||
userId: number;
|
||||
@ -82,11 +84,10 @@ export class SimplePeer {
|
||||
});
|
||||
|
||||
mediaManager.showGameOverlay();
|
||||
mediaManager.getCamera().finally(() => {
|
||||
//receive message start
|
||||
this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
|
||||
this.receiveWebrtcStart(message);
|
||||
});
|
||||
|
||||
//receive message start
|
||||
this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
|
||||
this.receiveWebrtcStart(message);
|
||||
});
|
||||
|
||||
this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => {
|
||||
@ -344,8 +345,15 @@ export class SimplePeer {
|
||||
if (!PeerConnection) {
|
||||
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){
|
||||
return;
|
||||
|
@ -5,6 +5,8 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import {blackListManager} from "./BlackListManager";
|
||||
import type {Subscription} from "rxjs";
|
||||
import type {UserSimplePeerInterface} from "./SimplePeer";
|
||||
import {get} from "svelte/store";
|
||||
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||
|
||||
@ -191,7 +193,7 @@ export class VideoPeer extends Peer {
|
||||
private pushVideoToRemoteUser() {
|
||||
try {
|
||||
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){
|
||||
return;
|
||||
|
@ -17,6 +17,10 @@ import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent";
|
||||
import type { ITiledMap } from "./Phaser/Map/ITiledMap";
|
||||
import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent";
|
||||
import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent";
|
||||
import type {PlaySoundEvent} from "./Api/Events/PlaySoundEvent";
|
||||
import type {StopSoundEvent} from "./Api/Events/StopSoundEvent";
|
||||
import type {LoadSoundEvent} from "./Api/Events/LoadSoundEvent";
|
||||
import SoundConfig = Phaser.Types.Sound.SoundConfig;
|
||||
|
||||
interface WorkAdventureApi {
|
||||
sendChatMessage(message: string, author: string): void;
|
||||
@ -39,6 +43,7 @@ interface WorkAdventureApi {
|
||||
restorePlayerControls(): void;
|
||||
displayBubble(): void;
|
||||
removeBubble(): void;
|
||||
loadSound(url : string): Sound;
|
||||
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void
|
||||
getCurrentUser(): Promise<User>
|
||||
getCurrentRoom(): Promise<Room>
|
||||
@ -91,7 +96,7 @@ interface ButtonDescriptor {
|
||||
callback: ButtonClickedCallback,
|
||||
}
|
||||
|
||||
class Popup {
|
||||
export class Popup {
|
||||
constructor(private id: number) {
|
||||
}
|
||||
|
||||
@ -108,12 +113,40 @@ class Popup {
|
||||
}
|
||||
}
|
||||
|
||||
/*function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}*/
|
||||
export class Sound {
|
||||
constructor(private url: string) {
|
||||
window.parent.postMessage({
|
||||
"type" : 'loadSound',
|
||||
"data": {
|
||||
url: this.url,
|
||||
} as LoadSoundEvent
|
||||
|
||||
},'*');
|
||||
}
|
||||
|
||||
public play(config : SoundConfig) {
|
||||
window.parent.postMessage({
|
||||
"type" : 'playSound',
|
||||
"data": {
|
||||
url: this.url,
|
||||
config
|
||||
} as PlaySoundEvent
|
||||
|
||||
},'*');
|
||||
return this.url;
|
||||
}
|
||||
public stop() {
|
||||
window.parent.postMessage({
|
||||
"type" : 'stopSound',
|
||||
"data": {
|
||||
url: this.url,
|
||||
} as StopSoundEvent
|
||||
|
||||
},'*');
|
||||
return this.url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getGameState(): Promise<GameStateEvent> {
|
||||
if (immutableData) {
|
||||
@ -256,7 +289,11 @@ window.WA = {
|
||||
}, '*');
|
||||
},
|
||||
|
||||
goToPage(url: string): void {
|
||||
loadSound(url: string) : Sound {
|
||||
return new Sound(url);
|
||||
},
|
||||
|
||||
goToPage(url : string) : void{
|
||||
window.parent.postMessage({
|
||||
"type": 'goToPage',
|
||||
"data": {
|
||||
|
@ -143,6 +143,11 @@ body .message-info.warning{
|
||||
bottom: 30px;
|
||||
border-radius: 15px 15px 15px 15px;
|
||||
max-height: 20%;
|
||||
transition: right 350ms;
|
||||
}
|
||||
|
||||
#div-myCamVideo.hide {
|
||||
right: -20vw;
|
||||
}
|
||||
|
||||
video#myCamVideo{
|
||||
@ -196,12 +201,12 @@ video#myCamVideo{
|
||||
display: inline-flex;
|
||||
bottom: 10px;
|
||||
right: 15px;
|
||||
width: 15vw;
|
||||
width: 180px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
justify-items: center;
|
||||
}
|
||||
/*btn animation*/
|
||||
@ -216,7 +221,6 @@ video#myCamVideo{
|
||||
border-radius: 48px;
|
||||
transform: translateY(20px);
|
||||
transition-timing-function: ease-in-out;
|
||||
margin-bottom: 20px;
|
||||
margin: 0 4%;
|
||||
}
|
||||
.btn-cam-action div.disabled {
|
||||
@ -248,6 +252,12 @@ video#myCamVideo{
|
||||
transition: all .2s;
|
||||
/*right: 224px;*/
|
||||
}
|
||||
.btn-monitor.hide {
|
||||
transform: translateY(60px);
|
||||
}
|
||||
.btn-cam-action:hover .btn-monitor.hide{
|
||||
transform: translateY(60px);
|
||||
}
|
||||
.btn-copy{
|
||||
pointer-events: auto;
|
||||
transition: all .3s;
|
||||
@ -346,6 +356,8 @@ video#myCamVideo{
|
||||
#myCamVideoSetup {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
.webrtcsetup.active{
|
||||
display: block;
|
||||
|
@ -5,6 +5,12 @@ var targetObjectTutoBubble ='Tutobubble';
|
||||
var targetObjectTutoChat ='tutoChat';
|
||||
var targetObjectTutoExplanation ='tutoExplanation';
|
||||
var popUpExplanation = undefined;
|
||||
var enterSoundUrl = "webrtc-in.mp3";
|
||||
var exitSoundUrl = "webrtc-out.mp3";
|
||||
var soundConfig = {
|
||||
volume : 0.2,
|
||||
loop : false
|
||||
}
|
||||
function launchTuto (){
|
||||
WA.openPopup(targetObjectTutoBubble, textFirstPopup, [
|
||||
{
|
||||
@ -25,7 +31,8 @@ function launchTuto (){
|
||||
label: "Got it!",
|
||||
className : "success",callback:(popup2 => {
|
||||
popup2.close();
|
||||
WA.restorePlayerControls();
|
||||
WA.restorePlayerControl();
|
||||
WA.loadSound(winSoundUrl).play(soundConfig);
|
||||
})
|
||||
}
|
||||
])
|
||||
@ -36,13 +43,14 @@ function launchTuto (){
|
||||
}
|
||||
}
|
||||
]);
|
||||
WA.disablePlayerControls();
|
||||
WA.disablePlayerControl();
|
||||
|
||||
}
|
||||
|
||||
|
||||
WA.onEnterZone('popupZone', () => {
|
||||
WA.displayBubble();
|
||||
WA.loadSound(enterSoundUrl).play(soundConfig);
|
||||
if (!isFirstTimeTuto) {
|
||||
isFirstTimeTuto = true;
|
||||
launchTuto();
|
||||
@ -71,4 +79,5 @@ WA.onEnterZone('popupZone', () => {
|
||||
WA.onLeaveZone('popupZone', () => {
|
||||
if (popUpExplanation !== undefined) popUpExplanation.close();
|
||||
WA.removeBubble();
|
||||
WA.loadSound(exitSoundUrl).play(soundConfig);
|
||||
})
|
||||
|
BIN
maps/Tuto/webrtc-in.mp3
Normal file
BIN
maps/Tuto/webrtc-in.mp3
Normal file
Binary file not shown.
BIN
maps/Tuto/webrtc-out.mp3
Normal file
BIN
maps/Tuto/webrtc-out.mp3
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
maps/tests/Audience.mp3
Normal file
BIN
maps/tests/Audience.mp3
Normal file
Binary file not shown.
44
maps/tests/SoundScript.js
Normal file
44
maps/tests/SoundScript.js
Normal file
@ -0,0 +1,44 @@
|
||||
var zonePlaySound = "PlaySound";
|
||||
var zonePlaySoundLoop = "playSoundLoop";
|
||||
var stopSound = "StopSound";
|
||||
var loopConfig ={
|
||||
volume : 0.5,
|
||||
loop : true
|
||||
}
|
||||
var configBase = {
|
||||
volume : 0.5,
|
||||
loop : false
|
||||
}
|
||||
var enterSoundUrl = "webrtc-in.mp3";
|
||||
var exitSoundUrl = "webrtc-out.mp3";
|
||||
var winSoundUrl = "Win.ogg";
|
||||
var enterSound;
|
||||
var exitSound;
|
||||
var winSound;
|
||||
loadAllSounds();
|
||||
winSound.play(configBase);
|
||||
WA.onEnterZone(zonePlaySound, () => {
|
||||
enterSound.play(configBase);
|
||||
})
|
||||
|
||||
WA.onEnterZone(zonePlaySoundLoop, () => {
|
||||
winSound.play(loopConfig);
|
||||
})
|
||||
|
||||
WA.onLeaveZone(zonePlaySoundLoop, () => {
|
||||
winSound.stop();
|
||||
})
|
||||
|
||||
WA.onEnterZone('popupZone', () => {
|
||||
|
||||
});
|
||||
|
||||
WA.onLeaveZone('popupZone', () => {
|
||||
|
||||
})
|
||||
|
||||
function loadAllSounds(){
|
||||
winSound = WA.loadSound(winSoundUrl);
|
||||
enterSound = WA.loadSound(enterSoundUrl);
|
||||
exitSound = WA.loadSound(exitSoundUrl);
|
||||
}
|
154
maps/tests/SoundTest.json
Normal file
154
maps/tests/SoundTest.json
Normal file
@ -0,0 +1,154 @@
|
||||
{ "compressionlevel":-1,
|
||||
"height":20,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":20,
|
||||
"id":2,
|
||||
"name":"start",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
"height":20,
|
||||
"id":4,
|
||||
"name":"floor",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":20,
|
||||
"id":3,
|
||||
"name":"playSound",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"zone",
|
||||
"type":"string",
|
||||
"value":"PlaySound"
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":20,
|
||||
"id":6,
|
||||
"name":"playSoundLoop",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"zone",
|
||||
"type":"string",
|
||||
"value":"playSoundLoop"
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":20,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":5,
|
||||
"name":"floorLayer",
|
||||
"objects":[
|
||||
{
|
||||
"height":19.296875,
|
||||
"id":2,
|
||||
"name":"",
|
||||
"rotation":0,
|
||||
"text":
|
||||
{
|
||||
"text":"Play Sound",
|
||||
"wrap":true
|
||||
},
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":107.109375,
|
||||
"x":258.4453125,
|
||||
"y":197.018229166667
|
||||
},
|
||||
{
|
||||
"height":19.296875,
|
||||
"id":3,
|
||||
"name":"",
|
||||
"rotation":0,
|
||||
"text":
|
||||
{
|
||||
"text":"Bonjour Monde",
|
||||
"wrap":true
|
||||
},
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":107.109375,
|
||||
"x":-348.221354166667,
|
||||
"y":257.018229166667
|
||||
},
|
||||
{
|
||||
"height":55.296875,
|
||||
"id":4,
|
||||
"name":"",
|
||||
"rotation":0,
|
||||
"text":
|
||||
{
|
||||
"text":"Play Sound Loop\nexit Zone Stop Sound \n",
|
||||
"wrap":true
|
||||
},
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":176.442708333333,
|
||||
"x":243.778645833333,
|
||||
"y":368.3515625
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":8,
|
||||
"nextobjectid":5,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"script",
|
||||
"type":"string",
|
||||
"value":"SoundScript.js"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.5.0",
|
||||
"tileheight":32,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":11,
|
||||
"firstgid":1,
|
||||
"image":"tileset1.png",
|
||||
"imageheight":352,
|
||||
"imagewidth":352,
|
||||
"margin":0,
|
||||
"name":"tileset1",
|
||||
"spacing":0,
|
||||
"tilecount":121,
|
||||
"tileheight":32,
|
||||
"tilewidth":32
|
||||
}],
|
||||
"tilewidth":32,
|
||||
"type":"map",
|
||||
"version":1.5,
|
||||
"width":20
|
||||
}
|
BIN
maps/tests/Win.ogg
Normal file
BIN
maps/tests/Win.ogg
Normal file
Binary file not shown.
@ -42,6 +42,14 @@
|
||||
<a href="#" class="testLink" data-testmap="script_api.json" target="_blank">Testing scripting API with a script</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="radio" name="test-scripting-sound"> Success <input type="radio" name="test-scripting-sound"> Failure <input type="radio" name="test-scripting-sound" checked> Pending
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="testLink" data-testmap="SoundTest.json" target="_blank">Testing scripting API loadSound() function</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="radio" name="test-autoresize"> Success <input type="radio" name="test-autoresize"> Failure <input type="radio" name="test-autoresize" checked> Pending
|
||||
|
BIN
maps/tests/webrtc-in.mp3
Normal file
BIN
maps/tests/webrtc-in.mp3
Normal file
Binary file not shown.
BIN
maps/tests/webrtc-out.mp3
Normal file
BIN
maps/tests/webrtc-out.mp3
Normal file
Binary file not shown.
@ -665,9 +665,9 @@ has@^1.0.3:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
iconv-lite@^0.4.24:
|
||||
version "0.4.24"
|
||||
|
@ -2097,9 +2097,9 @@ highlight.js@^9.12.0:
|
||||
integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==
|
||||
|
||||
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
html-tag@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -1251,9 +1251,9 @@ has-values@^1.0.0:
|
||||
kind-of "^4.0.0"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
http-errors@1.7.2:
|
||||
version "1.7.2"
|
||||
@ -1704,9 +1704,9 @@ lodash.once@^4.0.0:
|
||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
|
||||
|
||||
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
long@~3:
|
||||
version "3.2.0"
|
||||
|
@ -811,9 +811,9 @@ has@^1.0.3:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
http-errors@1.7.2:
|
||||
version "1.7.2"
|
||||
|
Loading…
Reference in New Issue
Block a user