From 71eb3f3b69c1cea1aefc8b313b8435172b4103d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gr=C3=A9goire=20parant?= Date: Wed, 5 May 2021 01:49:04 +0200 Subject: [PATCH] Improvement feature circle discussion (#977) * Improvment circle discussion - Change to lissen start event of WebRTC connection * Update help allow navigator and waring message - Show warning message. - Use help camera allow setting to show modal and help user. - Change feature to show the modal only when user have need the information on allow navigator access * Create soud for video discussion --- front/dist/index.tmpl.html | 19 +- .../resources/html/EnableCameraScene.html | 2 +- .../resources/html/helpCameraSettings.html | 7 + front/dist/resources/style/mobile-style.scss | 18 +- front/dist/resources/style/style.css | 94 +++++++-- .../dist/static/images/favicons/manifest.json | 186 ++++++++++++++---- front/src/Phaser/Game/GameManager.ts | 5 +- front/src/Phaser/Game/GameScene.ts | 2 +- front/src/Phaser/Login/EnableCameraScene.ts | 3 +- .../Phaser/Menu/HelpCameraSettingsScene.ts | 49 +++-- front/src/WebRtc/LayoutManager.ts | 39 +++- front/src/WebRtc/MediaManager.ts | 114 ++++++++++- front/src/WebRtc/SimplePeer.ts | 6 +- maps/Village/sol_intérieur.png | Bin 0 -> 20672 bytes 14 files changed, 434 insertions(+), 110 deletions(-) create mode 100644 maps/Village/sol_intérieur.png diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index 062622b8..c4763b6e 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -48,20 +48,27 @@
+
+ + + + + +
-
- - +
+ +
-
- - +
+ +
diff --git a/front/dist/resources/html/EnableCameraScene.html b/front/dist/resources/html/EnableCameraScene.html index 0763d7dd..2dda6cc1 100644 --- a/front/dist/resources/html/EnableCameraScene.html +++ b/front/dist/resources/html/EnableCameraScene.html @@ -48,7 +48,7 @@ text-align: center; margin: 0; position: absolute; - top: 44vh; + top: 40vh; width: 100%; } #enableCameraScene button { diff --git a/front/dist/resources/html/helpCameraSettings.html b/front/dist/resources/html/helpCameraSettings.html index a04668fe..ee8e16e2 100644 --- a/front/dist/resources/html/helpCameraSettings.html +++ b/front/dist/resources/html/helpCameraSettings.html @@ -60,6 +60,9 @@ font-size: 8px; margin: 0px 20px; } + #helpCameraSettings section p a{ + font-size: 8px; + } #helpCameraSettings section p.err{ color: #ff0000; } @@ -95,6 +98,10 @@

If you prefer to continue without allowing camera and microphone access, click on Continue

+ +
Refresh diff --git a/front/dist/resources/style/mobile-style.scss b/front/dist/resources/style/mobile-style.scss index d9ed4b2e..21753ebd 100644 --- a/front/dist/resources/style/mobile-style.scss +++ b/front/dist/resources/style/mobile-style.scss @@ -23,27 +23,17 @@ } .btn-cam-action { + min-width: 150px; + &:hover{ transform: translateY(20px); } div { + margin: 0 1%; &:hover { background-color: #666; } - - bottom: 30px; - - &.btn-micro { - right: 0; - } - - &.btn-monitor { - right: 130px; - } - - &.btn-video { - right: 65px; - } + margin-bottom: 30px; } } diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 97e363f9..213b00f2 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -98,7 +98,7 @@ body .message-info.warning{ } .video-container button.report:hover { - width: 150px; + width: 160px; } .video-container button.report img{ @@ -126,6 +126,7 @@ body .message-info.warning{ .video-container video{ height: 100%; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; } .video-container video:focus{ @@ -141,7 +142,7 @@ body .message-info.warning{ right: 15px; bottom: 30px; border-radius: 15px 15px 15px 15px; - max-height: 200px; + max-height: 20%; } video#myCamVideo{ @@ -153,19 +154,60 @@ video#myCamVideo{ /*height: 113px;*/ } +.sound-progress{ + display: none; + position: absolute; + right: 14px; + top: calc(50% - 5px); +} +.sound-progress.active{ + display: table-column; +} +.sound-progress span{ + position: absolute; + color: black; + background-color: #00000020; + width: 5px; + height: 5px; + border-radius: 50%; +} +.sound-progress span.active{ + background-color: #00c3ff66 +} +.sound-progress span:nth-child(1){ + top: calc(50% + 20px); +} +.sound-progress span:nth-child(2){ + top: calc(50% + 10px); +} +.sound-progress span:nth-child(3){ + top: calc(50% - 0px); +} +.sound-progress span:nth-child(4){ + top: calc(50% - 10px); +} +.sound-progress span:nth-child(5){ + top: calc(50% - 20px); +} .btn-cam-action { pointer-events: all; position: absolute; - bottom: 0px; - right: 0px; - width: 450px; - height: 150px; + display: inline-flex; + bottom: 10px; + right: 15px; + width: 15vw; + height: 40px; + text-align: center; + align-content: center; + align-items: center; + justify-content: center; + justify-items: center; } /*btn animation*/ .btn-cam-action div{ cursor: url('/resources/logos/cursor_pointer.png'), pointer; - position: absolute; + /*position: absolute;*/ border: solid 0px black; width: 44px; height: 44px; @@ -174,7 +216,8 @@ video#myCamVideo{ border-radius: 48px; transform: translateY(20px); transition-timing-function: ease-in-out; - bottom: 20px; + margin-bottom: 20px; + margin: 0 4%; } .btn-cam-action div.disabled { background: #d75555; @@ -193,17 +236,17 @@ video#myCamVideo{ .btn-micro{ pointer-events: auto; transition: all .3s; - right: 44px; + /*right: 44px;*/ } .btn-video{ pointer-events: auto; transition: all .25s; - right: 134px; + /*right: 134px;*/ } .btn-monitor{ pointer-events: auto; transition: all .2s; - right: 224px; + /*right: 224px;*/ } .btn-copy{ pointer-events: auto; @@ -497,7 +540,7 @@ input[type=range]:focus::-ms-fill-upper { position: absolute; width: 100%; height: 100%; - pointer-events: none; + pointer-events: all; /* TODO: DO WE NEED FLEX HERE???? WE WANT A SIDEBAR OF EXACTLY 25% (note: flex useful for direction!!!) */ } @@ -533,7 +576,7 @@ input[type=range]:focus::-ms-fill-upper { .sidebar { flex: 0 0 25%; display: flex; - pointer-events: none; + pointer-events: all; } .sidebar > div { @@ -547,6 +590,10 @@ input[type=range]:focus::-ms-fill-upper { margin: 0%; } +.sidebar > div video { + cursor: url('/resources/logos/cursor_pointer.png'), pointer; +} + /* Let's make sure videos are vertically centered if they need to be cropped */ .media-container { display: flex; @@ -1111,17 +1158,34 @@ div.action{ animation-iteration-count: infinite; animation-timing-function: ease-in-out; } +div.action.info, +div.action.warning, +div.action.danger{ + transition: all 1s ease; + animation: mymove 1s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; +} div.action p.action-body{ + cursor: url('/resources/logos/cursor_pointer.png'), pointer; padding: 10px; background-color: #2d2d2dba; color: #fff; font-size: 14px; font-weight: 500; text-align: center; - max-width: 250px; - margin-left: calc(50% - 125px); + max-width: 350px; + margin-left: calc(50% - 175px); border-radius: 15px; } +div.action.warning p.action-body{ + background-color: #ff9800eb; + color: #000; +} +div.action.danger p.action-body{ + background-color: #da0000e3; + color: #000; +} .popUpElement{ font-family: 'Press Start 2P'; text-align: left; diff --git a/front/dist/static/images/favicons/manifest.json b/front/dist/static/images/favicons/manifest.json index 013d4a6a..47ad9377 100644 --- a/front/dist/static/images/favicons/manifest.json +++ b/front/dist/static/images/favicons/manifest.json @@ -1,41 +1,149 @@ { - "name": "App", - "icons": [ - { - "src": "\/android-icon-36x36.png", - "sizes": "36x36", - "type": "image\/png", - "density": "0.75" - }, - { - "src": "\/android-icon-48x48.png", - "sizes": "48x48", - "type": "image\/png", - "density": "1.0" - }, - { - "src": "\/android-icon-72x72.png", - "sizes": "72x72", - "type": "image\/png", - "density": "1.5" - }, - { - "src": "\/android-icon-96x96.png", - "sizes": "96x96", - "type": "image\/png", - "density": "2.0" - }, - { - "src": "\/android-icon-144x144.png", - "sizes": "144x144", - "type": "image\/png", - "density": "3.0" - }, - { - "src": "\/android-icon-192x192.png", - "sizes": "192x192", - "type": "image\/png", - "density": "4.0" - } - ] + "short_name": "WA", + "name": "WorkAdventure", + "icons": [ + { + "src": "/static/images/favicons/apple-icon-57x57.png", + "sizes": "57x57", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-60x60.png", + "sizes": "60x60", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-76x76.png", + "sizes": "76x76", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-114x114.png", + "sizes": "114x114", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-120x120.png", + "sizes": "120x120", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-152x152.png", + "sizes": "152x152", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/apple-icon-180x180.png", + "sizes": "180x180", + "type": "image\/png" + }, + { + "src": "/static/images/favicons/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "/static/images/favicons/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "/static/images/favicons/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + + { + "src": "/static/images/favicons/favicon-16x16.png", + "sizes": "16x16", + "type": "image\/png", + "density": "1" + }, + { + "src": "/static/images/favicons/favicon-32x32.png", + "sizes": "32x32", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "/static/images/favicons/favicon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + + { + "src": "/static/images/favicons/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "1" + }, + { + "src": "/static/images/favicons/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1" + }, + { + "src": "/static/images/favicons/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "/static/images/favicons/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "/static/images/favicons/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "/static/images/favicons/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ], + "start_url": "/", + "background_color": "#000000", + "display_override": ["window-control-overlay", "minimal-ui"], + "display": "standalone", + "scope": "/", + "theme_color": "#000000", + "shortcuts": [ + { + "name": "WorkAdventures", + "short_name": "WA", + "description": "WorkAdventure application", + "url": "/", + "icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192" }] + } + ], + "description": "WorkAdventure application", + "screenshots": [], + "related_applications": [{ + "platform": "web", + "url": "https://workadventu.re" + }, { + "platform": "play", + "url": "https://play.workadventu.re" + }] } \ No newline at end of file diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index da10a8ca..6047d430 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -89,10 +89,7 @@ export class GameManager { console.log('starting '+ (this.currentGameSceneName || this.startRoom.id)) scenePlugin.start(this.currentGameSceneName || this.startRoom.id); scenePlugin.launch(MenuSceneName); - - if (!localUserStore.getHelpCameraSettingsShown()) { - scenePlugin.launch(HelpCameraSettingsSceneName);//700 - } + scenePlugin.launch(HelpCameraSettingsSceneName);//700 } public gameSceneIsCreated(scene: GameScene) { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 71d7868f..c433ed0f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1200,7 +1200,7 @@ ${escapedMessage} * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update(time: number, delta: number) : void { - mediaManager.setLastUpdateScene(); + mediaManager.updateScene(); this.currentTick = time; this.CurrentPlayer.moveUser(delta); diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 603da7b9..917dd44b 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -247,8 +247,7 @@ export class EnableCameraScene extends Phaser.Scene { update(time: number, delta: number): void { this.soundMeterSprite.setVolume(this.soundMeter.getVolume()); - - mediaManager.setLastUpdateScene(); + mediaManager.updateScene(); const middleX = this.getMiddleX(); this.tweens.add({ diff --git a/front/src/Phaser/Menu/HelpCameraSettingsScene.ts b/front/src/Phaser/Menu/HelpCameraSettingsScene.ts index cc6f40c6..f7dd5c2a 100644 --- a/front/src/Phaser/Menu/HelpCameraSettingsScene.ts +++ b/front/src/Phaser/Menu/HelpCameraSettingsScene.ts @@ -21,7 +21,6 @@ export class HelpCameraSettingsScene extends Phaser.Scene { } create(){ - localUserStore.setHelpCameraSettingsShown(); this.createHelpCameraSettings(); } @@ -31,6 +30,9 @@ export class HelpCameraSettingsScene extends Phaser.Scene { this.revealMenusAfterInit(this.helpCameraSettingsElement, helpCameraSettings); this.helpCameraSettingsElement.addListener('click'); this.helpCameraSettingsElement.on('click', (event:MouseEvent) => { + if((event?.target as HTMLInputElement).id === 'mailto') { + return; + } event.preventDefault(); if((event?.target as HTMLInputElement).id === 'helpCameraSettingsFormRefresh') { window.location.reload(); @@ -39,18 +41,27 @@ export class HelpCameraSettingsScene extends Phaser.Scene { } }); - if(!mediaManager.constraintsMedia.audio || !mediaManager.constraintsMedia.video){ + if(!localUserStore.getHelpCameraSettingsShown() && (!mediaManager.constraintsMedia.audio || !mediaManager.constraintsMedia.video)){ this.openHelpCameraSettingsOpened(); + localUserStore.setHelpCameraSettingsShown(); } + + mediaManager.setHelpCameraSettingsCallBack(() => { + this.openHelpCameraSettingsOpened(); + }); } private openHelpCameraSettingsOpened(): void{ HtmlUtils.getElementByIdOrFail('webRtcSetup').style.display = 'none'; this.helpCameraSettingsOpened = true; - if(window.navigator.userAgent.includes('Firefox')){ - HtmlUtils.getElementByIdOrFail('browserHelpSetting').innerHTML =''; - }else if(window.navigator.userAgent.includes('Chrome')){ - HtmlUtils.getElementByIdOrFail('browserHelpSetting').innerHTML =''; + try{ + if(window.navigator.userAgent.includes('Firefox')){ + HtmlUtils.getElementByIdOrFail('browserHelpSetting').innerHTML =''; + }else if(window.navigator.userAgent.includes('Chrome')){ + HtmlUtils.getElementByIdOrFail('browserHelpSetting').innerHTML =''; + } + }catch(err) { + console.error('openHelpCameraSettingsOpened => getElementByIdOrFail => error', err); } const middleY = this.getMiddleY(); const middleX = this.getMiddleX(); @@ -66,13 +77,13 @@ export class HelpCameraSettingsScene extends Phaser.Scene { private closeHelpCameraSettingsOpened(): void{ const middleX = this.getMiddleX(); - const helpCameraSettingsInfo = this.helpCameraSettingsElement.getChildByID('helpCameraSettings') as HTMLParagraphElement; + /*const helpCameraSettingsInfo = this.helpCameraSettingsElement.getChildByID('helpCameraSettings') as HTMLParagraphElement; helpCameraSettingsInfo.innerText = ''; - helpCameraSettingsInfo.style.display = 'none'; + helpCameraSettingsInfo.style.display = 'none';*/ this.helpCameraSettingsOpened = false; this.tweens.add({ targets: this.helpCameraSettingsElement, - y: -400, + y: -1000, x: middleX, duration: 1000, ease: 'Power3', @@ -89,15 +100,17 @@ export class HelpCameraSettingsScene extends Phaser.Scene { } update(time: number, delta: number): void { - const middleX = this.getMiddleX(); - const middleY = this.getMiddleY(); - this.tweens.add({ - targets: this.helpCameraSettingsElement, - x: middleX, - y: middleY, - duration: 1000, - ease: 'Power3' - }); + if(this.helpCameraSettingsOpened){ + const middleX = this.getMiddleX(); + const middleY = this.getMiddleY(); + this.tweens.add({ + targets: this.helpCameraSettingsElement, + x: middleX, + y: middleY, + duration: 1000, + ease: 'Power3' + }); + } } public onResize(ev: UIEvent): void { diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index a0b805d4..eed12333 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -346,7 +346,7 @@ 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){ @@ -354,10 +354,45 @@ class LayoutManager { this.actionButtonInformation.delete(id); } const previousEventCallback = this.actionButtonTrigger.get(id); - if(previousEventCallback){ + if(previousEventCallback && userInputManager){ userInputManager.removeSpaceEventListner(previousEventCallback); } } + + public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager){ + //delete previous element + 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'); + p.innerText = text; + + 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('main-container'); + mainContainer.appendChild(div); + //add trigger action + if(callBack){ + div.onpointerdown = () => { + callBack(); + this.removeActionButton(id, userInputManager); + }; + } + + //remove it after 10 sec + setTimeout(() => { + this.removeActionButton(id, userInputManager); + }, 10000) + } } const layoutManager = new LayoutManager(); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 09386970..d9a91940 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -4,6 +4,8 @@ import {discussionManager, SendMessageCallback} from "./DiscussionManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {localUserStore} from "../Connexion/LocalUserStore"; import {UserSimplePeerInterface} from "./SimplePeer"; +import {SoundMeter} from "../Phaser/Components/SoundMeter"; + declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any let videoConstraint: boolean|MediaTrackConstraints = { @@ -26,6 +28,7 @@ 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; // TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only) export class MediaManager { @@ -40,6 +43,7 @@ export class MediaManager { microphoneClose: HTMLImageElement; microphone: HTMLImageElement; webrtcInAudio: HTMLAudioElement; + mySoundMeterElement: HTMLDivElement; private webrtcOutAudio: HTMLAudioElement; constraintsMedia : MediaStreamConstraints = { audio: audioConstraint, @@ -49,6 +53,8 @@ export class MediaManager { startScreenSharingCallBacks : Set = new Set(); stopScreenSharingCallBacks : Set = new Set(); showReportModalCallBacks : Set = new Set(); + helpCameraSettingsCallBacks : Set = new Set(); + private microphoneBtn: HTMLDivElement; private cinemaBtn: HTMLDivElement; private monitorBtn: HTMLDivElement; @@ -63,6 +69,12 @@ export class MediaManager { private triggerCloseJistiFrame : Map = new Map(); + private userInputManager?: UserInputManager; + + private mySoundMeter?: SoundMeter|null; + private soundMeters: Map = new Map(); + private soundMeterElements: Map = new Map(); + constructor() { this.myCamVideo = HtmlUtils.getElementByIdOrFail('myCamVideo'); @@ -121,10 +133,16 @@ export class MediaManager { this.pingCameraStatus(); this.checkActiveUser(); //todo: desactivated in case of bug + + this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter')); + this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => { + this.mySoundMeterElement.children.item(index)?.classList.remove('active'); + }); } - public setLastUpdateScene(){ + public updateScene(){ this.lastUpdateScene = new Date(); + this.updateSoudMeter(); } public blurCamera() { @@ -225,6 +243,10 @@ export class MediaManager { }).catch((err) => { console.error(err); this.disableCameraStyle(); + + layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => { + this.showHelpCameraSettingsCallBack(); + }, this.userInputManager); }); } @@ -253,6 +275,10 @@ export class MediaManager { }).catch((err) => { console.error(err); this.disableMicrophoneStyle(); + + layoutManager.addInformation('warning', 'Microphone access denied. Click here and check navigators permissions.', () => { + this.showHelpCameraSettingsCallBack(); + }, this.userInputManager); }); } @@ -324,6 +350,10 @@ export class MediaManager { this.monitorClose.style.display = "block"; this.monitor.style.display = "none"; this.monitorBtn.classList.remove("enabled"); + + layoutManager.addInformation('warning', 'Screen sharing access denied. Click here and check navigators permissions.', () => { + this.showHelpCameraSettingsCallBack(); + }, this.userInputManager); }); } @@ -402,13 +432,14 @@ export class MediaManager { } } - return this.getLocalStream().catch(() => { - console.info('Error get camera, trying with video option at null'); + return this.getLocalStream().catch((err) => { + console.info('Error get camera, trying with video option at null =>', err); this.disableCameraStyle(); return this.getLocalStream().then((stream : MediaStream) => { this.hasCamera = false; return stream; }).catch((err) => { + this.disableMicrophoneStyle(); console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err); throw err; }); @@ -425,6 +456,13 @@ export class MediaManager { return navigator.mediaDevices.getUserMedia(this.constraintsMedia).then((stream : MediaStream) => { this.localStream = stream; this.myCamVideo.srcObject = this.localStream; + + //init sound meter + this.mySoundMeter = null; + if(this.constraintsMedia.audio){ + this.mySoundMeter = new SoundMeter(); + this.mySoundMeter.connectToSource(stream, new AudioContext()); + } return stream; }).catch((err: Error) => { throw err; @@ -451,6 +489,7 @@ export class MediaManager { track.stop(); } } + this.mySoundMeter?.stop(); } setCamera(id: string): Promise { @@ -496,6 +535,13 @@ export class MediaManager { +
+ + + + + +
`; @@ -585,6 +631,12 @@ export class MediaManager { throw `Unable to find video for ${userId}`; } remoteVideo.srcObject = stream; + + //sound metter + const soundMeter = new SoundMeter(); + soundMeter.connectToSource(stream, new AudioContext()); + this.soundMeters.set(userId, soundMeter); + this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail('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 @@ -600,6 +652,10 @@ export class MediaManager { layoutManager.remove(userId); this.remoteVideo.delete(userId); + this.soundMeters.get(userId)?.stop(); + this.soundMeters.delete(userId); + this.soundMeterElements.delete(userId); + //permit to remove user in discussion part this.removeParticipant(userId); } @@ -717,6 +773,7 @@ export class MediaManager { } public setUserInputManager(userInputManager : UserInputManager){ + this.userInputManager = userInputManager; discussionManager.setUserInputManager(userInputManager); } //check if user is active @@ -739,6 +796,57 @@ export class MediaManager { public setShowReportModalCallBacks(callback: ShowReportCallBack){ this.showReportModalCallBacks.add(callback); } + + public setHelpCameraSettingsCallBack(callback: HelpCameraSettingsCallBack){ + this.helpCameraSettingsCallBacks.add(callback); + } + + private showHelpCameraSettingsCallBack(){ + for(const callBack of this.helpCameraSettingsCallBacks){ + callBack(); + } + } + + 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'); + }); + } } export const mediaManager = new MediaManager(); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 73a87d14..7690c27d 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -82,15 +82,11 @@ export class SimplePeer { }); mediaManager.showGameOverlay(); - mediaManager.getCamera().then(() => { - + mediaManager.getCamera().finally(() => { //receive message start this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => { this.receiveWebrtcStart(message); }); - - }).catch((err) => { - console.error("err", err); }); this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => { diff --git a/maps/Village/sol_intérieur.png b/maps/Village/sol_intérieur.png new file mode 100644 index 0000000000000000000000000000000000000000..926a7dc23374b8cfe1e0d2dc6831357a4e1d6eb8 GIT binary patch literal 20672 zcmeIac|278-#RU0&t;WXQK1Fo*w8Na1H~3qaT2v zj3&Sz5AX*99m)oS4gp6nqbwWpcP%(H`_Mn498Fk({H@jqD*SKf5!sctYfR*E88>CgmiJ>u%FU?DcE70{FJ1ld6hZgf$)8vDwaBua-=_amIU_^w9oz9@SE8VHwsa+zBtOkg z|C|b%&Z>SnZRJZkKISbIqH}2{T6tY-?738ssc!?fN`LUTi&;D_n_mMFi>@^rc3-Om zH_-UGXF6TpO2b)usYUmFKZnp*teoR8n`r8FPJCjLP?QP_m zsSv-Krax@4u_fAC6}P8Blh_cVb5HCQjO|Zsr;k0e3byc_&{|u*B?MOt9o#k1Uph{< z@**v+mN2c}CG4+KhaEVI{i9daX^v7HyORjKeHrZ7N`FNJW$r1gW+&J;fio(3DC^NG^#B0P4<(qpDnVn5F9)71ThXr}cACV`)lwacY*6Kv#WC3`8n&G*#c zW_gy-`q4Qq;cbOv+@pbL0q$9sk)arrCq>48geL139S53KkyDl{cGAd}9T)I*MiA*~ zW^Se_T}oq{MOFFxZGTRg3xUY^+h+i<`pXzKU(bRb%GRTIn|t!!1!CxN`^Sa*`RqC9T!+<0=3CAvlLa zQgVt4m7OE(6nUtrwJm*DP^gy&Z6)T^K2h;P7b;F3q0MuGb!inC3tkt)7|@J4`BugU z){vVR!l`)z+PzzDSCjhSxNp7e@SQL1bX)S=>CcW*=Rjh;pC?@`B2WChM<1zz0ar9! z6>Rp)V|ka|nHW7L*;e~DERD=KoomBr2PM_S_Qax`t_J5g$59^OT{yHKt5Ds{ptokx zbi;U%zf(aj#_?YaR4ioyVG&M&rLN3h7^W$(s-w_b=n5%<6FtFx1&7h z?o|Hkv?jC!M7%56+Ps0#VJpm-YWH7 z|2WoIS}@W=Tcb5#U30#@&-Qc`Vy}*}Gpj5Ju3fqKdq1M7dM(JBT+utcD-|;Jbj-1%+UatPgsGIh7%e_t&h!S~2SJF$hQ-dH^HBLLx-?HC zI}H%`9LHM`%pT$#L~vnVJ8RU|`J9{fp28DEzm!Z~%|V$tP_}k+S@zv+rT~$W+~9M` z+nD{t&-3p?R-V&>cUIhXtL19^>3vIhS5xE$H__x6j zNdqgyI;EMN8k{#@&gFfnP}zgoC`nx1a{Q5zuH0%<*~28CQ{l?H{}c^&nL(m$^&iXo z?wYO_oD3V>ouFY=CTb@&ttP1Lt*fh&_*tFKO2J#cG9^p>JW|9##jXIM{(X8m;MNn@ zY1S8vpr(MKTH{dX$^94|sHy8&Z*@Xd^Y{$nrQsU6(p5q}b#$_f@AKCap7tFwK8n7i ztmf!51m)nMRonD_lRK>*-!48!ynGwS%;Q9J>0l*RC@>;dfLK9YDnKYn0--eM*ET}3 z5{49P7(P~MwK@+04%DHz`rClCA{^Oc=008hTdA#bvu+L@v_^BED?c6LjB`yrGJ?m~ z#j@8m&Q`*-znnMR;wFfBq+Glee2qJiCsWgCZ1Z$~mrFhKhC1t?MTISo*X`X;Cu)=P8jEJLD zv|(+fV(WneO;@N`f{3I40n7QAJD2-}c56@{x44oyEZ1C3DXAfT7hO0pbi?29H-Tub zEdQe>`rxmi-jZFrPX;Dt@;+8nlhS#80jJGAHm9YQ)%S=%Fo^%}tC5gLDe}}NKu;ks z|9;F3sObz4mOz981tYUPB{&W15kNU&9K&*eRn9`h3S&2JW`ItGdN+T$jRZVE1i<82cGh9peg?$`jmCtYq zS6M_Kn>!7$u~D^X7x-3~GgKkPU;mw5$<(p*W|6TfZq$|1Xq9(ewDd&)Z}nLbj$S7( zi4Atl-l4Gq`Br@Q-TAzo-J*aFk*|hnu%NB+{ijw;N~bMdc3T=9M1R9+U)}NUebH<8 zY`T2GOliB>)zdX5bZ?;2e=&aS$0!`Uo#(dc?9Xf0djWW)MsZ_BIRxQH$Brs94eNY?${NC{rV8pDq4x@$PN6lWXL zg<-ee!81vd?lesV46TgWb6GJ_6FLhmLxdf^_`O=GwAh?~-EK7j?Kq=`_z+AwPY=38 z9!VF3)sjxR?(fq}9Xn~-ojSKC$YN*XL;CvsLN{HT*X%@T^n_tIf3ec{L5QYjr zrUH9i6d`aPVw-e#wg?eE#dI&#HUXv`BpY?SI%3FK-OupB{HCsarepfZ)>eKG*x{(C z50e|d2Yb~=*$TnaNzDo+{E!QV>7gxOHpY%MwC2jK1cfZ*UZkYD`-ukxD4z>EaYT=N z`Aywg&16Gyyt98ttC&SLZDOuxs7duqXhC8hxsg603D_iFkl0x6M+Uc9C`Xhw(2L7=0a~O&yRX4?3jSks+ zJjdSfvl8@!6Wm5Ov)X()$F#lWWW85bR3Rf|&=`25mUd+LRz~?3G`|@U?LH`{;_duN zr1|dNz{bsEd@KS;o6l7yS6y_CN``84(Dht3(po0ZA@T|F&rb3lY7OUZiSjPll|FMP zskx;s-EZp6UIy$^s}>Nx(J6ij6sKw1Sv?8^CvZin<+E-hT_TV)iVJ;Dr%gel{t12s zGog_k^?cbsam;P%O-V4}hSfuLOwwlkbX@4LjK;s8<^5lR7q1aTfxvVidDzGvI7;<=@Z04^3QT%_O+mK_Zz zrueBDt$)^iR@_tAuMB1vbW73`T3`R1Y+z3Ku?wC>M{|blBM3o(PJc5sXV|)KndwY> zW~m5KR9~pr{P;&-LJ^Y~fS|h2GP(7JWc0qT1u*S%n1c(d-`mrfXNQvh8eh!r4x=|# zDRfN3X^9JNWGE6;t`|Iy9`6eigcz#GJ+2&3(YU%*l9!qsK)j`Ycj`np8Aq6&*FnwY z`q7m@NIe3;+~Ijx=%XhpR5bOpbSfa>@>9@;ELV5m1S-kR!T4#Qw%O(Pry*s$Pd?yW-kB^h70CUjzbhG~F;3B>})*0W4=6dP|ujFt%QIo6oo-7q|+1d~lT) zvVjAxYTJ}FC!3xxKHw(2n>YlEh)&zSV!UXv^m5|RDT39oq7fd2;h_Baw+2Df(1W^h87qNYyT*o!9SdNXtE2h*2Uu`>sVn+?K|MX|N;#8+0d2QV5q6XmMK=jbn0a{YNHom38D8QY;Bn7{P-OvN% zx(&@ebC5-SDhGMoO`kqFlKZ<#B^SK&qU_iCL5K~tk~lPLh!WFGBID}(AZLAWSfZPH z(5*PW&1h~Zt#Nb8|B!;8z?v?H4=Xq?x`rgxebY)1t{B^CIbopP_I9H}GJ&`#D)?PZ zjq63{b0P2P7J}V)Gg_@QDl&n`ZK-j%M!hY4CV@fiaAGb%=4CBF*zF}Ba6OPQpaHH2 zQUybyVi$nxQ(-ZIrvAAimcGOSNH3?i?9B!{C!h$nqK(39-enhwcXNDusS(!#de(PL33QDoc;2 zEKJ1J-;oL$B<`Hpnvw{v?BPp48CF>R@O9^jp`7u^_{SmVTCZuvt1YwsIgqNqc&GQ& z0?s3n>bpj+%{t#Kn`f+Qn}hA(@Hu1oj;0hU-QMfr*jx)x*dr2mg)TJ-wXz0|^p8&S zj($dHj4HTY-7Cd0OajV+38mVU*O&l|Z@;cv#cIQT+>qQ4r?Qhc(J-+Pb`*B>8+Y}W zPlAy6xvDu)-@o`2YOPP4=R<2`(<7wIezS>wLQOHZB^*1mFVoJ{bWCLb_PdL|!CR}M zx1M=2s2Vr_^UU{Fi+RcGgI{jCFGR8TeAO#W#&4i8b=5x~Y#@Sk4hOG(5N$Q*)z$Z) ztYLQEt#(o8AF`LC%}Y4hWD67D9P*%iW=GV{_pDa2>>&5=3|UsY8uRKR6;`P)CaTEO z@z|uml;GLN(Ss|Fw#=tn54+X;P~v0p7IEtNB$qZiyW1>Ji!fXNl!*vq`LWg)I4TNt zWagV5R2XRS7?;zM2vZ;JxWlVUmt)e1ULWL?57Vdlf;H_+OcaCdMPa+g>-DWwGW0VkMHW`#qV(h9gW1Q1OFk-)*+I@N zXCy%v!-E|x6v$T9b@$Vn2D`zEY3q)jyJbC~&|012z*6+qGw}`jw4p*NR|)syKW8!A z20$%OER~=TKzxd==@+h=6|*Yor>Xe<@WNt_1u<4#e3^{3E;(XXpVa*2=v1hI+XZ0= z4Js`>9NqiuDZPBD^S+Mr3SY4;bCFy$djA+`IQ_<>P?q}p1_B{MjQ-puX&;%rh zK?M86Cfe#p(DGXOi|w`yK#ZA{Sxk5{PbyfEOa6T1A+_4}B>3fR>>AkSAmp zU*2teFLIH0X;$TE2>VM=m?2#u6s;l$j$_JbY^N9AH16U22(0kVsln+x&qjHT1t-y< zI}iPB1+8(ej)!wZxVBaX^i~FL>WWj*nl#(kUj4BFWu>wv4;tyC1lPd4v#zsb(NLm# z{b6f9SDf6tsCqLSa1G;kVS+PD@dg-BYB1azEJmrlx9Asb*nNR)i4v6L48bBAsW zXI8awLEK^w|785W!RY#>b-nHRo@PR=3d`?kUeXL{$7_X?nAl;WfL;711& z4gdEOja`C$1l@?3@lStqAWhl-KpK9)G5hcNkJ~v4uBVxVIx~LvDkJUN6ZSG(Vny6o z!YZ5k@IY88|E)5;_)rYnWCq~9&f4V+VO#rtEuOjO$#0Cs>yH0gYG5}hh@q2VbIYQ1 z0Wb#ghd_Ts&oHAgXz0K1GQtKdZKPq_`on=rLl5Snnl*m7e~qEoCzZdX%le>%VAx9T z`Os2DG~oKAVx^B=47;%y{IXu!y7-Hm4C~Fh{E}|oxhI+--IuxTsuiJ!iz-aT7Jlra zfecy04Sq>1a#i(J91h753IQfz?U?8 zci@OFV|=yLz=i+#Kk`{mucTe{2mg2jca&Xn`>Qwn4;)@#{h=Ikg66T&X{9t+vNkFC&A zklv7apQj=Pb7p1ooqghIdIP_ox>h~MFb{-#xV1hnP;AbKDu++}kZQJF6)`br)KL&H z*5KxJe$J?Nb+WSjnaA_RN^kS5A&!SoBbYcAbX`Mudx#7x6H;L6!=$x3d?o>0E9doVuL#?LR&6Gu6-lsAhLQr3$H`zymE zaXnYXUq4-!&Zv1N{7_ja#p2NL;L`>JWaajK=`y;D(r?FBFlOi@>iZKwaWAblQ+N{Q z%pH~wdh5g;wo<@x>{FQ}MBRWWmlP^2eFP|X2skURS?1#2yJg@S=P-hXy%gxs<8aw$ zR_#~+B=)CEd_-=vhoJ^7Qu+tF+m!(xk` z)6$x>TS*z0Q*CO;0oi?$QBOh3v~??7KM1;fj)(vBxhFqlIc;zr4h->$#2IZG_SRzfOZoH5A5mR9t;X_?G(XoE1V$?GYyOCJ$03JS)Ha)ecyhhy6*deo@MXwrc+AmI zeEEzOSjq8{e+p2>cLH(~y#6T|JEHRT<}%aLUybVD-Vm5&=sQ&Sp#cEDd;@dD2yccx ztgk>Nhe<&gN&I33Rcy>WRa4$jMrf#^icQ>+ihX{tR(3+YJ}&h7w1G0Ef=k5Rq0=%+ zGFDwd-7NmeuFLYl9FdT{p=#09q#Jo&= zfI>s6gLrzM)=OfKY@IId#DSHVcl$vYR#*P8&uWuD8=_T4y_yOzl%A08WVvlR3w;iA zsTsczSm+uj>X#22^)aJS%($=X4$be^?R?2l8>m|dq&W_#4BV|!rteKq-ivyCGUe{j z9h$3gwpH z9$%}yK5Q#5bll7}}ce{9H+OGUKsj`~a)lVi*Zn<@h zHc(0#b&^~wOMbR_ao)(A;1+A_yvTEPchde@i2FF&EQ4-8WK=gYK@GgnSsSDbKVZg0D`UgNPB0B&XV1l#N1w z`=X$@nrBv#J^%5l3AiHVn$@|B0lTnOa(ek+y4-cM+#} zlS30~BkY5L0U^_nl)WW)dY-2BS#o^jmYX9(PcP?(@CPMMSypsYP3taHM189=7>Tsq zGdc!UCNYD+zm2H;I&lY|3OsyuwT7XD^0xOF!#ETxc}WjJO}_`t=+VzB`O(hc;nBHm^H_aYw^|%?xpkY#+VsXOsm5$6Jfz4#49R zEvVxlu-MDNK<`OrYw@st`miW03j`EVlY_%iD02|1ZzWH|`W%s|#+2g^V2=#gSov$8 zq6DgsDew^wt0d`COdz6l->N`mLO(Z{5>c)l*FSSh9jqlqC*B8 z+TzCI{Rx5x`xIpeCo^113rW{}=>(^L3t>>~G+O>i`SsJNMKN5h#{~q_*`Q$Nzrvp zW13>fm*ZM6W}KcJG#hatGtpSV8RzlSP;q)s zja4C#0)KG59S-SiI6v{K#$G_tcSiw0+M)gH_7YW_FVCzkz3-}%)};!dPC3mi*Cn#! z`|hs&{JUZg{ohq6Z_uE~OTnJdB#yRV6DM(PbxT}yppgCQo&Mn+ zvN|mez4Bg?6bLFI$sjfa$(`)vdD0#3eWg$n=Ps=vZ5mOX_K7;M25z#kRh|xJMV;OM z0XqeSgE@ScjMQmcQLvbgW|cfdoRkKtX{>6l&Fa$&EMf%hbaivncHb@0J|x7MUa+xZ z?6>?lFhjVw6?Q|A0Xo|MfDYryXXnZ){2_RlpESNHfXvpM7#I|SsJB@KJAIWeFOR8* z6OAWc^dq){X?eB?HQJ{-L;sDD2mC>@6|&>{O9NZFBZGd51?t^Ax-~2)R>yHtjXEm+ zkzstj0npJf}T`?KWtYo2Di%Jf&WMRl#NKB|7sNokj>k)u!So%1c=0_p1Rk|2qn z{>u%*_2*v#o1d)Y_imP7GZu9KWcemvS-z8b=w(^loIUh99sy0 zV;4wIYepNA6SaV7W6Nr!K!HHJ4!{a_ChOop!!gjsm|hV7kxq3|_GeVgwRj8E8r0UyF|`ujDUJI*rsB*JEM69%HQ&e#}U`$RiHnjGb=OC0GJ zI=0umHwA$_7cn^A4;GZZ4ZXE=_{uk#S#}L|P1F)350LPw(_7f-G~KiZk5cFW%_Mm) zABS!LG!u*1hpJM5lId1uRbXxqCy=9n!UTojk@@iwv*;&+La346iWHSkk77VB>WYz+ zll|8x@8Qemj37lTQqB{=MIE7(1tnoKPP;8AeE>5kfnP9_%mF-4FH&CKAqyWV^}*UU z;4VzeyUonu62A!I7Jm)QS~J$but>i)xA2HURO~~bCSbH zN&iSDC(*Wyt4Hwb#5tLBt%#7_hQt}x&38m1f)Xg|QfRVlNlq=?|16cUTgJWF8pJL^ zsI%cH(o-l@8MpG^^XH~j($`gR6@i@?TG3ddyv5L1z|2r#Fd>#~TZbio2i`l`CKyZE zKAW*vqdrZ%de8El?Z>xl6r1fv`u=!vV(Yf51O1+IP;5Rj)7R+ZL;YSZ1y&p8Oz9C$xNHPw)3?cL z&&qy_Uu%-0X{~hlzRs&{FG1eB)z~ji+JYml+9LObopvskq@B3%<;WdXxl$h$u4BsY z`ejpM93yI6sHHX=R*R-$*Z#~7m_BLiIXwIZ5j0>hgc5oOJU8Ed|MIwMZo-`e4ORab zb(_HBaLQ%5MSH+fCK~|0YDZ@4`O*kT z77FnuF4!ea!zk+uU4s1fQ3j#X|BmI8sQgt*xhj{?QMtvlW}FT71tJJ?dtPw5@68i} z4*i*UIc+dLXyAcWy2#^a_L~@8<#&QOl8fat%qk9p`aQOjy@t}Tx?zu*FLjoA`q}1X z!1b9}kdtki4mZ8o=DBy{oO(P4ZIvb`^R$yudB{Gtp4y7G-c9PihF#|Z>Myd7PEcnM z`we!w@h|we`Uebo&`=ZJ4>ziIrMPr6fn2go@ZtS-{=Qg_nnk3Wwg&XL@NXA*5+c}SCn&=*@L)~z#@zSKcd*o#UExOXi%gNEur2VMyeryAG zTUrnNCbViJH&HZJ9B3k3v!-~) z_gg2T`iKnHaJ&83eWa9do-nP5gD@L6tUG_ zOU$CMKaf}w1S&5@HQiy;YP3%;r3n>`!XEFLTrX8=dC$Ecj9np`-Kd^gXQYF_e36k3DhC}bAA z5uJn3eZ%K71C&-sYOXsPzWd-;NKp0++twkS`abf9UFV60_ocV(HxkODmeFhv43BZq zPBfg)0ZML<*qc}QPubpgvt7PNt`mOv5N&cXXChUsw+cLPIgYor3hhQ3_ZtZ7%h7eC`ZPXgqIJ!0(-w+Dr zxxzF#W5UjTG8+n6E>t-kSkA(%a#}$5x+5MCh)V~BB${1_8{3Y)kMHuPoOw(tk81ir zB+;W&?i|XC;IBZGPdZ$rzFy+c@8{6fFLWDrJ>;v+zg%2$$bkQk#em2heQ||Pb*hT6 zyB4TL>*z%9qAM|$L&B%MZQ9$nSx1^Zs(Uytv%>OKV883Yrs1HHYTB@Q8G*oeZsh8@ z){1SWhL7iO3&*JMmsE$WUby2XpROK~-db^SKM3CS1m5)O_kZbtlY1Ecwxo=`li1nn zJ;@X0AM<>U(EF&WbAl)=dRcYAgeS_5JU4UYVPd`FV3w15`pl>bwL`wZ9q8qSNSk|} zyi_j>Su+VNWOZqyE}GR5RA5I=xuW?YwXz^syHzeqs9s$WliF18@VOqX zc%2RD{CUeD!RxmDNW9Ts|q~qcj-% zY_~4RKf34>hMd=WF73;n%|7va;svV2QY7;(vaagPuDa9lTcTFZR?5{kjWDflVCLiN zV9a_NHXvWmKDZ>9ynO8i%{g;T5rSh56V_Jpi9U;bJEYuH6fBo@;`S;g&U+)en$p|n z7z~W#A3nDR>Hl^I(rm(OxUccn7? z8H{ds=TTuu#aOB929^*Zzr8!VHfMI}PbF^_?mw7?Ywx`&rEO?vDj8WkFbrJkS5_u{ zlXOfw-&_`YV&4wEM!ol7g73Fmq@VA*w$coq$w}1}CA+>jM zlk9o8>DkKDXvHA8%EB=Zf`cFzZ+bE3qeJlcP{U$FA}5D7t8KG!Lcfr3sx3p7IJu(c?01u2OY#hxx`QuL0DRlO%r?jC~fN7 zSbn6nAvAR(a_a$pIT{U#KY^s#MUm07;Kxld*elZ#u{W7?#3Yng@E)3cLWg6lT&wRA zV>vL|r{M`6k=|kmY8bEG-6<)sT~*8#-Nmw}C+kQrqgH;Z)5d>q$6mi@e|TU|V$fPV zXhpWa&P5ARCOXk5l+{vR^ZNOzXcN<0#)0Rd2OT0v6;c4{=pTnHp&`+t{MGyHnl=$_ z+{{P;kGq^lNBVw`NEqsF)?Z##Il-(u8yDr*W~+17IdJsM>62=luAln2b^B2GTwdHh)+e8I1K} zYFg`FB}5}ZO@p2Go$nPE?{ubkIV*a!Kf&B(f82{|0;`%J_Dj>uk!Q)D7lcOix1DFL z2GmdAjy-{dE!kQPax!@i5TkQ!NAj`~Deuq)wY`Hj=W4c2fXMwvoSGrn{ONi=@ZBNY zpru3oiIk4X-z+Z@gwB+GF1qlCF6u_G<4Tl4`~s)c#a1+L{z;ag(zrvO(5AtCfQ3vs zw3We;p^?7ychL_WIx}&(;U?_a`f1g@toNkvTaS=PpQ|ylp$`WWliM{7RbP6*X*yPeY0+yyeZmg@pk37xY{iyH?tzXb2El_-G#heVh-Og@>y1=OkL`G znV=5T6^GZ~X~F5w*$2Q%ie0|Xx2wwhekk4`reS38{0+OHbLaJQ3Rz1+m%k+i$4s%+t*yoROK7#NzY$=bb#-M$KrXweKo1=n5z`BS^U)K-D_T-{J)$4Oj zvtxzo)8sghjGsL^QkabPLk$uzVOLm!9FO^w3VBmm4yv!dXfFJ|0fw;`57pK6h;HSH z4Y!B;?w$Zwls%Si z%-=7QK3}15uuK`8Oq)w4BL#U|Lrieh!AksL@$0j$0*I1x?=9U}yyYdb%F^6e54&xO z^XO)mf0r+8%iB%V^W6`)0^yDqRfgO)s?93)>pO}U)^^b(#T?q6I^O86AIV`JDeAkq zo0MLtsp_`n@YPbz`E`X*pbIJSyvI4b8?et`?3Zph`#MoR^yB+TaT{nVMF0Mw4LI6t zE>@p4HmJke|iksVnPYaJ++usu{Ev2ieA~9R8 zo_f>qMlwX${1X@6l_hM}k!2N_^Vo)Cv|`8-*ZuJ8h&-q+f(P!PGYemk%H64IH<_27CsGe$y_hD6 zyl0JL*SO#lpq=yN<#V=n<*qjuW?xeiNoj9awvx3Ud4mtDo+DJIYd8PM?*8$Fo%prx zL60xE-37-k-u_lU2U7C3>&z|Yc1v%$=N&1@Bzb~V9Sf|{D6=BvV;`ea7Th_yV0=pcPSqm_>aAiyY=Eqb7}HK z+)?Gdhl~ej@##9cdon2GNyTT@v6}YC1zdq!+=>)V7-m@r{y+*$HWZk09(U0`pw ze1SY)%F}06m1J8-kG4X`zm2-Rd^=o(>#@H!={of8V=tgzx5=?T!dP<>u}-S`9c3Rv z$dL+v>X{MRp7^dDx_Hl(+pPdIX2Q(1QWiBUi4WrPBoqo6(Vy-NGyQIT=)ugqX-fEo zm3%AZd-}ZMF8AA=g6q~8w>ff_hgfkz$ixBu2e8zWvBhk+Sg5>qisjbxf4sP!?VfPs zw!LQKljKDkAwm52Mlpo8u(y_@+Vg6o(~VcOCAQultDDs0eWvb#1714+-l|4Rbx9x% zQN!F~&T9zl>6G2D$%4Qk;wv_G-^A8zSOtO_(xv}t{Uo_G@vHLoF0jRhV-4GO%D%Gk zutOAjsIge12nTjaIVDr}CZSyD!&X!d`tzxU$q$U;u>v)lPk!IN)3};bwwRM2A5zxc z$`?faD!V=Cbxog7olo<}l>DP}$?M5;v5)E3^xIze2Z(&&{bR;3aKG7gHwrUsHhc|BsDet;tJ*#Tz_7kI0A{g#KJfMaTYZ|0=v^d%K`b z+=3+An}R|HD9#xQ1$aO;@O5u}EbdK}6@@)&0(~73!*YIoDttO%kKM`wDz&KY#BBbD zBMmDR`dYH)Oh=eh7b5L-jthGuXNC01x|%E3`G=a~V9JF|3K19lZt|PzPBz{S*E!}r zC3}-Cs7%Doq-70wSeXdh0^GBY`_$;zj<)ShQ7nPD%|05eMp61x{-^8g{t$~=32%-R zHpa>f#p8q}^jR8S6+*_SAouK!(O(2r($?FDZolX08&dY&k?Bi8ySx~gUp+p}9&1S+ zwjtkUdgY{YZL;wasXy7e=(0YrWI-V#C(&dDJ|4hLq;=m(mK0+8>3Ne0Q*a256 zqPRGe6LILz(Al~92cYmL$^TMWJrkhI>7jap_+jIc^+=!oteejDkjFf{E4Lpl*Mr|* zSy5jcEV?2$h2{+^mE*#6X^rtP#WU>-JtPf*TNN88rgehzm6XrJT2~st=POBq-b*-^ zyFlxu%934Mo&*V?5GcxF7 z_eQ**G_!e^3L#zKk4bq$b>GExI$dHd?abVE?%2yO-q%DCfOl=!*-EVw(K5?s%MQ4J z>hE&9b06Fewd23C@!fCWnJC?m8Rt?YL`AxeJOmM=Sctj&m)pW+mETxT*k~zM)f40f z?I(^rQ5j4k0lT&Rk@YX+gNBl?y1&%-o9N!|av5445zKryLniL)e_L1`Yo`&P6tO1<6 zqxOhZ$pC^#tsiq6{MP1fboGZG|9k8zzRO3#SRAXPD4M&3(=fuLm}r83kk!?w&R$mH ze5`6@t_zI(KC^YF+3@4BF(fAIl*Ug2@J#hrqjwweBrhJx{%S@Ge%4m-YKH*cl>Jm3;^RHRAQ-<&IO;c+shgzT zZ|Qd!4J$W=Qci>p+(S9QmBT?(xasx4nj)}+IMM{h6W)AT=Ps;PR{0rzHo6xX1O+$l z>|TXTK6jfd_XOTp0l}26f?x<0##}poR)?&`O<3;DMEtd!<$;Y^R*6V~J;5LfJVe_; zV9g1X@L0WA2^`wXSV^N)W@_s`PiPA)lJ%AP?wgGW5?u9`j zlaZ+1yvqjz@~|98c~Sosm#RY|dQOVW0fiw{L)Vp*lR>+4X8tSY2c}?W2mNiO@gdMD zmO-`Q-M`nM=zY~+o#j_1-{g=r6c%V7zLBg_OFdB6EGQ)3@kp?Y7r&5)Gi7s&=y;=?*Sjh02^p7~4)jxLR zIM#8Xz^LE~9k4e4CBRm~FnGv0HpYOVEx)8pc$%sSH=Qvg>xKZf(t;y{l6!8eHVTuBzZ>`)Dm6}ms9?G4f_1(@qhd?I`Rh~L;WcQ=m=nD{zhQ^ zls+AQhHE6ChT&Htt$0*mjfK;Itqw#6yua><0gv$h?TO={UMg(v`J4~Pvr3VVUhjqCHy2X?T*a|QiHtsWH!0eI(DV1kV z+i^q=?%t+7Vb}^V*~H8yEYzy**-&>B3efx3ItU3k8Q=mFGr_E(CDsy=iR^!`!LaqI zd>K68XEZ}{!B-3!q_43xkQfp7%hzr(G|M9lmy2YZdfvhwUi{wgZ;>|`&Xn5Hru=OO z8XEDpYm0}geg`H(*@D&V&5?kdaAiBFZK2;@vl>IU(69?!4cW^K1)2=orvu@o3fSsj z!s|fF9w5A+uMWbCVXJ?K7XzIB?}Qiqi%bUy1Q%fZF950kBGi;?kAUQ?Pkw-(iPYwW zJ-fiwn6td$EA{GMQ1UOVu@+zcpm!9tn9UJ+f&q`w8?X-et=mb#@$)r~Nh(cAeCpzn zM-GBfEV|XoDEzD$18^8l%xZZ%fU?N^D)Q-2PXxnHXN&h!%5Fs(^RmC{pLo^mIXe8R z6}|sY&@o7X2F@NX77XEimJ52tJLJLrFEA8s9S8vlvWrmGPverJ4GhB#7xA)L6x;ox zL?A%L0jCwU>Hwx>!-$6fG0G0Ws@Uj(3rlk`JU%o+pF<$>y5h1y!`nnHgP-=RfXfb& z(0ppy=T1fmyB-?*yn8Gy6^r1 zWVkq#wZSGMM?8F(2isvqRF46i=IFhEAOY}tXC;yF3I3BA6e%D8icCf@6!?pL@}r73 zRR4kkpG807BQ7;|DkB?EIeqB>dD{esvp}ey0AvA>qu+4=CeX%543ynfh5MXjn2v$6 z2S6R#cj`NCFAMbW;eTQ|2XWNT1QrI|G~yDhyH29!l_-oaWVjWjCOIau^1h6>~MV{H@;M zyqR`hQ<&10`tGhEC8hT&gDHg%T=eh7iyeSJ2aK1&iZe{A!*{L%@-kR);Xu9DZSP|j z10nSdf9vWm*aUF@)kR4-Ko2nNg>V_;t5lo6bC>@uD*uzc4q()tVK3lIX24#50dL^{ zC+x*YHTIpKLQ<|c{NiuQZN3Ah1_+eFe7}i`#UsmFaKz_8zITw<2w=596kPkOVTao&O3t)YUc_iD&{}_zDn~fB9Mt!!`deBge0_s{12~kwph}z*@{Vw&J7y zN^bwk07CzN99|YIph$}=CI^n_bFyb+^&MSiUS-QfLEcGF19n_3O4?;)xUSWTE5fdr z{g+wssckdHXqZSwO^5(=5CZ267*^We6_kO3RU);2t1j>(+Rm?Hy2#Nc50^{fRof5# zxfl9xXm1F>2sMY46Ol)!`X%jfNjPi?wX&rrm^&0yYBp91M)#M??@JW#SdTGJo)YcNE&~g(o$6 z9DOU)lAh%TTVCWa$Lvmo{OBSd!C{kyAD)HfXMuclKx&T0)U~~H?(-ksZ{p|{Gm}(B z;NvNM_}dT`2T`qsOyG;0P~fYc8%eNRwU8Mes`!Dd|^TmI--oe2G@jJL^J+-ppK@#MxmN@`2PZd CNsr9{ literal 0 HcmV?d00001