Merge remote-tracking branch 'remotes/upstream/develop' into tiles-start-positions
This commit is contained in:
commit
7f61e9addd
182 changed files with 17118 additions and 4494 deletions
6
front/.gitignore
vendored
6
front/.gitignore
vendored
|
@ -1,5 +1,9 @@
|
|||
/node_modules/
|
||||
/dist/bundle.js
|
||||
/dist/*.js
|
||||
/dist/*.js.map
|
||||
/dist/*.js.LICENSE.txt
|
||||
/dist/main.*.css
|
||||
/dist/main.*.css.map
|
||||
/dist/tests/
|
||||
/yarn-error.log
|
||||
/dist/webpack.config.js
|
||||
|
|
1
front/.prettierignore
Normal file
1
front/.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
src/Messages/generated
|
4
front/.prettierrc.json
Normal file
4
front/.prettierrc.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4
|
||||
}
|
BIN
front/dist/resources/objects/layout_modes.png
vendored
BIN
front/dist/resources/objects/layout_modes.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 297 B |
8393
front/package-lock.json
generated
Normal file
8393
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -18,9 +18,11 @@
|
|||
"fork-ts-checker-webpack-plugin": "^6.2.9",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"jasmine": "^3.5.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"mini-css-extract-plugin": "^1.6.0",
|
||||
"node-polyfill-webpack-plugin": "^1.1.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.3.1",
|
||||
"sass": "^1.32.12",
|
||||
"sass-loader": "^11.1.0",
|
||||
"svelte": "^3.38.2",
|
||||
|
@ -61,7 +63,15 @@
|
|||
"test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch",
|
||||
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\""
|
||||
"precommit": "lint-staged",
|
||||
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
|
||||
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
|
||||
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
||||
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
12
front/src/Api/Events/DataLayerEvent.ts
Normal file
12
front/src/Api/Events/DataLayerEvent.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isDataLayerEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
data: tg.isObject,
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
|
||||
*/
|
||||
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;
|
16
front/src/Api/Events/GameStateEvent.ts
Normal file
16
front/src/Api/Events/GameStateEvent.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isGameStateEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
roomId: tg.isString,
|
||||
mapUrl: tg.isString,
|
||||
nickname: tg.isUnion(tg.isString, tg.isNull),
|
||||
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||
tags: tg.isArray(tg.isString),
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when the gameState is received by the script
|
||||
*/
|
||||
export type GameStateEvent = tg.GuardedType<typeof isGameStateEvent>;
|
17
front/src/Api/Events/HasPlayerMovedEvent.ts
Normal file
17
front/src/Api/Events/HasPlayerMovedEvent.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isHasPlayerMovedEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
direction: tg.isElementOf("right", "left", "up", "down"),
|
||||
moving: tg.isBoolean,
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber,
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* A message sent from the game to the iFrame to notify a movement from the current player.
|
||||
*/
|
||||
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
|
||||
|
||||
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;
|
|
@ -1,57 +1,71 @@
|
|||
|
||||
|
||||
import type { ButtonClickedEvent } from './ButtonClickedEvent';
|
||||
import type { ChatEvent } from './ChatEvent';
|
||||
import type { ClosePopupEvent } from './ClosePopupEvent';
|
||||
import type { EnterLeaveEvent } from './EnterLeaveEvent';
|
||||
import type { GoToPageEvent } from './GoToPageEvent';
|
||||
import type { LoadPageEvent } from './LoadPageEvent';
|
||||
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
|
||||
import type { OpenPopupEvent } from './OpenPopupEvent';
|
||||
import type { OpenTabEvent } from './OpenTabEvent';
|
||||
import type { UserInputChatEvent } from './UserInputChatEvent';
|
||||
import type { LoadSoundEvent} from "./LoadSoundEvent";
|
||||
import type {PlaySoundEvent} from "./PlaySoundEvent";
|
||||
|
||||
import type { GameStateEvent } from "./GameStateEvent";
|
||||
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
||||
import type { ChatEvent } from "./ChatEvent";
|
||||
import type { ClosePopupEvent } from "./ClosePopupEvent";
|
||||
import type { EnterLeaveEvent } from "./EnterLeaveEvent";
|
||||
import type { GoToPageEvent } from "./GoToPageEvent";
|
||||
import type { LoadPageEvent } from "./LoadPageEvent";
|
||||
import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
|
||||
import type { OpenPopupEvent } from "./OpenPopupEvent";
|
||||
import type { OpenTabEvent } from "./OpenTabEvent";
|
||||
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";
|
||||
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
|
||||
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
|
||||
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type IframeEventMap = {
|
||||
//getState: GameStateEvent,
|
||||
// updateTile: UpdateTileEvent
|
||||
loadPage: LoadPageEvent
|
||||
chat: ChatEvent,
|
||||
openPopup: OpenPopupEvent
|
||||
closePopup: ClosePopupEvent
|
||||
openTab: OpenTabEvent
|
||||
goToPage: GoToPageEvent
|
||||
openCoWebSite: OpenCoWebSiteEvent
|
||||
closeCoWebSite: null
|
||||
disablePlayerControls: null
|
||||
restorePlayerControls: null
|
||||
displayBubble: null
|
||||
removeBubble: null
|
||||
loadSound: LoadSoundEvent
|
||||
playSound: PlaySoundEvent
|
||||
stopSound: null,
|
||||
}
|
||||
loadPage: LoadPageEvent;
|
||||
chat: ChatEvent;
|
||||
openPopup: OpenPopupEvent;
|
||||
closePopup: ClosePopupEvent;
|
||||
openTab: OpenTabEvent;
|
||||
goToPage: GoToPageEvent;
|
||||
openCoWebSite: OpenCoWebSiteEvent;
|
||||
closeCoWebSite: null;
|
||||
disablePlayerControls: null;
|
||||
restorePlayerControls: null;
|
||||
displayBubble: null;
|
||||
removeBubble: null;
|
||||
onPlayerMove: undefined;
|
||||
showLayer: LayerEvent;
|
||||
hideLayer: LayerEvent;
|
||||
setProperty: SetPropertyEvent;
|
||||
getDataLayer: undefined;
|
||||
loadSound: LoadSoundEvent;
|
||||
playSound: PlaySoundEvent;
|
||||
stopSound: null;
|
||||
getState: undefined;
|
||||
registerMenuCommand: MenuItemRegisterEvent;
|
||||
};
|
||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||
type: T;
|
||||
data: IframeEventMap[T];
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string';
|
||||
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
|
||||
typeof event.type === "string";
|
||||
|
||||
export interface IframeResponseEventMap {
|
||||
userInputChat: UserInputChatEvent
|
||||
enterEvent: EnterLeaveEvent
|
||||
leaveEvent: EnterLeaveEvent
|
||||
buttonClickedEvent: ButtonClickedEvent
|
||||
// gameState: GameStateEvent
|
||||
userInputChat: UserInputChatEvent;
|
||||
enterEvent: EnterLeaveEvent;
|
||||
leaveEvent: EnterLeaveEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
gameState: GameStateEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
dataLayer: DataLayerEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
|
@ -59,4 +73,6 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';
|
||||
export const isIframeResponseEventWrapper = (event: {
|
||||
type?: string;
|
||||
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
|
||||
|
|
11
front/src/Api/Events/LayerEvent.ts
Normal file
11
front/src/Api/Events/LayerEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isLayerEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
name: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to show/hide a layer.
|
||||
*/
|
||||
export type LayerEvent = tg.GuardedType<typeof isLayerEvent>;
|
|
@ -1,11 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isLoadPageEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
export const isLoadPageEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
|
|
13
front/src/Api/Events/setPropertyEvent.ts
Normal file
13
front/src/Api/Events/setPropertyEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isSetPropertyEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
layerName: tg.isString,
|
||||
propertyName: tg.isString,
|
||||
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to change the value of the property of the layer
|
||||
*/
|
||||
export type SetPropertyEvent = tg.GuardedType<typeof isSetPropertyEvent>;
|
11
front/src/Api/Events/ui/MenuItemClickedEvent.ts
Normal file
11
front/src/Api/Events/ui/MenuItemClickedEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isMenuItemClickedEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
menuItem: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a menu item is clicked.
|
||||
*/
|
||||
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;
|
26
front/src/Api/Events/ui/MenuItemRegisterEvent.ts
Normal file
26
front/src/Api/Events/ui/MenuItemRegisterEvent.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
export const isMenuItemRegisterEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
menutItem: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a new menu item.
|
||||
*/
|
||||
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
|
||||
|
||||
export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
type: tg.isSingletonString("registerMenuCommand"),
|
||||
data: isMenuItemRegisterEvent,
|
||||
})
|
||||
.get();
|
||||
|
||||
const _registerMenuCommandStream: Subject<string> = new Subject();
|
||||
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
|
||||
|
||||
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
|
||||
_registerMenuCommandStream.next(event.menutItem);
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { Subject } from "rxjs";
|
||||
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
|
||||
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||
|
@ -10,12 +9,28 @@ import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
|
|||
import { scriptUtils } from "./ScriptUtils";
|
||||
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
|
||||
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
|
||||
import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent";
|
||||
import {
|
||||
IframeEvent,
|
||||
IframeEventMap,
|
||||
IframeResponseEvent,
|
||||
IframeResponseEventMap,
|
||||
isIframeEventWrapper,
|
||||
TypedMessageEvent,
|
||||
} from "./Events/IframeEvent";
|
||||
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
|
||||
import { isLoadPageEvent } from './Events/LoadPageEvent';
|
||||
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
|
||||
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
|
||||
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
|
||||
//import { isLoadPageEvent } from './Events/LoadPageEvent';
|
||||
import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
|
||||
import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
|
||||
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
|
||||
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
|
||||
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
|
||||
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||
import type { DataLayerEvent } from "./Events/DataLayerEvent";
|
||||
import type { GameStateEvent } from "./Events/GameStateEvent";
|
||||
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
|
||||
import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
||||
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||
|
||||
/**
|
||||
* Listens to messages from iframes and turn those messages into easy to use observables.
|
||||
* Also allows to send messages to those iframes.
|
||||
|
@ -33,7 +48,6 @@ class IframeListener {
|
|||
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
|
||||
public readonly goToPageStream = this._goToPageStream.asObservable();
|
||||
|
||||
|
||||
private readonly _loadPageStream: Subject<string> = new Subject();
|
||||
public readonly loadPageStream = this._loadPageStream.asObservable();
|
||||
|
||||
|
@ -58,6 +72,27 @@ class IframeListener {
|
|||
private readonly _removeBubbleStream: Subject<void> = new Subject();
|
||||
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
|
||||
|
||||
private readonly _showLayerStream: Subject<LayerEvent> = new Subject();
|
||||
public readonly showLayerStream = this._showLayerStream.asObservable();
|
||||
|
||||
private readonly _hideLayerStream: Subject<LayerEvent> = new Subject();
|
||||
public readonly hideLayerStream = this._hideLayerStream.asObservable();
|
||||
|
||||
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
||||
public readonly setPropertyStream = this._setPropertyStream.asObservable();
|
||||
|
||||
private readonly _gameStateStream: Subject<void> = new Subject();
|
||||
public readonly gameStateStream = this._gameStateStream.asObservable();
|
||||
|
||||
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
|
||||
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
|
||||
|
||||
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
|
||||
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
|
||||
|
||||
private readonly _unregisterMenuCommandStream: Subject<string> = new Subject();
|
||||
public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable();
|
||||
|
||||
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
|
||||
public readonly playSoundStream = this._playSoundStream.asObservable();
|
||||
|
||||
|
@ -68,21 +103,21 @@ class IframeListener {
|
|||
public readonly loadSoundStream = this._loadSoundStream.asObservable();
|
||||
|
||||
private readonly iframes = new Set<HTMLIFrameElement>();
|
||||
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
|
||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
private sendPlayerMove: boolean = false;
|
||||
|
||||
init() {
|
||||
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
||||
// Do we trust the sender of this message?
|
||||
// Let's only accept messages from the iframe that are allowed.
|
||||
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||
let foundSrc: string | undefined;
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
||||
// Do we trust the sender of this message?
|
||||
// Let's only accept messages from the iframe that are allowed.
|
||||
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||
let foundSrc: string | undefined;
|
||||
|
||||
foundSrc = [...this.scripts.keys()].find(key => {
|
||||
return this.scripts.get(key)?.contentWindow == message.source
|
||||
});
|
||||
|
||||
if (foundSrc === undefined) {
|
||||
for (const iframe of this.iframes) {
|
||||
let iframe: HTMLIFrameElement;
|
||||
for (iframe of this.iframes) {
|
||||
if (iframe.contentWindow === message.source) {
|
||||
foundSrc = iframe.src;
|
||||
break;
|
||||
|
@ -92,59 +127,77 @@ class IframeListener {
|
|||
if (foundSrc === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload = message.data;
|
||||
if (isIframeEventWrapper(payload)) {
|
||||
if (payload.type === 'chat' && isChatEvent(payload.data)) {
|
||||
this._chatStream.next(payload.data);
|
||||
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
|
||||
this._openPopupStream.next(payload.data);
|
||||
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
|
||||
this._closePopupStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
|
||||
scriptUtils.openTab(payload.data.url);
|
||||
}
|
||||
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)) {
|
||||
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
|
||||
const payload = message.data;
|
||||
if (isIframeEventWrapper(payload)) {
|
||||
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
|
||||
this._showLayerStream.next(payload.data);
|
||||
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
|
||||
this._hideLayerStream.next(payload.data);
|
||||
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
|
||||
this._setPropertyStream.next(payload.data);
|
||||
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
|
||||
this._chatStream.next(payload.data);
|
||||
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
|
||||
this._openPopupStream.next(payload.data);
|
||||
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
|
||||
this._closePopupStream.next(payload.data);
|
||||
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
|
||||
scriptUtils.openTab(payload.data.url);
|
||||
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
|
||||
scriptUtils.goToPage(payload.data.url);
|
||||
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
|
||||
this._loadPageStream.next(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)) {
|
||||
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
|
||||
} else if (payload.type === "closeCoWebSite") {
|
||||
scriptUtils.closeCoWebSite();
|
||||
} else if (payload.type === "disablePlayerControls") {
|
||||
this._disablePlayerControlStream.next();
|
||||
} else if (payload.type === "restorePlayerControls") {
|
||||
this._enablePlayerControlStream.next();
|
||||
} else if (payload.type === "displayBubble") {
|
||||
this._displayBubbleStream.next();
|
||||
} else if (payload.type === "removeBubble") {
|
||||
this._removeBubbleStream.next();
|
||||
} else if (payload.type == "getState") {
|
||||
this._gameStateStream.next();
|
||||
} else if (payload.type == "onPlayerMove") {
|
||||
this.sendPlayerMove = true;
|
||||
} else if (payload.type == "getDataLayer") {
|
||||
this._dataLayerChangeStream.next();
|
||||
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
||||
const data = payload.data.menutItem;
|
||||
// @ts-ignore
|
||||
this.iframeCloseCallbacks.get(iframe).push(() => {
|
||||
this._unregisterMenuCommandStream.next(data);
|
||||
});
|
||||
handleMenuItemRegistrationEvent(payload.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
else if (payload.type === 'closeCoWebSite') {
|
||||
scriptUtils.closeCoWebSite();
|
||||
}
|
||||
|
||||
else if (payload.type === 'disablePlayerControls') {
|
||||
this._disablePlayerControlStream.next();
|
||||
}
|
||||
else if (payload.type === 'restorePlayerControls') {
|
||||
this._enablePlayerControlStream.next();
|
||||
}
|
||||
else if (payload.type === 'displayBubble') {
|
||||
this._displayBubbleStream.next();
|
||||
}
|
||||
else if (payload.type === 'removeBubble') {
|
||||
this._removeBubbleStream.next();
|
||||
}else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){
|
||||
this._loadPageStream.next(payload.data.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}, false);
|
||||
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
|
||||
this.postMessage({
|
||||
type: "dataLayer",
|
||||
data: dataLayerEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendGameStateEvent(gameStateEvent: GameStateEvent) {
|
||||
this.postMessage({
|
||||
type: "gameState",
|
||||
data: gameStateEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,25 +205,29 @@ class IframeListener {
|
|||
*/
|
||||
registerIframe(iframe: HTMLIFrameElement): void {
|
||||
this.iframes.add(iframe);
|
||||
this.iframeCloseCallbacks.set(iframe, []);
|
||||
}
|
||||
|
||||
unregisterIframe(iframe: HTMLIFrameElement): void {
|
||||
this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => {
|
||||
callback();
|
||||
});
|
||||
this.iframes.delete(iframe);
|
||||
}
|
||||
|
||||
registerScript(scriptUrl: string): void {
|
||||
console.log('Loading map related script at ', scriptUrl)
|
||||
console.log("Loading map related script at ", scriptUrl);
|
||||
|
||||
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
|
||||
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
|
||||
// Using external iframe mode (
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = this.getIFrameId(scriptUrl);
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.id = IframeListener.getIFrameId(scriptUrl);
|
||||
iframe.style.display = "none";
|
||||
iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
|
||||
|
||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
iframe.sandbox.add("allow-scripts");
|
||||
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
|
@ -178,41 +235,45 @@ class IframeListener {
|
|||
this.registerIframe(iframe);
|
||||
} else {
|
||||
// production code
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = this.getIFrameId(scriptUrl);
|
||||
iframe.style.display = 'none';
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.id = IframeListener.getIFrameId(scriptUrl);
|
||||
iframe.style.display = "none";
|
||||
|
||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
|
||||
const html = '<!doctype html>\n' +
|
||||
'\n' +
|
||||
'<html lang="en">\n' +
|
||||
'<head>\n' +
|
||||
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
|
||||
'<script src="' + scriptUrl + '" ></script>\n' +
|
||||
'</head>\n' +
|
||||
'</html>\n';
|
||||
iframe.sandbox.add("allow-scripts");
|
||||
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
||||
|
||||
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
|
||||
iframe.srcdoc = html;
|
||||
iframe.srcdoc =
|
||||
"<!doctype html>\n" +
|
||||
"\n" +
|
||||
'<html lang="en">\n' +
|
||||
"<head>\n" +
|
||||
'<script src="' +
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
'/iframe_api.js" ></script>\n' +
|
||||
'<script src="' +
|
||||
scriptUrl +
|
||||
'" ></script>\n' +
|
||||
"<title></title>\n" +
|
||||
"</head>\n" +
|
||||
"</html>\n";
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
this.scripts.set(scriptUrl, iframe);
|
||||
this.registerIframe(iframe);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private getIFrameId(scriptUrl: string): string {
|
||||
return 'script' + btoa(scriptUrl);
|
||||
private static getIFrameId(scriptUrl: string): string {
|
||||
return "script" + btoa(scriptUrl);
|
||||
}
|
||||
|
||||
unregisterScript(scriptUrl: string): void {
|
||||
const iFrameId = this.getIFrameId(scriptUrl);
|
||||
const iFrameId = IframeListener.getIFrameId(scriptUrl);
|
||||
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
|
||||
if (!iframe) {
|
||||
throw new Error('Unknown iframe for script "' + scriptUrl + '"');
|
||||
|
@ -225,50 +286,58 @@ class IframeListener {
|
|||
|
||||
sendUserInputChat(message: string) {
|
||||
this.postMessage({
|
||||
'type': 'userInputChat',
|
||||
'data': {
|
||||
'message': message,
|
||||
} as UserInputChatEvent
|
||||
type: "userInputChat",
|
||||
data: {
|
||||
message: message,
|
||||
} as UserInputChatEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendEnterEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'enterEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
type: "enterEvent",
|
||||
data: {
|
||||
name: name,
|
||||
} as EnterLeaveEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendLeaveEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'leaveEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
type: "leaveEvent",
|
||||
data: {
|
||||
name: name,
|
||||
} as EnterLeaveEvent,
|
||||
});
|
||||
}
|
||||
|
||||
hasPlayerMoved(event: HasPlayerMovedEvent) {
|
||||
if (this.sendPlayerMove) {
|
||||
this.postMessage({
|
||||
type: "hasPlayerMoved",
|
||||
data: event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendButtonClickedEvent(popupId: number, buttonId: number): void {
|
||||
this.postMessage({
|
||||
'type': 'buttonClickedEvent',
|
||||
'data': {
|
||||
type: "buttonClickedEvent",
|
||||
data: {
|
||||
popupId,
|
||||
buttonId
|
||||
} as ButtonClickedEvent
|
||||
buttonId,
|
||||
} as ButtonClickedEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message... to all allowed iframes.
|
||||
*/
|
||||
private postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
|
||||
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
|
||||
for (const iframe of this.iframes) {
|
||||
iframe.contentWindow?.postMessage(message, '*');
|
||||
iframe.contentWindow?.postMessage(message, "*");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const iframeListener = new IframeListener();
|
||||
|
|
11
front/src/Api/iframe/Ui/MenuItem.ts
Normal file
11
front/src/Api/iframe/Ui/MenuItem.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent";
|
||||
import { iframeListener } from "../../IframeListener";
|
||||
|
||||
export function sendMenuClickedEvent(menuItem: string) {
|
||||
iframeListener.postMessage({
|
||||
type: "menuItemClicked",
|
||||
data: {
|
||||
menuItem: menuItem,
|
||||
} as MenuItemClickedEvent,
|
||||
});
|
||||
}
|
|
@ -1,30 +1,30 @@
|
|||
import type { ChatEvent } from '../Events/ChatEvent'
|
||||
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent'
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'
|
||||
import type { ChatEvent } from "../Events/ChatEvent";
|
||||
import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import {Subject} from "rxjs";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
const chatStream = new Subject<string>();
|
||||
|
||||
class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
|
||||
|
||||
callbacks = [apiCallback({
|
||||
callback: (event: UserInputChatEvent) => {
|
||||
chatStream.next(event.message);
|
||||
},
|
||||
type: "userInputChat",
|
||||
typeChecker: isUserInputChatEvent
|
||||
})]
|
||||
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
callback: (event: UserInputChatEvent) => {
|
||||
chatStream.next(event.message);
|
||||
},
|
||||
type: "userInputChat",
|
||||
typeChecker: isUserInputChatEvent,
|
||||
}),
|
||||
];
|
||||
|
||||
sendChatMessage(message: string, author: string) {
|
||||
sendToWorkadventure({
|
||||
type: 'chat',
|
||||
type: "chat",
|
||||
data: {
|
||||
'message': message,
|
||||
'author': author
|
||||
} as ChatEvent
|
||||
})
|
||||
message: message,
|
||||
author: author,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,4 +35,4 @@ class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatC
|
|||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureChatCommands()
|
||||
export default new WorkadventureChatCommands();
|
||||
|
|
|
@ -1,56 +1,54 @@
|
|||
import type { GoToPageEvent } from '../Events/GoToPageEvent';
|
||||
import type { OpenTabEvent } from '../Events/OpenTabEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent";
|
||||
|
||||
import type { GoToPageEvent } from "../Events/GoToPageEvent";
|
||||
import type { OpenTabEvent } from "../Events/OpenTabEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import type { OpenCoWebSiteEvent } from "../Events/OpenCoWebSiteEvent";
|
||||
import type { LoadPageEvent } from "../Events/LoadPageEvent";
|
||||
|
||||
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
|
||||
callbacks = []
|
||||
|
||||
callbacks = [];
|
||||
|
||||
openTab(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'openTab',
|
||||
"data": {
|
||||
url
|
||||
} as OpenTabEvent
|
||||
type: "openTab",
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
goToPage(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'goToPage',
|
||||
"data": {
|
||||
url
|
||||
} as GoToPageEvent
|
||||
type: "goToPage",
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
goToRoom(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'loadPage',
|
||||
"data": {
|
||||
url
|
||||
}
|
||||
type: "loadPage",
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openCoWebSite(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'openCoWebSite',
|
||||
"data": {
|
||||
url
|
||||
} as OpenCoWebSiteEvent
|
||||
type: "openCoWebSite",
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
closeCoWebSite(): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'closeCoWebSite',
|
||||
data: null
|
||||
type: "closeCoWebSite",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureNavigationCommands();
|
||||
|
|
29
front/src/Api/iframe/player.ts
Normal file
29
front/src/Api/iframe/player.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
||||
import { Subject } from "rxjs";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
||||
|
||||
const moveStream = new Subject<HasPlayerMovedEvent>();
|
||||
|
||||
class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: "hasPlayerMoved",
|
||||
typeChecker: isHasPlayerMovedEvent,
|
||||
callback: (payloadData) => {
|
||||
moveStream.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
onPlayerMove(callback: HasPlayerMovedEventCallback): void {
|
||||
moveStream.subscribe(callback);
|
||||
sendToWorkadventure({
|
||||
type: "onPlayerMove",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventurePlayerCommands();
|
|
@ -1,10 +1,52 @@
|
|||
import { Subject } from "rxjs";
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent';
|
||||
import { IframeApiContribution } from './IframeApiContribution';
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import type { LayerEvent } from "../Events/LayerEvent";
|
||||
import type { SetPropertyEvent } from "../Events/setPropertyEvent";
|
||||
import type { GameStateEvent } from "../Events/GameStateEvent";
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
import type { DataLayerEvent } from "../Events/DataLayerEvent";
|
||||
import { isGameStateEvent } from "../Events/GameStateEvent";
|
||||
import { isDataLayerEvent } from "../Events/DataLayerEvent";
|
||||
|
||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const dataLayerResolver = new Subject<DataLayerEvent>();
|
||||
const stateResolvers = new Subject<GameStateEvent>();
|
||||
|
||||
let immutableData: GameStateEvent;
|
||||
|
||||
interface Room {
|
||||
id: string;
|
||||
mapUrl: string;
|
||||
map: ITiledMap;
|
||||
startLayer: string | null;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string | undefined;
|
||||
nickName: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
function getGameState(): Promise<GameStateEvent> {
|
||||
if (immutableData) {
|
||||
return Promise.resolve(immutableData);
|
||||
} else {
|
||||
return new Promise<GameStateEvent>((resolver, thrower) => {
|
||||
stateResolvers.subscribe(resolver);
|
||||
sendToWorkadventure({ type: "getState", data: null });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getDataLayer(): Promise<DataLayerEvent> {
|
||||
return new Promise<DataLayerEvent>((resolver, thrower) => {
|
||||
dataLayerResolver.subscribe(resolver);
|
||||
sendToWorkadventure({ type: "getDataLayer", data: null });
|
||||
});
|
||||
}
|
||||
|
||||
class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
||||
callbacks = [
|
||||
|
@ -13,18 +55,30 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
|
|||
enterStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
type: "enterEvent",
|
||||
typeChecker: isEnterLeaveEvent
|
||||
typeChecker: isEnterLeaveEvent,
|
||||
}),
|
||||
apiCallback({
|
||||
type: "leaveEvent",
|
||||
typeChecker: isEnterLeaveEvent,
|
||||
callback: (payloadData) => {
|
||||
leaveStreams.get(payloadData.name)?.next();
|
||||
}
|
||||
})
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "gameState",
|
||||
typeChecker: isGameStateEvent,
|
||||
callback: (payloadData) => {
|
||||
stateResolvers.next(payloadData);
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "dataLayer",
|
||||
typeChecker: isDataLayerEvent,
|
||||
callback: (payloadData) => {
|
||||
dataLayerResolver.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
let subject = enterStreams.get(name);
|
||||
|
@ -33,7 +87,6 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
|
|||
enterStreams.set(name, subject);
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
|
||||
}
|
||||
onLeaveZone(name: string, callback: () => void): void {
|
||||
let subject = leaveStreams.get(name);
|
||||
|
@ -43,8 +96,39 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
|
|||
}
|
||||
subject.subscribe(callback);
|
||||
}
|
||||
|
||||
showLayer(layerName: string): void {
|
||||
sendToWorkadventure({ type: "showLayer", data: { name: layerName } });
|
||||
}
|
||||
hideLayer(layerName: string): void {
|
||||
sendToWorkadventure({ type: "hideLayer", data: { name: layerName } });
|
||||
}
|
||||
setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void {
|
||||
sendToWorkadventure({
|
||||
type: "setProperty",
|
||||
data: {
|
||||
layerName: layerName,
|
||||
propertyName: propertyName,
|
||||
propertyValue: propertyValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
getCurrentRoom(): Promise<Room> {
|
||||
return getGameState().then((gameState) => {
|
||||
return getDataLayer().then((mapJson) => {
|
||||
return {
|
||||
id: gameState.roomId,
|
||||
map: mapJson.data as ITiledMap,
|
||||
mapUrl: gameState.mapUrl,
|
||||
startLayer: gameState.startLayerName,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
getCurrentUser(): Promise<User> {
|
||||
return getGameState().then((gameState) => {
|
||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureRoomCommands();
|
||||
|
|
|
@ -1,40 +1,55 @@
|
|||
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
|
||||
import type { ClosePopupEvent } from '../Events/ClosePopupEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
|
||||
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
|
||||
import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import {Popup} from "./Ui/Popup";
|
||||
import type {ButtonClickedCallback, ButtonDescriptor} from "./Ui/ButtonDescriptor";
|
||||
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
|
||||
import { Popup } from "./Ui/Popup";
|
||||
|
||||
let popupId = 0;
|
||||
const popups: Map<number, Popup> = new Map<number, Popup>();
|
||||
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
|
||||
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
|
||||
number,
|
||||
Map<number, ButtonClickedCallback>
|
||||
>();
|
||||
|
||||
const menuCallbacks: Map<string, (command: string) => void> = new Map();
|
||||
|
||||
interface ZonedPopupOptions {
|
||||
zone: string
|
||||
objectLayerName?: string,
|
||||
popupText: string,
|
||||
delay?: number
|
||||
popupOptions: Array<ButtonDescriptor>
|
||||
zone: string;
|
||||
objectLayerName?: string;
|
||||
popupText: string;
|
||||
delay?: number;
|
||||
popupOptions: Array<ButtonDescriptor>;
|
||||
}
|
||||
|
||||
|
||||
class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
||||
|
||||
callbacks = [apiCallback({
|
||||
type: "buttonClickedEvent",
|
||||
typeChecker: isButtonClickedEvent,
|
||||
callback: (payloadData) => {
|
||||
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
|
||||
const popup = popups.get(payloadData.popupId);
|
||||
if (popup === undefined) {
|
||||
throw new Error('Could not find popup with ID "' + payloadData.popupId + '"');
|
||||
}
|
||||
if (callback) {
|
||||
callback(popup);
|
||||
}
|
||||
}
|
||||
})];
|
||||
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: "buttonClickedEvent",
|
||||
typeChecker: isButtonClickedEvent,
|
||||
callback: (payloadData) => {
|
||||
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
|
||||
const popup = popups.get(payloadData.popupId);
|
||||
if (popup === undefined) {
|
||||
throw new Error('Could not find popup with ID "' + payloadData.popupId + '"');
|
||||
}
|
||||
if (callback) {
|
||||
callback(popup);
|
||||
}
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "menuItemClicked",
|
||||
typeChecker: isMenuItemClickedEvent,
|
||||
callback: (event) => {
|
||||
const callback = menuCallbacks.get(event.menuItem);
|
||||
if (callback) {
|
||||
callback(event.menuItem);
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||
popupId++;
|
||||
|
@ -53,30 +68,40 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
|
|||
}
|
||||
|
||||
sendToWorkadventure({
|
||||
'type': 'openPopup',
|
||||
'data': {
|
||||
type: "openPopup",
|
||||
data: {
|
||||
popupId,
|
||||
targetObject,
|
||||
message,
|
||||
buttons: buttons.map((button) => {
|
||||
return {
|
||||
label: button.label,
|
||||
className: button.className
|
||||
className: button.className,
|
||||
};
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
popups.set(popupId, popup)
|
||||
popups.set(popupId, popup);
|
||||
return popup;
|
||||
}
|
||||
|
||||
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
|
||||
menuCallbacks.set(commandDescriptor, callback);
|
||||
sendToWorkadventure({
|
||||
type: "registerMenuCommand",
|
||||
data: {
|
||||
menutItem: commandDescriptor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
displayBubble(): void {
|
||||
sendToWorkadventure({ 'type': 'displayBubble', data: null });
|
||||
sendToWorkadventure({ type: "displayBubble", data: null });
|
||||
}
|
||||
|
||||
removeBubble(): void {
|
||||
sendToWorkadventure({ 'type': 'removeBubble', data: null });
|
||||
sendToWorkadventure({ type: "removeBubble", data: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="typescript">
|
||||
import {enableCameraSceneVisibilityStore, gameOverlayVisibilityStore} from "../Stores/MediaStore";
|
||||
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
|
||||
import CameraControls from "./CameraControls.svelte";
|
||||
import MyCamera from "./MyCamera.svelte";
|
||||
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
|
||||
|
@ -21,10 +21,13 @@
|
|||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
||||
import ErrorDialog from "./UI/ErrorDialog.svelte";
|
||||
import VideoOverlay from "./Video/VideoOverlay.svelte";
|
||||
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
|
||||
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
@ -68,6 +71,7 @@
|
|||
-->
|
||||
{#if $gameOverlayVisibilityStore}
|
||||
<div>
|
||||
<VideoOverlay></VideoOverlay>
|
||||
<MyCamera></MyCamera>
|
||||
<CameraControls></CameraControls>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
import cinemaCloseImg from "./images/cinema-close.svg";
|
||||
import microphoneImg from "./images/microphone.svg";
|
||||
import microphoneCloseImg from "./images/microphone-close.svg";
|
||||
import layoutPresentationImg from "./images/layout-presentation.svg";
|
||||
import layoutChatImg from "./images/layout-chat.svg";
|
||||
import {layoutModeStore} from "../Stores/StreamableCollectionStore";
|
||||
import {LayoutMode} from "../WebRtc/LayoutManager";
|
||||
import {peerStore} from "../Stores/PeerStore";
|
||||
|
||||
function screenSharingClick(): void {
|
||||
if ($requestedScreenSharingState === true) {
|
||||
|
@ -32,10 +37,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
function switchLayoutMode() {
|
||||
if ($layoutModeStore === LayoutMode.Presentation) {
|
||||
$layoutModeStore = LayoutMode.VideoChat;
|
||||
} else {
|
||||
$layoutModeStore = LayoutMode.Presentation;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="btn-cam-action">
|
||||
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
|
||||
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode">
|
||||
{:else}
|
||||
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
|
||||
{/if}
|
||||
</div>
|
||||
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
|
||||
{#if $requestedScreenSharingState}
|
||||
<img src={monitorImg} alt="Start screen sharing">
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
|
||||
<div class="horizontal-sound-meter" class:active={display}>
|
||||
{#each [...Array(NB_BARS).keys()] as i}
|
||||
{#each [...Array(NB_BARS).keys()] as i (i)}
|
||||
<div style={color(i, volume)}></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
export let stream: MediaStream|null;
|
||||
let volume = 0;
|
||||
|
||||
const NB_BARS = 5;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
const soundMeter = new SoundMeter();
|
||||
let display = false;
|
||||
|
@ -23,7 +21,7 @@
|
|||
|
||||
timeout = setInterval(() => {
|
||||
try{
|
||||
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
|
||||
volume = soundMeter.getVolume();
|
||||
//console.log(volume);
|
||||
}catch(err){
|
||||
|
||||
|
@ -45,9 +43,9 @@
|
|||
|
||||
|
||||
<div class="sound-progress" class:active={display}>
|
||||
<span class:active={volume > 1}></span>
|
||||
<span class:active={volume > 2}></span>
|
||||
<span class:active={volume > 3}></span>
|
||||
<span class:active={volume > 4}></span>
|
||||
<span class:active={volume > 5}></span>
|
||||
<span class:active={volume > 10}></span>
|
||||
<span class:active={volume > 15}></span>
|
||||
<span class:active={volume > 40}></span>
|
||||
<span class:active={volume > 70}></span>
|
||||
</div>
|
||||
|
|
35
front/src/Components/Video/ChatLayout.svelte
Normal file
35
front/src/Components/Video/ChatLayout.svelte
Normal file
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||
import {afterUpdate, onDestroy} from "svelte";
|
||||
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||
import MediaBox from "./MediaBox.svelte";
|
||||
|
||||
let cssClass = 'one-col';
|
||||
|
||||
const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => {
|
||||
const nbUsers = displayableMedias.size;
|
||||
if (nbUsers <= 1) {
|
||||
cssClass = 'one-col';
|
||||
} else if (nbUsers <= 4) {
|
||||
cssClass = 'two-col';
|
||||
} else if (nbUsers <= 9) {
|
||||
cssClass = 'three-col';
|
||||
} else {
|
||||
cssClass = 'four-col';
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
biggestAvailableAreaStore.recompute();
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="chat-mode {cssClass}">
|
||||
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||
<MediaBox streamable={peer}></MediaBox>
|
||||
{/each}
|
||||
</div>
|
16
front/src/Components/Video/LocalStreamMediaBox.svelte
Normal file
16
front/src/Components/Video/LocalStreamMediaBox.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="typescript">
|
||||
import type {ScreenSharingLocalMedia} from "../../Stores/ScreenSharingStore";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {srcObject} from "./utils";
|
||||
|
||||
export let peer : ScreenSharingLocalMedia;
|
||||
let stream = peer.stream;
|
||||
export let cssClass : string|undefined;
|
||||
</script>
|
||||
|
||||
|
||||
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
|
||||
{#if stream}
|
||||
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
</div>
|
20
front/src/Components/Video/MediaBox.svelte
Normal file
20
front/src/Components/Video/MediaBox.svelte
Normal file
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||
import VideoMediaBox from "./VideoMediaBox.svelte";
|
||||
import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte";
|
||||
import {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||
import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte";
|
||||
import type {Streamable} from "../../Stores/StreamableCollectionStore";
|
||||
|
||||
export let streamable: Streamable;
|
||||
</script>
|
||||
|
||||
<div class="media-container">
|
||||
{#if streamable instanceof VideoPeer}
|
||||
<VideoMediaBox peer={streamable}/>
|
||||
{:else if streamable instanceof ScreenSharingPeer}
|
||||
<ScreenSharingMediaBox peer={streamable}/>
|
||||
{:else}
|
||||
<LocalStreamMediaBox peer={streamable} cssClass=""/>
|
||||
{/if}
|
||||
</div>
|
24
front/src/Components/Video/PresentationLayout.svelte
Normal file
24
front/src/Components/Video/PresentationLayout.svelte
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {afterUpdate} from "svelte";
|
||||
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||
import MediaBox from "./MediaBox.svelte";
|
||||
|
||||
afterUpdate(() => {
|
||||
biggestAvailableAreaStore.recompute();
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="main-section">
|
||||
{#if $videoFocusStore }
|
||||
<MediaBox streamable={$videoFocusStore}></MediaBox>
|
||||
{/if}
|
||||
</div>
|
||||
<aside class="sidebar">
|
||||
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||
{#if peer !== $videoFocusStore }
|
||||
<MediaBox streamable={peer}></MediaBox>
|
||||
{/if}
|
||||
{/each}
|
||||
</aside>
|
33
front/src/Components/Video/ScreenSharingMediaBox.svelte
Normal file
33
front/src/Components/Video/ScreenSharingMediaBox.svelte
Normal file
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import type {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {getColorByString, srcObject} from "./utils";
|
||||
|
||||
export let peer: ScreenSharingPeer;
|
||||
let streamStore = peer.streamStore;
|
||||
let name = peer.userName;
|
||||
let statusStore = peer.statusStore;
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-container">
|
||||
{#if $statusStore === 'connecting'}
|
||||
<div class="connecting-spinner"></div>
|
||||
{/if}
|
||||
{#if $statusStore === 'error'}
|
||||
<div class="rtc-error"></div>
|
||||
{/if}
|
||||
{#if $streamStore === null}
|
||||
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||
{:else}
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.video-container {
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
48
front/src/Components/Video/VideoMediaBox.svelte
Normal file
48
front/src/Components/Video/VideoMediaBox.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import type {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||
import SoundMeterWidget from "../SoundMeterWidget.svelte";
|
||||
import microphoneCloseImg from "../images/microphone-close.svg";
|
||||
import reportImg from "./images/report.svg";
|
||||
import blockSignImg from "./images/blockSign.svg";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
|
||||
import {getColorByString, srcObject} from "./utils";
|
||||
|
||||
export let peer: VideoPeer;
|
||||
let streamStore = peer.streamStore;
|
||||
let name = peer.userName;
|
||||
let statusStore = peer.statusStore;
|
||||
let constraintStore = peer.constraintsStore;
|
||||
|
||||
function openReport(peer: VideoPeer): void {
|
||||
showReportScreenStore.set({ userId:peer.userId, userName: peer.userName });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-container">
|
||||
{#if $statusStore === 'connecting'}
|
||||
<div class="connecting-spinner"></div>
|
||||
{/if}
|
||||
{#if $statusStore === 'error'}
|
||||
<div class="rtc-error"></div>
|
||||
{/if}
|
||||
{#if !$constraintStore || $constraintStore.video === false}
|
||||
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||
{/if}
|
||||
{#if $constraintStore && $constraintStore.audio === false}
|
||||
<img src={microphoneCloseImg} alt="Muted">
|
||||
{/if}
|
||||
<button class="report" on:click={() => openReport(peer)}>
|
||||
<img alt="Report this user" src={reportImg}>
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
{#if $streamStore }
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||
{#if $constraintStore && $constraintStore.audio !== false}
|
||||
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||
{/if}
|
||||
</div>
|
||||
|
23
front/src/Components/Video/VideoOverlay.svelte
Normal file
23
front/src/Components/Video/VideoOverlay.svelte
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import {LayoutMode} from "../../WebRtc/LayoutManager";
|
||||
import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
|
||||
import PresentationLayout from "./PresentationLayout.svelte";
|
||||
import ChatLayout from "./ChatLayout.svelte";
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-overlay">
|
||||
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||
<PresentationLayout />
|
||||
{:else }
|
||||
<ChatLayout />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.video-overlay {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
22
front/src/Components/Video/images/blockSign.svg
Normal file
22
front/src/Components/Video/images/blockSign.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg2985" version="1.1" inkscape:version="0.48.4 r9939" width="485.33627" height="485.33627" sodipodi:docname="600px-France_road_sign_B1j.svg[1].png">
|
||||
<metadata id="metadata2991">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs id="defs2989"/>
|
||||
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1272" inkscape:window-height="745" id="namedview2987" showgrid="false" inkscape:snap-global="true" inkscape:snap-grids="true" inkscape:snap-bbox="true" inkscape:bbox-paths="true" inkscape:bbox-nodes="true" inkscape:snap-bbox-edge-midpoints="true" inkscape:snap-bbox-midpoints="true" inkscape:object-paths="true" inkscape:snap-intersection-paths="true" inkscape:object-nodes="true" inkscape:snap-smooth-nodes="true" inkscape:snap-midpoints="true" inkscape:snap-object-midpoints="true" inkscape:snap-center="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:zoom="0.59970176" inkscape:cx="390.56499" inkscape:cy="244.34365" inkscape:window-x="86" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
|
||||
<inkscape:grid type="xygrid" id="grid2995" empspacing="5" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="-57.33186px" originy="-57.33186px"/>
|
||||
</sodipodi:namedview>
|
||||
<g inkscape:groupmode="layer" id="layer1" inkscape:label="1" style="display:inline" transform="translate(-57.33186,-57.33186)">
|
||||
<path sodipodi:type="arc" style="color:#000000;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2997" sodipodi:cx="300" sodipodi:cy="300" sodipodi:rx="240" sodipodi:ry="240" d="M 540,300 C 540,432.54834 432.54834,540 300,540 167.45166,540 60,432.54834 60,300 60,167.45166 167.45166,60 300,60 432.54834,60 540,167.45166 540,300 z" transform="matrix(1.0058783,0,0,1.0058783,-1.76349,-1.76349)"/>
|
||||
<path sodipodi:type="arc" style="color:#000000;fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4005" sodipodi:cx="304.75" sodipodi:cy="214.75" sodipodi:rx="44.75" sodipodi:ry="44.75" d="m 349.5,214.75 c 0,24.71474 -20.03526,44.75 -44.75,44.75 -24.71474,0 -44.75,-20.03526 -44.75,-44.75 0,-24.71474 20.03526,-44.75 44.75,-44.75 24.71474,0 44.75,20.03526 44.75,44.75 z" transform="matrix(5.1364411,0,0,5.1364411,-1265.3304,-803.05073)"/>
|
||||
<rect style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="rect4001" width="345" height="80.599998" x="127.5" y="259.70001"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
1
front/src/Components/Video/images/report.svg
Normal file
1
front/src/Components/Video/images/report.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.1 KiB |
27
front/src/Components/Video/utils.ts
Normal file
27
front/src/Components/Video/utils.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
export function getColorByString(str: string): string | null {
|
||||
let hash = 0;
|
||||
if (str.length === 0) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash;
|
||||
}
|
||||
let color = "#";
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 255;
|
||||
color += ("00" + value.toString(16)).substr(-2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
||||
node.srcObject = stream;
|
||||
return {
|
||||
update(newStream: MediaStream) {
|
||||
if (node.srcObject != newStream) {
|
||||
node.srcObject = newStream;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
1
front/src/Components/images/layout-chat.svg
Normal file
1
front/src/Components/images/layout-chat.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48.97 39.04"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M35.08,11.78v8.75H24.31V11.78H35.08m1-1H23.31V21.53H36.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,26.05V34.8H24.11V26.05H34.87m1-1H23.11V35.8H35.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,40.07v8.75H24.11V40.07H34.87m1-1H23.11V49.82H35.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M53.08,11.78v8.75H42.31V11.78H53.08m1-1H41.31V21.53H54.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,26.05V34.8H42.11V26.05H52.87m1-1H41.11V35.8H53.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,40.07v8.75H42.11V40.07H52.87m1-1H41.11V49.82H53.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M71.08,11.78v8.75H60.31V11.78H71.08m1-1H59.31V21.53H72.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,26.05V34.8H60.11V26.05H70.87m1-1H59.11V35.8H71.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,40.07v8.75H60.11V40.07H70.87m1-1H59.11V49.82H71.87V39.07Z" transform="translate(-23.11 -10.78)"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
front/src/Components/images/layout-presentation.svg
Normal file
1
front/src/Components/images/layout-presentation.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.83 54"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.5" y="0.5" width="63.13" height="53"/><path class="cls-1" d="M67.12,6V58H5V6H67.12m1-1H4V59H68.12V5Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="0.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,6.25V18.12H73.37V6.25H87.83m1-1H72.37V19.12H88.83V5.25Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="17.69" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,23.19V35.05H73.37V23.19H87.83m1-1H72.37V36.05H88.83V22.19Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="34.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,40.25V52.12H73.37V40.25H87.83m1-1H72.37V53.12H88.83V39.25Z" transform="translate(-4 -5)"/></svg>
|
After Width: | Height: | Size: 873 B |
|
@ -1,4 +1,4 @@
|
|||
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
|
||||
import { PUSHER_URL, UPLOADER_URL } from "../Enum/EnvironmentVariable";
|
||||
import Axios from "axios";
|
||||
import {
|
||||
BatchMessage,
|
||||
|
@ -11,7 +11,8 @@ import {
|
|||
RoomJoinedMessage,
|
||||
ServerToClientMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SilentMessage, StopGlobalMessage,
|
||||
SilentMessage,
|
||||
StopGlobalMessage,
|
||||
UserJoinedMessage,
|
||||
UserLeftMessage,
|
||||
UserMovedMessage,
|
||||
|
@ -31,37 +32,43 @@ import {
|
|||
EmotePromptMessage,
|
||||
SendUserMessage,
|
||||
BanUserMessage,
|
||||
} from "../Messages/generated/messages_pb"
|
||||
} from "../Messages/generated/messages_pb";
|
||||
|
||||
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
|
||||
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import {ProtobufClientUtils} from "../Network/ProtobufClientUtils";
|
||||
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
|
||||
import {
|
||||
EventMessage,
|
||||
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface,
|
||||
MessageUserJoined, OnConnectInterface, PlayGlobalMessageInterface, PositionInterface,
|
||||
GroupCreatedUpdatedMessageInterface,
|
||||
ItemEventMessageInterface,
|
||||
MessageUserJoined,
|
||||
OnConnectInterface,
|
||||
PlayGlobalMessageInterface,
|
||||
PositionInterface,
|
||||
RoomJoinedMessageInterface,
|
||||
ViewportInterface, WebRtcDisconnectMessageInterface,
|
||||
ViewportInterface,
|
||||
WebRtcDisconnectMessageInterface,
|
||||
WebRtcSignalReceivedMessageInterface,
|
||||
} from "./ConnexionModels";
|
||||
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
|
||||
import {adminMessagesService} from "./AdminMessagesService";
|
||||
import {worldFullMessageStream} from "./WorldFullMessageStream";
|
||||
import {worldFullWarningStream} from "./WorldFullWarningStream";
|
||||
import {connectionManager} from "./ConnectionManager";
|
||||
import {emoteEventStream} from "./EmoteEventStream";
|
||||
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
||||
import { adminMessagesService } from "./AdminMessagesService";
|
||||
import { worldFullMessageStream } from "./WorldFullMessageStream";
|
||||
import { worldFullWarningStream } from "./WorldFullWarningStream";
|
||||
import { connectionManager } from "./ConnectionManager";
|
||||
import { emoteEventStream } from "./EmoteEventStream";
|
||||
|
||||
const manualPingDelay = 20000;
|
||||
|
||||
export class RoomConnection implements RoomConnection {
|
||||
private readonly socket: WebSocket;
|
||||
private userId: number|null = null;
|
||||
private userId: number | null = null;
|
||||
private listeners: Map<string, Function[]> = new Map<string, Function[]>();
|
||||
private static websocketFactory: null|((url: string)=>any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
private closed: boolean = false;
|
||||
private tags: string[] = [];
|
||||
|
||||
public static setWebsocketFactory(websocketFactory: (url: string)=>any): void { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
|
||||
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
RoomConnection.websocketFactory = websocketFactory;
|
||||
}
|
||||
|
||||
|
@ -70,28 +77,35 @@ export class RoomConnection implements RoomConnection {
|
|||
* @param token A JWT token containing the UUID of the user
|
||||
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
|
||||
*/
|
||||
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null) {
|
||||
public constructor(
|
||||
token: string | null,
|
||||
roomId: string,
|
||||
name: string,
|
||||
characterLayers: string[],
|
||||
position: PositionInterface,
|
||||
viewport: ViewportInterface,
|
||||
companion: string | null
|
||||
) {
|
||||
let url = new URL(PUSHER_URL, window.location.toString()).toString();
|
||||
url = url.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
if (!url.endsWith('/')) {
|
||||
url += '/';
|
||||
url = url.replace("http://", "ws://").replace("https://", "wss://");
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
url += 'room';
|
||||
url += '?roomId='+(roomId ?encodeURIComponent(roomId):'');
|
||||
url += '&token='+(token ?encodeURIComponent(token):'');
|
||||
url += '&name='+encodeURIComponent(name);
|
||||
url += "room";
|
||||
url += "?roomId=" + (roomId ? encodeURIComponent(roomId) : "");
|
||||
url += "&token=" + (token ? encodeURIComponent(token) : "");
|
||||
url += "&name=" + encodeURIComponent(name);
|
||||
for (const layer of characterLayers) {
|
||||
url += '&characterLayers='+encodeURIComponent(layer);
|
||||
url += "&characterLayers=" + encodeURIComponent(layer);
|
||||
}
|
||||
url += '&x='+Math.floor(position.x);
|
||||
url += '&y='+Math.floor(position.y);
|
||||
url += '&top='+Math.floor(viewport.top);
|
||||
url += '&bottom='+Math.floor(viewport.bottom);
|
||||
url += '&left='+Math.floor(viewport.left);
|
||||
url += '&right='+Math.floor(viewport.right);
|
||||
|
||||
if (typeof companion === 'string') {
|
||||
url += '&companion='+encodeURIComponent(companion);
|
||||
url += "&x=" + Math.floor(position.x);
|
||||
url += "&y=" + Math.floor(position.y);
|
||||
url += "&top=" + Math.floor(viewport.top);
|
||||
url += "&bottom=" + Math.floor(viewport.bottom);
|
||||
url += "&left=" + Math.floor(viewport.left);
|
||||
url += "&right=" + Math.floor(viewport.right);
|
||||
if (typeof companion === "string") {
|
||||
url += "&companion=" + encodeURIComponent(companion);
|
||||
}
|
||||
|
||||
if (RoomConnection.websocketFactory) {
|
||||
|
@ -100,9 +114,9 @@ export class RoomConnection implements RoomConnection {
|
|||
this.socket = new WebSocket(url);
|
||||
}
|
||||
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.binaryType = "arraybuffer";
|
||||
|
||||
let interval: ReturnType<typeof setInterval>|undefined = undefined;
|
||||
let interval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
|
||||
this.socket.onopen = (ev) => {
|
||||
//we manually ping every 20s to not be logged out by the server, even when the game is in background.
|
||||
|
@ -110,7 +124,7 @@ export class RoomConnection implements RoomConnection {
|
|||
interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
|
||||
};
|
||||
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
this.socket.addEventListener("close", (event) => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
@ -127,7 +141,7 @@ export class RoomConnection implements RoomConnection {
|
|||
|
||||
if (message.hasBatchmessage()) {
|
||||
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
|
||||
let event: string|null = null;
|
||||
let event: string | null = null;
|
||||
let payload;
|
||||
if (subMessage.hasUsermovedmessage()) {
|
||||
event = EventMessage.USER_MOVED;
|
||||
|
@ -151,7 +165,7 @@ export class RoomConnection implements RoomConnection {
|
|||
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
||||
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
||||
} else {
|
||||
throw new Error('Unexpected batch message type');
|
||||
throw new Error("Unexpected batch message type");
|
||||
}
|
||||
|
||||
if (event) {
|
||||
|
@ -161,7 +175,7 @@ export class RoomConnection implements RoomConnection {
|
|||
} else if (message.hasRoomjoinedmessage()) {
|
||||
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
|
||||
|
||||
const items: { [itemId: number] : unknown } = {};
|
||||
const items: { [itemId: number]: unknown } = {};
|
||||
for (const item of roomJoinedMessage.getItemList()) {
|
||||
items[item.getItemid()] = JSON.parse(item.getStatejson());
|
||||
}
|
||||
|
@ -172,8 +186,8 @@ export class RoomConnection implements RoomConnection {
|
|||
this.dispatch(EventMessage.CONNECT, {
|
||||
connection: this,
|
||||
room: {
|
||||
items
|
||||
} as RoomJoinedMessageInterface
|
||||
items,
|
||||
} as RoomJoinedMessageInterface,
|
||||
});
|
||||
} else if (message.hasWorldfullmessage()) {
|
||||
worldFullMessageStream.onMessage();
|
||||
|
@ -181,10 +195,13 @@ export class RoomConnection implements RoomConnection {
|
|||
} else if (message.hasWorldconnexionmessage()) {
|
||||
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
|
||||
this.closed = true;
|
||||
}else if (message.hasWebrtcsignaltoclientmessage()) {
|
||||
} else if (message.hasWebrtcsignaltoclientmessage()) {
|
||||
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
|
||||
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
|
||||
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage());
|
||||
this.dispatch(
|
||||
EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL,
|
||||
message.getWebrtcscreensharingsignaltoclientmessage()
|
||||
);
|
||||
} else if (message.hasWebrtcstartmessage()) {
|
||||
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
|
||||
} else if (message.hasWebrtcdisconnectmessage()) {
|
||||
|
@ -206,10 +223,9 @@ export class RoomConnection implements RoomConnection {
|
|||
} else if (message.hasRefreshroommessage()) {
|
||||
//todo: implement a way to notify the user the room was refreshed.
|
||||
} else {
|
||||
throw new Error('Unknown message received');
|
||||
throw new Error("Unknown message received");
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private dispatch(event: string, payload: unknown): void {
|
||||
|
@ -238,22 +254,22 @@ export class RoomConnection implements RoomConnection {
|
|||
this.closed = true;
|
||||
}
|
||||
|
||||
private toPositionMessage(x : number, y : number, direction : string, moving: boolean): PositionMessage {
|
||||
private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessage {
|
||||
const positionMessage = new PositionMessage();
|
||||
positionMessage.setX(Math.floor(x));
|
||||
positionMessage.setY(Math.floor(y));
|
||||
let directionEnum: Direction;
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
case "up":
|
||||
directionEnum = Direction.UP;
|
||||
break;
|
||||
case 'down':
|
||||
case "down":
|
||||
directionEnum = Direction.DOWN;
|
||||
break;
|
||||
case 'left':
|
||||
case "left":
|
||||
directionEnum = Direction.LEFT;
|
||||
break;
|
||||
case 'right':
|
||||
case "right":
|
||||
directionEnum = Direction.RIGHT;
|
||||
break;
|
||||
default:
|
||||
|
@ -275,8 +291,8 @@ export class RoomConnection implements RoomConnection {
|
|||
return viewportMessage;
|
||||
}
|
||||
|
||||
public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{
|
||||
if(!this.socket){
|
||||
public sharePosition(x: number, y: number, direction: string, moving: boolean, viewport: ViewportInterface): void {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -328,15 +344,17 @@ export class RoomConnection implements RoomConnection {
|
|||
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
|
||||
const position = message.getPosition();
|
||||
if (position === undefined) {
|
||||
throw new Error('Invalid JOIN_ROOM message');
|
||||
throw new Error("Invalid JOIN_ROOM message");
|
||||
}
|
||||
|
||||
const characterLayers = message.getCharacterlayersList().map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
|
||||
return {
|
||||
name: characterLayer.getName(),
|
||||
img: characterLayer.getUrl()
|
||||
}
|
||||
})
|
||||
const characterLayers = message
|
||||
.getCharacterlayersList()
|
||||
.map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
|
||||
return {
|
||||
name: characterLayer.getName(),
|
||||
img: characterLayer.getUrl(),
|
||||
};
|
||||
});
|
||||
|
||||
const companion = message.getCompanion();
|
||||
|
||||
|
@ -346,8 +364,8 @@ export class RoomConnection implements RoomConnection {
|
|||
characterLayers,
|
||||
visitCardUrl: message.getVisitcardurl(),
|
||||
position: ProtobufClientUtils.toPointInterface(position),
|
||||
companion: companion ? companion.getName() : null
|
||||
}
|
||||
companion: companion ? companion.getName() : null,
|
||||
};
|
||||
}
|
||||
|
||||
public onUserMoved(callback: (message: UserMovedMessage) => void): void {
|
||||
|
@ -373,7 +391,9 @@ export class RoomConnection implements RoomConnection {
|
|||
});
|
||||
}
|
||||
|
||||
public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void {
|
||||
public onGroupUpdatedOrCreated(
|
||||
callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void
|
||||
): void {
|
||||
this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
|
||||
callback(this.toGroupCreatedUpdatedMessage(message));
|
||||
});
|
||||
|
@ -382,14 +402,14 @@ export class RoomConnection implements RoomConnection {
|
|||
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
|
||||
const position = message.getPosition();
|
||||
if (position === undefined) {
|
||||
throw new Error('Missing position in GROUP_CREATE_UPDATE');
|
||||
throw new Error("Missing position in GROUP_CREATE_UPDATE");
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: message.getGroupid(),
|
||||
position: position.toObject(),
|
||||
groupSize: message.getGroupsize()
|
||||
}
|
||||
groupSize: message.getGroupsize(),
|
||||
};
|
||||
}
|
||||
|
||||
public onGroupDeleted(callback: (groupId: number) => void): void {
|
||||
|
@ -405,7 +425,7 @@ export class RoomConnection implements RoomConnection {
|
|||
}
|
||||
|
||||
public onConnectError(callback: (error: Event) => void): void {
|
||||
this.socket.addEventListener('error', callback)
|
||||
this.socket.addEventListener("error", callback);
|
||||
}
|
||||
|
||||
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
|
||||
|
@ -477,11 +497,11 @@ export class RoomConnection implements RoomConnection {
|
|||
}
|
||||
|
||||
public onServerDisconnected(callback: () => void): void {
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
this.socket.addEventListener("close", (event) => {
|
||||
if (this.closed === true || connectionManager.unloading) {
|
||||
return;
|
||||
}
|
||||
console.log('Socket closed with code '+event.code+". Reason: "+event.reason);
|
||||
console.log("Socket closed with code " + event.code + ". Reason: " + event.reason);
|
||||
if (event.code === 1000) {
|
||||
// Normal closure case
|
||||
return;
|
||||
|
@ -491,14 +511,14 @@ export class RoomConnection implements RoomConnection {
|
|||
}
|
||||
|
||||
public getUserId(): number {
|
||||
if (this.userId === null) throw 'UserId cannot be null!'
|
||||
if (this.userId === null) throw "UserId cannot be null!";
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
|
||||
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
|
||||
callback({
|
||||
userId: message.getUserid()
|
||||
userId: message.getUserid(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -522,21 +542,22 @@ export class RoomConnection implements RoomConnection {
|
|||
itemId: message.getItemid(),
|
||||
event: message.getEvent(),
|
||||
parameters: JSON.parse(message.getParametersjson()),
|
||||
state: JSON.parse(message.getStatejson())
|
||||
state: JSON.parse(message.getStatejson()),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public uploadAudio(file : FormData){
|
||||
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: {data:{}}) => {
|
||||
return res.data;
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
public uploadAudio(file: FormData) {
|
||||
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file)
|
||||
.then((res: { data: {} }) => {
|
||||
return res.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
|
||||
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
|
||||
callback({
|
||||
|
@ -559,7 +580,7 @@ export class RoomConnection implements RoomConnection {
|
|||
});
|
||||
}
|
||||
|
||||
public emitGlobalMessage(message: PlayGlobalMessageInterface){
|
||||
public emitGlobalMessage(message: PlayGlobalMessageInterface) {
|
||||
const playGlobalMessage = new PlayGlobalMessage();
|
||||
playGlobalMessage.setId(message.id);
|
||||
playGlobalMessage.setType(message.type);
|
||||
|
@ -571,7 +592,7 @@ export class RoomConnection implements RoomConnection {
|
|||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||
}
|
||||
|
||||
public emitReportPlayerMessage(reportedUserId: number, reportComment: string ): void {
|
||||
public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void {
|
||||
const reportPlayerMessage = new ReportPlayerMessage();
|
||||
reportPlayerMessage.setReporteduserid(reportedUserId);
|
||||
reportPlayerMessage.setReportcomment(reportComment);
|
||||
|
@ -582,7 +603,7 @@ export class RoomConnection implements RoomConnection {
|
|||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||
}
|
||||
|
||||
public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string|undefined ): void {
|
||||
public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string | undefined): void {
|
||||
const queryJitsiJwtMessage = new QueryJitsiJwtMessage();
|
||||
queryJitsiJwtMessage.setJitsiroom(jitsiRoom);
|
||||
if (tag !== undefined) {
|
||||
|
@ -606,16 +627,20 @@ export class RoomConnection implements RoomConnection {
|
|||
}
|
||||
|
||||
public isAdmin(): boolean {
|
||||
return this.hasTag('admin');
|
||||
return this.hasTag("admin");
|
||||
}
|
||||
|
||||
public emitEmoteEvent(emoteName: string): void {
|
||||
const emoteMessage = new EmotePromptMessage();
|
||||
emoteMessage.setEmote(emoteName)
|
||||
emoteMessage.setEmote(emoteName);
|
||||
|
||||
const clientToServerMessage = new ClientToServerMessage();
|
||||
clientToServerMessage.setEmotepromptmessage(emoteMessage);
|
||||
|
||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||
}
|
||||
|
||||
public getAllTags(): string[] {
|
||||
return this.tags;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {ResizableScene} from "../Login/ResizableScene";
|
||||
import { ResizableScene } from "../Login/ResizableScene";
|
||||
import GameObject = Phaser.GameObjects.GameObject;
|
||||
import Events = Phaser.Scenes.Events;
|
||||
import AnimationEvents = Phaser.Animations.Events;
|
||||
import StructEvents = Phaser.Structs.Events;
|
||||
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable";
|
||||
import { SKIP_RENDER_OPTIMIZATIONS } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
/**
|
||||
* A scene that can track its dirty/pristine state.
|
||||
*/
|
||||
export abstract class DirtyScene extends ResizableScene {
|
||||
private isAlreadyTracking: boolean = false;
|
||||
protected dirty:boolean = true;
|
||||
private objectListChanged:boolean = true;
|
||||
protected dirty: boolean = true;
|
||||
private objectListChanged: boolean = true;
|
||||
private physicsEnabled: boolean = false;
|
||||
|
||||
/**
|
||||
|
@ -37,6 +37,7 @@ export abstract class DirtyScene extends ResizableScene {
|
|||
|
||||
this.events.on(Events.RENDER, () => {
|
||||
this.objectListChanged = false;
|
||||
this.dirty = false;
|
||||
});
|
||||
|
||||
this.physics.disableUpdate();
|
||||
|
@ -58,7 +59,6 @@ export abstract class DirtyScene extends ResizableScene {
|
|||
this.physicsEnabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private trackAnimation(): void {
|
||||
|
@ -70,7 +70,7 @@ export abstract class DirtyScene extends ResizableScene {
|
|||
}
|
||||
|
||||
public markDirty(): void {
|
||||
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true);
|
||||
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => (this.dirty = true));
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
|
|
|
@ -1,31 +1,24 @@
|
|||
import {GameScene} from "./GameScene";
|
||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
||||
import type {Room} from "../../Connexion/Room";
|
||||
import {MenuScene, MenuSceneName} from "../Menu/MenuScene";
|
||||
import {LoginSceneName} from "../Login/LoginScene";
|
||||
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene";
|
||||
import {EnableCameraSceneName} from "../Login/EnableCameraScene";
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {get} from "svelte/store";
|
||||
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
|
||||
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
|
||||
|
||||
export interface HasMovedEvent {
|
||||
direction: string;
|
||||
moving: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
import { GameScene } from "./GameScene";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import type { Room } from "../../Connexion/Room";
|
||||
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
|
||||
import { LoginSceneName } from "../Login/LoginScene";
|
||||
import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||
import { EnableCameraSceneName } from "../Login/EnableCameraScene";
|
||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import { get } from "svelte/store";
|
||||
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
|
||||
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
|
||||
|
||||
/**
|
||||
* This class should be responsible for any scene starting/stopping
|
||||
*/
|
||||
export class GameManager {
|
||||
private playerName: string|null;
|
||||
private characterLayers: string[]|null;
|
||||
private companion: string|null;
|
||||
private startRoom!:Room;
|
||||
currentGameSceneName: string|null = null;
|
||||
private playerName: string | null;
|
||||
private characterLayers: string[] | null;
|
||||
private companion: string | null;
|
||||
private startRoom!: Room;
|
||||
currentGameSceneName: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.playerName = localUserStore.getName();
|
||||
|
@ -56,23 +49,22 @@ export class GameManager {
|
|||
localUserStore.setCharacterLayers(layers);
|
||||
}
|
||||
|
||||
getPlayerName(): string|null {
|
||||
getPlayerName(): string | null {
|
||||
return this.playerName;
|
||||
}
|
||||
|
||||
getCharacterLayers(): string[] {
|
||||
if (!this.characterLayers) {
|
||||
throw 'characterLayers are not set';
|
||||
throw "characterLayers are not set";
|
||||
}
|
||||
return this.characterLayers;
|
||||
}
|
||||
|
||||
|
||||
setCompanion(companion: string|null): void {
|
||||
setCompanion(companion: string | null): void {
|
||||
this.companion = companion;
|
||||
}
|
||||
|
||||
getCompanion(): string|null {
|
||||
getCompanion(): string | null {
|
||||
return this.companion;
|
||||
}
|
||||
|
||||
|
@ -81,18 +73,21 @@ export class GameManager {
|
|||
const mapDetail = await room.getMapDetail();
|
||||
|
||||
const gameIndex = scenePlugin.getIndex(roomID);
|
||||
if(gameIndex === -1){
|
||||
const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
|
||||
if (gameIndex === -1) {
|
||||
const game: Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
|
||||
scenePlugin.add(roomID, game, false);
|
||||
}
|
||||
}
|
||||
|
||||
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
|
||||
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id))
|
||||
console.log("starting " + (this.currentGameSceneName || this.startRoom.id));
|
||||
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
|
||||
scenePlugin.launch(MenuSceneName);
|
||||
|
||||
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
|
||||
if (
|
||||
!localUserStore.getHelpCameraSettingsShown() &&
|
||||
(!get(requestedMicrophoneState) || !get(requestedCameraState))
|
||||
) {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
localUserStore.setHelpCameraSettingsShown();
|
||||
}
|
||||
|
@ -109,7 +104,7 @@ export class GameManager {
|
|||
* This will close the socket connections and stop the gameScene, but won't remove it.
|
||||
*/
|
||||
leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void {
|
||||
if (this.currentGameSceneName === null) throw 'No current scene id set!';
|
||||
if (this.currentGameSceneName === null) throw "No current scene id set!";
|
||||
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene;
|
||||
gameScene.cleanupClosingScene();
|
||||
scene.scene.stop(this.currentGameSceneName);
|
||||
|
@ -128,13 +123,13 @@ export class GameManager {
|
|||
scene.scene.start(this.currentGameSceneName);
|
||||
scene.scene.wake(MenuSceneName);
|
||||
} else {
|
||||
scene.scene.run(fallbackSceneName)
|
||||
scene.scene.run(fallbackSceneName);
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentGameScene(scene: Phaser.Scene): GameScene {
|
||||
if (this.currentGameSceneName === null) throw 'No current scene id set!';
|
||||
return scene.scene.get(this.currentGameSceneName) as GameScene
|
||||
if (this.currentGameSceneName === null) throw "No current scene id set!";
|
||||
return scene.scene.get(this.currentGameSceneName) as GameScene;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import type { ITiledMap, ITiledMapLayerProperty } from "../Map/ITiledMap";
|
||||
import { LayersIterator } from "../Map/LayersIterator";
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
|
||||
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
||||
|
||||
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void;
|
||||
export type PropertyChangeCallback = (
|
||||
newValue: string | number | boolean | undefined,
|
||||
oldValue: string | number | boolean | undefined,
|
||||
allProps: Map<string, string | boolean | number>
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* A wrapper around a ITiledMap interface to provide additional capabilities.
|
||||
|
@ -12,41 +18,52 @@ export class GameMap {
|
|||
private lastProperties = new Map<string, string | boolean | number>();
|
||||
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||
|
||||
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {}
|
||||
public readonly layersIterator: LayersIterator;
|
||||
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {};
|
||||
public readonly flatLayers: ITiledMapLayer[];
|
||||
public readonly phaserLayers: TilemapLayer[] = [];
|
||||
|
||||
public exitUrls: Array<string> = []
|
||||
public exitUrls: Array<string> = [];
|
||||
|
||||
public hasStartTile = false;
|
||||
|
||||
public constructor(private map: ITiledMap) {
|
||||
this.layersIterator = new LayersIterator(map);
|
||||
|
||||
public constructor(
|
||||
private map: ITiledMap,
|
||||
phaserMap: Phaser.Tilemaps.Tilemap,
|
||||
terrains: Array<Phaser.Tilemaps.Tileset>
|
||||
) {
|
||||
this.flatLayers = flattenGroupLayersMap(map);
|
||||
let depth = -2;
|
||||
for (const layer of this.flatLayers) {
|
||||
if (layer.type === "tilelayer") {
|
||||
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
|
||||
}
|
||||
if (layer.type === "objectgroup" && layer.name === "floorLayer") {
|
||||
depth = DEPTH_OVERLAY_INDEX;
|
||||
}
|
||||
}
|
||||
for (const tileset of map.tilesets) {
|
||||
tileset?.tiles?.forEach(tile => {
|
||||
tileset?.tiles?.forEach((tile) => {
|
||||
if (tile.properties) {
|
||||
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties
|
||||
tile.properties.forEach(prop => {
|
||||
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties;
|
||||
tile.properties.forEach((prop) => {
|
||||
if (prop.name == "exitUrl" && typeof prop.value == "string") {
|
||||
this.exitUrls.push(prop.value);
|
||||
} else if (prop.name == "start") {
|
||||
this.hasStartTile = true
|
||||
this.hasStartTile = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
|
||||
if (this.tileSetPropertyMap[index]) {
|
||||
return this.tileSetPropertyMap[index]
|
||||
return this.tileSetPropertyMap[index];
|
||||
}
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the position of the current player (in pixels)
|
||||
* This will trigger events if properties are changing.
|
||||
|
@ -88,8 +105,8 @@ export class GameMap {
|
|||
private getProperties(key: number): Map<string, string | boolean | number> {
|
||||
const properties = new Map<string, string | boolean | number>();
|
||||
|
||||
for (const layer of this.layersIterator) {
|
||||
if (layer.type !== 'tilelayer') {
|
||||
for (const layer of this.flatLayers) {
|
||||
if (layer.type !== "tilelayer") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -99,7 +116,7 @@ export class GameMap {
|
|||
if (tiles[key] == 0) {
|
||||
continue;
|
||||
}
|
||||
tileIndex = tiles[key]
|
||||
tileIndex = tiles[key];
|
||||
}
|
||||
|
||||
// There is a tile in this layer, let's embed the properties
|
||||
|
@ -113,20 +130,29 @@ export class GameMap {
|
|||
}
|
||||
|
||||
if (tileIndex) {
|
||||
this.tileSetPropertyMap[tileIndex]?.forEach(property => {
|
||||
this.tileSetPropertyMap[tileIndex]?.forEach((property) => {
|
||||
if (property.value) {
|
||||
properties.set(property.name, property.value)
|
||||
properties.set(property.name, property.value);
|
||||
} else if (properties.has(property.name)) {
|
||||
properties.delete(property.name)
|
||||
properties.delete(property.name);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) {
|
||||
public getMap(): ITiledMap {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
private trigger(
|
||||
propName: string,
|
||||
oldValue: string | number | boolean | undefined,
|
||||
newValue: string | number | boolean | undefined,
|
||||
allProps: Map<string, string | boolean | number>
|
||||
) {
|
||||
const callbacksArray = this.callbacks.get(propName);
|
||||
if (callbacksArray !== undefined) {
|
||||
for (const callback of callbacksArray) {
|
||||
|
@ -146,4 +172,18 @@ export class GameMap {
|
|||
}
|
||||
callbacksArray.push(callback);
|
||||
}
|
||||
|
||||
public findLayer(layerName: string): ITiledMapLayer | undefined {
|
||||
return this.flatLayers.find((layer) => layer.name === layerName);
|
||||
}
|
||||
|
||||
public findPhaserLayer(layerName: string): TilemapLayer | undefined {
|
||||
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
|
||||
}
|
||||
|
||||
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
|
||||
for (const phaserLayer of this.phaserLayers) {
|
||||
phaserLayer.tileset.push(terrain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,14 @@
|
|||
import type {HasMovedEvent} from "./GameManager";
|
||||
import {MAX_EXTRAPOLATION_TIME} from "../../Enum/EnvironmentVariable";
|
||||
import type {PositionInterface} from "../../Connexion/ConnexionModels";
|
||||
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
|
||||
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
||||
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||
|
||||
export class PlayerMovement {
|
||||
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) {
|
||||
}
|
||||
public constructor(
|
||||
private startPosition: PositionInterface,
|
||||
private startTick: number,
|
||||
private endPosition: HasPlayerMovedEvent,
|
||||
private endTick: number
|
||||
) {}
|
||||
|
||||
public isOutdated(tick: number): boolean {
|
||||
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
|
||||
|
@ -17,21 +21,25 @@ export class PlayerMovement {
|
|||
return tick > this.endTick + MAX_EXTRAPOLATION_TIME;
|
||||
}
|
||||
|
||||
public getPosition(tick: number): HasMovedEvent {
|
||||
public getPosition(tick: number): HasPlayerMovedEvent {
|
||||
// Special case: end position reached and end position is not moving
|
||||
if (tick >= this.endTick && this.endPosition.moving === false) {
|
||||
//console.log('Movement finished ', this.endPosition)
|
||||
return this.endPosition;
|
||||
}
|
||||
|
||||
const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x;
|
||||
const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y;
|
||||
const x =
|
||||
(this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
|
||||
this.startPosition.x;
|
||||
const y =
|
||||
(this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
|
||||
this.startPosition.y;
|
||||
//console.log('Computed position ', x, y)
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
direction: this.endPosition.direction,
|
||||
moving: true
|
||||
}
|
||||
moving: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
* This class is in charge of computing the position of all players.
|
||||
* Player movement is delayed by 200ms so position depends on ticks.
|
||||
*/
|
||||
import type {PlayerMovement} from "./PlayerMovement";
|
||||
import type {HasMovedEvent} from "./GameManager";
|
||||
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||
import type { PlayerMovement } from "./PlayerMovement";
|
||||
|
||||
export class PlayersPositionInterpolator {
|
||||
playerMovements: Map<number, PlayerMovement> = new Map<number, PlayerMovement>();
|
||||
|
||||
updatePlayerPosition(userId: number, playerMovement: PlayerMovement) : void {
|
||||
updatePlayerPosition(userId: number, playerMovement: PlayerMovement): void {
|
||||
this.playerMovements.set(userId, playerMovement);
|
||||
}
|
||||
|
||||
|
@ -16,15 +16,15 @@ export class PlayersPositionInterpolator {
|
|||
this.playerMovements.delete(userId);
|
||||
}
|
||||
|
||||
getUpdatedPositions(tick: number) : Map<number, HasMovedEvent> {
|
||||
const positions = new Map<number, HasMovedEvent>();
|
||||
getUpdatedPositions(tick: number): Map<number, HasPlayerMovedEvent> {
|
||||
const positions = new Map<number, HasPlayerMovedEvent>();
|
||||
this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => {
|
||||
if (playerMovement.isOutdated(tick)) {
|
||||
//console.log("outdated")
|
||||
this.playerMovements.delete(userId);
|
||||
}
|
||||
//console.log("moving")
|
||||
positions.set(userId, playerMovement.getPosition(tick))
|
||||
positions.set(userId, playerMovement.getPosition(tick));
|
||||
});
|
||||
return positions;
|
||||
}
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import type { PositionInterface } from '../../Connexion/ConnexionModels';
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from '../Map/ITiledMap';
|
||||
import type { GameMap } from './GameMap';
|
||||
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
|
||||
import type { GameMap } from "./GameMap";
|
||||
|
||||
|
||||
const defaultStartLayerName = 'start';
|
||||
const defaultStartLayerName = "start";
|
||||
|
||||
export class StartPositionCalculator {
|
||||
|
||||
public startPosition!: PositionInterface
|
||||
public startPosition!: PositionInterface;
|
||||
|
||||
constructor(
|
||||
private readonly gameMap: GameMap,
|
||||
private readonly mapFile: ITiledMap,
|
||||
private readonly initPosition: PositionInterface | null,
|
||||
private readonly startLayerName: string | null) {
|
||||
public readonly startLayerName: string | null
|
||||
) {
|
||||
this.initStartXAndStartY();
|
||||
}
|
||||
private initStartXAndStartY() {
|
||||
|
@ -32,34 +31,39 @@ export class StartPositionCalculator {
|
|||
}
|
||||
// Still no start position? Something is wrong with the map, we need a "start" layer.
|
||||
if (this.startPosition === undefined) {
|
||||
console.warn('This map is missing a layer named "start" that contains the available default start positions.');
|
||||
console.warn(
|
||||
'This map is missing a layer named "start" that contains the available default start positions.'
|
||||
);
|
||||
// Let's start in the middle of the map
|
||||
this.startPosition = {
|
||||
x: this.mapFile.width * 16,
|
||||
y: this.mapFile.height * 16
|
||||
y: this.mapFile.height * 16,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param selectedLayer this is always the layer that is selected with the hash in the url
|
||||
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points
|
||||
*/
|
||||
*
|
||||
* @param selectedLayer this is always the layer that is selected with the hash in the url
|
||||
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points
|
||||
*/
|
||||
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
|
||||
if (!selectedOrDefaultLayer) {
|
||||
selectedOrDefaultLayer = defaultStartLayerName
|
||||
selectedOrDefaultLayer = defaultStartLayerName;
|
||||
}
|
||||
for (const layer of this.gameMap.layersIterator) {
|
||||
if ((selectedOrDefaultLayer === layer.name || layer.name.endsWith('/' + selectedOrDefaultLayer)) && layer.type === 'tilelayer' && (selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))) {
|
||||
if (
|
||||
(selectedOrDefaultLayer === layer.name || layer.name.endsWith("/" + selectedOrDefaultLayer)) &&
|
||||
layer.type === "tilelayer" &&
|
||||
(selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))
|
||||
) {
|
||||
const startPosition = this.startUser(layer, selectedLayer);
|
||||
this.startPosition = {
|
||||
x: startPosition.x + this.mapFile.tilewidth / 2,
|
||||
y: startPosition.y + this.mapFile.tileheight / 2
|
||||
}
|
||||
y: startPosition.y + this.mapFile.tileheight / 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private isStartLayer(layer: ITiledMapLayer): boolean {
|
||||
|
@ -67,14 +71,14 @@ export class StartPositionCalculator {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param selectedLayer this is always the layer that is selected with the hash in the url
|
||||
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points
|
||||
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points
|
||||
*/
|
||||
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
|
||||
const tiles = selectedOrDefaultLayer.data;
|
||||
if (typeof (tiles) === 'string') {
|
||||
throw new Error('The content of a JSON map must be filled as a JSON array, not as a string');
|
||||
if (typeof tiles === "string") {
|
||||
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
|
||||
}
|
||||
const possibleStartPositions: PositionInterface[] = [];
|
||||
tiles.forEach((objectKey: number, key: number) => {
|
||||
|
@ -86,8 +90,11 @@ export class StartPositionCalculator {
|
|||
|
||||
if (selectedLayer && this.gameMap.hasStartTile) {
|
||||
const properties = this.gameMap.getPropertiesForIndex(objectKey);
|
||||
if (!properties.length || !properties.some(property => property.name == "start" && property.value == selectedLayer)) {
|
||||
return
|
||||
if (
|
||||
!properties.length ||
|
||||
!properties.some((property) => property.name == "start" && property.value == selectedLayer)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
|
||||
|
@ -97,7 +104,7 @@ export class StartPositionCalculator {
|
|||
console.warn('The start layer "' + selectedOrDefaultLayer.name + '" for this map is empty.');
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
// Choose one of the available start positions at random amongst the list of available start positions.
|
||||
|
@ -109,10 +116,12 @@ export class StartPositionCalculator {
|
|||
if (!properties) {
|
||||
return undefined;
|
||||
}
|
||||
const obj = properties.find((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase());
|
||||
const obj = properties.find(
|
||||
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||
);
|
||||
if (obj === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return obj.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* Represents the interface for the Tiled exported data structure (JSON). Used
|
||||
* when loading resources via Resource loader.
|
||||
*/
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||
|
||||
export interface ITiledMap {
|
||||
width: number;
|
||||
height: number;
|
||||
|
@ -34,7 +36,7 @@ export interface ITiledMap {
|
|||
export interface ITiledMapLayerProperty {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string|boolean|number|undefined;
|
||||
value: string | boolean | number | undefined;
|
||||
}
|
||||
|
||||
/*export interface ITiledMapLayerBooleanProperty {
|
||||
|
@ -46,7 +48,7 @@ export interface ITiledMapLayerProperty {
|
|||
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
|
||||
|
||||
export interface ITiledMapGroupLayer {
|
||||
id?: number,
|
||||
id?: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
properties?: ITiledMapLayerProperty[];
|
||||
|
@ -62,8 +64,8 @@ export interface ITiledMapGroupLayer {
|
|||
}
|
||||
|
||||
export interface ITiledMapTileLayer {
|
||||
id?: number,
|
||||
data: number[]|string;
|
||||
id?: number;
|
||||
data: number[] | string;
|
||||
height: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
|
@ -81,10 +83,11 @@ export interface ITiledMapTileLayer {
|
|||
* Draw order (topdown (default), index)
|
||||
*/
|
||||
draworder?: string;
|
||||
phaserLayer?: TilemapLayer;
|
||||
}
|
||||
|
||||
export interface ITiledMapObjectLayer {
|
||||
id?: number,
|
||||
id?: number;
|
||||
height: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
|
@ -114,7 +117,7 @@ export interface ITiledMapObject {
|
|||
gid: number;
|
||||
height: number;
|
||||
name: string;
|
||||
properties: {[key: string]: string};
|
||||
properties: { [key: string]: string };
|
||||
rotation: number;
|
||||
type: string;
|
||||
visible: boolean;
|
||||
|
@ -130,26 +133,26 @@ export interface ITiledMapObject {
|
|||
/**
|
||||
* Polygon points
|
||||
*/
|
||||
polygon: {x: number, y: number}[];
|
||||
polygon: { x: number; y: number }[];
|
||||
|
||||
/**
|
||||
* Polyline points
|
||||
*/
|
||||
polyline: {x: number, y: number}[];
|
||||
polyline: { x: number; y: number }[];
|
||||
|
||||
text?: ITiledText
|
||||
text?: ITiledText;
|
||||
}
|
||||
|
||||
export interface ITiledText {
|
||||
text: string,
|
||||
wrap?: boolean,
|
||||
fontfamily?: string,
|
||||
pixelsize?: number,
|
||||
color?: string,
|
||||
underline?: boolean,
|
||||
italic?: boolean,
|
||||
strikeout?: boolean,
|
||||
halign?: "center"|"right"|"justify"|"left"
|
||||
text: string;
|
||||
wrap?: boolean;
|
||||
fontfamily?: string;
|
||||
pixelsize?: number;
|
||||
color?: string;
|
||||
underline?: boolean;
|
||||
italic?: boolean;
|
||||
strikeout?: boolean;
|
||||
halign?: "center" | "right" | "justify" | "left";
|
||||
}
|
||||
|
||||
export interface ITiledTileSet {
|
||||
|
@ -160,7 +163,7 @@ export interface ITiledTileSet {
|
|||
imagewidth: number;
|
||||
margin: number;
|
||||
name: string;
|
||||
properties: {[key: string]: string};
|
||||
properties: { [key: string]: string };
|
||||
spacing: number;
|
||||
tilecount: number;
|
||||
tileheight: number;
|
||||
|
@ -176,10 +179,10 @@ export interface ITiledTileSet {
|
|||
}
|
||||
|
||||
export interface ITile {
|
||||
id: number,
|
||||
type?: string
|
||||
id: number;
|
||||
type?: string;
|
||||
|
||||
properties?: Array<ITiledMapLayerProperty>
|
||||
properties?: Array<ITiledMapLayerProperty>;
|
||||
}
|
||||
|
||||
export interface ITiledMapTerrain {
|
||||
|
|
21
front/src/Phaser/Map/LayersFlattener.ts
Normal file
21
front/src/Phaser/Map/LayersFlattener.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type { ITiledMap, ITiledMapLayer } from "./ITiledMap";
|
||||
|
||||
/**
|
||||
* Flatten the grouped layers
|
||||
*/
|
||||
export function flattenGroupLayersMap(map: ITiledMap) {
|
||||
const flatLayers: ITiledMapLayer[] = [];
|
||||
flattenGroupLayers(map.layers, "", flatLayers);
|
||||
return flatLayers;
|
||||
}
|
||||
|
||||
function flattenGroupLayers(layers: ITiledMapLayer[], prefix: string, flatLayers: ITiledMapLayer[]) {
|
||||
for (const layer of layers) {
|
||||
if (layer.type === "group") {
|
||||
flattenGroupLayers(layer.layers, prefix + layer.name + "/", flatLayers);
|
||||
} else {
|
||||
layer.name = prefix + layer.name;
|
||||
flatLayers.push(layer);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap";
|
||||
|
||||
/**
|
||||
* Iterates over the layers of a map, flattening the grouped layers
|
||||
*/
|
||||
export class LayersIterator implements IterableIterator<ITiledMapLayer> {
|
||||
|
||||
private layers: ITiledMapLayer[] = [];
|
||||
private pointer: number = 0;
|
||||
|
||||
constructor(private map: ITiledMap) {
|
||||
this.initLayersList(map.layers, '');
|
||||
}
|
||||
|
||||
private initLayersList(layers : ITiledMapLayer[], prefix : string) {
|
||||
for (const layer of layers) {
|
||||
if (layer.type === 'group') {
|
||||
this.initLayersList(layer.layers, prefix + layer.name + '/');
|
||||
} else {
|
||||
const layerWithNewName = { ...layer };
|
||||
layerWithNewName.name = prefix+layerWithNewName.name;
|
||||
this.layers.push(layerWithNewName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public next(): IteratorResult<ITiledMapLayer> {
|
||||
if (this.pointer < this.layers.length) {
|
||||
return {
|
||||
done: false,
|
||||
value: this.layers[this.pointer++]
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
done: true,
|
||||
value: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<ITiledMapLayer> {
|
||||
return new LayersIterator(this.map);
|
||||
}
|
||||
}
|
|
@ -1,24 +1,29 @@
|
|||
import {LoginScene, LoginSceneName} from "../Login/LoginScene";
|
||||
import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene";
|
||||
import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene";
|
||||
import {gameManager} from "../Game/GameManager";
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {mediaManager} from "../../WebRtc/MediaManager";
|
||||
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
|
||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../../Url/UrlManager";
|
||||
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
|
||||
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
|
||||
import {menuIconVisible} from "../../Stores/MenuStore";
|
||||
import {videoConstraintStore} from "../../Stores/MediaStore";
|
||||
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import {get} from "svelte/store";
|
||||
import { LoginScene, LoginSceneName } from "../Login/LoginScene";
|
||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||
import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene";
|
||||
import { gameManager } from "../Game/GameManager";
|
||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import { GameConnexionTypes } from "../../Url/UrlManager";
|
||||
import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
|
||||
import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
|
||||
import { menuIconVisible } from "../../Stores/MenuStore";
|
||||
import { videoConstraintStore } from "../../Stores/MediaStore";
|
||||
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
import { Subscription } from "rxjs";
|
||||
import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterEvent";
|
||||
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
|
||||
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export const MenuSceneName = 'MenuScene';
|
||||
const gameMenuKey = 'gameMenu';
|
||||
const gameMenuIconKey = 'gameMenuIcon';
|
||||
const gameSettingsMenuKey = 'gameSettingsMenu';
|
||||
const gameShare = 'gameShare';
|
||||
export const MenuSceneName = "MenuScene";
|
||||
const gameMenuKey = "gameMenu";
|
||||
const gameMenuIconKey = "gameMenuIcon";
|
||||
const gameSettingsMenuKey = "gameSettingsMenu";
|
||||
const gameShare = "gameShare";
|
||||
|
||||
const closedSideMenuX = -1000;
|
||||
const openedSideMenuX = 0;
|
||||
|
@ -39,19 +44,49 @@ export class MenuScene extends Phaser.Scene {
|
|||
private menuButton!: Phaser.GameObjects.DOMElement;
|
||||
private warningContainer: WarningContainer | null = null;
|
||||
private warningContainerTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
private subscriptions = new Subscription();
|
||||
constructor() {
|
||||
super({key: MenuSceneName});
|
||||
super({ key: MenuSceneName });
|
||||
|
||||
this.gameQualityValue = localUserStore.getGameQualityValue();
|
||||
this.videoQualityValue = localUserStore.getVideoQualityValue();
|
||||
|
||||
this.subscriptions.add(
|
||||
registerMenuCommandStream.subscribe((menuCommand) => {
|
||||
this.addMenuOption(menuCommand);
|
||||
})
|
||||
);
|
||||
|
||||
this.subscriptions.add(
|
||||
iframeListener.unregisterMenuCommandStream.subscribe((menuCommand) => {
|
||||
this.destroyMenu(menuCommand);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
preload () {
|
||||
this.load.html(gameMenuKey, 'resources/html/gameMenu.html');
|
||||
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
|
||||
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html');
|
||||
this.load.html(gameShare, 'resources/html/gameShare.html');
|
||||
reset() {
|
||||
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
|
||||
for (let index = addedMenuItems.length - 1; index >= 0; index--) {
|
||||
addedMenuItems[index].remove();
|
||||
}
|
||||
}
|
||||
|
||||
public addMenuOption(menuText: string) {
|
||||
const wrappingSection = document.createElement("section");
|
||||
const escapedHtml = HtmlUtils.escapeHtml(menuText);
|
||||
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`;
|
||||
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
|
||||
if (menuItemContainer) {
|
||||
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove();
|
||||
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"));
|
||||
}
|
||||
}
|
||||
|
||||
preload() {
|
||||
this.load.html(gameMenuKey, "resources/html/gameMenu.html");
|
||||
this.load.html(gameMenuIconKey, "resources/html/gameMenuIcon.html");
|
||||
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
|
||||
this.load.html(gameShare, "resources/html/gameShare.html");
|
||||
this.load.html(gameReportKey, gameReportRessource);
|
||||
this.load.html(warningContainerKey, warningContainerHtml);
|
||||
}
|
||||
|
@ -60,42 +95,46 @@ export class MenuScene extends Phaser.Scene {
|
|||
menuIconVisible.set(true);
|
||||
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
|
||||
this.menuElement.setOrigin(0);
|
||||
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu');
|
||||
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
|
||||
|
||||
const middleX = (window.innerWidth / 3) - 298;
|
||||
const middleX = window.innerWidth / 3 - 298;
|
||||
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
|
||||
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality');
|
||||
|
||||
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
|
||||
|
||||
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
|
||||
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
|
||||
this.gameShareElement.addListener('click');
|
||||
this.gameShareElement.on('click', (event:MouseEvent) => {
|
||||
this.gameShareElement.addListener("click");
|
||||
this.gameShareElement.on("click", (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') {
|
||||
if ((event?.target as HTMLInputElement).id === "gameShareFormSubmit") {
|
||||
this.copyLink();
|
||||
}else if((event?.target as HTMLInputElement).id === 'gameShareFormCancel') {
|
||||
} else if ((event?.target as HTMLInputElement).id === "gameShareFormCancel") {
|
||||
this.closeGameShare();
|
||||
}
|
||||
});
|
||||
|
||||
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
|
||||
mediaManager.setShowReportModalCallBacks((userId, userName) => {
|
||||
this.closeAll();
|
||||
this.gameReportElement.open(parseInt(userId), userName);
|
||||
this.gameReportElement = new ReportMenu(
|
||||
this,
|
||||
connectionManager.getConnexionType === GameConnexionTypes.anonymous
|
||||
);
|
||||
showReportScreenStore.subscribe((user) => {
|
||||
if (user !== null) {
|
||||
this.closeAll();
|
||||
this.gameReportElement.open(user.userId, user.userName);
|
||||
}
|
||||
});
|
||||
|
||||
this.input.keyboard.on('keyup-TAB', () => {
|
||||
this.input.keyboard.on("keyup-TAB", () => {
|
||||
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
|
||||
});
|
||||
this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey);
|
||||
this.menuButton.addListener('click');
|
||||
this.menuButton.on('click', () => {
|
||||
this.menuButton.addListener("click");
|
||||
this.menuButton.on("click", () => {
|
||||
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
|
||||
});
|
||||
|
||||
this.menuElement.addListener('click');
|
||||
this.menuElement.on('click', this.onMenuClick.bind(this));
|
||||
this.menuElement.addListener("click");
|
||||
this.menuElement.on("click", this.onMenuClick.bind(this));
|
||||
|
||||
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
|
||||
}
|
||||
|
@ -112,7 +151,7 @@ export class MenuScene extends Phaser.Scene {
|
|||
public revealMenuIcon(): void {
|
||||
//TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function
|
||||
try {
|
||||
(this.menuButton.getChildByID('menuIcon') as HTMLElement).hidden = false;
|
||||
(this.menuButton.getChildByID("menuIcon") as HTMLElement).hidden = false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
@ -122,22 +161,22 @@ export class MenuScene extends Phaser.Scene {
|
|||
if (this.sideMenuOpened) return;
|
||||
this.closeAll();
|
||||
this.sideMenuOpened = true;
|
||||
this.menuButton.getChildByID('openMenuButton').innerHTML = 'X';
|
||||
this.menuButton.getChildByID("openMenuButton").innerHTML = "X";
|
||||
const connection = gameManager.getCurrentGameScene(this).connection;
|
||||
if (connection && connection.isAdmin()) {
|
||||
const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement;
|
||||
const adminSection = this.menuElement.getChildByID("adminConsoleSection") as HTMLElement;
|
||||
adminSection.hidden = false;
|
||||
}
|
||||
//TODO bind with future metadata of card
|
||||
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
|
||||
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement;
|
||||
adminSection.hidden = false;
|
||||
const adminSection = this.menuElement.getChildByID("socialLinks") as HTMLElement;
|
||||
adminSection.hidden = false;
|
||||
//}
|
||||
this.tweens.add({
|
||||
targets: this.menuElement,
|
||||
x: openedSideMenuX,
|
||||
duration: 500,
|
||||
ease: 'Power3'
|
||||
ease: "Power3",
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -150,23 +189,22 @@ export class MenuScene extends Phaser.Scene {
|
|||
}
|
||||
this.warningContainerTimeout = setTimeout(() => {
|
||||
this.warningContainer?.destroy();
|
||||
this.warningContainer = null
|
||||
this.warningContainerTimeout = null
|
||||
this.warningContainer = null;
|
||||
this.warningContainerTimeout = null;
|
||||
}, 120000);
|
||||
|
||||
}
|
||||
|
||||
private closeSideMenu(): void {
|
||||
if (!this.sideMenuOpened) return;
|
||||
this.sideMenuOpened = false;
|
||||
this.closeAll();
|
||||
this.menuButton.getChildByID('openMenuButton').innerHTML = `<img src="/static/images/menu.svg">`;
|
||||
this.menuButton.getChildByID("openMenuButton").innerHTML = `<img src="/static/images/menu.svg">`;
|
||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||
this.tweens.add({
|
||||
targets: this.menuElement,
|
||||
x: closedSideMenuX,
|
||||
duration: 500,
|
||||
ease: 'Power3'
|
||||
ease: "Power3",
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -180,29 +218,33 @@ export class MenuScene extends Phaser.Scene {
|
|||
|
||||
this.settingsMenuOpened = true;
|
||||
|
||||
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
|
||||
gameQualitySelect.value = ''+this.gameQualityValue;
|
||||
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
|
||||
videoQualitySelect.value = ''+this.videoQualityValue;
|
||||
const gameQualitySelect = this.gameQualityMenuElement.getChildByID("select-game-quality") as HTMLInputElement;
|
||||
gameQualitySelect.value = "" + this.gameQualityValue;
|
||||
const videoQualitySelect = this.gameQualityMenuElement.getChildByID("select-video-quality") as HTMLInputElement;
|
||||
videoQualitySelect.value = "" + this.videoQualityValue;
|
||||
|
||||
this.gameQualityMenuElement.addListener('click');
|
||||
this.gameQualityMenuElement.on('click', (event:MouseEvent) => {
|
||||
this.gameQualityMenuElement.addListener("click");
|
||||
this.gameQualityMenuElement.on("click", (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') {
|
||||
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
|
||||
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
|
||||
if ((event?.target as HTMLInputElement).id === "gameQualityFormSubmit") {
|
||||
const gameQualitySelect = this.gameQualityMenuElement.getChildByID(
|
||||
"select-game-quality"
|
||||
) as HTMLInputElement;
|
||||
const videoQualitySelect = this.gameQualityMenuElement.getChildByID(
|
||||
"select-video-quality"
|
||||
) as HTMLInputElement;
|
||||
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
|
||||
} else if((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') {
|
||||
} else if ((event?.target as HTMLInputElement).id === "gameQualityFormCancel") {
|
||||
this.closeGameQualityMenu();
|
||||
}
|
||||
});
|
||||
|
||||
let middleY = this.scale.height / 2 - 392/2;
|
||||
if(middleY < 0){
|
||||
let middleY = this.scale.height / 2 - 392 / 2;
|
||||
if (middleY < 0) {
|
||||
middleY = 0;
|
||||
}
|
||||
let middleX = this.scale.width / 2 - 457/2;
|
||||
if(middleX < 0){
|
||||
let middleX = this.scale.width / 2 - 457 / 2;
|
||||
if (middleX < 0) {
|
||||
middleX = 0;
|
||||
}
|
||||
this.tweens.add({
|
||||
|
@ -210,7 +252,7 @@ export class MenuScene extends Phaser.Scene {
|
|||
y: middleY,
|
||||
x: middleX,
|
||||
duration: 1000,
|
||||
ease: 'Power3'
|
||||
ease: "Power3",
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -218,17 +260,16 @@ export class MenuScene extends Phaser.Scene {
|
|||
if (!this.settingsMenuOpened) return;
|
||||
this.settingsMenuOpened = false;
|
||||
|
||||
this.gameQualityMenuElement.removeListener('click');
|
||||
this.gameQualityMenuElement.removeListener("click");
|
||||
this.tweens.add({
|
||||
targets: this.gameQualityMenuElement,
|
||||
y: -400,
|
||||
duration: 1000,
|
||||
ease: 'Power3'
|
||||
ease: "Power3",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private openGameShare(): void{
|
||||
private openGameShare(): void {
|
||||
if (this.gameShareOpened) {
|
||||
this.closeGameShare();
|
||||
return;
|
||||
|
@ -236,17 +277,17 @@ export class MenuScene extends Phaser.Scene {
|
|||
//close all
|
||||
this.closeAll();
|
||||
|
||||
const gameShareLink = this.gameShareElement.getChildByID('gameShareLink') as HTMLInputElement;
|
||||
const gameShareLink = this.gameShareElement.getChildByID("gameShareLink") as HTMLInputElement;
|
||||
gameShareLink.value = location.toString();
|
||||
|
||||
this.gameShareOpened = true;
|
||||
|
||||
let middleY = this.scale.height / 2 - 85;
|
||||
if(middleY < 0){
|
||||
if (middleY < 0) {
|
||||
middleY = 0;
|
||||
}
|
||||
let middleX = this.scale.width / 2 - 200;
|
||||
if(middleX < 0){
|
||||
if (middleX < 0) {
|
||||
middleX = 0;
|
||||
}
|
||||
this.tweens.add({
|
||||
|
@ -254,58 +295,64 @@ export class MenuScene extends Phaser.Scene {
|
|||
y: middleY,
|
||||
x: middleX,
|
||||
duration: 1000,
|
||||
ease: 'Power3'
|
||||
ease: "Power3",
|
||||
});
|
||||
}
|
||||
|
||||
private closeGameShare(): void{
|
||||
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement;
|
||||
gameShareInfo.innerText = '';
|
||||
gameShareInfo.style.display = 'none';
|
||||
private closeGameShare(): void {
|
||||
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
|
||||
gameShareInfo.innerText = "";
|
||||
gameShareInfo.style.display = "none";
|
||||
this.gameShareOpened = false;
|
||||
this.tweens.add({
|
||||
targets: this.gameShareElement,
|
||||
y: -400,
|
||||
duration: 1000,
|
||||
ease: 'Power3'
|
||||
ease: "Power3",
|
||||
});
|
||||
}
|
||||
|
||||
private onMenuClick(event:MouseEvent) {
|
||||
if((event?.target as HTMLInputElement).classList.contains('not-button')){
|
||||
private onMenuClick(event: MouseEvent) {
|
||||
const htmlMenuItem = event?.target as HTMLInputElement;
|
||||
if (htmlMenuItem.classList.contains("not-button")) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
if (htmlMenuItem.classList.contains("fromApi")) {
|
||||
sendMenuClickedEvent(htmlMenuItem.id);
|
||||
return;
|
||||
}
|
||||
|
||||
switch ((event?.target as HTMLInputElement).id) {
|
||||
case 'changeNameButton':
|
||||
case "changeNameButton":
|
||||
this.closeSideMenu();
|
||||
gameManager.leaveGame(this, LoginSceneName, new LoginScene());
|
||||
break;
|
||||
case 'sparkButton':
|
||||
case "sparkButton":
|
||||
this.gotToCreateMapPage();
|
||||
break;
|
||||
case 'changeSkinButton':
|
||||
case "changeSkinButton":
|
||||
this.closeSideMenu();
|
||||
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
|
||||
break;
|
||||
case 'changeCompanionButton':
|
||||
case "changeCompanionButton":
|
||||
this.closeSideMenu();
|
||||
gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene());
|
||||
break;
|
||||
case 'closeButton':
|
||||
case "closeButton":
|
||||
this.closeSideMenu();
|
||||
break;
|
||||
case 'shareButton':
|
||||
case "shareButton":
|
||||
this.openGameShare();
|
||||
break;
|
||||
case 'editGameSettingsButton':
|
||||
case "editGameSettingsButton":
|
||||
this.openGameSettingsMenu();
|
||||
break;
|
||||
case 'toggleFullscreen':
|
||||
case "toggleFullscreen":
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
case 'adminConsoleButton':
|
||||
case "adminConsoleButton":
|
||||
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||
} else {
|
||||
|
@ -317,12 +364,12 @@ export class MenuScene extends Phaser.Scene {
|
|||
|
||||
private async copyLink() {
|
||||
await navigator.clipboard.writeText(location.toString());
|
||||
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement;
|
||||
gameShareInfo.innerText = 'Link copied, you can share it now!';
|
||||
gameShareInfo.style.display = 'block';
|
||||
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
|
||||
gameShareInfo.innerText = "Link copied, you can share it now!";
|
||||
gameShareInfo.style.display = "block";
|
||||
}
|
||||
|
||||
private saveSetting(valueGame: number, valueVideo: number){
|
||||
private saveSetting(valueGame: number, valueVideo: number) {
|
||||
if (valueGame !== this.gameQualityValue) {
|
||||
this.gameQualityValue = valueGame;
|
||||
localUserStore.setGameQualityValue(valueGame);
|
||||
|
@ -339,27 +386,31 @@ export class MenuScene extends Phaser.Scene {
|
|||
private gotToCreateMapPage() {
|
||||
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
|
||||
//TODO fix me: this button can to send us on WorkAdventure BO.
|
||||
const sparkHost = 'https://workadventu.re/getting-started';
|
||||
window.open(sparkHost, '_blank');
|
||||
const sparkHost = "https://workadventu.re/getting-started";
|
||||
window.open(sparkHost, "_blank");
|
||||
}
|
||||
|
||||
private closeAll(){
|
||||
private closeAll() {
|
||||
this.closeGameQualityMenu();
|
||||
this.closeGameShare();
|
||||
this.gameReportElement.close();
|
||||
}
|
||||
|
||||
private toggleFullscreen() {
|
||||
const body = document.querySelector('body')
|
||||
const body = document.querySelector("body");
|
||||
if (body) {
|
||||
if (document.fullscreenElement ?? document.fullscreen) {
|
||||
document.exitFullscreen()
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
body.requestFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public destroyMenu(menu: string) {
|
||||
this.menuElement.getChildByID(menu).remove();
|
||||
}
|
||||
|
||||
public isDirty(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
|
126
front/src/Stores/BiggestAvailableAreaStore.ts
Normal file
126
front/src/Stores/BiggestAvailableAreaStore.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { get, writable } from "svelte/store";
|
||||
import type { Box } from "../WebRtc/LayoutManager";
|
||||
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||
import { LayoutMode } from "../WebRtc/LayoutManager";
|
||||
import { layoutModeStore } from "./StreamableCollectionStore";
|
||||
|
||||
/**
|
||||
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
|
||||
*/
|
||||
function findBiggestAvailableArea(): Box {
|
||||
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
|
||||
if (get(layoutModeStore) === LayoutMode.VideoChat) {
|
||||
const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div");
|
||||
const htmlChildren = Array.from(children.values());
|
||||
|
||||
// No chat? Let's go full center
|
||||
if (htmlChildren.length === 0) {
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
||||
// Compute area between top right of the last div and bottom right of window
|
||||
const area1 =
|
||||
(game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) * (game.offsetHeight - lastDiv.offsetTop);
|
||||
|
||||
// Compute area between bottom of last div and bottom of the screen on whole width
|
||||
const area2 = game.offsetWidth * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
||||
|
||||
if (area1 < 0 && area2 < 0) {
|
||||
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight,
|
||||
};
|
||||
}
|
||||
if (area1 <= area2) {
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
||||
yStart: lastDiv.offsetTop,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Possible destinations: at the center bottom or at the right bottom.
|
||||
const mainSectionChildren = Array.from(
|
||||
document.querySelectorAll<HTMLDivElement>("div.main-section > div").values()
|
||||
);
|
||||
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>("aside.sidebar > div").values());
|
||||
|
||||
// No presentation? Let's center on the screen
|
||||
if (mainSectionChildren.length === 0) {
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// At this point, we know we have at least one element in the main section.
|
||||
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length - 1];
|
||||
|
||||
const presentationArea =
|
||||
(game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) *
|
||||
(lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
||||
|
||||
let leftSideBar: number;
|
||||
let bottomSideBar: number;
|
||||
if (sidebarChildren.length === 0) {
|
||||
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("sidebar").offsetLeft;
|
||||
bottomSideBar = 0;
|
||||
} else {
|
||||
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
|
||||
leftSideBar = lastSideBarChildren.offsetLeft;
|
||||
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
||||
}
|
||||
const sideBarArea = (game.offsetWidth - leftSideBar) * (game.offsetHeight - bottomSideBar);
|
||||
|
||||
if (presentationArea <= sideBarArea) {
|
||||
return {
|
||||
xStart: leftSideBar,
|
||||
yStart: bottomSideBar,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
||||
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth, // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
||||
yEnd: game.offsetHeight,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A store that contains the list of (video) peers we are connected to.
|
||||
*/
|
||||
function createBiggestAvailableAreaStore() {
|
||||
const { subscribe, set } = writable<Box>({ xStart: 0, yStart: 0, xEnd: 1, yEnd: 1 });
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
recompute: () => {
|
||||
set(findBiggestAvailableArea());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const biggestAvailableAreaStore = createBiggestAvailableAreaStore();
|
17
front/src/Stores/GameOverlayStoreVisibility.ts
Normal file
17
front/src/Stores/GameOverlayStoreVisibility.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
/**
|
||||
* 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),
|
||||
};
|
||||
}
|
||||
|
||||
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();
|
|
@ -1,13 +1,14 @@
|
|||
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 {BrowserTooOldError} from "./Errors/BrowserTooOldError";
|
||||
import {errorStore} from "./ErrorStore";
|
||||
import {isIOS} from "../WebRtc/DeviceUtils";
|
||||
import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS";
|
||||
import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
|
||||
import { localUserStore } from "../Connexion/LocalUserStore";
|
||||
import { userMovingStore } from "./GameStore";
|
||||
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||
import { BrowserTooOldError } from "./Errors/BrowserTooOldError";
|
||||
import { errorStore } from "./ErrorStore";
|
||||
import { isIOS } from "../WebRtc/DeviceUtils";
|
||||
import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
|
||||
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
|
||||
import { peerStore } from "./PeerStore";
|
||||
import { privacyShutdownStore } from "./PrivacyShutdownStore";
|
||||
|
||||
/**
|
||||
* A store that contains the camera state requested by the user (on or off).
|
||||
|
@ -35,35 +36,6 @@ function createRequestedMicrophoneState() {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
@ -79,49 +51,13 @@ function createEnableCameraSceneVisibilityStore() {
|
|||
|
||||
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;
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const unsubscribe = requestedCameraState.subscribe((enabled) => {
|
||||
if (enabled === true) {
|
||||
|
@ -135,7 +71,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
|||
} else {
|
||||
set(false);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return function stop() {
|
||||
unsubscribe();
|
||||
|
@ -146,7 +82,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
|||
* 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;
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const unsubscribe = userMovingStore.subscribe((moving) => {
|
||||
if (moving === true) {
|
||||
|
@ -158,45 +94,51 @@ const userMoved5SecondsAgoStore = readable(false, function start(set) {
|
|||
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 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;
|
||||
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);
|
||||
document.addEventListener("mousemove", detectInBottomRight);
|
||||
|
||||
return function stop() {
|
||||
document.removeEventListener('mousemove', detectInBottomRight);
|
||||
}
|
||||
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;
|
||||
});
|
||||
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.
|
||||
|
@ -207,28 +149,30 @@ function createVideoConstraintStore() {
|
|||
height: { min: 400, ideal: 720 },
|
||||
frameRate: { ideal: localUserStore.getVideoQualityValue() },
|
||||
facingMode: "user",
|
||||
resizeMode: 'crop-and-scale',
|
||||
aspectRatio: 1.777777778
|
||||
resizeMode: "crop-and-scale",
|
||||
aspectRatio: 1.777777778,
|
||||
} as MediaTrackConstraints);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
setDeviceId: (deviceId: string|undefined) => update((constraints) => {
|
||||
if (deviceId !== undefined) {
|
||||
constraints.deviceId = {
|
||||
exact: deviceId
|
||||
};
|
||||
} else {
|
||||
delete constraints.deviceId;
|
||||
}
|
||||
setDeviceId: (deviceId: string | undefined) =>
|
||||
update((constraints) => {
|
||||
if (deviceId !== undefined) {
|
||||
constraints.deviceId = {
|
||||
exact: deviceId,
|
||||
};
|
||||
} else {
|
||||
delete constraints.deviceId;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}),
|
||||
setFrameRate: (frameRate: number) => update((constraints) => {
|
||||
constraints.frameRate = { ideal: frameRate };
|
||||
return constraints;
|
||||
}),
|
||||
setFrameRate: (frameRate: number) =>
|
||||
update((constraints) => {
|
||||
constraints.frameRate = { ideal: frameRate };
|
||||
|
||||
return constraints;
|
||||
})
|
||||
return constraints;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -242,39 +186,39 @@ function createAudioConstraintStore() {
|
|||
//TODO: make these values configurable in the game settings menu and store them in localstorage
|
||||
autoGainControl: false,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
} as boolean|MediaTrackConstraints);
|
||||
noiseSuppression: true,
|
||||
} as boolean | MediaTrackConstraints);
|
||||
|
||||
let selectedDeviceId = null;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
setDeviceId: (deviceId: string|undefined) => update((constraints) => {
|
||||
selectedDeviceId = deviceId;
|
||||
setDeviceId: (deviceId: string | undefined) =>
|
||||
update((constraints) => {
|
||||
selectedDeviceId = deviceId;
|
||||
|
||||
if (typeof(constraints) === 'boolean') {
|
||||
constraints = {}
|
||||
}
|
||||
if (deviceId !== undefined) {
|
||||
constraints.deviceId = {
|
||||
exact: selectedDeviceId
|
||||
};
|
||||
} else {
|
||||
delete constraints.deviceId;
|
||||
}
|
||||
if (typeof constraints === "boolean") {
|
||||
constraints = {};
|
||||
}
|
||||
if (deviceId !== undefined) {
|
||||
constraints.deviceId = {
|
||||
exact: selectedDeviceId,
|
||||
};
|
||||
} else {
|
||||
delete constraints.deviceId;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
})
|
||||
return constraints;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export const audioConstraintStore = createAudioConstraintStore();
|
||||
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
|
||||
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
|
||||
let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
|
||||
let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
|
||||
|
||||
/**
|
||||
* A store containing the media constraints we want to apply.
|
||||
|
@ -289,7 +233,8 @@ export const mediaStreamConstraintsStore = derived(
|
|||
audioConstraintStore,
|
||||
privacyShutdownStore,
|
||||
cameraEnergySavingStore,
|
||||
], (
|
||||
],
|
||||
(
|
||||
[
|
||||
$requestedCameraState,
|
||||
$requestedMicrophoneState,
|
||||
|
@ -299,92 +244,97 @@ export const mediaStreamConstraintsStore = derived(
|
|||
$audioConstraintStore,
|
||||
$privacyShutdownStore,
|
||||
$cameraEnergySavingStore,
|
||||
], set
|
||||
],
|
||||
set
|
||||
) => {
|
||||
let currentVideoConstraint: boolean | MediaTrackConstraints = $videoConstraintStore;
|
||||
let currentAudioConstraint: boolean | MediaTrackConstraints = $audioConstraintStore;
|
||||
|
||||
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(() => {
|
||||
if ($enableCameraSceneVisibilityStore) {
|
||||
set({
|
||||
video: currentVideoConstraint,
|
||||
audio: currentAudioConstraint,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, {
|
||||
video: false,
|
||||
audio: false
|
||||
} as MediaStreamConstraints);
|
||||
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,
|
||||
type: "success";
|
||||
stream: MediaStream | null;
|
||||
// The constraints that we got (and not the one that have been requested)
|
||||
constraints: MediaStreamConstraints
|
||||
constraints: MediaStreamConstraints;
|
||||
}
|
||||
|
||||
interface StreamErrorValue {
|
||||
type: "error",
|
||||
error: Error,
|
||||
constraints: MediaStreamConstraints
|
||||
type: "error";
|
||||
error: Error;
|
||||
constraints: MediaStreamConstraints;
|
||||
}
|
||||
|
||||
let currentStream : MediaStream|null = null;
|
||||
let currentStream: MediaStream | null = null;
|
||||
|
||||
/**
|
||||
* Stops the camera from filming
|
||||
|
@ -411,84 +361,94 @@ function stopMicrophone(): void {
|
|||
/**
|
||||
* 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 };
|
||||
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.');
|
||||
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,
|
||||
});
|
||||
return;
|
||||
} else if (isIOS()) {
|
||||
set({
|
||||
type: "error",
|
||||
error: new WebviewOnOldIOS(),
|
||||
constraints,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
set({
|
||||
type: "error",
|
||||
error: new BrowserTooOldError(),
|
||||
constraints,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (constraints.audio === false) {
|
||||
stopMicrophone();
|
||||
}
|
||||
if (constraints.video === false) {
|
||||
stopCamera();
|
||||
}
|
||||
|
||||
if (constraints.audio === false && constraints.video === false) {
|
||||
currentStream = null;
|
||||
set({
|
||||
type: 'error',
|
||||
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'),
|
||||
constraints
|
||||
});
|
||||
return;
|
||||
} else if (isIOS()) {
|
||||
set({
|
||||
type: 'error',
|
||||
error: new WebviewOnOldIOS(),
|
||||
constraints
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
set({
|
||||
type: 'error',
|
||||
error: new BrowserTooOldError(),
|
||||
constraints
|
||||
type: "success",
|
||||
stream: null,
|
||||
constraints,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
(async () => {
|
||||
try {
|
||||
stopMicrophone();
|
||||
stopCamera();
|
||||
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
set({
|
||||
type: 'error',
|
||||
error: e,
|
||||
constraints
|
||||
type: "success",
|
||||
stream: currentStream,
|
||||
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
|
||||
});
|
||||
}
|
||||
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;
|
||||
/*constraints.video = false;
|
||||
if (constraints.audio === false) {
|
||||
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
|
||||
set({
|
||||
|
@ -517,9 +477,10 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
|
|||
});
|
||||
}
|
||||
}*/
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
|
||||
|
@ -536,12 +497,15 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
|
|||
|
||||
const queryDeviceList = () => {
|
||||
// Note: so far, we are ignoring any failures.
|
||||
navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => {
|
||||
set(mediaDeviceInfos);
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
throw e;
|
||||
});
|
||||
navigator.mediaDevices
|
||||
.enumerateDevices()
|
||||
.then((mediaDeviceInfos) => {
|
||||
set(mediaDeviceInfos);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
const unsubscribe = localStreamStore.subscribe((streamResult) => {
|
||||
|
@ -554,23 +518,23 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
|
|||
});
|
||||
|
||||
if (navigator.mediaDevices) {
|
||||
navigator.mediaDevices.addEventListener('devicechange', queryDeviceList);
|
||||
navigator.mediaDevices.addEventListener("devicechange", queryDeviceList);
|
||||
}
|
||||
|
||||
return function stop() {
|
||||
unsubscribe();
|
||||
if (navigator.mediaDevices) {
|
||||
navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList);
|
||||
navigator.mediaDevices.removeEventListener("devicechange", queryDeviceList);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
|
||||
return $deviceListStore.filter(device => device.kind === 'videoinput');
|
||||
return $deviceListStore.filter((device) => device.kind === "videoinput");
|
||||
});
|
||||
|
||||
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
|
||||
return $deviceListStore.filter(device => device.kind === 'audioinput');
|
||||
return $deviceListStore.filter((device) => device.kind === "audioinput");
|
||||
});
|
||||
|
||||
// TODO: detect the new webcam and automatically switch on it.
|
||||
|
@ -583,7 +547,7 @@ cameraListStore.subscribe((devices) => {
|
|||
|
||||
// If we cannot find the device ID, let's remove it.
|
||||
// @ts-ignore
|
||||
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
|
||||
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
|
||||
videoConstraintStore.setDeviceId(undefined);
|
||||
}
|
||||
});
|
||||
|
@ -591,7 +555,7 @@ cameraListStore.subscribe((devices) => {
|
|||
microphoneListStore.subscribe((devices) => {
|
||||
// If the selected camera is unplugged, let's remove the constraint on deviceId
|
||||
const constraints = get(audioConstraintStore);
|
||||
if (typeof constraints === 'boolean') {
|
||||
if (typeof constraints === "boolean") {
|
||||
return;
|
||||
}
|
||||
if (!constraints.deviceId) {
|
||||
|
@ -600,13 +564,13 @@ microphoneListStore.subscribe((devices) => {
|
|||
|
||||
// If we cannot find the device ID, let's remove it.
|
||||
// @ts-ignore
|
||||
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
|
||||
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
|
||||
audioConstraintStore.setDeviceId(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
localStreamStore.subscribe(streamResult => {
|
||||
if (streamResult.type === 'error') {
|
||||
localStreamStore.subscribe((streamResult) => {
|
||||
if (streamResult.type === "error") {
|
||||
if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) {
|
||||
errorStore.addErrorMessage(streamResult.error);
|
||||
}
|
||||
|
|
|
@ -1,36 +1,121 @@
|
|||
import { derived, writable, Writable } from "svelte/store";
|
||||
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
|
||||
import type {SimplePeer} from "../WebRtc/SimplePeer";
|
||||
import { readable, writable } from "svelte/store";
|
||||
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
|
||||
import { VideoPeer } from "../WebRtc/VideoPeer";
|
||||
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
|
||||
|
||||
/**
|
||||
* A store that contains the camera state requested by the user (on or off).
|
||||
* A store that contains the list of (video) peers we are connected to.
|
||||
*/
|
||||
function createPeerStore() {
|
||||
let users = new Map<number, UserSimplePeerInterface>();
|
||||
let peers = new Map<number, VideoPeer>();
|
||||
|
||||
const { subscribe, set, update } = writable(users);
|
||||
const { subscribe, set, update } = writable(peers);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||
users = new Map<number, UserSimplePeerInterface>();
|
||||
set(users);
|
||||
peers = new Map<number, VideoPeer>();
|
||||
set(peers);
|
||||
simplePeer.registerPeerConnectionListener({
|
||||
onConnect(user: UserSimplePeerInterface) {
|
||||
update(users => {
|
||||
users.set(user.userId, user);
|
||||
return users;
|
||||
});
|
||||
onConnect(peer: RemotePeer) {
|
||||
if (peer instanceof VideoPeer) {
|
||||
update((users) => {
|
||||
users.set(peer.userId, peer);
|
||||
return users;
|
||||
});
|
||||
}
|
||||
},
|
||||
onDisconnect(userId: number) {
|
||||
update(users => {
|
||||
update((users) => {
|
||||
users.delete(userId);
|
||||
return users;
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A store that contains the list of screen sharing peers we are connected to.
|
||||
*/
|
||||
function createScreenSharingPeerStore() {
|
||||
let peers = new Map<number, ScreenSharingPeer>();
|
||||
|
||||
const { subscribe, set, update } = writable(peers);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||
peers = new Map<number, ScreenSharingPeer>();
|
||||
set(peers);
|
||||
simplePeer.registerPeerConnectionListener({
|
||||
onConnect(peer: RemotePeer) {
|
||||
if (peer instanceof ScreenSharingPeer) {
|
||||
update((users) => {
|
||||
users.set(peer.userId, peer);
|
||||
return users;
|
||||
});
|
||||
}
|
||||
},
|
||||
onDisconnect(userId: number) {
|
||||
update((users) => {
|
||||
users.delete(userId);
|
||||
return users;
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const peerStore = createPeerStore();
|
||||
export const screenSharingPeerStore = createScreenSharingPeerStore();
|
||||
|
||||
/**
|
||||
* A store that contains ScreenSharingPeer, ONLY if those ScreenSharingPeer are emitting a stream towards us!
|
||||
*/
|
||||
function createScreenSharingStreamStore() {
|
||||
let peers = new Map<number, ScreenSharingPeer>();
|
||||
|
||||
return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) {
|
||||
let unsubscribes: (() => void)[] = [];
|
||||
|
||||
const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => {
|
||||
for (const unsubscribe of unsubscribes) {
|
||||
unsubscribe();
|
||||
}
|
||||
unsubscribes = [];
|
||||
|
||||
peers = new Map<number, ScreenSharingPeer>();
|
||||
|
||||
screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => {
|
||||
if (screenSharingPeer.isReceivingScreenSharingStream()) {
|
||||
peers.set(key, screenSharingPeer);
|
||||
}
|
||||
|
||||
unsubscribes.push(
|
||||
screenSharingPeer.streamStore.subscribe((stream) => {
|
||||
if (stream) {
|
||||
peers.set(key, screenSharingPeer);
|
||||
} else {
|
||||
peers.delete(key);
|
||||
}
|
||||
set(peers);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
set(peers);
|
||||
});
|
||||
|
||||
return function stop() {
|
||||
unsubscribe();
|
||||
for (const unsubscribe of unsubscribes) {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const screenSharingStreamStore = createScreenSharingStreamStore();
|
||||
|
|
36
front/src/Stores/PrivacyShutdownStore.ts
Normal file
36
front/src/Stores/PrivacyShutdownStore.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { get, writable } from "svelte/store";
|
||||
import { peerStore } from "./PeerStore";
|
||||
import { visibilityStore } from "./VisibilityStore";
|
||||
|
||||
/**
|
||||
* 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();
|
|
@ -1,18 +1,10 @@
|
|||
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";
|
||||
import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
|
||||
import { peerStore } from "./PeerStore";
|
||||
import type { LocalStreamStoreValue } from "./MediaStore";
|
||||
import { DivImportance } from "../WebRtc/LayoutManager";
|
||||
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
|
||||
|
||||
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
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).
|
||||
|
@ -29,7 +21,7 @@ function createRequestedScreenSharingState() {
|
|||
|
||||
export const requestedScreenSharingState = createRequestedScreenSharingState();
|
||||
|
||||
let currentStream : MediaStream|null = null;
|
||||
let currentStream: MediaStream | null = null;
|
||||
|
||||
/**
|
||||
* Stops the camera from filming
|
||||
|
@ -43,27 +35,17 @@ function stopScreenSharing(): void {
|
|||
currentStream = null;
|
||||
}
|
||||
|
||||
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
|
||||
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
|
||||
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;
|
||||
[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) {
|
||||
|
@ -84,7 +66,10 @@ export const screenSharingConstraintsStore = derived(
|
|||
}
|
||||
|
||||
// Let's make the changes only if the new value is different from the old one.
|
||||
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
|
||||
if (
|
||||
previousComputedVideoConstraint != currentVideoConstraint ||
|
||||
previousComputedAudioConstraint != currentAudioConstraint
|
||||
) {
|
||||
previousComputedVideoConstraint = currentVideoConstraint;
|
||||
previousComputedAudioConstraint = currentAudioConstraint;
|
||||
// Let's copy the objects.
|
||||
|
@ -100,85 +85,89 @@ export const screenSharingConstraintsStore = derived(
|
|||
audio: currentAudioConstraint,
|
||||
});
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
video: false,
|
||||
audio: false
|
||||
} as MediaStreamConstraints);
|
||||
|
||||
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;
|
||||
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 && 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 {
|
||||
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
|
||||
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
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
requestedScreenSharingState.disableScreenSharing();
|
||||
set({
|
||||
type: 'success',
|
||||
stream: currentStream,
|
||||
constraints
|
||||
type: "success",
|
||||
stream: null,
|
||||
constraints,
|
||||
});
|
||||
return;
|
||||
} catch (e) {
|
||||
currentStream = null;
|
||||
requestedScreenSharingState.disableScreenSharing();
|
||||
console.info("Error. Unable to share screen.", e);
|
||||
set({
|
||||
type: 'error',
|
||||
error: e,
|
||||
constraints
|
||||
});
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
let currentStreamPromise: Promise<MediaStream>;
|
||||
if (navigator.getDisplayMedia) {
|
||||
currentStreamPromise = navigator.getDisplayMedia({ constraints });
|
||||
} else if (navigator.mediaDevices && 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;
|
||||
requestedScreenSharingState.disableScreenSharing();
|
||||
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.
|
||||
|
@ -191,3 +180,32 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
|
|||
|
||||
set($peerStore.size !== 0);
|
||||
});
|
||||
|
||||
export interface ScreenSharingLocalMedia {
|
||||
uniqueId: string;
|
||||
stream: MediaStream | null;
|
||||
//subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber;
|
||||
}
|
||||
|
||||
/**
|
||||
* The representation of the screen sharing stream.
|
||||
*/
|
||||
export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia | null>(null, function start(set) {
|
||||
const localMedia: ScreenSharingLocalMedia = {
|
||||
uniqueId: "localScreenSharingStream",
|
||||
stream: null,
|
||||
};
|
||||
|
||||
const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => {
|
||||
if (screenSharingLocalStream.type === "success") {
|
||||
localMedia.stream = screenSharingLocalStream.stream;
|
||||
} else {
|
||||
localMedia.stream = null;
|
||||
}
|
||||
set(localMedia);
|
||||
});
|
||||
|
||||
return function stop() {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
|
|
3
front/src/Stores/ShowReportScreenStore.ts
Normal file
3
front/src/Stores/ShowReportScreenStore.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
export const showReportScreenStore = writable<{ userId: number; userName: string } | null>(null);
|
36
front/src/Stores/StreamableCollectionStore.ts
Normal file
36
front/src/Stores/StreamableCollectionStore.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { derived, get, Readable, writable } from "svelte/store";
|
||||
import { ScreenSharingLocalMedia, screenSharingLocalMedia } from "./ScreenSharingStore";
|
||||
import { peerStore, screenSharingStreamStore } from "./PeerStore";
|
||||
import type { RemotePeer } from "../WebRtc/SimplePeer";
|
||||
import { LayoutMode } from "../WebRtc/LayoutManager";
|
||||
|
||||
export type Streamable = RemotePeer | ScreenSharingLocalMedia;
|
||||
|
||||
export const layoutModeStore = writable<LayoutMode>(LayoutMode.Presentation);
|
||||
|
||||
/**
|
||||
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
|
||||
*/
|
||||
function createStreamableCollectionStore(): Readable<Map<string, Streamable>> {
|
||||
return derived(
|
||||
[screenSharingStreamStore, peerStore, screenSharingLocalMedia],
|
||||
([$screenSharingStreamStore, $peerStore, $screenSharingLocalMedia], set) => {
|
||||
const peers = new Map<string, Streamable>();
|
||||
|
||||
const addPeer = (peer: Streamable) => {
|
||||
peers.set(peer.uniqueId, peer);
|
||||
};
|
||||
|
||||
$screenSharingStreamStore.forEach(addPeer);
|
||||
$peerStore.forEach(addPeer);
|
||||
|
||||
if ($screenSharingLocalMedia?.stream) {
|
||||
addPeer($screenSharingLocalMedia);
|
||||
}
|
||||
|
||||
set(peers);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const streamableCollectionStore = createStreamableCollectionStore();
|
49
front/src/Stores/VideoFocusStore.ts
Normal file
49
front/src/Stores/VideoFocusStore.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { writable } from "svelte/store";
|
||||
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
|
||||
import { VideoPeer } from "../WebRtc/VideoPeer";
|
||||
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
|
||||
import type { Streamable } from "./StreamableCollectionStore";
|
||||
|
||||
/**
|
||||
* A store that contains the peer / media that has currently the "importance" focus.
|
||||
*/
|
||||
function createVideoFocusStore() {
|
||||
const { subscribe, set, update } = writable<Streamable | null>(null);
|
||||
|
||||
let focusedMedia: Streamable | null = null;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
focus: (media: Streamable) => {
|
||||
focusedMedia = media;
|
||||
set(media);
|
||||
},
|
||||
removeFocus: () => {
|
||||
focusedMedia = null;
|
||||
set(null);
|
||||
},
|
||||
toggleFocus: (media: Streamable) => {
|
||||
if (media !== focusedMedia) {
|
||||
focusedMedia = media;
|
||||
} else {
|
||||
focusedMedia = null;
|
||||
}
|
||||
set(focusedMedia);
|
||||
},
|
||||
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||
simplePeer.registerPeerConnectionListener({
|
||||
onConnect(peer: RemotePeer) {},
|
||||
onDisconnect(userId: number) {
|
||||
if (
|
||||
(focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) &&
|
||||
focusedMedia.userId === userId
|
||||
) {
|
||||
set(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const videoFocusStore = createVideoFocusStore();
|
16
front/src/Stores/VisibilityStore.ts
Normal file
16
front/src/Stores/VisibilityStore.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { readable } from "svelte/store";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
import {HtmlUtils} from "./HtmlUtils";
|
||||
import type {ShowReportCallBack} from "./MediaManager";
|
||||
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
||||
import {connectionManager} from "../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
||||
import {iframeListener} from "../Api/IframeListener";
|
||||
import { HtmlUtils } from "./HtmlUtils";
|
||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||
import { connectionManager } from "../Connexion/ConnectionManager";
|
||||
import { GameConnexionTypes } from "../Url/UrlManager";
|
||||
import { iframeListener } from "../Api/IframeListener";
|
||||
import { showReportScreenStore } from "../Stores/ShowReportScreenStore";
|
||||
|
||||
export type SendMessageCallback = (message:string) => void;
|
||||
export type SendMessageCallback = (message: string) => void;
|
||||
|
||||
export class DiscussionManager {
|
||||
private mainContainer: HTMLDivElement;
|
||||
|
@ -15,80 +15,81 @@ export class DiscussionManager {
|
|||
private nbpParticipants?: HTMLParagraphElement;
|
||||
private divMessages?: HTMLParagraphElement;
|
||||
|
||||
private participants: Map<number|string, HTMLDivElement> = new Map<number|string, HTMLDivElement>();
|
||||
private participants: Map<number | string, HTMLDivElement> = new Map<number | string, HTMLDivElement>();
|
||||
|
||||
private activeDiscussion: boolean = false;
|
||||
|
||||
private sendMessageCallBack : Map<number|string, SendMessageCallback> = new Map<number|string, SendMessageCallback>();
|
||||
private sendMessageCallBack: Map<number | string, SendMessageCallback> = new Map<
|
||||
number | string,
|
||||
SendMessageCallback
|
||||
>();
|
||||
|
||||
private userInputManager?: UserInputManager;
|
||||
|
||||
constructor() {
|
||||
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
||||
this.createDiscussPart(''); //todo: why do we always use empty string?
|
||||
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||
this.createDiscussPart(""); //todo: why do we always use empty string?
|
||||
|
||||
iframeListener.chatStream.subscribe((chatEvent) => {
|
||||
this.addMessage(chatEvent.author, chatEvent.message, false);
|
||||
this.showDiscussion();
|
||||
});
|
||||
this.onSendMessageCallback('iframe_listener', (message) => {
|
||||
this.onSendMessageCallback("iframe_listener", (message) => {
|
||||
iframeListener.sendUserInputChat(message);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private createDiscussPart(name: string) {
|
||||
this.divDiscuss = document.createElement('div');
|
||||
this.divDiscuss.classList.add('discussion');
|
||||
this.divDiscuss = document.createElement("div");
|
||||
this.divDiscuss.classList.add("discussion");
|
||||
|
||||
const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button');
|
||||
buttonCloseDiscussion.classList.add('close-btn');
|
||||
const buttonCloseDiscussion: HTMLButtonElement = document.createElement("button");
|
||||
buttonCloseDiscussion.classList.add("close-btn");
|
||||
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
|
||||
buttonCloseDiscussion.addEventListener('click', () => {
|
||||
buttonCloseDiscussion.addEventListener("click", () => {
|
||||
this.hideDiscussion();
|
||||
});
|
||||
this.divDiscuss.appendChild(buttonCloseDiscussion);
|
||||
|
||||
const myName: HTMLParagraphElement = document.createElement('p');
|
||||
const myName: HTMLParagraphElement = document.createElement("p");
|
||||
myName.innerText = name.toUpperCase();
|
||||
this.nbpParticipants = document.createElement('p');
|
||||
this.nbpParticipants.innerText = 'PARTICIPANTS (1)';
|
||||
this.nbpParticipants = document.createElement("p");
|
||||
this.nbpParticipants.innerText = "PARTICIPANTS (1)";
|
||||
|
||||
this.divParticipants = document.createElement('div');
|
||||
this.divParticipants.classList.add('participants');
|
||||
this.divParticipants = document.createElement("div");
|
||||
this.divParticipants.classList.add("participants");
|
||||
|
||||
this.divMessages = document.createElement('div');
|
||||
this.divMessages.classList.add('messages');
|
||||
this.divMessages.innerHTML = "<h2>Local messages</h2>"
|
||||
this.divMessages = document.createElement("div");
|
||||
this.divMessages.classList.add("messages");
|
||||
this.divMessages.innerHTML = "<h2>Local messages</h2>";
|
||||
|
||||
this.divDiscuss.appendChild(myName);
|
||||
this.divDiscuss.appendChild(this.nbpParticipants);
|
||||
this.divDiscuss.appendChild(this.divParticipants);
|
||||
this.divDiscuss.appendChild(this.divMessages);
|
||||
|
||||
const sendDivMessage: HTMLDivElement = document.createElement('div');
|
||||
sendDivMessage.classList.add('send-message');
|
||||
const inputMessage: HTMLInputElement = document.createElement('input');
|
||||
const sendDivMessage: HTMLDivElement = document.createElement("div");
|
||||
sendDivMessage.classList.add("send-message");
|
||||
const inputMessage: HTMLInputElement = document.createElement("input");
|
||||
inputMessage.onfocus = () => {
|
||||
if(this.userInputManager) {
|
||||
if (this.userInputManager) {
|
||||
this.userInputManager.disableControls();
|
||||
}
|
||||
}
|
||||
};
|
||||
inputMessage.onblur = () => {
|
||||
if(this.userInputManager) {
|
||||
if (this.userInputManager) {
|
||||
this.userInputManager.restoreControls();
|
||||
}
|
||||
}
|
||||
};
|
||||
inputMessage.type = "text";
|
||||
inputMessage.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputMessage.addEventListener("keyup", (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if(inputMessage.value === null
|
||||
|| inputMessage.value === ''
|
||||
|| inputMessage.value === undefined) {
|
||||
if (inputMessage.value === null || inputMessage.value === "" || inputMessage.value === undefined) {
|
||||
return;
|
||||
}
|
||||
this.addMessage(name, inputMessage.value, true);
|
||||
for(const callback of this.sendMessageCallBack.values()) {
|
||||
for (const callback of this.sendMessageCallBack.values()) {
|
||||
callback(inputMessage.value);
|
||||
}
|
||||
inputMessage.value = "";
|
||||
|
@ -100,48 +101,44 @@ export class DiscussionManager {
|
|||
//append in main container
|
||||
this.mainContainer.appendChild(this.divDiscuss);
|
||||
|
||||
this.addParticipant('me', 'Moi', undefined, true);
|
||||
this.addParticipant("me", "Moi", undefined, true);
|
||||
}
|
||||
|
||||
public addParticipant(
|
||||
userId: number|string,
|
||||
name: string|undefined,
|
||||
img?: string|undefined,
|
||||
isMe: boolean = false,
|
||||
showReportCallBack?: ShowReportCallBack
|
||||
userId: number | "me",
|
||||
name: string | undefined,
|
||||
img?: string | undefined,
|
||||
isMe: boolean = false
|
||||
) {
|
||||
const divParticipant: HTMLDivElement = document.createElement('div');
|
||||
divParticipant.classList.add('participant');
|
||||
const divParticipant: HTMLDivElement = document.createElement("div");
|
||||
divParticipant.classList.add("participant");
|
||||
divParticipant.id = `participant-${userId}`;
|
||||
|
||||
const divImgParticipant: HTMLImageElement = document.createElement('img');
|
||||
divImgParticipant.src = 'resources/logos/boy.svg';
|
||||
const divImgParticipant: HTMLImageElement = document.createElement("img");
|
||||
divImgParticipant.src = "resources/logos/boy.svg";
|
||||
if (img !== undefined) {
|
||||
divImgParticipant.src = img;
|
||||
}
|
||||
const divPParticipant: HTMLParagraphElement = document.createElement('p');
|
||||
if(!name){
|
||||
name = 'Anonymous';
|
||||
const divPParticipant: HTMLParagraphElement = document.createElement("p");
|
||||
if (!name) {
|
||||
name = "Anonymous";
|
||||
}
|
||||
divPParticipant.innerText = name;
|
||||
|
||||
divParticipant.appendChild(divImgParticipant);
|
||||
divParticipant.appendChild(divPParticipant);
|
||||
|
||||
if(
|
||||
!isMe
|
||||
&& connectionManager.getConnexionType
|
||||
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
|
||||
if (
|
||||
!isMe &&
|
||||
connectionManager.getConnexionType &&
|
||||
connectionManager.getConnexionType !== GameConnexionTypes.anonymous &&
|
||||
userId !== "me"
|
||||
) {
|
||||
const reportBanUserAction: HTMLButtonElement = document.createElement('button');
|
||||
reportBanUserAction.classList.add('report-btn')
|
||||
reportBanUserAction.innerText = 'Report';
|
||||
reportBanUserAction.addEventListener('click', () => {
|
||||
if(showReportCallBack) {
|
||||
showReportCallBack(`${userId}`, name);
|
||||
}else{
|
||||
console.info('report feature is not activated!');
|
||||
}
|
||||
const reportBanUserAction: HTMLButtonElement = document.createElement("button");
|
||||
reportBanUserAction.classList.add("report-btn");
|
||||
reportBanUserAction.innerText = "Report";
|
||||
reportBanUserAction.addEventListener("click", () => {
|
||||
showReportScreenStore.set({ userId: userId, userName: name ? name : "" });
|
||||
});
|
||||
divParticipant.appendChild(reportBanUserAction);
|
||||
}
|
||||
|
@ -161,16 +158,16 @@ export class DiscussionManager {
|
|||
}
|
||||
|
||||
public addMessage(name: string, message: string, isMe: boolean = false) {
|
||||
const divMessage: HTMLDivElement = document.createElement('div');
|
||||
divMessage.classList.add('message');
|
||||
if(isMe){
|
||||
divMessage.classList.add('me');
|
||||
const divMessage: HTMLDivElement = document.createElement("div");
|
||||
divMessage.classList.add("message");
|
||||
if (isMe) {
|
||||
divMessage.classList.add("me");
|
||||
}
|
||||
|
||||
const pMessage: HTMLParagraphElement = document.createElement('p');
|
||||
const pMessage: HTMLParagraphElement = document.createElement("p");
|
||||
const date = new Date();
|
||||
if(isMe){
|
||||
name = 'Me';
|
||||
if (isMe) {
|
||||
name = "Me";
|
||||
} else {
|
||||
name = HtmlUtils.escapeHtml(name);
|
||||
}
|
||||
|
@ -180,9 +177,9 @@ export class DiscussionManager {
|
|||
</span>`;
|
||||
divMessage.appendChild(pMessage);
|
||||
|
||||
const userMessage: HTMLParagraphElement = document.createElement('p');
|
||||
const userMessage: HTMLParagraphElement = document.createElement("p");
|
||||
userMessage.innerHTML = HtmlUtils.urlify(message);
|
||||
userMessage.classList.add('body');
|
||||
userMessage.classList.add("body");
|
||||
divMessage.appendChild(userMessage);
|
||||
this.divMessages?.appendChild(divMessage);
|
||||
|
||||
|
@ -190,14 +187,14 @@ export class DiscussionManager {
|
|||
setTimeout(() => {
|
||||
this.divMessages?.scroll({
|
||||
top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y,
|
||||
behavior: 'smooth'
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
public removeParticipant(userId: number|string){
|
||||
public removeParticipant(userId: number | string) {
|
||||
const element = this.participants.get(userId);
|
||||
if(element){
|
||||
if (element) {
|
||||
element.remove();
|
||||
this.participants.delete(userId);
|
||||
}
|
||||
|
@ -206,29 +203,29 @@ export class DiscussionManager {
|
|||
this.sendMessageCallBack.delete(userId);
|
||||
}
|
||||
|
||||
public onSendMessageCallback(userId: string|number, callback: SendMessageCallback): void {
|
||||
public onSendMessageCallback(userId: string | number, callback: SendMessageCallback): void {
|
||||
this.sendMessageCallBack.set(userId, callback);
|
||||
}
|
||||
|
||||
get activatedDiscussion(){
|
||||
get activatedDiscussion() {
|
||||
return this.activeDiscussion;
|
||||
}
|
||||
|
||||
private showDiscussion(){
|
||||
private showDiscussion() {
|
||||
this.activeDiscussion = true;
|
||||
this.divDiscuss?.classList.add('active');
|
||||
this.divDiscuss?.classList.add("active");
|
||||
}
|
||||
|
||||
private hideDiscussion(){
|
||||
private hideDiscussion() {
|
||||
this.activeDiscussion = false;
|
||||
this.divDiscuss?.classList.remove('active');
|
||||
this.divDiscuss?.classList.remove("active");
|
||||
}
|
||||
|
||||
public setUserInputManager(userInputManager : UserInputManager){
|
||||
public setUserInputManager(userInputManager: UserInputManager) {
|
||||
this.userInputManager = userInputManager;
|
||||
}
|
||||
|
||||
public showDiscussionPart(){
|
||||
public showDiscussionPart() {
|
||||
this.showDiscussion();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||
import {HtmlUtils} from "./HtmlUtils";
|
||||
import { HtmlUtils } from "./HtmlUtils";
|
||||
|
||||
export enum LayoutMode {
|
||||
// All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle.
|
||||
|
@ -15,329 +15,40 @@ export enum DivImportance {
|
|||
Normal = "Normal",
|
||||
}
|
||||
|
||||
/**
|
||||
* Classes implementing this interface can be notified when the center of the screen (the player position) should be
|
||||
* changed.
|
||||
*/
|
||||
export interface CenterListener {
|
||||
onCenterChange(): void;
|
||||
}
|
||||
export const ON_ACTION_TRIGGER_BUTTON = "onaction";
|
||||
|
||||
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
|
||||
export const TRIGGER_WEBSITE_PROPERTIES = "openWebsiteTrigger";
|
||||
export const TRIGGER_JITSI_PROPERTIES = "jitsiTrigger";
|
||||
|
||||
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
|
||||
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger';
|
||||
export const WEBSITE_MESSAGE_PROPERTIES = "openWebsiteTriggerMessage";
|
||||
export const JITSI_MESSAGE_PROPERTIES = "jitsiTriggerMessage";
|
||||
|
||||
export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage';
|
||||
export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage';
|
||||
export const AUDIO_VOLUME_PROPERTY = "audioVolume";
|
||||
export const AUDIO_LOOP_PROPERTY = "audioLoop";
|
||||
|
||||
export const AUDIO_VOLUME_PROPERTY = 'audioVolume';
|
||||
export const AUDIO_LOOP_PROPERTY = 'audioLoop';
|
||||
export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
|
||||
|
||||
/**
|
||||
* This class is in charge of the video-conference layout.
|
||||
* It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode.
|
||||
*/
|
||||
class LayoutManager {
|
||||
private mode: LayoutMode = LayoutMode.Presentation;
|
||||
|
||||
private importantDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||
private listener: CenterListener|null = null;
|
||||
|
||||
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
|
||||
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||
|
||||
public setListener(centerListener: CenterListener|null) {
|
||||
this.listener = centerListener;
|
||||
}
|
||||
|
||||
public add(importance: DivImportance, userId: string, html: string): void {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
div.id = "user-"+userId;
|
||||
div.className = "media-container"
|
||||
div.onclick = () => {
|
||||
const parentId = div.parentElement?.id;
|
||||
if (parentId === 'sidebar' || parentId === 'chat-mode') {
|
||||
this.focusOn(userId);
|
||||
} else {
|
||||
this.removeFocusOn(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (importance === DivImportance.Important) {
|
||||
this.importantDivs.set(userId, div);
|
||||
|
||||
// If this is the first video with high importance, let's switch mode automatically.
|
||||
if (this.importantDivs.size === 1 && this.mode === LayoutMode.VideoChat) {
|
||||
this.switchLayoutMode(LayoutMode.Presentation);
|
||||
}
|
||||
} else if (importance === DivImportance.Normal) {
|
||||
this.normalDivs.set(userId, div);
|
||||
} else {
|
||||
throw new Error('Unexpected importance');
|
||||
}
|
||||
|
||||
this.positionDiv(div, importance);
|
||||
this.adjustVideoChatClass();
|
||||
this.listener?.onCenterChange();
|
||||
}
|
||||
|
||||
private positionDiv(elem: HTMLDivElement, importance: DivImportance): void {
|
||||
if (this.mode === LayoutMode.VideoChat) {
|
||||
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode');
|
||||
chatModeDiv.appendChild(elem);
|
||||
} else {
|
||||
if (importance === DivImportance.Important) {
|
||||
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section');
|
||||
mainSectionDiv.appendChild(elem);
|
||||
} else if (importance === DivImportance.Normal) {
|
||||
const sideBarDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar');
|
||||
sideBarDiv.appendChild(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the screen in presentation mode and move elem in presentation mode (and all other videos in normal mode)
|
||||
*/
|
||||
private focusOn(userId: string): void {
|
||||
const focusedDiv = this.getDivByUserId(userId);
|
||||
for (const [importantUserId, importantDiv] of this.importantDivs.entries()) {
|
||||
//this.positionDiv(importantDiv, DivImportance.Normal);
|
||||
this.importantDivs.delete(importantUserId);
|
||||
this.normalDivs.set(importantUserId, importantDiv);
|
||||
}
|
||||
this.normalDivs.delete(userId);
|
||||
this.importantDivs.set(userId, focusedDiv);
|
||||
//this.positionDiv(focusedDiv, DivImportance.Important);
|
||||
this.switchLayoutMode(LayoutMode.Presentation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes userId from presentation mode
|
||||
*/
|
||||
private removeFocusOn(userId: string): void {
|
||||
const importantDiv = this.importantDivs.get(userId);
|
||||
if (importantDiv === undefined) {
|
||||
throw new Error('Div with user id "'+userId+'" is not in important mode');
|
||||
}
|
||||
this.normalDivs.set(userId, importantDiv);
|
||||
this.importantDivs.delete(userId);
|
||||
|
||||
this.positionDiv(importantDiv, DivImportance.Normal);
|
||||
}
|
||||
|
||||
private getDivByUserId(userId: string): HTMLDivElement {
|
||||
let div = this.importantDivs.get(userId);
|
||||
if (div !== undefined) {
|
||||
return div;
|
||||
}
|
||||
div = this.normalDivs.get(userId);
|
||||
if (div !== undefined) {
|
||||
return div;
|
||||
}
|
||||
throw new Error('Could not find media with user id '+userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the DIV matching userId.
|
||||
*/
|
||||
public remove(userId: string): void {
|
||||
console.log('Removing video for userID '+userId+'.');
|
||||
let div = this.importantDivs.get(userId);
|
||||
if (div !== undefined) {
|
||||
div.remove();
|
||||
this.importantDivs.delete(userId);
|
||||
this.adjustVideoChatClass();
|
||||
this.listener?.onCenterChange();
|
||||
return;
|
||||
}
|
||||
|
||||
div = this.normalDivs.get(userId);
|
||||
if (div !== undefined) {
|
||||
div.remove();
|
||||
this.normalDivs.delete(userId);
|
||||
this.adjustVideoChatClass();
|
||||
this.listener?.onCenterChange();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Cannot remove userID '+userId+'. Already removed?');
|
||||
//throw new Error('Could not find user ID "'+userId+'"');
|
||||
}
|
||||
|
||||
private adjustVideoChatClass(): void {
|
||||
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode');
|
||||
chatModeDiv.classList.remove('one-col', 'two-col', 'three-col', 'four-col');
|
||||
|
||||
const nbUsers = this.importantDivs.size + this.normalDivs.size;
|
||||
|
||||
if (nbUsers <= 1) {
|
||||
chatModeDiv.classList.add('one-col');
|
||||
} else if (nbUsers <= 4) {
|
||||
chatModeDiv.classList.add('two-col');
|
||||
} else if (nbUsers <= 9) {
|
||||
chatModeDiv.classList.add('three-col');
|
||||
} else {
|
||||
chatModeDiv.classList.add('four-col');
|
||||
}
|
||||
}
|
||||
|
||||
public switchLayoutMode(layoutMode: LayoutMode) {
|
||||
this.mode = layoutMode;
|
||||
|
||||
if (layoutMode === LayoutMode.Presentation) {
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'flex';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'flex';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'none';
|
||||
} else {
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'none';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'none';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'grid';
|
||||
}
|
||||
|
||||
for (const div of this.importantDivs.values()) {
|
||||
this.positionDiv(div, DivImportance.Important);
|
||||
}
|
||||
for (const div of this.normalDivs.values()) {
|
||||
this.positionDiv(div, DivImportance.Normal);
|
||||
}
|
||||
this.listener?.onCenterChange();
|
||||
}
|
||||
|
||||
public getLayoutMode(): LayoutMode {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
/*public getGameCenter(): {x: number, y: number} {
|
||||
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
|
||||
*/
|
||||
public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} {
|
||||
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas');
|
||||
if (this.mode === LayoutMode.VideoChat) {
|
||||
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
|
||||
const htmlChildren = Array.from(children.values());
|
||||
|
||||
// No chat? Let's go full center
|
||||
if (htmlChildren.length === 0) {
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
||||
// Compute area between top right of the last div and bottom right of window
|
||||
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
|
||||
* (game.offsetHeight - lastDiv.offsetTop);
|
||||
|
||||
// Compute area between bottom of last div and bottom of the screen on whole width
|
||||
const area2 = game.offsetWidth
|
||||
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
||||
|
||||
if (area1 < 0 && area2 < 0) {
|
||||
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
if (area1 <= area2) {
|
||||
console.log('lastDiv', lastDiv.offsetTop, lastDiv.offsetHeight);
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
} else {
|
||||
console.log('lastDiv', lastDiv.offsetTop);
|
||||
return {
|
||||
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
||||
yStart: lastDiv.offsetTop,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Possible destinations: at the center bottom or at the right bottom.
|
||||
const mainSectionChildren = Array.from(document.querySelectorAll<HTMLDivElement>('div.main-section > div').values());
|
||||
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('aside.sidebar > div').values());
|
||||
|
||||
// No presentation? Let's center on the screen
|
||||
if (mainSectionChildren.length === 0) {
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we know we have at least one element in the main section.
|
||||
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1];
|
||||
|
||||
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
|
||||
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
||||
|
||||
let leftSideBar: number;
|
||||
let bottomSideBar: number;
|
||||
if (sidebarChildren.length === 0) {
|
||||
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').offsetLeft;
|
||||
bottomSideBar = 0;
|
||||
} else {
|
||||
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
|
||||
leftSideBar = lastSideBarChildren.offsetLeft;
|
||||
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
||||
}
|
||||
const sideBarArea = (game.offsetWidth - leftSideBar)
|
||||
* (game.offsetHeight - bottomSideBar);
|
||||
|
||||
if (presentationArea <= sideBarArea) {
|
||||
return {
|
||||
xStart: leftSideBar,
|
||||
yStart: bottomSideBar,
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
xStart: 0,
|
||||
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
||||
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
|
||||
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) {
|
||||
//delete previous element
|
||||
this.removeActionButton(id, userInputManager);
|
||||
|
||||
//create div and text html component
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('action-body');
|
||||
const p = document.createElement("p");
|
||||
p.classList.add("action-body");
|
||||
p.innerText = text;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('action');
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("action");
|
||||
div.id = id;
|
||||
div.appendChild(p);
|
||||
|
||||
this.actionButtonInformation.set(id, div);
|
||||
|
||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||
mainContainer.appendChild(div);
|
||||
|
||||
//add trigger action
|
||||
|
@ -346,42 +57,42 @@ class LayoutManager {
|
|||
userInputManager.addSpaceEventListner(callBack);
|
||||
}
|
||||
|
||||
public removeActionButton(id: string, userInputManager?: UserInputManager){
|
||||
public removeActionButton(id: string, userInputManager?: UserInputManager) {
|
||||
//delete previous element
|
||||
const previousDiv = this.actionButtonInformation.get(id);
|
||||
if(previousDiv){
|
||||
if (previousDiv) {
|
||||
previousDiv.remove();
|
||||
this.actionButtonInformation.delete(id);
|
||||
}
|
||||
const previousEventCallback = this.actionButtonTrigger.get(id);
|
||||
if(previousEventCallback && userInputManager){
|
||||
if (previousEventCallback && userInputManager) {
|
||||
userInputManager.removeSpaceEventListner(previousEventCallback);
|
||||
}
|
||||
}
|
||||
|
||||
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager){
|
||||
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) {
|
||||
//delete previous element
|
||||
for ( const [key, value] of this.actionButtonInformation ) {
|
||||
for (const [key, value] of this.actionButtonInformation) {
|
||||
this.removeActionButton(key, userInputManager);
|
||||
}
|
||||
|
||||
//create div and text html component
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('action-body');
|
||||
const p = document.createElement("p");
|
||||
p.classList.add("action-body");
|
||||
p.innerText = text;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('action');
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("action");
|
||||
div.classList.add(id);
|
||||
div.id = id;
|
||||
div.appendChild(p);
|
||||
|
||||
this.actionButtonInformation.set(id, div);
|
||||
|
||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
||||
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||
mainContainer.appendChild(div);
|
||||
//add trigger action
|
||||
if(callBack){
|
||||
if (callBack) {
|
||||
div.onpointerdown = () => {
|
||||
callBack();
|
||||
this.removeActionButton(id, userInputManager);
|
||||
|
@ -391,7 +102,7 @@ class LayoutManager {
|
|||
//remove it after 10 sec
|
||||
setTimeout(() => {
|
||||
this.removeActionButton(id, userInputManager);
|
||||
}, 10000)
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,116 +1,76 @@
|
|||
import {DivImportance, layoutManager} from "./LayoutManager";
|
||||
import {HtmlUtils} from "./HtmlUtils";
|
||||
import {discussionManager, SendMessageCallback} from "./DiscussionManager";
|
||||
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
||||
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,
|
||||
} from "../Stores/MediaStore";
|
||||
import {
|
||||
screenSharingLocalStreamStore
|
||||
} from "../Stores/ScreenSharingStore";
|
||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
||||
import { DivImportance, layoutManager } from "./LayoutManager";
|
||||
import { HtmlUtils } from "./HtmlUtils";
|
||||
import { discussionManager, SendMessageCallback } from "./DiscussionManager";
|
||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||
import { localUserStore } from "../Connexion/LocalUserStore";
|
||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
||||
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
|
||||
import { localStreamStore } from "../Stores/MediaStore";
|
||||
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
||||
|
||||
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
|
||||
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
|
||||
export type StartScreenSharingCallback = (media: MediaStream) => void;
|
||||
export type StopScreenSharingCallback = (media: MediaStream) => void;
|
||||
export type ReportCallback = (message: string) => void;
|
||||
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void;
|
||||
export type HelpCameraSettingsCallBack = () => void;
|
||||
|
||||
import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
|
||||
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
|
||||
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
|
||||
|
||||
export class MediaManager {
|
||||
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
//mySoundMeterElement: HTMLDivElement;
|
||||
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||
showReportModalCallBacks : Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
|
||||
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||
|
||||
private focused : boolean = true;
|
||||
private focused: boolean = true;
|
||||
|
||||
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
|
||||
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
|
||||
|
||||
private userInputManager?: UserInputManager;
|
||||
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
/*private mySoundMeter?: SoundMeter|null;
|
||||
private soundMeters: Map<string, SoundMeter> = new Map<string, SoundMeter>();
|
||||
private soundMeterElements: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();*/
|
||||
|
||||
constructor() {
|
||||
|
||||
this.pingCameraStatus();
|
||||
|
||||
//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');
|
||||
});*/
|
||||
|
||||
//Check of ask notification navigator permission
|
||||
this.getNotification();
|
||||
|
||||
localStreamStore.subscribe((result) => {
|
||||
if (result.type === 'error') {
|
||||
if (result.type === "error") {
|
||||
console.error(result.error);
|
||||
layoutManager.addInformation('warning', 'Camera access denied. Click here and check your browser permissions.', () => {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
}, this.userInputManager);
|
||||
layoutManager.addInformation(
|
||||
"warning",
|
||||
"Camera access denied. Click here and check your browser permissions.",
|
||||
() => {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
},
|
||||
this.userInputManager
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
let isScreenSharing = false;
|
||||
screenSharingLocalStreamStore.subscribe((result) => {
|
||||
if (result.type === 'error') {
|
||||
if (result.type === "error") {
|
||||
console.error(result.error);
|
||||
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check your browser permissions.', () => {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
}, this.userInputManager);
|
||||
layoutManager.addInformation(
|
||||
"warning",
|
||||
"Screen sharing denied. Click here and check your browser permissions.",
|
||||
() => {
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
},
|
||||
this.userInputManager
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.stream !== null) {
|
||||
isScreenSharing = true;
|
||||
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
|
||||
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = result.stream;
|
||||
} else {
|
||||
if (isScreenSharing) {
|
||||
isScreenSharing = false;
|
||||
this.removeActiveScreenSharingVideo('me');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/*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 stability of sound meter calculation
|
||||
//this.updateSoudMeter();
|
||||
}
|
||||
|
||||
public showGameOverlay(): void {
|
||||
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
||||
gameOverlay.classList.add('active');
|
||||
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
|
||||
gameOverlay.classList.add("active");
|
||||
|
||||
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
|
||||
const functionTrigger = () => {
|
||||
this.triggerCloseJitsiFrameButton();
|
||||
}
|
||||
buttonCloseFrame.removeEventListener('click', () => {
|
||||
};
|
||||
buttonCloseFrame.removeEventListener("click", () => {
|
||||
buttonCloseFrame.blur();
|
||||
functionTrigger();
|
||||
});
|
||||
|
@ -119,14 +79,14 @@ export class MediaManager {
|
|||
}
|
||||
|
||||
public hideGameOverlay(): void {
|
||||
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
||||
gameOverlay.classList.remove('active');
|
||||
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
|
||||
gameOverlay.classList.remove("active");
|
||||
|
||||
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
|
||||
const functionTrigger = () => {
|
||||
this.triggerCloseJitsiFrameButton();
|
||||
}
|
||||
buttonCloseFrame.addEventListener('click', () => {
|
||||
};
|
||||
buttonCloseFrame.addEventListener("click", () => {
|
||||
buttonCloseFrame.blur();
|
||||
functionTrigger();
|
||||
});
|
||||
|
@ -134,86 +94,24 @@ export class MediaManager {
|
|||
gameOverlayVisibilityStore.hideGameOverlay();
|
||||
}
|
||||
|
||||
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
|
||||
const userId = ''+user.userId
|
||||
|
||||
userName = userName.toUpperCase();
|
||||
const color = this.getColorByString(userName);
|
||||
|
||||
const html = `
|
||||
<div id="div-${userId}" class="video-container">
|
||||
<div class="connecting-spinner"></div>
|
||||
<div class="rtc-error" style="display: none"></div>
|
||||
<i id="name-${userId}" style="background-color: ${color};">${userName}</i>
|
||||
<img id="microphone-${userId}" title="mute" src="resources/logos/microphone-close.svg">
|
||||
<button id="report-${userId}" class="report">
|
||||
<img title="report this user" src="resources/logos/report.svg">
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
<video id="${userId}" autoplay playsinline></video>
|
||||
<img src="resources/logos/blockSign.svg" id="blocking-${userId}" class="block-logo">
|
||||
<div id="soundMeter-${userId}" class="sound-progress">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
layoutManager.add(DivImportance.Normal, userId, html);
|
||||
|
||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
|
||||
|
||||
//permit to create participant in discussion part
|
||||
const showReportUser = () => {
|
||||
for(const callBack of this.showReportModalCallBacks){
|
||||
callBack(userId, userName);
|
||||
}
|
||||
};
|
||||
this.addNewParticipant(userId, userName, undefined, showReportUser);
|
||||
|
||||
const reportBanUserActionEl: HTMLImageElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>(`report-${userId}`);
|
||||
reportBanUserActionEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showReportUser();
|
||||
});
|
||||
}
|
||||
|
||||
addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){
|
||||
|
||||
userId = this.getScreenSharingId(userId);
|
||||
const html = `
|
||||
<div id="div-${userId}" class="video-container">
|
||||
<video id="${userId}" autoplay playsinline></video>
|
||||
</div>
|
||||
`;
|
||||
|
||||
layoutManager.add(divImportance, userId, html);
|
||||
|
||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
|
||||
}
|
||||
|
||||
private getScreenSharingId(userId: string): string {
|
||||
return `screen-sharing-${userId}`;
|
||||
}
|
||||
|
||||
disabledMicrophoneByUserId(userId: number){
|
||||
disabledMicrophoneByUserId(userId: number) {
|
||||
const element = document.getElementById(`microphone-${userId}`);
|
||||
if(!element){
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'?
|
||||
element.classList.add("active"); //todo: why does a method 'disable' add a class 'active'?
|
||||
}
|
||||
|
||||
enabledMicrophoneByUserId(userId: number){
|
||||
enabledMicrophoneByUserId(userId: number) {
|
||||
const element = document.getElementById(`microphone-${userId}`);
|
||||
if(!element){
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'?
|
||||
element.classList.remove("active"); //todo: why does a method 'enable' remove a class 'active'?
|
||||
}
|
||||
|
||||
disabledVideoByUserId(userId: number) {
|
||||
|
@ -227,130 +125,54 @@ export class MediaManager {
|
|||
}
|
||||
}
|
||||
|
||||
enabledVideoByUserId(userId: number){
|
||||
enabledVideoByUserId(userId: number) {
|
||||
let element = document.getElementById(`${userId}`);
|
||||
if(element){
|
||||
if (element) {
|
||||
element.style.opacity = "1";
|
||||
}
|
||||
element = document.getElementById(`name-${userId}`);
|
||||
if(element){
|
||||
if (element) {
|
||||
element.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
toggleBlockLogo(userId: number, show: boolean): void {
|
||||
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-'+userId);
|
||||
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
|
||||
}
|
||||
addStreamRemoteVideo(userId: string, stream : MediaStream): void {
|
||||
const remoteVideo = this.remoteVideo.get(userId);
|
||||
if (remoteVideo === undefined) {
|
||||
throw `Unable to find video for ${userId}`;
|
||||
}
|
||||
remoteVideo.srcObject = stream;
|
||||
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
//sound metter
|
||||
/*const soundMeter = new SoundMeter();
|
||||
soundMeter.connectToSource(stream, new AudioContext());
|
||||
this.soundMeters.set(userId, soundMeter);
|
||||
this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail<HTMLImageElement>('soundMeter-'+userId));*/
|
||||
}
|
||||
addStreamRemoteScreenSharing(userId: string, stream : MediaStream){
|
||||
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
|
||||
const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId));
|
||||
if (remoteVideo === undefined) {
|
||||
this.addScreenSharingActiveVideo(userId);
|
||||
}
|
||||
|
||||
this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream);
|
||||
}
|
||||
|
||||
removeActiveVideo(userId: string){
|
||||
layoutManager.remove(userId);
|
||||
this.remoteVideo.delete(userId);
|
||||
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
/*this.soundMeters.get(userId)?.stop();
|
||||
this.soundMeters.delete(userId);
|
||||
this.soundMeterElements.delete(userId);*/
|
||||
|
||||
//permit to remove user in discussion part
|
||||
this.removeParticipant(userId);
|
||||
}
|
||||
removeActiveScreenSharingVideo(userId: string) {
|
||||
this.removeActiveVideo(this.getScreenSharingId(userId))
|
||||
}
|
||||
|
||||
isConnecting(userId: string): void {
|
||||
const connectingSpinnerDiv = this.getSpinner(userId);
|
||||
if (connectingSpinnerDiv === null) {
|
||||
return;
|
||||
}
|
||||
connectingSpinnerDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
isConnected(userId: string): void {
|
||||
const connectingSpinnerDiv = this.getSpinner(userId);
|
||||
if (connectingSpinnerDiv === null) {
|
||||
return;
|
||||
}
|
||||
connectingSpinnerDiv.style.display = 'none';
|
||||
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>("blocking-" + userId);
|
||||
show ? blockLogoElement.classList.add("active") : blockLogoElement.classList.remove("active");
|
||||
}
|
||||
|
||||
isError(userId: string): void {
|
||||
console.info("isError", `div-${userId}`);
|
||||
const element = document.getElementById(`div-${userId}`);
|
||||
if(!element){
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null;
|
||||
const errorDiv = element.getElementsByClassName("rtc-error").item(0) as HTMLDivElement | null;
|
||||
if (errorDiv === null) {
|
||||
return;
|
||||
}
|
||||
errorDiv.style.display = 'block';
|
||||
errorDiv.style.display = "block";
|
||||
}
|
||||
isErrorScreenSharing(userId: string): void {
|
||||
this.isError(this.getScreenSharingId(userId));
|
||||
}
|
||||
|
||||
|
||||
private getSpinner(userId: string): HTMLDivElement|null {
|
||||
private getSpinner(userId: string): HTMLDivElement | null {
|
||||
const element = document.getElementById(`div-${userId}`);
|
||||
if(!element){
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
|
||||
return connnectingSpinnerDiv;
|
||||
const connectingSpinnerDiv = element
|
||||
.getElementsByClassName("connecting-spinner")
|
||||
.item(0) as HTMLDivElement | null;
|
||||
return connectingSpinnerDiv;
|
||||
}
|
||||
|
||||
private getColorByString(str: String) : String|null {
|
||||
let hash = 0;
|
||||
if (str.length === 0) return null;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash;
|
||||
}
|
||||
let color = '#';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 255;
|
||||
color += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
public addNewParticipant(userId: number|string, name: string|undefined, img?: string, showReportUserCallBack?: ShowReportCallBack){
|
||||
discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack);
|
||||
}
|
||||
|
||||
public removeParticipant(userId: number|string){
|
||||
discussionManager.removeParticipant(userId);
|
||||
}
|
||||
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){
|
||||
public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
|
||||
this.triggerCloseJistiFrame.set(id, Function);
|
||||
}
|
||||
|
||||
public removeTriggerCloseJitsiFrameButton(id: String){
|
||||
public removeTriggerCloseJitsiFrameButton(id: String) {
|
||||
this.triggerCloseJistiFrame.delete(id);
|
||||
}
|
||||
|
||||
|
@ -359,86 +181,26 @@ export class MediaManager {
|
|||
callback();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* For some reasons, the microphone muted icon or the stream is not always up to date.
|
||||
* Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser)
|
||||
**/
|
||||
private pingCameraStatus(){
|
||||
/*setInterval(() => {
|
||||
console.log('ping camera status');
|
||||
this.triggerUpdatedLocalStreamCallbacks(this.localStream);
|
||||
}, 30000);*/
|
||||
}
|
||||
|
||||
public addNewMessage(name: string, message: string, isMe: boolean = false){
|
||||
public addNewMessage(name: string, message: string, isMe: boolean = false) {
|
||||
discussionManager.addMessage(name, message, isMe);
|
||||
|
||||
//when there are new message, show discussion
|
||||
if(!discussionManager.activatedDiscussion) {
|
||||
if (!discussionManager.activatedDiscussion) {
|
||||
discussionManager.showDiscussionPart();
|
||||
}
|
||||
}
|
||||
|
||||
public addSendMessageCallback(userId: string|number, callback: SendMessageCallback){
|
||||
public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) {
|
||||
discussionManager.onSendMessageCallback(userId, callback);
|
||||
}
|
||||
|
||||
get activatedDiscussion(){
|
||||
return discussionManager.activatedDiscussion;
|
||||
}
|
||||
|
||||
public setUserInputManager(userInputManager : UserInputManager){
|
||||
public setUserInputManager(userInputManager: UserInputManager) {
|
||||
this.userInputManager = userInputManager;
|
||||
discussionManager.setUserInputManager(userInputManager);
|
||||
}
|
||||
|
||||
public setShowReportModalCallBacks(callback: ShowReportCallBack){
|
||||
this.showReportModalCallBacks.add(callback);
|
||||
}
|
||||
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
/*updateSoudMeter(){
|
||||
try{
|
||||
const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0));
|
||||
this.setVolumeSoundMeter(volume, this.mySoundMeterElement);
|
||||
|
||||
for(const indexUserId of this.soundMeters.keys()){
|
||||
const soundMeter = this.soundMeters.get(indexUserId);
|
||||
const soundMeterElement = this.soundMeterElements.get(indexUserId);
|
||||
if(!soundMeter || !soundMeterElement){
|
||||
return;
|
||||
}
|
||||
const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0));
|
||||
this.setVolumeSoundMeter(volumeByUser, soundMeterElement);
|
||||
}
|
||||
}catch(err){
|
||||
//console.error(err);
|
||||
}
|
||||
}*/
|
||||
|
||||
private setVolumeSoundMeter(volume: number, element: HTMLDivElement){
|
||||
if(volume <= 0 && !element.classList.contains('active')){
|
||||
return;
|
||||
}
|
||||
element.classList.remove('active');
|
||||
if(volume <= 0){
|
||||
return;
|
||||
}
|
||||
element.classList.add('active');
|
||||
element.childNodes.forEach((value: ChildNode, index) => {
|
||||
const elementChildre = element.children.item(index);
|
||||
if(!elementChildre){
|
||||
return;
|
||||
}
|
||||
elementChildre.classList.remove('active');
|
||||
if((index +1) > volume){
|
||||
return;
|
||||
}
|
||||
elementChildre.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
public getNotification(){
|
||||
public getNotification() {
|
||||
//Get notification
|
||||
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
|
||||
if (this.checkNotificationPromise()) {
|
||||
|
@ -460,24 +222,24 @@ export class MediaManager {
|
|||
private checkNotificationPromise(): boolean {
|
||||
try {
|
||||
Notification.requestPermission().then();
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public createNotification(userName: string){
|
||||
if(this.focused){
|
||||
public createNotification(userName: string) {
|
||||
if (this.focused) {
|
||||
return;
|
||||
}
|
||||
if (window.Notification && Notification.permission === "granted") {
|
||||
const title = 'WorkAdventure';
|
||||
const title = "WorkAdventure";
|
||||
const options = {
|
||||
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`,
|
||||
icon: '/resources/logos/logo-WA-min.png',
|
||||
image: '/resources/logos/logo-WA-min.png',
|
||||
badge: '/resources/logos/logo-WA-min.png',
|
||||
icon: "/resources/logos/logo-WA-min.png",
|
||||
image: "/resources/logos/logo-WA-min.png",
|
||||
badge: "/resources/logos/logo-WA-min.png",
|
||||
};
|
||||
new Notification(title, options);
|
||||
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import type * as SimplePeerNamespace from "simple-peer";
|
||||
import {mediaManager} from "./MediaManager";
|
||||
import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
|
||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
|
||||
import type {UserSimplePeerInterface} from "./SimplePeer";
|
||||
import { mediaManager } from "./MediaManager";
|
||||
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
|
||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
|
||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||
import { Readable, readable, writable, Writable } from "svelte/store";
|
||||
import { videoFocusStore } from "../Stores/VideoFocusStore";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||
|
||||
/**
|
||||
* A peer connection used to transmit video / audio signals between 2 peers.
|
||||
|
@ -14,71 +16,117 @@ export class ScreenSharingPeer extends Peer {
|
|||
/**
|
||||
* Whether this connection is currently receiving a video stream from a remote user.
|
||||
*/
|
||||
private isReceivingStream:boolean = false;
|
||||
private isReceivingStream: boolean = false;
|
||||
public toClose: boolean = false;
|
||||
public _connected: boolean = false;
|
||||
private userId: number;
|
||||
public readonly userId: number;
|
||||
public readonly uniqueId: string;
|
||||
public readonly streamStore: Readable<MediaStream | null>;
|
||||
public readonly statusStore: Readable<PeerStatus>;
|
||||
|
||||
constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, stream: MediaStream | null) {
|
||||
constructor(
|
||||
user: UserSimplePeerInterface,
|
||||
initiator: boolean,
|
||||
public readonly userName: string,
|
||||
private connection: RoomConnection,
|
||||
stream: MediaStream | null
|
||||
) {
|
||||
super({
|
||||
initiator: initiator ? initiator : false,
|
||||
//reconnectTimer: 10000,
|
||||
config: {
|
||||
iceServers: [
|
||||
{
|
||||
urls: STUN_SERVER.split(',')
|
||||
urls: STUN_SERVER.split(","),
|
||||
},
|
||||
TURN_SERVER !== '' ? {
|
||||
urls: TURN_SERVER.split(','),
|
||||
username: user.webRtcUser || TURN_USER,
|
||||
credential: user.webRtcPassword || TURN_PASSWORD
|
||||
} : undefined,
|
||||
].filter((value) => value !== undefined)
|
||||
}
|
||||
TURN_SERVER !== ""
|
||||
? {
|
||||
urls: TURN_SERVER.split(","),
|
||||
username: user.webRtcUser || TURN_USER,
|
||||
credential: user.webRtcPassword || TURN_PASSWORD,
|
||||
}
|
||||
: undefined,
|
||||
].filter((value) => value !== undefined),
|
||||
},
|
||||
});
|
||||
|
||||
this.userId = user.userId;
|
||||
this.uniqueId = "screensharing_" + this.userId;
|
||||
|
||||
this.streamStore = readable<MediaStream | null>(null, (set) => {
|
||||
const onStream = (stream: MediaStream | null) => {
|
||||
videoFocusStore.focus(this);
|
||||
set(stream);
|
||||
};
|
||||
const onData = (chunk: Buffer) => {
|
||||
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
|
||||
// It seems there is no native way to detect that.
|
||||
// TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event
|
||||
const message = JSON.parse(chunk.toString("utf8"));
|
||||
if (message.streamEnded !== true) {
|
||||
console.error("Unexpected message on screen sharing peer connection");
|
||||
return;
|
||||
}
|
||||
set(null);
|
||||
};
|
||||
|
||||
this.on("stream", onStream);
|
||||
this.on("data", onData);
|
||||
|
||||
return () => {
|
||||
this.off("stream", onStream);
|
||||
this.off("data", onData);
|
||||
};
|
||||
});
|
||||
|
||||
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
||||
const onConnect = () => {
|
||||
set("connected");
|
||||
};
|
||||
const onError = () => {
|
||||
set("error");
|
||||
};
|
||||
const onClose = () => {
|
||||
set("closed");
|
||||
};
|
||||
|
||||
this.on("connect", onConnect);
|
||||
this.on("error", onError);
|
||||
this.on("close", onClose);
|
||||
|
||||
return () => {
|
||||
this.off("connect", onConnect);
|
||||
this.off("error", onError);
|
||||
this.off("close", onClose);
|
||||
};
|
||||
});
|
||||
|
||||
//start listen signal for the peer connection
|
||||
this.on('signal', (data: unknown) => {
|
||||
this.on("signal", (data: unknown) => {
|
||||
this.sendWebrtcScreenSharingSignal(data);
|
||||
});
|
||||
|
||||
this.on('stream', (stream: MediaStream) => {
|
||||
this.on("stream", (stream: MediaStream) => {
|
||||
this.stream(stream);
|
||||
});
|
||||
|
||||
this.on('close', () => {
|
||||
this.on("close", () => {
|
||||
this._connected = false;
|
||||
this.toClose = true;
|
||||
this.destroy();
|
||||
});
|
||||
|
||||
this.on('data', (chunk: Buffer) => {
|
||||
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
|
||||
// It seems there is no native way to detect that.
|
||||
const message = JSON.parse(chunk.toString('utf8'));
|
||||
if (message.streamEnded !== true) {
|
||||
console.error('Unexpected message on screen sharing peer connection');
|
||||
return;
|
||||
}
|
||||
mediaManager.removeActiveScreenSharingVideo("" + this.userId);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.on('error', (err: any) => {
|
||||
this.on("error", (err: any) => {
|
||||
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
|
||||
//mediaManager.isErrorScreenSharing(this.userId);
|
||||
});
|
||||
|
||||
this.on('connect', () => {
|
||||
this.on("connect", () => {
|
||||
this._connected = true;
|
||||
// FIXME: we need to put the loader on the screen sharing connection
|
||||
mediaManager.isConnected("" + this.userId);
|
||||
console.info(`connect => ${this.userId}`);
|
||||
});
|
||||
|
||||
this.once('finish', () => {
|
||||
this.once("finish", () => {
|
||||
this._onFinish();
|
||||
});
|
||||
|
||||
|
@ -88,10 +136,9 @@ export class ScreenSharingPeer extends Peer {
|
|||
}
|
||||
|
||||
private sendWebrtcScreenSharingSignal(data: unknown) {
|
||||
//console.log("sendWebrtcScreenSharingSignal", data);
|
||||
try {
|
||||
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e);
|
||||
}
|
||||
}
|
||||
|
@ -100,13 +147,9 @@ export class ScreenSharingPeer extends Peer {
|
|||
* Sends received stream to screen.
|
||||
*/
|
||||
private stream(stream?: MediaStream) {
|
||||
//console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream);
|
||||
//console.log(`stream => ${this.userId} => `, stream);
|
||||
if(!stream){
|
||||
mediaManager.removeActiveScreenSharingVideo("" + this.userId);
|
||||
if (!stream) {
|
||||
this.isReceivingStream = false;
|
||||
} else {
|
||||
mediaManager.addStreamRemoteScreenSharing("" + this.userId, stream);
|
||||
this.isReceivingStream = true;
|
||||
}
|
||||
}
|
||||
|
@ -117,35 +160,34 @@ export class ScreenSharingPeer extends Peer {
|
|||
|
||||
public destroy(error?: Error): void {
|
||||
try {
|
||||
this._connected = false
|
||||
if(!this.toClose){
|
||||
this._connected = false;
|
||||
if (!this.toClose) {
|
||||
return;
|
||||
}
|
||||
mediaManager.removeActiveScreenSharingVideo("" + this.userId);
|
||||
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
||||
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
|
||||
//console.log('Closing connection with '+userId);
|
||||
super.destroy(error);
|
||||
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
|
||||
} catch (err) {
|
||||
console.error("ScreenSharingPeer::destroy", err)
|
||||
console.error("ScreenSharingPeer::destroy", err);
|
||||
}
|
||||
}
|
||||
|
||||
_onFinish () {
|
||||
if (this.destroyed) return
|
||||
_onFinish() {
|
||||
if (this.destroyed) return;
|
||||
const destroySoon = () => {
|
||||
this.destroy();
|
||||
}
|
||||
};
|
||||
if (this._connected) {
|
||||
destroySoon();
|
||||
} else {
|
||||
this.once('connect', destroySoon);
|
||||
this.once("connect", destroySoon);
|
||||
}
|
||||
}
|
||||
|
||||
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
|
||||
this.removeStream(stream);
|
||||
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true})));
|
||||
this.write(new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true })));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,28 @@ import type {
|
|||
WebRtcDisconnectMessageInterface,
|
||||
WebRtcSignalReceivedMessageInterface,
|
||||
} from "../Connexion/ConnexionModels";
|
||||
import {
|
||||
mediaManager,
|
||||
StartScreenSharingCallback,
|
||||
StopScreenSharingCallback,
|
||||
UpdatedLocalStreamCallback
|
||||
} from "./MediaManager";
|
||||
import {ScreenSharingPeer} from "./ScreenSharingPeer";
|
||||
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
|
||||
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, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
|
||||
import {DivImportance, layoutManager} from "./LayoutManager";
|
||||
import {HtmlUtils} from "./HtmlUtils";
|
||||
import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager";
|
||||
import { ScreenSharingPeer } from "./ScreenSharingPeer";
|
||||
import { MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer } from "./VideoPeer";
|
||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||
import { blackListManager } from "./BlackListManager";
|
||||
import { get } from "svelte/store";
|
||||
import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
||||
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||
import { discussionManager } from "./DiscussionManager";
|
||||
|
||||
export interface UserSimplePeerInterface{
|
||||
export interface UserSimplePeerInterface {
|
||||
userId: number;
|
||||
name?: string;
|
||||
initiator?: boolean;
|
||||
webRtcUser?: string|undefined;
|
||||
webRtcPassword?: string|undefined;
|
||||
webRtcUser?: string | undefined;
|
||||
webRtcPassword?: string | undefined;
|
||||
}
|
||||
|
||||
export type RemotePeer = VideoPeer | ScreenSharingPeer;
|
||||
|
||||
export interface PeerConnectionListener {
|
||||
onConnect(user: UserSimplePeerInterface): void;
|
||||
onConnect(user: RemotePeer): void;
|
||||
|
||||
onDisconnect(userId: number): void;
|
||||
}
|
||||
|
@ -47,36 +41,40 @@ export class SimplePeer {
|
|||
private readonly unsubscribers: (() => void)[] = [];
|
||||
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
|
||||
private readonly userId: number;
|
||||
private lastWebrtcUserName: string|undefined;
|
||||
private lastWebrtcPassword: string|undefined;
|
||||
private lastWebrtcUserName: string | undefined;
|
||||
private lastWebrtcPassword: string | undefined;
|
||||
|
||||
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
|
||||
// We need to go through this weird bound function pointer in order to be able to "free" this reference later.
|
||||
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
|
||||
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
|
||||
|
||||
this.unsubscribers.push(localStreamStore.subscribe((streamResult) => {
|
||||
this.sendLocalVideoStream(streamResult);
|
||||
}));
|
||||
this.unsubscribers.push(
|
||||
localStreamStore.subscribe((streamResult) => {
|
||||
this.sendLocalVideoStream(streamResult);
|
||||
})
|
||||
);
|
||||
|
||||
let localScreenCapture: MediaStream|null = null;
|
||||
let localScreenCapture: MediaStream | null = null;
|
||||
|
||||
this.unsubscribers.push(screenSharingLocalStreamStore.subscribe((streamResult) => {
|
||||
if (streamResult.type === 'error') {
|
||||
// Let's ignore screen sharing errors, we will deal with those in a different way.
|
||||
return;
|
||||
}
|
||||
|
||||
if (streamResult.stream !== null) {
|
||||
localScreenCapture = streamResult.stream;
|
||||
this.sendLocalScreenSharingStream(localScreenCapture);
|
||||
} else {
|
||||
if (localScreenCapture) {
|
||||
this.stopLocalScreenSharingStream(localScreenCapture);
|
||||
localScreenCapture = null;
|
||||
this.unsubscribers.push(
|
||||
screenSharingLocalStreamStore.subscribe((streamResult) => {
|
||||
if (streamResult.type === "error") {
|
||||
// Let's ignore screen sharing errors, we will deal with those in a different way.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if (streamResult.stream !== null) {
|
||||
localScreenCapture = streamResult.stream;
|
||||
this.sendLocalScreenSharingStream(localScreenCapture);
|
||||
} else {
|
||||
if (localScreenCapture) {
|
||||
this.stopLocalScreenSharingStream(localScreenCapture);
|
||||
localScreenCapture = null;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.userId = Connection.getUserId();
|
||||
this.initialise();
|
||||
|
@ -94,7 +92,6 @@ export class SimplePeer {
|
|||
* permit to listen when user could start visio
|
||||
*/
|
||||
private initialise() {
|
||||
|
||||
//receive signal by gemer
|
||||
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
|
||||
this.receiveWebrtcSignal(message);
|
||||
|
@ -124,13 +121,12 @@ export class SimplePeer {
|
|||
// This would be symmetrical to the way we handle disconnection.
|
||||
|
||||
//start connection
|
||||
//console.log('receiveWebrtcStart. Initiator: ', user.initiator)
|
||||
if(!user.initiator){
|
||||
if (!user.initiator) {
|
||||
return;
|
||||
}
|
||||
const streamResult = get(localStreamStore);
|
||||
let stream : MediaStream | null = null;
|
||||
if (streamResult.type === 'success' && streamResult.stream) {
|
||||
let stream: MediaStream | null = null;
|
||||
if (streamResult.type === "success" && streamResult.stream) {
|
||||
stream = streamResult.stream;
|
||||
}
|
||||
|
||||
|
@ -140,15 +136,15 @@ export class SimplePeer {
|
|||
/**
|
||||
* create peer connection to bind users
|
||||
*/
|
||||
private createPeerConnection(user : UserSimplePeerInterface, localStream: MediaStream | null) : VideoPeer | null {
|
||||
const peerConnection = this.PeerConnectionArray.get(user.userId)
|
||||
private createPeerConnection(user: UserSimplePeerInterface, localStream: MediaStream | null): VideoPeer | null {
|
||||
const peerConnection = this.PeerConnectionArray.get(user.userId);
|
||||
if (peerConnection) {
|
||||
if (peerConnection.destroyed) {
|
||||
peerConnection.toClose = true;
|
||||
peerConnection.destroy();
|
||||
const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId);
|
||||
if (!peerConnexionDeleted) {
|
||||
throw 'Error to delete peer connection';
|
||||
throw "Error to delete peer connection";
|
||||
}
|
||||
//return this.createPeerConnection(user, localStream);
|
||||
} else {
|
||||
|
@ -159,85 +155,103 @@ export class SimplePeer {
|
|||
|
||||
let name = user.name;
|
||||
if (!name) {
|
||||
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId);
|
||||
if (userSearch) {
|
||||
name = userSearch.name;
|
||||
}
|
||||
name = this.getName(user.userId);
|
||||
}
|
||||
|
||||
mediaManager.removeActiveVideo("" + user.userId);
|
||||
|
||||
mediaManager.addActiveVideo(user, name);
|
||||
discussionManager.removeParticipant(user.userId);
|
||||
|
||||
this.lastWebrtcUserName = user.webRtcUser;
|
||||
this.lastWebrtcPassword = user.webRtcPassword;
|
||||
|
||||
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream);
|
||||
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
|
||||
|
||||
//permit to send message
|
||||
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
|
||||
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), userId: this.userId, message: message})));
|
||||
mediaManager.addSendMessageCallback(user.userId, (message: string) => {
|
||||
peer.write(
|
||||
new Buffer(
|
||||
JSON.stringify({
|
||||
type: MESSAGE_TYPE_MESSAGE,
|
||||
name: this.myName.toUpperCase(),
|
||||
userId: this.userId,
|
||||
message: message,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
peer.toClose = false;
|
||||
// When a connection is established to a video stream, and if a screen sharing is taking place,
|
||||
// the user sharing screen should also initiate a connection to the remote user!
|
||||
peer.on('connect', () => {
|
||||
peer.on("connect", () => {
|
||||
const streamResult = get(screenSharingLocalStreamStore);
|
||||
if (streamResult.type === 'success' && streamResult.stream !== null) {
|
||||
if (streamResult.type === "success" && streamResult.stream !== null) {
|
||||
this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream);
|
||||
}
|
||||
});
|
||||
|
||||
//Create a notification for first user in circle discussion
|
||||
if(this.PeerConnectionArray.size === 0){
|
||||
mediaManager.createNotification(user.name??'');
|
||||
if (this.PeerConnectionArray.size === 0) {
|
||||
mediaManager.createNotification(user.name ?? "");
|
||||
}
|
||||
this.PeerConnectionArray.set(user.userId, peer);
|
||||
|
||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||
peerConnectionListener.onConnect(user);
|
||||
peerConnectionListener.onConnect(peer);
|
||||
}
|
||||
return peer;
|
||||
}
|
||||
|
||||
private getName(userId: number): string {
|
||||
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId);
|
||||
if (userSearch) {
|
||||
return userSearch.name || "";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create peer connection to bind users
|
||||
*/
|
||||
private createPeerScreenSharingConnection(user : UserSimplePeerInterface, stream: MediaStream | null) : ScreenSharingPeer | null{
|
||||
private createPeerScreenSharingConnection(
|
||||
user: UserSimplePeerInterface,
|
||||
stream: MediaStream | null
|
||||
): ScreenSharingPeer | null {
|
||||
const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId);
|
||||
if(peerConnection){
|
||||
if(peerConnection.destroyed){
|
||||
if (peerConnection) {
|
||||
if (peerConnection.destroyed) {
|
||||
peerConnection.toClose = true;
|
||||
peerConnection.destroy();
|
||||
const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId);
|
||||
if(!peerConnexionDeleted){
|
||||
throw 'Error to delete peer connection';
|
||||
if (!peerConnexionDeleted) {
|
||||
throw "Error to delete peer connection";
|
||||
}
|
||||
this.createPeerConnection(user, stream);
|
||||
}else {
|
||||
} else {
|
||||
peerConnection.toClose = false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// We should display the screen sharing ONLY if we are not initiator
|
||||
if (!user.initiator) {
|
||||
mediaManager.removeActiveScreenSharingVideo("" + user.userId);
|
||||
mediaManager.addScreenSharingActiveVideo("" + user.userId);
|
||||
}
|
||||
|
||||
// Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing)
|
||||
if (user.webRtcUser === undefined) {
|
||||
user.webRtcUser = this.lastWebrtcUserName;
|
||||
user.webRtcPassword = this.lastWebrtcPassword;
|
||||
}
|
||||
|
||||
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream);
|
||||
const name = this.getName(user.userId);
|
||||
|
||||
const peer = new ScreenSharingPeer(
|
||||
user,
|
||||
user.initiator ? user.initiator : false,
|
||||
name,
|
||||
this.Connection,
|
||||
stream
|
||||
);
|
||||
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
|
||||
|
||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||
peerConnectionListener.onConnect(user);
|
||||
peerConnectionListener.onConnect(peer);
|
||||
}
|
||||
return peer;
|
||||
}
|
||||
|
@ -245,11 +259,13 @@ export class SimplePeer {
|
|||
/**
|
||||
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
||||
*/
|
||||
private closeConnection(userId : number) {
|
||||
private closeConnection(userId: number) {
|
||||
try {
|
||||
const peer = this.PeerConnectionArray.get(userId);
|
||||
if (peer === undefined) {
|
||||
console.warn("closeConnection => Tried to close connection for user "+userId+" but could not find user");
|
||||
console.warn(
|
||||
"closeConnection => Tried to close connection for user " + userId + " but could not find user"
|
||||
);
|
||||
return;
|
||||
}
|
||||
//create temp perr to close
|
||||
|
@ -260,18 +276,18 @@ export class SimplePeer {
|
|||
|
||||
this.closeScreenSharingConnection(userId);
|
||||
|
||||
const userIndex = this.Users.findIndex(user => user.userId === userId);
|
||||
if(userIndex < 0){
|
||||
throw 'Couldn\'t delete user';
|
||||
const userIndex = this.Users.findIndex((user) => user.userId === userId);
|
||||
if (userIndex < 0) {
|
||||
throw "Couldn't delete user";
|
||||
} else {
|
||||
this.Users.splice(userIndex, 1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("closeConnection", err)
|
||||
console.error("closeConnection", err);
|
||||
}
|
||||
|
||||
//if user left discussion, clear array peer connection of sharing
|
||||
if(this.Users.length === 0) {
|
||||
if (this.Users.length === 0) {
|
||||
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
|
||||
this.closeScreenSharingConnection(userId);
|
||||
this.PeerScreenSharingConnectionArray.delete(userId);
|
||||
|
@ -286,12 +302,16 @@ export class SimplePeer {
|
|||
/**
|
||||
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
||||
*/
|
||||
private closeScreenSharingConnection(userId : number) {
|
||||
private closeScreenSharingConnection(userId: number) {
|
||||
try {
|
||||
mediaManager.removeActiveScreenSharingVideo("" + userId);
|
||||
//mediaManager.removeActiveScreenSharingVideo("" + userId);
|
||||
const peer = this.PeerScreenSharingConnectionArray.get(userId);
|
||||
if (peer === undefined) {
|
||||
console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user")
|
||||
console.warn(
|
||||
"closeScreenSharingConnection => Tried to close connection for user " +
|
||||
userId +
|
||||
" but could not find user"
|
||||
);
|
||||
return;
|
||||
}
|
||||
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
||||
|
@ -304,7 +324,7 @@ export class SimplePeer {
|
|||
}*/
|
||||
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
|
||||
} catch (err) {
|
||||
console.error("closeConnection", err)
|
||||
console.error("closeConnection", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -331,10 +351,10 @@ export class SimplePeer {
|
|||
private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
|
||||
try {
|
||||
//if offer type, create peer connection
|
||||
if(data.signal.type === "offer"){
|
||||
if (data.signal.type === "offer") {
|
||||
const streamResult = get(localStreamStore);
|
||||
let stream : MediaStream | null = null;
|
||||
if (streamResult.type === 'success' && streamResult.stream) {
|
||||
let stream: MediaStream | null = null;
|
||||
if (streamResult.type === "success" && streamResult.stream) {
|
||||
stream = streamResult.stream;
|
||||
}
|
||||
|
||||
|
@ -344,7 +364,7 @@ export class SimplePeer {
|
|||
if (peer !== undefined) {
|
||||
peer.signal(data.signal);
|
||||
} else {
|
||||
console.error('Could not find peer whose ID is "'+data.userId+'" in PeerConnectionArray');
|
||||
console.error('Could not find peer whose ID is "' + data.userId + '" in PeerConnectionArray');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`receiveWebrtcSignal => ${data.userId}`, e);
|
||||
|
@ -355,22 +375,24 @@ export class SimplePeer {
|
|||
if (blackListManager.isBlackListed(data.userId)) return;
|
||||
console.log("receiveWebrtcScreenSharingSignal", data);
|
||||
const streamResult = get(screenSharingLocalStreamStore);
|
||||
let stream : MediaStream | null = null;
|
||||
if (streamResult.type === 'success' && streamResult.stream !== null) {
|
||||
let stream: MediaStream | null = null;
|
||||
if (streamResult.type === "success" && streamResult.stream !== null) {
|
||||
stream = streamResult.stream;
|
||||
}
|
||||
|
||||
try {
|
||||
//if offer type, create peer connection
|
||||
if(data.signal.type === "offer"){
|
||||
if (data.signal.type === "offer") {
|
||||
this.createPeerScreenSharingConnection(data, stream);
|
||||
}
|
||||
const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
|
||||
if (peer !== undefined) {
|
||||
peer.signal(data.signal);
|
||||
} else {
|
||||
console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal');
|
||||
console.info('Attempt to create new peer connexion');
|
||||
console.error(
|
||||
'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal'
|
||||
);
|
||||
console.info("Attempt to create new peer connexion");
|
||||
if (stream) {
|
||||
this.sendLocalScreenSharingStreamToUser(data.userId, stream);
|
||||
}
|
||||
|
@ -387,17 +409,19 @@ export class SimplePeer {
|
|||
try {
|
||||
const PeerConnection = this.PeerConnectionArray.get(userId);
|
||||
if (!PeerConnection) {
|
||||
throw new Error('While adding media, cannot find user with ID ' + userId);
|
||||
throw new Error("While adding media, cannot find user with ID " + userId);
|
||||
}
|
||||
|
||||
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints})));
|
||||
PeerConnection.write(
|
||||
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints }))
|
||||
);
|
||||
|
||||
if (streamResult.type === 'error') {
|
||||
if (streamResult.type === "error") {
|
||||
return;
|
||||
}
|
||||
const localStream: MediaStream | null = streamResult.stream;
|
||||
|
||||
if(!localStream){
|
||||
if (!localStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -407,7 +431,7 @@ export class SimplePeer {
|
|||
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
PeerConnection.addTrack(track, localStream);
|
||||
}
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
console.error(`pushVideoToRemoteUser => ${userId}`, e);
|
||||
}
|
||||
}
|
||||
|
@ -415,7 +439,7 @@ export class SimplePeer {
|
|||
private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
|
||||
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
|
||||
if (!PeerConnection) {
|
||||
throw new Error('While pushing screen sharing, cannot find user with ID ' + userId);
|
||||
throw new Error("While pushing screen sharing, cannot find user with ID " + userId);
|
||||
}
|
||||
|
||||
for (const track of localScreenCapture.getTracks()) {
|
||||
|
@ -424,7 +448,7 @@ export class SimplePeer {
|
|||
return;
|
||||
}
|
||||
|
||||
public sendLocalVideoStream(streamResult: LocalStreamStoreValue){
|
||||
public sendLocalVideoStream(streamResult: LocalStreamStoreValue) {
|
||||
for (const user of this.Users) {
|
||||
this.pushVideoToRemoteUser(user.userId, streamResult);
|
||||
}
|
||||
|
@ -458,9 +482,12 @@ export class SimplePeer {
|
|||
|
||||
const screenSharingUser: UserSimplePeerInterface = {
|
||||
userId,
|
||||
initiator: true
|
||||
initiator: true,
|
||||
};
|
||||
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser, localScreenCapture);
|
||||
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(
|
||||
screenSharingUser,
|
||||
localScreenCapture
|
||||
);
|
||||
if (!PeerConnectionScreenSharing) {
|
||||
return;
|
||||
}
|
||||
|
@ -469,7 +496,7 @@ export class SimplePeer {
|
|||
private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void {
|
||||
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
|
||||
if (!PeerConnectionScreenSharing) {
|
||||
throw new Error('Weird, screen sharing connection to user ' + userId + 'not found')
|
||||
throw new Error("Weird, screen sharing connection to user " + userId + "not found");
|
||||
}
|
||||
|
||||
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import type * as SimplePeerNamespace from "simple-peer";
|
||||
import {mediaManager} from "./MediaManager";
|
||||
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
|
||||
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";
|
||||
import { mediaManager } from "./MediaManager";
|
||||
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
|
||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||
import { blackListManager } from "./BlackListManager";
|
||||
import type { Subscription } from "rxjs";
|
||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||
import { get, readable, Readable } from "svelte/store";
|
||||
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
||||
import { discussionManager } from "./DiscussionManager";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||
|
||||
export const MESSAGE_TYPE_CONSTRAINT = 'constraint';
|
||||
export const MESSAGE_TYPE_MESSAGE = 'message';
|
||||
export const MESSAGE_TYPE_BLOCKED = 'blocked';
|
||||
export const MESSAGE_TYPE_UNBLOCKED = 'unblocked';
|
||||
export type PeerStatus = "connecting" | "connected" | "error" | "closed";
|
||||
|
||||
export const MESSAGE_TYPE_CONSTRAINT = "constraint";
|
||||
export const MESSAGE_TYPE_MESSAGE = "message";
|
||||
export const MESSAGE_TYPE_BLOCKED = "blocked";
|
||||
export const MESSAGE_TYPE_UNBLOCKED = "unblocked";
|
||||
/**
|
||||
* A peer connection used to transmit video / audio signals between 2 peers.
|
||||
*/
|
||||
|
@ -22,60 +25,130 @@ export class VideoPeer extends Peer {
|
|||
public _connected: boolean = false;
|
||||
private remoteStream!: MediaStream;
|
||||
private blocked: boolean = false;
|
||||
private userId: number;
|
||||
private userName: string;
|
||||
public readonly userId: number;
|
||||
public readonly uniqueId: string;
|
||||
private onBlockSubscribe: Subscription;
|
||||
private onUnBlockSubscribe: Subscription;
|
||||
public readonly streamStore: Readable<MediaStream | null>;
|
||||
public readonly statusStore: Readable<PeerStatus>;
|
||||
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
|
||||
|
||||
constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, localStream: MediaStream | null) {
|
||||
constructor(
|
||||
public user: UserSimplePeerInterface,
|
||||
initiator: boolean,
|
||||
public readonly userName: string,
|
||||
private connection: RoomConnection,
|
||||
localStream: MediaStream | null
|
||||
) {
|
||||
super({
|
||||
initiator: initiator ? initiator : false,
|
||||
//reconnectTimer: 10000,
|
||||
config: {
|
||||
iceServers: [
|
||||
{
|
||||
urls: STUN_SERVER.split(',')
|
||||
urls: STUN_SERVER.split(","),
|
||||
},
|
||||
TURN_SERVER !== '' ? {
|
||||
urls: TURN_SERVER.split(','),
|
||||
username: user.webRtcUser || TURN_USER,
|
||||
credential: user.webRtcPassword || TURN_PASSWORD
|
||||
} : undefined,
|
||||
].filter((value) => value !== undefined)
|
||||
}
|
||||
TURN_SERVER !== ""
|
||||
? {
|
||||
urls: TURN_SERVER.split(","),
|
||||
username: user.webRtcUser || TURN_USER,
|
||||
credential: user.webRtcPassword || TURN_PASSWORD,
|
||||
}
|
||||
: undefined,
|
||||
].filter((value) => value !== undefined),
|
||||
},
|
||||
});
|
||||
|
||||
this.userId = user.userId;
|
||||
this.userName = user.name || '';
|
||||
this.uniqueId = "video_" + this.userId;
|
||||
|
||||
this.streamStore = readable<MediaStream | null>(null, (set) => {
|
||||
const onStream = (stream: MediaStream | null) => {
|
||||
set(stream);
|
||||
};
|
||||
const onData = (chunk: Buffer) => {
|
||||
this.on("data", (chunk: Buffer) => {
|
||||
const message = JSON.parse(chunk.toString("utf8"));
|
||||
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||
if (!message.video) {
|
||||
set(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.on("stream", onStream);
|
||||
this.on("data", onData);
|
||||
|
||||
return () => {
|
||||
this.off("stream", onStream);
|
||||
this.off("data", onData);
|
||||
};
|
||||
});
|
||||
|
||||
this.constraintsStore = readable<MediaStreamConstraints | null>(null, (set) => {
|
||||
const onData = (chunk: Buffer) => {
|
||||
const message = JSON.parse(chunk.toString("utf8"));
|
||||
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||
set(message);
|
||||
}
|
||||
};
|
||||
|
||||
this.on("data", onData);
|
||||
|
||||
return () => {
|
||||
this.off("data", onData);
|
||||
};
|
||||
});
|
||||
|
||||
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
||||
const onConnect = () => {
|
||||
set("connected");
|
||||
};
|
||||
const onError = () => {
|
||||
set("error");
|
||||
};
|
||||
const onClose = () => {
|
||||
set("closed");
|
||||
};
|
||||
|
||||
this.on("connect", onConnect);
|
||||
this.on("error", onError);
|
||||
this.on("close", onClose);
|
||||
|
||||
return () => {
|
||||
this.off("connect", onConnect);
|
||||
this.off("error", onError);
|
||||
this.off("close", onClose);
|
||||
};
|
||||
});
|
||||
|
||||
//start listen signal for the peer connection
|
||||
this.on('signal', (data: unknown) => {
|
||||
this.on("signal", (data: unknown) => {
|
||||
this.sendWebrtcSignal(data);
|
||||
});
|
||||
|
||||
this.on('stream', (stream: MediaStream) => this.stream(stream));
|
||||
this.on("stream", (stream: MediaStream) => this.stream(stream));
|
||||
|
||||
this.on('close', () => {
|
||||
this.on("close", () => {
|
||||
this._connected = false;
|
||||
this.toClose = true;
|
||||
this.destroy();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.on('error', (err: any) => {
|
||||
this.on("error", (err: any) => {
|
||||
console.error(`error => ${this.userId} => ${err.code}`, err);
|
||||
mediaManager.isError("" + this.userId);
|
||||
});
|
||||
|
||||
this.on('connect', () => {
|
||||
this.on("connect", () => {
|
||||
this._connected = true;
|
||||
mediaManager.isConnected("" + this.userId);
|
||||
console.info(`connect => ${this.userId}`);
|
||||
});
|
||||
|
||||
this.on('data', (chunk: Buffer) => {
|
||||
const message = JSON.parse(chunk.toString('utf8'));
|
||||
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||
this.on("data", (chunk: Buffer) => {
|
||||
const message = JSON.parse(chunk.toString("utf8"));
|
||||
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||
if (message.audio) {
|
||||
mediaManager.enabledMicrophoneByUserId(this.userId);
|
||||
} else {
|
||||
|
@ -87,23 +160,23 @@ export class VideoPeer extends Peer {
|
|||
} else {
|
||||
mediaManager.disabledVideoByUserId(this.userId);
|
||||
}
|
||||
} else if(message.type === MESSAGE_TYPE_MESSAGE) {
|
||||
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
|
||||
if (!blackListManager.isBlackListed(message.userId)) {
|
||||
mediaManager.addNewMessage(message.name, message.message);
|
||||
}
|
||||
} else if(message.type === MESSAGE_TYPE_BLOCKED) {
|
||||
} else if (message.type === MESSAGE_TYPE_BLOCKED) {
|
||||
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
|
||||
// Find a way to block A's output stream in A's js client
|
||||
//However, the output stream stream B is correctly blocked in A client
|
||||
this.blocked = true;
|
||||
this.toggleRemoteStream(false);
|
||||
} else if(message.type === MESSAGE_TYPE_UNBLOCKED) {
|
||||
} else if (message.type === MESSAGE_TYPE_UNBLOCKED) {
|
||||
this.blocked = false;
|
||||
this.toggleRemoteStream(true);
|
||||
}
|
||||
});
|
||||
|
||||
this.once('finish', () => {
|
||||
this.once("finish", () => {
|
||||
this._onFinish();
|
||||
});
|
||||
|
||||
|
@ -122,23 +195,32 @@ export class VideoPeer extends Peer {
|
|||
});
|
||||
|
||||
if (blackListManager.isBlackListed(this.userId)) {
|
||||
this.sendBlockMessage(true)
|
||||
this.sendBlockMessage(true);
|
||||
}
|
||||
}
|
||||
|
||||
private sendBlockMessage(blocking: boolean) {
|
||||
this.write(new Buffer(JSON.stringify({type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED, name: this.userName.toUpperCase(), userId: this.userId, message: ''})));
|
||||
this.write(
|
||||
new Buffer(
|
||||
JSON.stringify({
|
||||
type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED,
|
||||
name: this.userName.toUpperCase(),
|
||||
userId: this.userId,
|
||||
message: "",
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private toggleRemoteStream(enable: boolean) {
|
||||
this.remoteStream.getTracks().forEach(track => track.enabled = enable);
|
||||
this.remoteStream.getTracks().forEach((track) => (track.enabled = enable));
|
||||
mediaManager.toggleBlockLogo(this.userId, !enable);
|
||||
}
|
||||
|
||||
private sendWebrtcSignal(data: unknown) {
|
||||
try {
|
||||
this.connection.sendWebrtcSignal(data, this.userId);
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
console.error(`sendWebrtcSignal => ${this.userId}`, e);
|
||||
}
|
||||
}
|
||||
|
@ -152,8 +234,7 @@ export class VideoPeer extends Peer {
|
|||
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
|
||||
this.toggleRemoteStream(false);
|
||||
}
|
||||
mediaManager.addStreamRemoteVideo("" + this.userId, stream);
|
||||
}catch (err){
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
@ -163,45 +244,47 @@ export class VideoPeer extends Peer {
|
|||
*/
|
||||
public destroy(error?: Error): void {
|
||||
try {
|
||||
this._connected = false
|
||||
if(!this.toClose){
|
||||
this._connected = false;
|
||||
if (!this.toClose) {
|
||||
return;
|
||||
}
|
||||
this.onBlockSubscribe.unsubscribe();
|
||||
this.onUnBlockSubscribe.unsubscribe();
|
||||
mediaManager.removeActiveVideo("" + this.userId);
|
||||
discussionManager.removeParticipant(this.userId);
|
||||
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
||||
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
|
||||
super.destroy(error);
|
||||
} catch (err) {
|
||||
console.error("VideoPeer::destroy", err)
|
||||
console.error("VideoPeer::destroy", err);
|
||||
}
|
||||
}
|
||||
|
||||
_onFinish () {
|
||||
if (this.destroyed) return
|
||||
_onFinish() {
|
||||
if (this.destroyed) return;
|
||||
const destroySoon = () => {
|
||||
this.destroy();
|
||||
}
|
||||
};
|
||||
if (this._connected) {
|
||||
destroySoon();
|
||||
} else {
|
||||
this.once('connect', destroySoon);
|
||||
this.once("connect", destroySoon);
|
||||
}
|
||||
}
|
||||
|
||||
private pushVideoToRemoteUser(localStream: MediaStream | null) {
|
||||
try {
|
||||
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore)})));
|
||||
this.write(
|
||||
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore) }))
|
||||
);
|
||||
|
||||
if(!localStream){
|
||||
if (!localStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const track of localStream.getTracks()) {
|
||||
this.addTrack(track, localStream);
|
||||
}
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import {registeredCallbacks} from "./Api/iframe/registeredCallbacks";
|
||||
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
|
||||
import {
|
||||
IframeResponseEvent,
|
||||
IframeResponseEventMap,
|
||||
isIframeResponseEventWrapper,
|
||||
TypedMessageEvent
|
||||
TypedMessageEvent,
|
||||
} from "./Api/Events/IframeEvent";
|
||||
import chat from "./Api/iframe/chat";
|
||||
import type {IframeCallback} from './Api/iframe/IframeApiContribution';
|
||||
import type { IframeCallback } from "./Api/iframe/IframeApiContribution";
|
||||
import nav from "./Api/iframe/nav";
|
||||
import controls from "./Api/iframe/controls";
|
||||
import ui from "./Api/iframe/ui";
|
||||
import sound from "./Api/iframe/sound";
|
||||
import room from "./Api/iframe/room";
|
||||
import type {ButtonDescriptor} from "./Api/iframe/Ui/ButtonDescriptor";
|
||||
import type {Popup} from "./Api/iframe/Ui/Popup";
|
||||
import type {Sound} from "./Api/iframe/Sound/Sound";
|
||||
import player from "./Api/iframe/player";
|
||||
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
|
||||
import type { Popup } from "./Api/iframe/Ui/Popup";
|
||||
import type { Sound } from "./Api/iframe/Sound/Sound";
|
||||
|
||||
const wa = {
|
||||
ui,
|
||||
|
@ -23,6 +24,7 @@ const wa = {
|
|||
chat,
|
||||
sound,
|
||||
room,
|
||||
player,
|
||||
|
||||
// All methods below are deprecated and should not be used anymore.
|
||||
// They are kept here for backward compatibility.
|
||||
|
@ -31,14 +33,16 @@ const wa = {
|
|||
* @deprecated Use WA.chat.sendChatMessage instead
|
||||
*/
|
||||
sendChatMessage(message: string, author: string): void {
|
||||
console.warn('Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead');
|
||||
console.warn("Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead");
|
||||
chat.sendChatMessage(message, author);
|
||||
},
|
||||
/**
|
||||
* @deprecated Use WA.chat.disablePlayerControls instead
|
||||
*/
|
||||
disablePlayerControls(): void {
|
||||
console.warn('Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead');
|
||||
console.warn(
|
||||
"Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead"
|
||||
);
|
||||
controls.disablePlayerControls();
|
||||
},
|
||||
|
||||
|
@ -46,7 +50,9 @@ const wa = {
|
|||
* @deprecated Use WA.controls.restorePlayerControls instead
|
||||
*/
|
||||
restorePlayerControls(): void {
|
||||
console.warn('Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead');
|
||||
console.warn(
|
||||
"Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead"
|
||||
);
|
||||
controls.restorePlayerControls();
|
||||
},
|
||||
|
||||
|
@ -54,7 +60,7 @@ const wa = {
|
|||
* @deprecated Use WA.ui.displayBubble instead
|
||||
*/
|
||||
displayBubble(): void {
|
||||
console.warn('Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead');
|
||||
console.warn("Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead");
|
||||
ui.displayBubble();
|
||||
},
|
||||
|
||||
|
@ -62,7 +68,7 @@ const wa = {
|
|||
* @deprecated Use WA.ui.removeBubble instead
|
||||
*/
|
||||
removeBubble(): void {
|
||||
console.warn('Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead');
|
||||
console.warn("Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead");
|
||||
ui.removeBubble();
|
||||
},
|
||||
|
||||
|
@ -70,23 +76,23 @@ const wa = {
|
|||
* @deprecated Use WA.nav.openTab instead
|
||||
*/
|
||||
openTab(url: string): void {
|
||||
console.warn('Method WA.openTab is deprecated. Please use WA.nav.openTab instead');
|
||||
console.warn("Method WA.openTab is deprecated. Please use WA.nav.openTab instead");
|
||||
nav.openTab(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Use WA.sound.loadSound instead
|
||||
*/
|
||||
loadSound(url: string) : Sound {
|
||||
console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead');
|
||||
loadSound(url: string): Sound {
|
||||
console.warn("Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead");
|
||||
return sound.loadSound(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Use WA.nav.goToPage instead
|
||||
*/
|
||||
goToPage(url : string) : void {
|
||||
console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead');
|
||||
goToPage(url: string): void {
|
||||
console.warn("Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead");
|
||||
nav.goToPage(url);
|
||||
},
|
||||
|
||||
|
@ -94,15 +100,15 @@ const wa = {
|
|||
* @deprecated Use WA.nav.goToRoom instead
|
||||
*/
|
||||
goToRoom(url: string): void {
|
||||
console.warn('Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead');
|
||||
console.warn("Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead");
|
||||
nav.goToRoom(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Use WA.nav.openCoWebSite instead
|
||||
*/
|
||||
openCoWebSite(url : string) : void{
|
||||
console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead');
|
||||
openCoWebSite(url: string): void {
|
||||
console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead");
|
||||
nav.openCoWebSite(url);
|
||||
},
|
||||
|
||||
|
@ -110,7 +116,7 @@ const wa = {
|
|||
* @deprecated Use WA.nav.closeCoWebSite instead
|
||||
*/
|
||||
closeCoWebSite(): void {
|
||||
console.warn('Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead');
|
||||
console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead");
|
||||
nav.closeCoWebSite();
|
||||
},
|
||||
|
||||
|
@ -118,28 +124,28 @@ const wa = {
|
|||
* @deprecated Use WA.controls.restorePlayerControls instead
|
||||
*/
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||
console.warn('Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead');
|
||||
console.warn("Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead");
|
||||
return ui.openPopup(targetObject, message, buttons);
|
||||
},
|
||||
/**
|
||||
* @deprecated Use WA.chat.onChatMessage instead
|
||||
*/
|
||||
onChatMessage(callback: (message: string) => void): void {
|
||||
console.warn('Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead');
|
||||
console.warn("Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead");
|
||||
chat.onChatMessage(callback);
|
||||
},
|
||||
/**
|
||||
* @deprecated Use WA.room.onEnterZone instead
|
||||
*/
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
console.warn('Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead');
|
||||
console.warn("Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead");
|
||||
room.onEnterZone(name, callback);
|
||||
},
|
||||
/**
|
||||
* @deprecated Use WA.room.onLeaveZone instead
|
||||
*/
|
||||
onLeaveZone(name: string, callback: () => void): void {
|
||||
console.warn('Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead');
|
||||
console.warn("Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead");
|
||||
room.onLeaveZone(name, callback);
|
||||
},
|
||||
};
|
||||
|
@ -147,30 +153,32 @@ const wa = {
|
|||
export type WorkAdventureApi = typeof wa;
|
||||
|
||||
declare global {
|
||||
|
||||
interface Window {
|
||||
WA: WorkAdventureApi
|
||||
WA: WorkAdventureApi;
|
||||
}
|
||||
let WA: WorkAdventureApi
|
||||
let WA: WorkAdventureApi;
|
||||
}
|
||||
|
||||
window.WA = wa;
|
||||
|
||||
window.addEventListener('message', <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
|
||||
if (message.source !== window.parent) {
|
||||
return; // Skip message in this event listener
|
||||
}
|
||||
const payload = message.data;
|
||||
console.debug(payload);
|
||||
|
||||
if (isIframeResponseEventWrapper(payload)) {
|
||||
const payloadData = payload.data;
|
||||
|
||||
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined
|
||||
if (callback?.typeChecker(payloadData)) {
|
||||
callback?.callback(payloadData)
|
||||
window.addEventListener(
|
||||
"message",
|
||||
<T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
|
||||
if (message.source !== window.parent) {
|
||||
return; // Skip message in this event listener
|
||||
}
|
||||
}
|
||||
const payload = message.data;
|
||||
console.debug(payload);
|
||||
|
||||
// ...
|
||||
});
|
||||
if (isIframeResponseEventWrapper(payload)) {
|
||||
const payloadData = payload.data;
|
||||
|
||||
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined;
|
||||
if (callback?.typeChecker(payloadData)) {
|
||||
callback?.callback(payloadData);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
);
|
||||
|
|
|
@ -35,102 +35,109 @@ body .message-info.info{
|
|||
body .message-info.warning{
|
||||
background: #ffa500d6;
|
||||
}
|
||||
.video-container{
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
background-color: #00000099;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
}
|
||||
.video-container i{
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
left: calc(50% - 50px);
|
||||
top: calc(50% - 50px);
|
||||
background-color: black;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
padding-top: 32px;
|
||||
font-size: 28px;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-container img{
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
padding: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
.video-container img.block-logo {
|
||||
left: 30%;
|
||||
bottom: 15%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 90vh;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
}
|
||||
|
||||
.video-container button.report{
|
||||
display: block;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
background-color: black;
|
||||
border-radius: 15px;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 35px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
transition: all .5s ease;
|
||||
}
|
||||
i {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
left: calc(50% - 50px);
|
||||
top: calc(50% - 50px);
|
||||
background-color: black;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
padding-top: 32px;
|
||||
font-size: 28px;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-container:hover button.report{
|
||||
width: 35px;
|
||||
padding: 10px;
|
||||
}
|
||||
img {
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
padding: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.video-container button.report:hover {
|
||||
width: 160px;
|
||||
}
|
||||
img.block-logo {
|
||||
left: 30%;
|
||||
bottom: 15%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.video-container button.report img{
|
||||
position: absolute;
|
||||
display: block;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
.video-container button.report span{
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 36px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
}
|
||||
.video-container img.active {
|
||||
display: block !important;
|
||||
}
|
||||
button.report{
|
||||
display: block;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
background-color: black;
|
||||
border-radius: 15px;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 35px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
transition: all .5s ease;
|
||||
|
||||
.video-container video{
|
||||
height: 100%;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
}
|
||||
img{
|
||||
position: absolute;
|
||||
display: block;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.video-container video:focus{
|
||||
outline: none;
|
||||
span {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 36px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||
}
|
||||
|
||||
img.active {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover button.report{
|
||||
width: 35px;
|
||||
padding: 10px;
|
||||
|
||||
&:hover {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
video:focus{
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.video-container.div-myCamVideo{
|
||||
|
@ -204,7 +211,7 @@ video.myCamVideo{
|
|||
display: inline-flex;
|
||||
bottom: 10px;
|
||||
right: 15px;
|
||||
width: 180px;
|
||||
width: 240px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
|
@ -224,7 +231,7 @@ video.myCamVideo{
|
|||
background: #666;
|
||||
box-shadow: 2px 2px 24px #444;
|
||||
border-radius: 48px;
|
||||
transform: translateY(20px);
|
||||
transform: translateY(15px);
|
||||
transition-timing-function: ease-in-out;
|
||||
margin: 0 4%;
|
||||
}
|
||||
|
@ -263,6 +270,16 @@ video.myCamVideo{
|
|||
.btn-cam-action:hover .btn-monitor.hide{
|
||||
transform: translateY(60px);
|
||||
}
|
||||
.btn-layout{
|
||||
pointer-events: auto;
|
||||
transition: all .15s;
|
||||
}
|
||||
.btn-layout.hide {
|
||||
transform: translateY(60px);
|
||||
}
|
||||
.btn-cam-action:hover .btn-layout.hide{
|
||||
transform: translateY(60px);
|
||||
}
|
||||
.btn-copy{
|
||||
pointer-events: auto;
|
||||
transition: all .3s;
|
||||
|
|
|
@ -1,147 +1,155 @@
|
|||
import "jasmine";
|
||||
import {Room} from "../../../src/Connexion/Room";
|
||||
import {LayersIterator} from "../../../src/Phaser/Map/LayersIterator";
|
||||
import { Room } from "../../../src/Connexion/Room";
|
||||
import { flattenGroupLayersMap } from "../../../src/Phaser/Map/LayersFlattener";
|
||||
import type { ITiledMapLayer } from "../../../src/Phaser/Map/ITiledMap";
|
||||
|
||||
describe("Layers iterator", () => {
|
||||
describe("Layers flattener", () => {
|
||||
it("should iterate maps with no group", () => {
|
||||
const layersIterator = new LayersIterator({
|
||||
"compressionlevel":-1,
|
||||
"height":2,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
let flatLayers: ITiledMapLayer[] = [];
|
||||
flatLayers = flattenGroupLayersMap({
|
||||
compressionlevel: -1,
|
||||
height: 2,
|
||||
infinite: false,
|
||||
layers: [
|
||||
{
|
||||
"data":[0, 0, 0, 0],
|
||||
"height":2,
|
||||
"id":1,
|
||||
"name":"Tile Layer 1",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":2,
|
||||
"x":0,
|
||||
"y":0
|
||||
data: [0, 0, 0, 0],
|
||||
height: 2,
|
||||
id: 1,
|
||||
name: "Tile Layer 1",
|
||||
opacity: 1,
|
||||
type: "tilelayer",
|
||||
visible: true,
|
||||
width: 2,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0],
|
||||
"height":2,
|
||||
"id":1,
|
||||
"name":"Tile Layer 2",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":2,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":2,
|
||||
"nextobjectid":1,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"2021.03.23",
|
||||
"tileheight":32,
|
||||
"tilesets":[],
|
||||
"tilewidth":32,
|
||||
"type":"map",
|
||||
"version":1.5,
|
||||
"width":2
|
||||
})
|
||||
data: [0, 0, 0, 0],
|
||||
height: 2,
|
||||
id: 1,
|
||||
name: "Tile Layer 2",
|
||||
opacity: 1,
|
||||
type: "tilelayer",
|
||||
visible: true,
|
||||
width: 2,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
],
|
||||
nextlayerid: 2,
|
||||
nextobjectid: 1,
|
||||
orientation: "orthogonal",
|
||||
renderorder: "right-down",
|
||||
tiledversion: "2021.03.23",
|
||||
tileheight: 32,
|
||||
tilesets: [],
|
||||
tilewidth: 32,
|
||||
type: "map",
|
||||
version: 1.5,
|
||||
width: 2,
|
||||
});
|
||||
|
||||
const layers = [];
|
||||
for (const layer of layersIterator) {
|
||||
for (const layer of flatLayers) {
|
||||
layers.push(layer.name);
|
||||
}
|
||||
expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']);
|
||||
expect(layers).toEqual(["Tile Layer 1", "Tile Layer 2"]);
|
||||
});
|
||||
|
||||
it("should iterate maps with recursive groups", () => {
|
||||
const layersIterator = new LayersIterator({
|
||||
"compressionlevel":-1,
|
||||
"height":2,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
let flatLayers: ITiledMapLayer[] = [];
|
||||
flatLayers = flattenGroupLayersMap({
|
||||
compressionlevel: -1,
|
||||
height: 2,
|
||||
infinite: false,
|
||||
layers: [
|
||||
{
|
||||
"id":6,
|
||||
"layers":[
|
||||
id: 6,
|
||||
layers: [
|
||||
{
|
||||
"id":5,
|
||||
"layers":[
|
||||
id: 5,
|
||||
layers: [
|
||||
{
|
||||
"data":[0, 0, 0, 0],
|
||||
"height":2,
|
||||
"id":10,
|
||||
"name":"Tile3",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":2,
|
||||
"x":0,
|
||||
"y":0
|
||||
data: [0, 0, 0, 0],
|
||||
height: 2,
|
||||
id: 10,
|
||||
name: "Tile3",
|
||||
opacity: 1,
|
||||
type: "tilelayer",
|
||||
visible: true,
|
||||
width: 2,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0],
|
||||
"height":2,
|
||||
"id":9,
|
||||
"name":"Tile2",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":2,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"name":"Group 3",
|
||||
"opacity":1,
|
||||
"type":"group",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
data: [0, 0, 0, 0],
|
||||
height: 2,
|
||||
id: 9,
|
||||
name: "Tile2",
|
||||
opacity: 1,
|
||||
type: "tilelayer",
|
||||
visible: true,
|
||||
width: 2,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
],
|
||||
name: "Group 3",
|
||||
opacity: 1,
|
||||
type: "group",
|
||||
visible: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
"id":7,
|
||||
"layers":[
|
||||
id: 7,
|
||||
layers: [
|
||||
{
|
||||
"data":[0, 0, 0, 0],
|
||||
"height":2,
|
||||
"id":8,
|
||||
"name":"Tile1",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":2,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"name":"Group 2",
|
||||
"opacity":1,
|
||||
"type":"group",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"name":"Group 1",
|
||||
"opacity":1,
|
||||
"type":"group",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":11,
|
||||
"nextobjectid":1,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"2021.03.23",
|
||||
"tileheight":32,
|
||||
"tilesets":[],
|
||||
"tilewidth":32,
|
||||
"type":"map",
|
||||
"version":1.5,
|
||||
"width":2
|
||||
})
|
||||
data: [0, 0, 0, 0],
|
||||
height: 2,
|
||||
id: 8,
|
||||
name: "Tile1",
|
||||
opacity: 1,
|
||||
type: "tilelayer",
|
||||
visible: true,
|
||||
width: 2,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
],
|
||||
name: "Group 2",
|
||||
opacity: 1,
|
||||
type: "group",
|
||||
visible: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
],
|
||||
name: "Group 1",
|
||||
opacity: 1,
|
||||
type: "group",
|
||||
visible: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
],
|
||||
nextlayerid: 11,
|
||||
nextobjectid: 1,
|
||||
orientation: "orthogonal",
|
||||
renderorder: "right-down",
|
||||
tiledversion: "2021.03.23",
|
||||
tileheight: 32,
|
||||
tilesets: [],
|
||||
tilewidth: 32,
|
||||
type: "map",
|
||||
version: 1.5,
|
||||
width: 2,
|
||||
});
|
||||
|
||||
const layers = [];
|
||||
for (const layer of layersIterator) {
|
||||
for (const layer of flatLayers) {
|
||||
layers.push(layer.name);
|
||||
}
|
||||
expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']);
|
||||
expect(layers).toEqual(["Group 1/Group 3/Tile3", "Group 1/Group 3/Tile2", "Group 1/Group 2/Tile1"]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,35 +1,33 @@
|
|||
import type {Configuration} from "webpack";
|
||||
import type { Configuration } from "webpack";
|
||||
import type WebpackDevServer from "webpack-dev-server";
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import path from "path";
|
||||
import webpack from "webpack";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
||||
import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
|
||||
import {DISPLAY_TERMS_OF_USE} from "./src/Enum/EnvironmentVariable";
|
||||
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
|
||||
import { DISPLAY_TERMS_OF_USE } from "./src/Enum/EnvironmentVariable";
|
||||
|
||||
const mode = process.env.NODE_ENV ?? 'development';
|
||||
const isProduction = mode === 'production';
|
||||
const mode = process.env.NODE_ENV ?? "development";
|
||||
const isProduction = mode === "production";
|
||||
const isDevelopment = !isProduction;
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
'main': './src/index.ts',
|
||||
'iframe_api': './src/iframe_api.ts'
|
||||
main: "./src/index.ts",
|
||||
iframe_api: "./src/iframe_api.ts",
|
||||
},
|
||||
mode: mode,
|
||||
devtool: isDevelopment ? 'inline-source-map' : 'source-map',
|
||||
devtool: isDevelopment ? "inline-source-map" : "source-map",
|
||||
devServer: {
|
||||
contentBase: './dist',
|
||||
host: '0.0.0.0',
|
||||
contentBase: "./dist",
|
||||
host: "0.0.0.0",
|
||||
sockPort: 80,
|
||||
disableHostCheck: true,
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /^_\/.*$/, to: '/index.html' }
|
||||
],
|
||||
disableDotRule: true
|
||||
rewrites: [{ from: /^_\/.*$/, to: "/index.html" }],
|
||||
disableDotRule: true,
|
||||
},
|
||||
},
|
||||
module: {
|
||||
|
@ -38,7 +36,7 @@ module.exports = {
|
|||
test: /\.tsx?$/,
|
||||
//use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
loader: 'ts-loader',
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
|
@ -47,13 +45,16 @@ module.exports = {
|
|||
test: /\.scss$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader, {
|
||||
loader: 'css-loader',
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
//url: false,
|
||||
sourceMap: true
|
||||
}
|
||||
}, 'sass-loader'],
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
"sass-loader",
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
|
@ -61,23 +62,23 @@ module.exports = {
|
|||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
//url: false,
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(html|svelte)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
loader: "svelte-loader",
|
||||
options: {
|
||||
compilerOptions: {
|
||||
// Dev mode must be enabled for HMR to work!
|
||||
dev: isDevelopment
|
||||
dev: isDevelopment,
|
||||
},
|
||||
emitCss: isProduction,
|
||||
hotReload: isDevelopment,
|
||||
|
@ -90,17 +91,27 @@ module.exports = {
|
|||
scss: true,
|
||||
sass: true,
|
||||
}),
|
||||
onwarn: function (warning: { code: string }, handleWarning: (warning: { code: string }) => void) {
|
||||
onwarn: function (
|
||||
warning: { code: string },
|
||||
handleWarning: (warning: { code: string }) => void
|
||||
) {
|
||||
// See https://github.com/sveltejs/svelte/issues/4946#issuecomment-662168782
|
||||
|
||||
if (warning.code === 'a11y-no-onchange') { return }
|
||||
if (warning.code === 'a11y-autofocus') { return }
|
||||
if (warning.code === "a11y-no-onchange") {
|
||||
return;
|
||||
}
|
||||
if (warning.code === "a11y-autofocus") {
|
||||
return;
|
||||
}
|
||||
if (warning.code === "a11y-media-has-caption") {
|
||||
return;
|
||||
}
|
||||
|
||||
// process as usual
|
||||
handleWarning(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4
|
||||
|
@ -108,86 +119,82 @@ module.exports = {
|
|||
{
|
||||
test: /node_modules\/svelte\/.*\.mjs$/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(eot|svg|png|gif|jpg)$/,
|
||||
exclude: /node_modules/,
|
||||
type: 'asset'
|
||||
type: "asset",
|
||||
},
|
||||
{
|
||||
test: /\.(woff(2)?|ttf)$/,
|
||||
type: 'asset',
|
||||
type: "asset",
|
||||
generator: {
|
||||
filename: 'fonts/[name][ext]'
|
||||
}
|
||||
|
||||
}
|
||||
filename: "fonts/[name][ext]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
svelte: path.resolve('node_modules', 'svelte')
|
||||
svelte: path.resolve("node_modules", "svelte"),
|
||||
},
|
||||
extensions: [ '.tsx', '.ts', '.js', '.svelte' ],
|
||||
mainFields: ['svelte', 'browser', 'module', 'main']
|
||||
extensions: [".tsx", ".ts", ".js", ".svelte"],
|
||||
mainFields: ["svelte", "browser", "module", "main"],
|
||||
},
|
||||
output: {
|
||||
filename: (pathData) => {
|
||||
// Add a content hash only for the main bundle.
|
||||
// We want the iframe_api.js file to keep its name as it will be referenced from outside iframes.
|
||||
return pathData.chunk?.name === 'main' ? 'js/[name].[contenthash].js': '[name].js';
|
||||
return pathData.chunk?.name === "main" ? "js/[name].[contenthash].js" : "[name].js";
|
||||
},
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/'
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
publicPath: "/",
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
eslint: {
|
||||
files: './src/**/*.ts'
|
||||
}
|
||||
files: "./src/**/*.ts",
|
||||
},
|
||||
}),
|
||||
new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./dist/index.tmpl.html.tmp",
|
||||
minify: {
|
||||
collapseWhitespace: true,
|
||||
keepClosingSlash: true,
|
||||
removeComments: false,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true,
|
||||
},
|
||||
chunks: ["main"],
|
||||
}),
|
||||
new MiniCssExtractPlugin({filename: '[name].[contenthash].css'}),
|
||||
new HtmlWebpackPlugin(
|
||||
{
|
||||
template: './dist/index.tmpl.html.tmp',
|
||||
minify: {
|
||||
collapseWhitespace: true,
|
||||
keepClosingSlash: true,
|
||||
removeComments: false,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true
|
||||
},
|
||||
chunks: ['main']
|
||||
}
|
||||
),
|
||||
new webpack.ProvidePlugin({
|
||||
Phaser: 'phaser'
|
||||
Phaser: "phaser",
|
||||
}),
|
||||
new NodePolyfillPlugin(),
|
||||
new webpack.EnvironmentPlugin({
|
||||
'API_URL': null,
|
||||
'SKIP_RENDER_OPTIMIZATIONS': false,
|
||||
'DISABLE_NOTIFICATIONS': false,
|
||||
'PUSHER_URL': undefined,
|
||||
'UPLOADER_URL': null,
|
||||
'ADMIN_URL': null,
|
||||
'DEBUG_MODE': null,
|
||||
'STUN_SERVER': null,
|
||||
'TURN_SERVER': null,
|
||||
'TURN_USER': null,
|
||||
'TURN_PASSWORD': null,
|
||||
'JITSI_URL': null,
|
||||
'JITSI_PRIVATE_MODE': null,
|
||||
'START_ROOM_URL': null,
|
||||
'MAX_USERNAME_LENGTH': 8,
|
||||
'MAX_PER_GROUP': 4,
|
||||
'DISPLAY_TERMS_OF_USE': false,
|
||||
})
|
||||
API_URL: null,
|
||||
SKIP_RENDER_OPTIMIZATIONS: false,
|
||||
DISABLE_NOTIFICATIONS: false,
|
||||
PUSHER_URL: undefined,
|
||||
UPLOADER_URL: null,
|
||||
ADMIN_URL: null,
|
||||
DEBUG_MODE: null,
|
||||
STUN_SERVER: null,
|
||||
TURN_SERVER: null,
|
||||
TURN_USER: null,
|
||||
TURN_PASSWORD: null,
|
||||
JITSI_URL: null,
|
||||
JITSI_PRIVATE_MODE: null,
|
||||
START_ROOM_URL: null,
|
||||
MAX_USERNAME_LENGTH: 8,
|
||||
MAX_PER_GROUP: 4,
|
||||
DISPLAY_TERMS_OF_USE: false,
|
||||
}),
|
||||
],
|
||||
|
||||
} as Configuration & WebpackDevServer.Configuration;
|
||||
|
|
229
front/yarn.lock
229
front/yarn.lock
|
@ -569,6 +569,14 @@ after@0.8.2:
|
|||
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
|
||||
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
|
||||
|
||||
aggregate-error@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
|
||||
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
|
||||
dependencies:
|
||||
clean-stack "^2.0.0"
|
||||
indent-string "^4.0.0"
|
||||
|
||||
ajv-errors@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
|
||||
|
@ -609,6 +617,13 @@ ansi-colors@^4.1.1:
|
|||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
|
||||
|
||||
ansi-escapes@^4.3.0:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
|
||||
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
|
||||
dependencies:
|
||||
type-fest "^0.21.3"
|
||||
|
||||
ansi-html@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
|
||||
|
@ -1121,7 +1136,7 @@ chalk@^2.0.0, chalk@^2.4.1:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.0:
|
||||
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
|
||||
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
|
||||
|
@ -1208,6 +1223,26 @@ clean-css@^4.2.3:
|
|||
dependencies:
|
||||
source-map "~0.6.0"
|
||||
|
||||
clean-stack@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
||||
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
|
||||
|
||||
cli-cursor@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
|
||||
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
|
||||
dependencies:
|
||||
restore-cursor "^3.1.0"
|
||||
|
||||
cli-truncate@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
|
||||
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
|
||||
dependencies:
|
||||
slice-ansi "^3.0.0"
|
||||
string-width "^4.2.0"
|
||||
|
||||
cliui@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
|
||||
|
@ -1278,7 +1313,7 @@ commander@^4.1.1:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||
|
||||
commander@^7.0.0:
|
||||
commander@^7.0.0, commander@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
@ -1381,6 +1416,17 @@ cosmiconfig@^6.0.0:
|
|||
path-type "^4.0.0"
|
||||
yaml "^1.7.2"
|
||||
|
||||
cosmiconfig@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
|
||||
integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==
|
||||
dependencies:
|
||||
"@types/parse-json" "^4.0.0"
|
||||
import-fresh "^3.2.1"
|
||||
parse-json "^5.0.0"
|
||||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
create-ecdh@^4.0.0:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
|
||||
|
@ -1536,6 +1582,11 @@ decode-uri-component@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
|
||||
|
||||
dedent@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
|
||||
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
|
||||
|
||||
deep-equal@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
|
||||
|
@ -1823,7 +1874,7 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0:
|
|||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enquirer@^2.3.5:
|
||||
enquirer@^2.3.5, enquirer@^2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
|
||||
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
|
||||
|
@ -2426,6 +2477,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
|
|||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
get-own-enumerable-property-symbols@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
||||
integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
|
||||
|
||||
get-stream@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||
|
@ -2822,6 +2878,11 @@ imurmurhash@^0.1.4:
|
|||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
|
||||
|
||||
indent-string@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
|
||||
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
|
||||
|
||||
indexof@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
||||
|
@ -3060,6 +3121,11 @@ is-number@^7.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
is-obj@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
|
||||
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
|
||||
|
||||
is-path-cwd@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
|
||||
|
@ -3099,6 +3165,11 @@ is-regex@^1.0.4, is-regex@^1.1.2:
|
|||
call-bind "^1.0.2"
|
||||
has-symbols "^1.0.2"
|
||||
|
||||
is-regexp@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
|
||||
integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk=
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
|
@ -3132,6 +3203,11 @@ is-typed-array@^1.1.3:
|
|||
foreach "^2.0.5"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-unicode-supported@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
|
||||
|
||||
is-windows@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||
|
@ -3309,6 +3385,40 @@ linked-list-typescript@^1.0.11:
|
|||
resolved "https://registry.yarnpkg.com/linked-list-typescript/-/linked-list-typescript-1.0.15.tgz#faeed93cf9203f102e2158c29edcddda320abe82"
|
||||
integrity sha512-RIyUu9lnJIyIaMe63O7/aFv/T2v3KsMFuXMBbUQCHX+cgtGro86ETDj5ed0a8gQL2+DFjzYYsgVG4I36/cUwgw==
|
||||
|
||||
lint-staged@^11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712"
|
||||
integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw==
|
||||
dependencies:
|
||||
chalk "^4.1.1"
|
||||
cli-truncate "^2.1.0"
|
||||
commander "^7.2.0"
|
||||
cosmiconfig "^7.0.0"
|
||||
debug "^4.3.1"
|
||||
dedent "^0.7.0"
|
||||
enquirer "^2.3.6"
|
||||
execa "^5.0.0"
|
||||
listr2 "^3.8.2"
|
||||
log-symbols "^4.1.0"
|
||||
micromatch "^4.0.4"
|
||||
normalize-path "^3.0.0"
|
||||
please-upgrade-node "^3.2.0"
|
||||
string-argv "0.3.1"
|
||||
stringify-object "^3.3.0"
|
||||
|
||||
listr2@^3.8.2:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f"
|
||||
integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw==
|
||||
dependencies:
|
||||
cli-truncate "^2.1.0"
|
||||
colorette "^1.2.2"
|
||||
log-update "^4.0.0"
|
||||
p-map "^4.0.0"
|
||||
rxjs "^6.6.7"
|
||||
through "^2.3.8"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
load-json-file@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
|
||||
|
@ -3363,6 +3473,24 @@ lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17
|
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
log-symbols@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
||||
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
is-unicode-supported "^0.1.0"
|
||||
|
||||
log-update@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
|
||||
integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
|
||||
dependencies:
|
||||
ansi-escapes "^4.3.0"
|
||||
cli-cursor "^3.1.0"
|
||||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
loglevel@^1.6.8:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
||||
|
@ -3477,7 +3605,7 @@ micromatch@^3.1.10, micromatch@^3.1.4:
|
|||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.2"
|
||||
|
||||
micromatch@^4.0.0, micromatch@^4.0.2:
|
||||
micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
|
||||
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
|
||||
|
@ -3842,7 +3970,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
|||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^5.1.2:
|
||||
onetime@^5.1.0, onetime@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
|
||||
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
|
||||
|
@ -3918,6 +4046,13 @@ p-map@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
||||
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
||||
|
||||
p-map@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
|
||||
integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
|
||||
dependencies:
|
||||
aggregate-error "^3.0.0"
|
||||
|
||||
p-retry@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328"
|
||||
|
@ -4171,6 +4306,13 @@ pkg-dir@^4.2.0:
|
|||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
please-upgrade-node@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
|
||||
integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
|
||||
dependencies:
|
||||
semver-compare "^1.0.0"
|
||||
|
||||
portfinder@^1.0.26:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
|
||||
|
@ -4240,6 +4382,11 @@ prelude-ls@^1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
|
||||
integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
|
||||
|
||||
pretty-error@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"
|
||||
|
@ -4569,6 +4716,14 @@ resolve@^1.10.0, resolve@^1.9.0:
|
|||
is-core-module "^2.2.0"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
restore-cursor@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
|
||||
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
|
||||
dependencies:
|
||||
onetime "^5.1.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
ret@~0.1.10:
|
||||
version "0.1.15"
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
|
@ -4613,7 +4768,7 @@ run-parallel@^1.1.9:
|
|||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^6.6.3:
|
||||
rxjs@^6.6.3, rxjs@^6.6.7:
|
||||
version "6.6.7"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
|
||||
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
|
||||
|
@ -4703,6 +4858,11 @@ selfsigned@^1.10.8:
|
|||
dependencies:
|
||||
node-forge "^0.10.0"
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
||||
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
|
@ -4843,7 +5003,7 @@ shell-quote@^1.6.1:
|
|||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
|
||||
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.3:
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||
|
@ -4866,6 +5026,15 @@ slash@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
slice-ansi@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
|
||||
integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
slice-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
|
||||
|
@ -5097,6 +5266,11 @@ stream-http@^3.2.0:
|
|||
readable-stream "^3.6.0"
|
||||
xtend "^4.0.2"
|
||||
|
||||
string-argv@0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
|
||||
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
|
||||
|
||||
string-width@^3.0.0, string-width@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
|
||||
|
@ -5106,7 +5280,7 @@ string-width@^3.0.0, string-width@^3.1.0:
|
|||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^5.1.0"
|
||||
|
||||
string-width@^4.2.0:
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
|
||||
integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
|
||||
|
@ -5154,6 +5328,15 @@ string_decoder@~1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
stringify-object@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629"
|
||||
integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==
|
||||
dependencies:
|
||||
get-own-enumerable-property-symbols "^3.0.0"
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
|
@ -5329,6 +5512,11 @@ text-table@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||
|
||||
through@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
thunky@^1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
|
||||
|
@ -5449,6 +5637,11 @@ type-fest@^0.20.2:
|
|||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||
|
||||
type-fest@^0.21.3:
|
||||
version "0.21.3"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
type-fest@^0.8.1:
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||
|
@ -5836,6 +6029,24 @@ wrap-ansi@^5.1.0:
|
|||
string-width "^3.0.0"
|
||||
strip-ansi "^5.0.0"
|
||||
|
||||
wrap-ansi@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
@ -5873,7 +6084,7 @@ yallist@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yaml@^1.7.2:
|
||||
yaml@^1.10.0, yaml@^1.7.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue