diff --git a/src/components/audio-params.js b/src/components/audio-params.js new file mode 100644 index 0000000000..204b56676e --- /dev/null +++ b/src/components/audio-params.js @@ -0,0 +1,238 @@ +import { Vector3 } from "three"; +import { AudioNormalizer } from "../utils/audio-normalizer"; + +export const SourceType = Object.freeze({ MEDIA_VIDEO: 0, AVATAR_AUDIO_SOURCE: 1, AVATAR_RIG: 2 }); + +const MUTE_DELAY_SECS = 1; + +AFRAME.registerComponent("audio-params", { + dependencies: ["media-views", "avatar-audio-source"], + schema: { + enabled: { default: true }, + isLocal: { default: false }, + position: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, + orientation: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, + distanceModel: { default: "inverse", oneOf: ["linear", "inverse", "exponential"] }, + rolloffFactor: { default: 3 }, + refDistance: { default: 1 }, + maxDistance: { default: 20 }, + clippingThreshold: { default: 0.015 }, + prevGain: { default: 1.0 }, + isClipped: { default: false }, + gain: { default: 1.0 }, + sourceType: { default: -1 }, + attenuation: { default: 1.0 }, + distance: { default: 0.0 }, + squaredDistance: { default: 0.0 } + }, + + init() { + this.data.position = new Vector3(0.0, 0.0, 0.0); + this.data.orientation = new Vector3(0.0, 0.0, 0.0); + this.normalizer = null; + this.el.sceneEl?.systems["audio-debug"].registerSource(this); + if (!this.data.isLocal) { + this.el.sceneEl?.systems["audio-gain"].registerSource(this); + } + + if (this.data.isLocal) { + this.data.sourceType = SourceType.AVATAR_RIG; + } else if (this.el.components["media-video"]) { + this.data.sourceType = SourceType.MEDIA_VIDEO; + this.data.gain = this.el.components["media-video"].data.volume; + this.el.addEventListener("media-volume-changed", this.sourceVolumeChanged.bind(this)); + } else if (this.el.components["avatar-audio-source"]) { + this.data.sourceType = SourceType.AVATAR_AUDIO_SOURCE; + this.data.gain = + this.el.parentEl.parentEl.querySelector("[avatar-volume-controls]").components["avatar-volume-controls"]?.data + .volume || 1.0; + this.el.parentEl.parentEl.addEventListener("avatar-volume-changed", this.sourceVolumeChanged.bind(this)); + } + }, + + remove() { + this.normalizer = null; + this.el.sceneEl?.systems["audio-debug"].unregisterSource(this); + if (!this.data.isLocal) { + this.el.sceneEl?.systems["audio-gain"].unregisterSource(this); + } + + if (this.el.components["media-video"]) { + this.el.removeEventListener("media-volume-changed", this.sourceVolumeChanged); + } else if (this.el.components["avatar-audio-source"]) { + this.el.parentEl.parentEl.removeEventListener("avatar-volume-changed", this.sourceVolumeChanged); + } + }, + + tick() { + const audio = this.audio(); + if (audio) { + if (audio.updateMatrixWorld) { + audio.updateMatrixWorld(true); + } + this.data.position = new THREE.Vector3( + audio.panner.positionX.value, + audio.panner.positionY.value, + audio.panner.positionZ.value + ); + this.data.orientation = new THREE.Vector3( + audio.panner.orientationX.value, + audio.panner.orientationY.value, + audio.panner.orientationZ.value + ); + this.data.distanceModel = audio.panner.distanceModel; + this.data.rolloffFactor = audio.panner.rolloffFactor; + this.data.refDistance = audio.panner.refDistance; + this.data.coneInnerAngle = audio.panner.coneInnerAngle; + this.data.coneOuterAngle = audio.panner.coneOuterAngle; + this.updateDistances(); + this.updateAttenuation(); + } + + if (this.normalizer !== null) { + this.normalizer.apply(); + } else { + // We one only enable the Normalizer for avatar-audio-source components + if (this.data.sourceType === SourceType.AVATAR_AUDIO_SOURCE) { + this.enableNormalizer(); + } + } + }, + + audio() { + if (this.data.sourceType === SourceType.AVATAR_RIG) { + // Create fake parametes for the avatar rig as it doens't have an audio source. + const audioParams = this.el.sceneEl.systems["hubs-systems"].audioSettingsSystem.audioSettings; + const avatarRigObj = document.getElementById("avatar-rig").querySelector(".camera").object3D; + avatarRigObj.updateMatrixWorld(true); + const position = new THREE.Vector3(); + avatarRigObj.getWorldPosition(position); + const direction = new THREE.Vector3(0, 0, -1); + const worldQuat = new THREE.Quaternion(); + avatarRigObj.getWorldQuaternion(worldQuat); + direction.applyQuaternion(worldQuat); + return { + panner: { + orientationX: { + value: direction.x + }, + orientationY: { + value: direction.y + }, + orientationZ: { + value: direction.z + }, + positionX: { + value: position.x + }, + positionY: { + value: position.y + }, + positionZ: { + value: position.z + }, + distanceModel: audioParams.avatarDistanceModel, + maxDistance: audioParams.avatarMaxDistance, + refDistance: audioParams.avatarRefDistance, + rolloffFactor: audioParams.avatarRolloffFactor, + coneInnerAngle: audioParams.avatarConeInnerAngle, + coneOuterAngle: audioParams.avatarConeOuterAngle + } + }; + } else if (this.data.sourceType === SourceType.MEDIA_VIDEO) { + const audio = this.el.getObject3D("sound"); + return audio instanceof THREE.PositionalAudio ? audio : null; + } else if (this.data.sourceType === SourceType.AVATAR_AUDIO_SOURCE) { + const audio = this.el.getObject3D("avatar-audio-source"); + return audio instanceof THREE.PositionalAudio ? audio : null; + } + }, + + enableNormalizer() { + const audio = this.audio(); + if (audio) { + const avatarAudioSource = this.el.components["avatar-audio-source"]; + if (avatarAudioSource) { + this.normalizer = new AudioNormalizer(audio); + avatarAudioSource.el.addEventListener("sound-source-set", () => { + const audio = avatarAudioSource && avatarAudioSource.el.getObject3D(avatarAudioSource.attrName); + if (audio) { + this.normalizer = new AudioNormalizer(audio); + this.normalizer.apply(); + } + }); + } + } + }, + + updateDistances() { + const listenerPos = new THREE.Vector3(); + this.el.sceneEl.audioListener.getWorldPosition(listenerPos); + this.data.distance = this.data.position.distanceTo(listenerPos); + this.data.squaredDistance = this.data.position.distanceToSquared(listenerPos); + }, + + updateAttenuation() { + if (this.data.distanceModel === "linear") { + this.data.attenuation = + 1.0 - + this.data.rolloffFactor * + ((this.data.distance - this.data.refDistance) / (this.data.maxDistance - this.data.refDistance)); + } else if (this.data.distanceModel === "inverse") { + this.data.attenuation = + this.data.refDistance / + (this.data.refDistance + + this.data.rolloffFactor * (Math.max(this.data.distance, this.data.refDistance) - this.data.refDistance)); + } else if (this.data.distanceModel === "exponential") { + this.data.attenuation = Math.pow( + Math.max(this.data.distance, this.data.refDistance) / this.data.refDistance, + -this.data.rolloffFactor + ); + } + }, + + clipGain(gain) { + const audio = this.audio(); + this.data.isClipped = true; + this.data.prevGain = this.data.gain; + this.data.gain = gain; + this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain; + audio.gain.gain.exponentialRampToValueAtTime(this.data.gain, audio.context.currentTime + MUTE_DELAY_SECS); + }, + + unclipGain() { + const audio = this.audio(); + this.data.isClipped = false; + this.data.gain = this.data.prevGain; + this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain; + audio?.gain?.gain.exponentialRampToValueAtTime(this.data.gain, audio.context.currentTime + MUTE_DELAY_SECS); + }, + + updateGain(newGain) { + const audio = this.audio(); + if (this.data.isClipped) { + this.data.prevGain = newGain; + } else { + this.data.prevGain = this.data.gain; + this.data.gain = newGain; + } + this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain; + audio?.gain?.gain.exponentialRampToValueAtTime(this.data.gain, audio.context.currentTime + MUTE_DELAY_SECS); + }, + + sourceVolumeChanged({ detail: volume }) { + let globalVolume = 100; + const { audioOutputMode, globalVoiceVolume, globalMediaVolume } = window.APP.store.state.preferences; + if (this.data.sourceType === SourceType.MEDIA_VIDEO) { + globalVolume = globalMediaVolume; + } else if (this.data.sourceType === SourceType.AVATAR_AUDIO_SOURCE) { + globalVolume = globalVoiceVolume; + } + const volumeModifier = (globalVolume !== undefined ? globalVolume : 100) / 100; + let newGain = volumeModifier * volume; + if (audioOutputMode === "audio") { + newGain = this.data.gain * Math.min(1, 10 / Math.max(1, this.data.squaredDistance)); + } + this.updateGain(newGain); + } +}); diff --git a/src/components/avatar-audio-source.js b/src/components/avatar-audio-source.js index 86cc294b02..0b35792bb8 100644 --- a/src/components/avatar-audio-source.js +++ b/src/components/avatar-audio-source.js @@ -40,14 +40,6 @@ async function getMediaStream(el) { return stream; } -function setPositionalAudioProperties(audio, settings) { - audio.setDistanceModel(settings.distanceModel); - audio.setMaxDistance(settings.maxDistance); - audio.setRefDistance(settings.refDistance); - audio.setRolloffFactor(settings.rolloffFactor); - audio.setDirectionalCone(settings.innerAngle, settings.outerAngle, settings.outerGain); -} - AFRAME.registerComponent("avatar-audio-source", { schema: { positional: { default: true }, @@ -74,7 +66,7 @@ AFRAME.registerComponent("avatar-audio-source", { const audioListener = this.el.sceneEl.audioListener; const audio = this.data.positional ? new THREE.PositionalAudio(audioListener) : new THREE.Audio(audioListener); if (this.data.positional) { - setPositionalAudioProperties(audio, this.data); + this.setPositionalAudioProperties(audio, this.data); } if (SHOULD_CREATE_SILENT_AUDIO_ELS) { @@ -102,7 +94,7 @@ AFRAME.registerComponent("avatar-audio-source", { this.el.sceneEl.systems["hubs-systems"].audioSettingsSystem.registerAvatarAudioSource(this); // We subscribe to audio stream notifications for this peer to update the audio source // This could happen in case there is an ICE failure that requires a transport recreation. - NAF.connection.adapter.on("stream_updated", this._onStreamUpdated, this); + NAF.connection.adapter?.on("stream_updated", this._onStreamUpdated, this); this.createAudio(); }, @@ -139,7 +131,7 @@ AFRAME.registerComponent("avatar-audio-source", { this.destroyAudio(); this.createAudio(); } else if (this.data.positional) { - setPositionalAudioProperties(audio, this.data); + this.setPositionalAudioProperties(audio, this.data); } }, @@ -147,6 +139,16 @@ AFRAME.registerComponent("avatar-audio-source", { this.el.sceneEl.systems["hubs-systems"].audioSettingsSystem.unregisterAvatarAudioSource(this); NAF.connection.adapter.off("stream_updated", this._onStreamUpdated); this.destroyAudio(); + }, + + setPositionalAudioProperties(audio, settings) { + audio.setDistanceModel(settings.distanceModel); + audio.setMaxDistance(settings.maxDistance); + audio.setRefDistance(settings.refDistance); + audio.setRolloffFactor(settings.rolloffFactor); + audio.panner.coneInnerAngle = settings.coneInnerAngle; + audio.panner.coneOuterAngle = settings.coneOuterAngle; + audio.panner.coneOuterGain = settings.coneOuterGain; } }); diff --git a/src/components/avatar-volume-controls.js b/src/components/avatar-volume-controls.js index 7170874506..97810556a9 100644 --- a/src/components/avatar-volume-controls.js +++ b/src/components/avatar-volume-controls.js @@ -3,96 +3,6 @@ const MAX_VOLUME = 8; const SMALL_STEP = 1 / (VOLUME_LABELS.length / 2); const BIG_STEP = (MAX_VOLUME - 1) / (VOLUME_LABELS.length / 2); -// Inserts analyser and gain nodes after audio source. -// Analyses audio source volume and adjusts gain value -// to make it in a certain range. -class AudioNormalizer { - constructor(audio) { - this.audio = audio; - this.analyser = audio.context.createAnalyser(); - this.connected = false; - - // To analyse volume, 32 fftsize may be good enough - this.analyser.fftSize = 32; - this.gain = audio.context.createGain(); - this.timeData = new Uint8Array(this.analyser.frequencyBinCount); - this.volumes = []; - this.volumeSum = 0; - } - - apply() { - if (window.APP.store.state.preferences.audioNormalization) { - if (!this.connected) { - this.connect(); - } - } else { - if (this.connected) { - this.disconnect(); - } - return; - } - - // Adjusts volume in "a rule of the thumb" way - // Any better algorithm? - - // Regards the RMS of time-domain data as volume. - // Is this a right approach? - // Using the RMS of frequency-domain data would be another option. - this.analyser.getByteTimeDomainData(this.timeData); - const squareSum = this.timeData.reduce((sum, num) => sum + Math.pow(num - 128, 2), 0); - const volume = Math.sqrt(squareSum / this.analyser.frequencyBinCount); - const baseVolume = window.APP.store.state.preferences.audioNormalization; - - // Regards volume under certain threshold as "not speaking" and skips. - // I'm not sure if 0.4 is an appropriate threshold. - if (volume >= Math.min(0.4, baseVolume)) { - this.volumeSum += volume; - this.volumes.push(volume); - // Sees only recent volume history because there is a chance - // that a speaker changes their master input volume. - // I'm not sure if 600 is an appropriate number. - while (this.volumes.length > 600) { - this.volumeSum -= this.volumes.shift(); - } - // Adjusts volume after getting many enough volume history. - // I'm not sure if 60 is an appropriate number. - if (this.volumes.length >= 60) { - const averageVolume = this.volumeSum / this.volumes.length; - this.gain.gain.setTargetAtTime(baseVolume / averageVolume, this.audio.context.currentTime, 0.01); - } - } - } - - connect() { - // Hacks. THREE.Audio connects audio nodes when source is set. - // If audio is not played yet, THREE.Audio.setFilters() doesn't - // reset connections. Then manually caling .connect()/disconnect() here. - // This might be a bug of Three.js and should be fixed in Three.js side? - if (this.audio.source && !this.audio.isPlaying) { - this.audio.disconnect(); - } - const filters = this.audio.getFilters(); - filters.unshift(this.analyser, this.gain); - this.audio.setFilters(filters); - if (this.audio.source && !this.audio.isPlaying) { - this.audio.connect(); - } - this.connected = true; - } - - disconnect() { - if (this.audio.source && !this.audio.isPlaying) { - this.audio.disconnect(); - } - const filters = [this.analyser, this.gain]; - this.audio.setFilters(this.audio.getFilters().filter(filter => !filters.includes(filter))); - if (this.audio.source && !this.audio.isPlaying) { - this.audio.connect(); - } - this.connected = false; - } -} - AFRAME.registerComponent("avatar-volume-controls", { schema: { volume: { type: "number", default: 1.0 } @@ -118,7 +28,9 @@ AFRAME.registerComponent("avatar-volume-controls", { }, changeVolumeBy(v) { - this.el.setAttribute("avatar-volume-controls", "volume", THREE.Math.clamp(this.data.volume + v, 0, MAX_VOLUME)); + const vol = THREE.Math.clamp(this.data.volume + v, 0, MAX_VOLUME); + this.el.setAttribute("avatar-volume-controls", "volume", vol); + this.el.emit("avatar-volume-changed", vol); this.updateVolumeLabel(); }, @@ -132,43 +44,6 @@ AFRAME.registerComponent("avatar-volume-controls", { this.changeVolumeBy(-1 * step); }, - update: (function() { - const positionA = new THREE.Vector3(); - const positionB = new THREE.Vector3(); - return function update() { - const audio = this.avatarAudioSource && this.avatarAudioSource.el.getObject3D(this.avatarAudioSource.attrName); - if (!audio) { - return; - } - - if (!this.normalizer) { - this.normalizer = new AudioNormalizer(audio); - this.avatarAudioSource.el.addEventListener("sound-source-set", () => { - const audio = - this.avatarAudioSource && this.avatarAudioSource.el.getObject3D(this.avatarAudioSource.attrName); - if (audio) { - this.normalizer = new AudioNormalizer(audio); - this.normalizer.apply(); - } - }); - } - - this.normalizer.apply(); - - const { audioOutputMode, globalVoiceVolume } = window.APP.store.state.preferences; - const volumeModifier = (globalVoiceVolume !== undefined ? globalVoiceVolume : 100) / 100; - let gain = volumeModifier * this.data.volume; - if (audioOutputMode === "audio") { - this.avatarAudioSource.el.object3D.getWorldPosition(positionA); - this.el.sceneEl.audioListener.getWorldPosition(positionB); - const squaredDistance = positionA.distanceToSquared(positionB); - gain = gain * Math.min(1, 10 / Math.max(1, squaredDistance)); - } - - audio.gain.gain.value = gain; - }; - })(), - updateVolumeLabel() { const numBars = Math.min( VOLUME_LABELS.length - 1, @@ -177,19 +52,5 @@ AFRAME.registerComponent("avatar-volume-controls", { : Math.floor(VOLUME_LABELS.length / 2 + (this.data.volume - 1) / BIG_STEP) ); this.volumeLabel.setAttribute("text", "value", this.data.volume === 0 ? "Muted" : VOLUME_LABELS[numBars]); - }, - - tick() { - if (!this.avatarAudioSource && !this.searchFailed) { - // Walk up to Spine and then search down. - const sourceEl = this.el.parentNode.parentNode.querySelector("[avatar-audio-source]"); - if (!sourceEl || !sourceEl.components["avatar-audio-source"]) { - this.searchFailed = true; - return; - } - this.avatarAudioSource = sourceEl.components["avatar-audio-source"]; - } - - this.update(); } }); diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 04b187841d..185e92578a 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -171,6 +171,7 @@ AFRAME.registerComponent("media-loader", { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-pager"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.setAttribute("media-image", { src: "error" }); this.clearLoadingTimeout(); @@ -348,6 +349,7 @@ AFRAME.registerComponent("media-loader", { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-pager"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-image"); } @@ -460,6 +462,7 @@ AFRAME.registerComponent("media-loader", { linkedMediaElementAudioSource }) ); + this.el.setAttribute("audio-params", {}); if (this.el.components["position-at-border__freeze"]) { this.el.setAttribute("position-at-border__freeze", { isFlat: true }); } @@ -469,6 +472,7 @@ AFRAME.registerComponent("media-loader", { } else if (contentType.startsWith("image/")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-pager"); this.el.addEventListener( @@ -511,6 +515,7 @@ AFRAME.registerComponent("media-loader", { } else if (contentType.startsWith("application/pdf")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-image"); this.el.setAttribute( "media-pdf", @@ -544,6 +549,7 @@ AFRAME.registerComponent("media-loader", { ) { this.el.removeAttribute("media-image"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-pager"); this.el.addEventListener( @@ -577,6 +583,7 @@ AFRAME.registerComponent("media-loader", { } else if (contentType.startsWith("text/html")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-pager"); this.el.addEventListener( diff --git a/src/components/media-views.js b/src/components/media-views.js index f43d7a8831..4e6d54ecd0 100644 --- a/src/components/media-views.js +++ b/src/components/media-views.js @@ -289,8 +289,6 @@ AFRAME.registerComponent("media-video", { this.changeVolumeBy = this.changeVolumeBy.bind(this); this.togglePlaying = this.togglePlaying.bind(this); - this.distanceBasedAttenuation = 1; - this.lastUpdate = 0; this.videoMutedAt = 0; this.localSnapCount = 0; @@ -404,7 +402,9 @@ AFRAME.registerComponent("media-video", { }, changeVolumeBy(v) { - this.el.setAttribute("media-video", "volume", THREE.Math.clamp(this.data.volume + v, 0, 1)); + const vol = THREE.Math.clamp(this.data.volume + v, 0, 1); + this.el.setAttribute("media-video", "volume", vol); + this.el.emit("media-volume-changed", vol); this.updateVolumeLabel(); }, @@ -465,6 +465,7 @@ AFRAME.registerComponent("media-video", { if (this._ignorePauseStateChanges) return; this.el.setAttribute("media-video", "videoPaused", this.video.paused); + this.el.setAttribute("audio-params", { enabled: !this.video.paused }); if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) { this.el.emit("owned-video-state-changed"); @@ -482,15 +483,6 @@ AFRAME.registerComponent("media-video", { this.tryUpdateVideoPlaybackState(this.data.videoPaused); } } - - // Volume is local, always update it - if (this.audio && window.APP.store.state.preferences.audioOutputMode !== "audio") { - const globalMediaVolume = - window.APP.store.state.preferences.globalMediaVolume !== undefined - ? window.APP.store.state.preferences.globalMediaVolume - : 100; - this.audio.gain.gain.value = (globalMediaVolume / 100) * this.data.volume; - } }, tryUpdateVideoPlaybackState(pause, currentTime) { @@ -554,7 +546,6 @@ AFRAME.registerComponent("media-video", { if (!disablePositionalAudio && this.data.audioType === "pannernode") { this.audio = new THREE.PositionalAudio(this.el.sceneEl.audioListener); this.setPositionalAudioProperties(); - this.distanceBasedAttenuation = 1; } else { this.audio = new THREE.Audio(this.el.sceneEl.audioListener); } @@ -862,6 +853,8 @@ AFRAME.registerComponent("media-video", { videoEl.onerror = failLoad; if (this.data.audioSrc) { + videoEl.muted = true; + // If there's an audio src, create an audio element to play it that we keep in sync // with the video while this component is active. audioEl = createVideoOrAudioEl("audio"); @@ -932,8 +925,6 @@ AFRAME.registerComponent("media-video", { }, tick: (() => { - const positionA = new THREE.Vector3(); - const positionB = new THREE.Vector3(); return function() { if (!this.video) return; @@ -977,20 +968,6 @@ AFRAME.registerComponent("media-video", { this.lastUpdate = now; } } - - if (this.audio) { - if (window.APP.store.state.preferences.audioOutputMode === "audio") { - this.el.object3D.getWorldPosition(positionA); - this.el.sceneEl.audioListener.getWorldPosition(positionB); - const distance = positionA.distanceTo(positionB); - this.distanceBasedAttenuation = Math.min(1, 10 / Math.max(1, distance * distance)); - const globalMediaVolume = - window.APP.store.state.preferences.globalMediaVolume !== undefined - ? window.APP.store.state.preferences.globalMediaVolume - : 100; - this.audio.gain.gain.value = (globalMediaVolume / 100) * this.data.volume * this.distanceBasedAttenuation; - } - } }; })(), diff --git a/src/components/player-info.js b/src/components/player-info.js index 374c046e5a..79c1afce7d 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -183,6 +183,8 @@ AFRAME.registerComponent("player-info", { el.setAttribute("emit-scene-event-on-remove", "event:action_end_video_sharing"); } } + + this.el.querySelector("[audio-params]")?.setAttribute("audio-params", { enabled: !this.data.muted }); }, handleModelError() { window.APP.store.resetToRandomDefaultAvatar(); diff --git a/src/hub.html b/src/hub.html index ce292d573b..0734b85a95 100644 --- a/src/hub.html +++ b/src/hub.html @@ -220,7 +220,8 @@