Migrating EnableCameraScene to Svelte

This commit is contained in:
David Négrier 2021-06-01 16:17:36 +02:00
parent c7b3e3cd44
commit bf7083effc
10 changed files with 274 additions and 326 deletions

View File

@ -1,12 +1,22 @@
<script lang="typescript">
import {Game} from "../../Phaser/Game/Game";
import {EnableCameraScene} from "../../Phaser/Login/EnableCameraScene";
import {localStreamStore} from "../../Stores/MediaStore";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {
audioConstraintStore,
cameraListStore,
localStreamStore,
microphoneListStore,
videoConstraintStore
} from "../../Stores/MediaStore";
import {onDestroy} from "svelte";
import HorizontalSoundMeterWidget from "./HorizontalSoundMeterWidget.svelte";
import cinemaCloseImg from "../images/cinema-close.svg";
export let game: Game;
let selectedCamera : string|null = null;
let selectedMicrophone : string|null = null;
const enableCameraScene = game.scene.scenes.find((scene) => scene instanceof EnableCameraScene);
const enableCameraScene = game.scene.getScene(EnableCameraSceneName);
function submit() {
enableCameraScene.login();
@ -23,28 +33,84 @@
}
}
let stream : MediaStream|null;
let stream: MediaStream | null;
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
stream = value.stream;
if (stream !== null) {
const videoTracks = stream.getVideoTracks();
if (videoTracks.length > 0) {
selectedCamera = videoTracks[0].getSettings().deviceId;
}
const audioTracks = stream.getAudioTracks();
if (audioTracks.length > 0) {
selectedMicrophone = audioTracks[0].getSettings().deviceId;
}
}
} else {
stream = null;
selectedCamera = null;
selectedMicrophone = null;
}
});
onDestroy(unsubscribe);
function normalizeDeviceName(label: string): string {
// remove text in parenthesis
return label.replace(/\([^()]*\)/g, '').trim();
}
function selectCamera() {
videoConstraintStore.setDeviceId(selectedCamera);
}
function selectMicrophone() {
audioConstraintStore.setDeviceId(selectedMicrophone);
}
</script>
<form class="enableCameraScene" on:submit|preventDefault={submit}>
<section class="title text-center">
<section class="text-center">
<h2>Turn on your camera and microphone</h2>
</section>
<div id="webRtcSetup" class="webrtcsetup">
<img class="background-img" src="resources/logos/cinema-close.svg" alt="" class:hide={$localStreamStore.stream}>
<video id="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted></video>
<img class="background-img" src={cinemaCloseImg} alt="" class:hide={$localStreamStore.stream}>
<video id="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted class:hide={!$localStreamStore.stream}></video>
</div>
<HorizontalSoundMeterWidget stream={stream}></HorizontalSoundMeterWidget>
<section class="selectWebcamForm">
{#if $cameraListStore.length > 1 }
<div class="nes-select">
<select bind:value={selectedCamera} on:change={selectCamera}>
{#each $cameraListStore as camera}
<option value={camera.deviceId}>
{normalizeDeviceName(camera.label)}
</option>
{/each}
</select>
</div>
{/if}
{#if $microphoneListStore.length > 1 }
<div class="nes-select">
<select bind:value={selectedMicrophone} on:change={selectMicrophone}>
{#each $microphoneListStore as microphone}
<option value={microphone.deviceId}>
{normalizeDeviceName(microphone.label)}
</option>
{/each}
</select>
</div>
{/if}
</section>
<!--<section class="text-center">
<video id="myCamVideoSetup" autoplay muted></video>
@ -73,19 +139,37 @@
/*max-height: 48vh;
width: 42vw;
max-width: 300px;*/
overflow: hidden;
section.title {
/*section.title {
position: absolute;
top: 1vh;
width: 100%;
}*/
section.selectWebcamForm {
margin-top: 3vh;
margin-bottom: 3vh;
min-height: 10vh;
width: 50%;
margin-left: auto;
margin-right: auto;
select {
font-family: "Press Start 2P";
margin-top: 1vh;
margin-bottom: 1vh;
option {
font-family: "Press Start 2P";
}
}
}
section.action{
text-align: center;
margin: 0;
position: absolute;
top: 85vh;
/*position: absolute;
top: 85vh;*/
width: 100%;
}
@ -103,29 +187,31 @@
}
.webrtcsetup{
position: absolute;
/*position: absolute;
top: 140px;
left: 0;
right: 0;
right: 0;*/
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
height: 50%;
width: 50%;
height: 28.125vw;
width: 50vw;
border: white 6px solid;
display: flex;
align-items: center;
justify-content: center;
img.background-img {
position: relative;
display: block;
/*position: relative;*/
/*display: block;*/
width: 40%;
height: 60%;
margin-left: auto;
margin-right: auto;
top: 50%;
transform: translateY(-50%);
/*height: 60%;*/
/*top: 50%;*/
/*transform: translateY(-50%);*/
}
img.hide {
.hide {
display: none;
}
}

View File

@ -0,0 +1,78 @@
<script lang="typescript">
import {SoundMeter} from "../../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte";
export let stream: MediaStream | null;
let volume = 0;
const NB_BARS = 20;
let timeout;
const soundMeter = new SoundMeter();
$: {
if (stream && stream.getAudioTracks().length > 0) {
soundMeter.connectToSource(stream, new AudioContext());
if (timeout) {
clearInterval(timeout);
}
timeout = setInterval(() => {
try{
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
//console.log(volume);
}catch(err){
}
}, 100);
}
}
onDestroy(() => {
soundMeter.stop();
if (timeout) {
clearInterval(timeout);
}
})
function color(i: number, volume: number) {
console.log(i, volume)
const red = 255 * i / NB_BARS;
const green = 255 * (1 - i / NB_BARS);
let alpha = 1;
if (i >= volume) {
alpha = 0.5;
}
return 'background-color:rgba('+red+', '+green+', 0, '+alpha+')';
}
</script>
<div class="horizontal-sound-meter" class:active={stream?.getAudioTracks().length > 0}>
{#each [...Array(NB_BARS).keys()] as i}
<div style={color(i, volume)}></div>
{/each}
</div>
<style lang="scss">
.horizontal-sound-meter {
display: flex;
flex-direction: row;
width: 50%;
height: 30px;
margin-left: auto;
margin-right: auto;
margin-top: 1vh;
}
.horizontal-sound-meter div {
margin-left: 5px;
flex-grow: 1;
}
</style>

View File

@ -19,7 +19,7 @@
<form class="helpCameraSettings nes-container" on:submit|preventDefault={close}>
<section>
<h2>Camera/Microphone access needed</h2>
<h2>Camera / Microphone access needed</h2>
<p class="err">Permission denied</p>
<p>You must allow camera and microphone access in your browser.</p>
<p>

View File

@ -2,28 +2,40 @@
import {SoundMeter} from "../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte";
export let stream: MediaStream;
export let stream: MediaStream|null;
let volume = 0;
console.log('stream', stream);
const NB_BARS = 5;
if (stream.getAudioTracks().length > 0) {
const soundMeter = new SoundMeter();
soundMeter.connectToSource(stream, new AudioContext());
let timeout;
const soundMeter = new SoundMeter();
const timeout = setInterval(() => {
try{
volume = parseInt((soundMeter.getVolume() / 10).toFixed(0));
console.log(volume);
}catch(err){
$: {
if (stream && stream.getAudioTracks().length > 0) {
soundMeter.connectToSource(stream, new AudioContext());
if (timeout) {
clearInterval(timeout);
}
}, 100);
onDestroy(() => {
clearInterval(timeout);
})
timeout = setInterval(() => {
try{
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
//console.log(volume);
}catch(err){
}
}, 100);
}
}
onDestroy(() => {
soundMeter.stop();
if (timeout) {
clearInterval(timeout);
}
})
</script>

View File

@ -27,6 +27,10 @@ export class SoundMeter {
public connectToSource(stream: MediaStream, context: AudioContext): void
{
if (this.source !== undefined) {
this.stop();
}
this.init(context);
this.source = this.context?.createMediaStreamSource(stream);

View File

@ -1,44 +0,0 @@
import Container = Phaser.GameObjects.Container;
import type {Scene} from "phaser";
import GameObject = Phaser.GameObjects.GameObject;
import Rectangle = Phaser.GameObjects.Rectangle;
export class SoundMeterSprite extends Container {
private rectangles: Rectangle[] = new Array<Rectangle>();
private static readonly NB_BARS = 20;
constructor(scene: Scene, x?: number, y?: number, children?: GameObject[]) {
super(scene, x, y, children);
for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
const rectangle = new Rectangle(scene, i * 13, 0, 10, 20, (Math.round(255 - i * 255 / SoundMeterSprite.NB_BARS) << 8) + (Math.round(i * 255 / SoundMeterSprite.NB_BARS) << 16));
this.add(rectangle);
this.rectangles.push(rectangle);
}
}
/**
* A number between 0 and 100
*
* @param volume
*/
public setVolume(volume: number): void {
const normalizedVolume = volume / 100 * SoundMeterSprite.NB_BARS;
for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
if (normalizedVolume < i) {
this.rectangles[i].alpha = 0.5;
} else {
this.rectangles[i].alpha = 1;
}
}
}
public getWidth(): number {
return SoundMeterSprite.NB_BARS * 13;
}
}

View File

@ -3,7 +3,6 @@ import {TextField} from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import {mediaManager} from "../../WebRtc/MediaManager";
import {SoundMeter} from "../Components/SoundMeter";
import {SoundMeterSprite} from "../Components/SoundMeterSprite";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
@ -11,280 +10,41 @@ import Zone = Phaser.GameObjects.Zone;
import { MenuScene } from "../Menu/MenuScene";
import {ResizableScene} from "./ResizableScene";
import {
audioConstraintStore,
localStreamStore,
enableCameraSceneVisibilityStore,
mediaStreamConstraintsStore,
videoConstraintStore
} from "../../Stores/MediaStore";
import type {Unsubscriber} from "svelte/store";
export const EnableCameraSceneName = "EnableCameraScene";
enum LoginTextures {
playButton = "play_button",
icon = "icon",
mainFont = "main_font",
arrowRight = "arrow_right",
arrowUp = "arrow_up"
}
export class EnableCameraScene extends ResizableScene {
private textField!: TextField;
private cameraNameField!: TextField;
private arrowLeft!: Image;
private arrowRight!: Image;
private arrowDown!: Image;
private arrowUp!: Image;
private microphonesList: MediaDeviceInfo[] = new Array<MediaDeviceInfo>();
private camerasList: MediaDeviceInfo[] = new Array<MediaDeviceInfo>();
private cameraSelected: number = 0;
private microphoneSelected: number = 0;
private soundMeter: SoundMeter;
private soundMeterSprite!: SoundMeterSprite;
private microphoneNameField!: TextField;
private mobileTapZone!: Zone;
private localStreamStoreUnsubscriber!: Unsubscriber;
constructor() {
super({
key: EnableCameraSceneName
});
this.soundMeter = new SoundMeter();
}
preload() {
this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
this.load.image(LoginTextures.arrowRight, "resources/objects/arrow_right.png");
this.load.image(LoginTextures.arrowUp, "resources/objects/arrow_up.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
}
create() {
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
//this.scale.setZoom(ZOOM_LEVEL);
//Phaser.Display.Align.In.BottomCenter(this.pressReturnField, zone);
/* FIX ME */
this.textField = new TextField(this, this.scale.width / 2, 20, '');
// For mobile purposes - we need a big enough touchable area.
this.mobileTapZone = this.add.zone(this.scale.width / 2,this.scale.height - 30,200,50)
.setInteractive().on("pointerdown", () => {
this.login();
});
this.cameraNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 60, '');
this.microphoneNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 40, '');
this.arrowRight = new Image(this, 0, 0, LoginTextures.arrowRight);
this.arrowRight.setVisible(false);
this.arrowRight.setInteractive().on('pointerdown', this.nextCam.bind(this));
this.add.existing(this.arrowRight);
this.arrowLeft = new Image(this, 0, 0, LoginTextures.arrowRight);
this.arrowLeft.setVisible(false);
this.arrowLeft.flipX = true;
this.arrowLeft.setInteractive().on('pointerdown', this.previousCam.bind(this));
this.add.existing(this.arrowLeft);
this.arrowUp = new Image(this, 0, 0, LoginTextures.arrowUp);
this.arrowUp.setVisible(false);
this.arrowUp.setInteractive().on('pointerdown', this.previousMic.bind(this));
this.add.existing(this.arrowUp);
this.arrowDown = new Image(this, 0, 0, LoginTextures.arrowUp);
this.arrowDown.setVisible(false);
this.arrowDown.flipY = true;
this.arrowDown.setInteractive().on('pointerdown', this.nextMic.bind(this));
this.add.existing(this.arrowDown);
this.input.keyboard.on('keyup-ENTER', () => {
this.login();
});
this.localStreamStoreUnsubscriber = localStreamStore.subscribe((result) => {
if (result.type === 'error') {
// TODO: we could handle the error in a specific way on the EnableCameraScene page.
// TODO: we could handle the error in a specific way on the EnableCameraScene page.
return;
//throw result.error;
}
this.getDevices();
if (result.stream !== null) {
this.setupStream(result.stream);
}
});
/*const mediaPromise = mediaManager.getCamera();
mediaPromise.then(this.getDevices.bind(this));
mediaPromise.then(this.setupStream.bind(this));*/
this.input.keyboard.on('keydown-RIGHT', this.nextCam.bind(this));
this.input.keyboard.on('keydown-LEFT', this.previousCam.bind(this));
this.input.keyboard.on('keydown-DOWN', this.nextMic.bind(this));
this.input.keyboard.on('keydown-UP', this.previousMic.bind(this));
this.soundMeterSprite = new SoundMeterSprite(this, 50, 50);
this.soundMeterSprite.setVisible(false);
this.add.existing(this.soundMeterSprite);
enableCameraSceneVisibilityStore.showEnableCameraScene();
setTimeout(() => {
this.onResize();
}, 100);
}
private previousCam(): void {
if (this.cameraSelected === 0 || this.camerasList.length === 0) {
return;
}
this.cameraSelected--;
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
//mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
}
private nextCam(): void {
if (this.cameraSelected === this.camerasList.length - 1 || this.camerasList.length === 0) {
return;
}
this.cameraSelected++;
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
// TODO: the change of camera should be OBSERVED (reactive)
//mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
}
private previousMic(): void {
if (this.microphoneSelected === 0 || this.microphonesList.length === 0) {
return;
}
this.microphoneSelected--;
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
//mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
}
private nextMic(): void {
if (this.microphoneSelected === this.microphonesList.length - 1 || this.microphonesList.length === 0) {
return;
}
this.microphoneSelected++;
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
// TODO: the change of camera should be OBSERVED (reactive)
//mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
}
/**
* Function called each time a camera is changed
*/
private setupStream(stream: MediaStream): void {
this.soundMeter.connectToSource(stream, new window.AudioContext());
this.soundMeterSprite.setVisible(true);
this.updateWebCamName();
}
private updateWebCamName(): void {
if (this.camerasList.length > 1) {
let label = this.camerasList[this.cameraSelected].label;
// remove text in parenthesis
label = label.replace(/\([^()]*\)/g, '').trim();
// remove accents
label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
this.cameraNameField.text = label;
this.arrowRight.setVisible(this.cameraSelected < this.camerasList.length - 1);
this.arrowLeft.setVisible(this.cameraSelected > 0);
}
if (this.microphonesList.length > 1) {
let label = this.microphonesList[this.microphoneSelected].label;
// remove text in parenthesis
label = label.replace(/\([^()]*\)/g, '').trim();
// remove accents
label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
this.microphoneNameField.text = label;
this.arrowDown.setVisible(this.microphoneSelected < this.microphonesList.length - 1);
this.arrowUp.setVisible(this.microphoneSelected > 0);
}
}
public onResize(): void {
let div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
let bounds = div.getBoundingClientRect();
if (!div.srcObject) {
div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('webRtcSetup');
bounds = div.getBoundingClientRect();
}
this.textField.x = this.game.renderer.width / 2;
this.mobileTapZone.x = this.game.renderer.width / 2;
this.cameraNameField.x = this.game.renderer.width / 2;
this.microphoneNameField.x = this.game.renderer.width / 2;
this.cameraNameField.y = bounds.top / this.scale.zoom - 8;
this.soundMeterSprite.x = this.game.renderer.width / 2 - this.soundMeterSprite.getWidth() / 2;
this.soundMeterSprite.y = bounds.bottom / this.scale.zoom + 16;
this.microphoneNameField.y = this.soundMeterSprite.y + 22;
this.arrowRight.x = bounds.right / this.scale.zoom + 16;
this.arrowRight.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
this.arrowLeft.x = bounds.left / this.scale.zoom - 16;
this.arrowLeft.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
this.arrowDown.x = this.microphoneNameField.x + this.microphoneNameField.width / 2 + 16;
this.arrowDown.y = this.microphoneNameField.y;
this.arrowUp.x = this.microphoneNameField.x - this.microphoneNameField.width / 2 - 16;
this.arrowUp.y = this.microphoneNameField.y;
/*const actionBtn = document.querySelector<HTMLDivElement>('#enableCameraScene .action');
if (actionBtn !== null) {
actionBtn.style.top = (this.scale.height - 65) + 'px';
}*/
}
update(time: number, delta: number): void {
this.soundMeterSprite.setVolume(this.soundMeter.getVolume());
}
public login(): void {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.soundMeter.stop();
enableCameraSceneVisibilityStore.hideEnableCameraScene();
this.localStreamStoreUnsubscriber();
//mediaManager.stopCamera();
//mediaManager.stopMicrophone();
this.scene.sleep(EnableCameraSceneName);
gameManager.goToStartingMap(this.scene);
}
private async getDevices() {
// TODO: switch this in a store.
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
this.microphonesList = [];
this.camerasList = [];
for (const mediaDeviceInfo of mediaDeviceInfos) {
if (mediaDeviceInfo.kind === 'audioinput') {
this.microphonesList.push(mediaDeviceInfo);
} else if (mediaDeviceInfo.kind === 'videoinput') {
this.camerasList.push(mediaDeviceInfo);
}
}
this.updateWebCamName();
}
}

View File

@ -508,3 +508,47 @@ export const obtainedMediaConstraintStore = derived(localStreamStore, ($localStr
return $localStreamStore.constraints;
});
/**
* Device list
*/
export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(set) {
let deviceListCanBeQueried = false;
const queryDeviceList = () => {
// Note: so far, we are ignoring any failures.
navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => {
set(mediaDeviceInfos);
}).catch((e) => {
console.error(e);
throw e;
});
};
const unsubscribe = localStreamStore.subscribe((streamResult) => {
if (streamResult.type === "success" && streamResult.stream !== null) {
if (deviceListCanBeQueried === false) {
queryDeviceList();
deviceListCanBeQueried = true;
}
}
});
if (navigator.mediaDevices) {
navigator.mediaDevices.addEventListener('devicechange', queryDeviceList);
}
return function stop() {
unsubscribe();
if (navigator.mediaDevices) {
navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList);
}
};
});
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'videoinput');
});
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'audioinput');
});

View File

@ -126,7 +126,7 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({constraints});
} else if (navigator.mediaDevices.getDisplayMedia) {
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints});
} else {
stopScreenSharing();
@ -183,7 +183,7 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
* A store containing whether the screen sharing button should be displayed or hidden.
*/
export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) => {
if (!navigator.getDisplayMedia && !navigator.mediaDevices.getDisplayMedia) {
if (!navigator.getDisplayMedia && (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia)) {
set(false);
return;
}

View File

@ -89,7 +89,15 @@ module.exports = {
preprocess: sveltePreprocess({
scss: true,
sass: true,
})
}),
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 }
// process as usual
handleWarning(warning);
}
}
}
},