Newer
Older
import React, { Component } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import copy from "copy-to-clipboard";
import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect";
import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl";
import en from "react-intl/locale-data/en";
import MovingAverage from "moving-average";
import styles from "../assets/stylesheets/ui-root.scss";
import entryStyles from "../assets/stylesheets/entry.scss";
johnshaughnessy
committed
import { ReactAudioContext, WithHoverSound } from "./wrap-with-audio";
import { lang, messages } from "../utils/i18n";
import {
TwoDEntryButton,
DeviceEntryButton,
GenericEntryButton,
DaydreamEntryButton,
SafariEntryButton
} from "./entry-buttons.js";
import ProfileEntryPanel from "./profile-entry-panel";
import HelpDialog from "./help-dialog.js";
import SafariDialog from "./safari-dialog.js";
import WebVRRecommendDialog from "./webvr-recommend-dialog.js";
import CreateObjectDialog from "./create-object-dialog.js";
import PresenceList from "./presence-list.js";
import { spawnChatMessage } from "./chat-message";
import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
// This is a list of regexes that match the microphone labels of HMDs.
//
// If entering VR mode, and if any of these regexes match an audio device,
// the user will be prevented from entering VR until one of those devices is
// selected as the microphone.
//
// Note that this doesn't have to be exhaustive: if no devices match any regex
// then we rely upon the user to select the proper mic.
const HMD_MIC_REGEXES = [/\Wvive\W/i, /\Wrift\W/i];
async function grantedMicLabels() {
const mediaDevices = await navigator.mediaDevices.enumerateDevices();
return mediaDevices.filter(d => d.label && d.kind === "audioinput").map(d => d.label);
}
const AUTO_EXIT_TIMER_SECONDS = 10;
class UIRoot extends Component {
static propTypes = {
concurrentLoadDetector: PropTypes.object,
Brian Peiris
committed
disableAutoExitOnConcurrentLoad: PropTypes.bool,
Brian Peiris
committed
enableScreenSharing: PropTypes.bool,
isBotMode: PropTypes.bool,
Brian Peiris
committed
store: PropTypes.object,
scene: PropTypes.object,
availableVREntryTypes: PropTypes.object,
platformUnsupportedReason: PropTypes.string,
Brian Peiris
committed
hubName: PropTypes.string,
presenceLogEntries: PropTypes.array,
presences: PropTypes.object,
subscriptions: PropTypes.object,
initialIsSubscribed: PropTypes.bool
miniInviteActivated: false,
requestedScreen: false,
Brian Peiris
committed
videoTrack: null,
audioTrack: null,
micUpdateInterval: null,
autoExitTimerStartedAt: null,
autoExitTimerInterval: null,
secondsRemainingBeforeAutoExit: Infinity,
muted: false,
frozen: false,
this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad);
this.micLevelMovingAverage = MovingAverage(100);
this.props.scene.addEventListener("loaded", this.onSceneLoaded);
this.props.scene.addEventListener("stateadded", this.onAframeStateChanged);
this.props.scene.addEventListener("stateremoved", this.onAframeStateChanged);
this.props.scene.addEventListener("exit", this.exit);
const scene = this.props.scene;
this.setState({
audioContext: {
playSound: sound => {
scene.emit(sound);
},
onMouseLeave: () => {
// scene.emit("play_sound-hud_mouse_leave");
}
}
});
}
componentWillUnmount() {
this.props.scene.removeEventListener("loaded", this.onSceneLoaded);
this.props.scene.removeEventListener("exit", this.exit);
updateSubscribedState = () => {
const isSubscribed = this.props.subscriptions && this.props.subscriptions.isSubscribed();
this.setState({ isSubscribed });
};
onSceneLoaded = () => {
this.setState({ sceneLoaded: true });
// TODO: we need to come up with a cleaner way to handle the shared state between aframe and react than emmitting events and setting state on the scene
if (!(e.detail === "muted" || e.detail === "frozen" || e.detail === "spacebubble")) return;
[e.detail]: this.props.scene.is(e.detail)
toggleMute = () => {
this.props.scene.emit("action_mute");
};
toggleFreeze = () => {
this.props.scene.emit("action_freeze");
};
toggleSpaceBubble = () => {
this.props.scene.emit("action_space_bubble");
};
Kevin Lee
committed
spawnPen = () => {
Kevin Lee
committed
};
if (!this.props.subscriptions) return;
await this.props.subscriptions.toggle();
this.updateSubscribedState();
};
const promptForNameAndAvatarBeforeEntry = !this.props.store.state.activity.hasChangedName;
if (promptForNameAndAvatarBeforeEntry) {
this.setState({ showProfileEntry: true });
}
this.goToEntryStep(ENTRY_STEPS.device);
this.enterDaydream();
} else if (this.props.forcedVREntryType.startsWith("vr")) {
} else if (this.props.forcedVREntryType.startsWith("2d")) {
goToEntryStep = entryStep => {
this.setState({ entryStep: entryStep, showInviteDialog: false });
};
const toneClip = document.querySelector("#test-tone");
clearTimeout(this.testToneTimeout);
this.setState({ tonePlaying: true });
const toneLength = 1393;
this.testToneTimeout = setTimeout(() => {
this.setState({ tonePlaying: false });
}, toneLength);
const toneClip = document.querySelector("#test-tone");
if (this.props.disableAutoExitOnConcurrentLoad) return;
const autoExitTimerInterval = setInterval(() => {
let secondsRemainingBeforeAutoExit = Infinity;
if (this.state.autoExitTimerStartedAt) {
const secondsSinceStart = (new Date() - this.state.autoExitTimerStartedAt) / 1000;
secondsRemainingBeforeAutoExit = Math.max(0, Math.floor(AUTO_EXIT_TIMER_SECONDS - secondsSinceStart));
}
this.setState({ secondsRemainingBeforeAutoExit });
this.checkForAutoExit();
}, 500);
this.setState({ autoExitTimerStartedAt: new Date(), autoExitTimerInterval });
};
checkForAutoExit = () => {
if (this.state.secondsRemainingBeforeAutoExit !== 0) return;
this.endAutoExitTimer();
this.exit();
exit = () => {
this.props.exitScene();
this.setState({ exited: true });
isWaitingForAutoExit = () => {
return this.state.secondsRemainingBeforeAutoExit <= AUTO_EXIT_TIMER_SECONDS;
endAutoExitTimer = () => {
clearInterval(this.state.autoExitTimerInterval);
this.setState({
autoExitTimerStartedAt: null,
autoExitTimerInterval: null,
secondsRemainingBeforeAutoExit: Infinity
});
};
hasGrantedMicPermissions = async () => {
if (this.state.requestedScreen) {
// There is no way to tell if you've granted mic permissions in a previous session if we've
Brian Peiris
committed
// already prompted for screen sharing permissions, so we have to assume that we've never granted permissions.
// Fortunately, if you *have* granted permissions permanently, there won't be a second browser prompt, but we
Brian Peiris
committed
// can't determine that before hand.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1449783 for a potential solution in the future.
return false;
// If we haven't requested the screen in this session, check if we've granted permissions in a previous session.
return (await grantedMicLabels()).length > 0;
}
performDirectEntryFlow = async enterInVR => {
this.setState({ enterInVR });
const hasGrantedMic = await this.hasGrantedMicPermissions();
this.goToEntryStep(ENTRY_STEPS.mic_grant);
await this.performDirectEntryFlow(false);
enterVR = async () => {
if (this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.maybe) {
await this.performDirectEntryFlow(true);
} else {
enterDaydream = async () => {
Brian Peiris
committed
const constraints = { audio: { deviceId: { exact: [ev.target.value] } } };
await this.fetchAudioTrack(constraints);
await this.setupNewMediaStream();
const { lastUsedMicDeviceId } = this.props.store.state.settings;
// Try to fetch last used mic, if there was one.
hasAudio = await this.fetchAudioTrack({ audio: { deviceId: { ideal: lastUsedMicDeviceId } } });
hasAudio = await this.fetchAudioTrack({ audio: true });
}
Brian Peiris
committed
await this.setupNewMediaStream();
Brian Peiris
committed
setStateAndRequestScreen = async e => {
const checked = e.target.checked;
Brian Peiris
committed
await this.setState({ requestedScreen: true, shareScreen: checked });
if (checked) {
this.fetchVideoTrack({
video: {
mediaSource: "screen",
// Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything
// other than your current monitor that has a different aspect ratio.
height: 720,
frameRate: 30
}
});
} else {
Brian Peiris
committed
this.setState({ videoTrack: null });
}
Brian Peiris
committed
fetchVideoTrack = async constraints => {
Brian Peiris
committed
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
this.setState({ videoTrack: mediaStream.getVideoTracks()[0] });
Brian Peiris
committed
fetchAudioTrack = async constraints => {
Brian Peiris
committed
if (this.state.audioTrack) {
this.state.audioTrack.stop();
}
try {
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
this.setState({ audioTrack: mediaStream.getAudioTracks()[0] });
return true;
} catch (e) {
// Error fetching audio track, most likely a permission denial.
this.setState({ audioTrack: null });
return false;
}
Brian Peiris
committed
const mediaStream = new MediaStream();
Brian Peiris
committed
if (this.state.videoTrack) {
mediaStream.addTrack(this.state.videoTrack);
}
// we should definitely have an audioTrack at this point unless they denied mic access
if (this.state.audioTrack) {
mediaStream.addTrack(this.state.audioTrack);
const AudioContext = window.AudioContext || window.webkitAudioContext;
const micLevelAudioContext = new AudioContext();
const micSource = micLevelAudioContext.createMediaStreamSource(mediaStream);
const analyser = micLevelAudioContext.createAnalyser();
analyser.fftSize = 32;
const levels = new Uint8Array(analyser.frequencyBinCount);
const micUpdateInterval = setInterval(() => {
let v = 0;
for (let x = 0; x < levels.length; x++) {
// Multiplier to increase visual indicator.
// We use a moving average to smooth out the visual animation or else it would twitch too fast for
// the css renderer to keep up.
this.micLevelMovingAverage.push(Date.now(), level * multiplier);
const average = this.micLevelMovingAverage.movingAverage();
this.setState(state => {
if (Math.abs(average - state.micLevel) > 0.0001) {
return { micLevel: average };
}
});
const micDeviceId = this.micDeviceIdForMicLabel(this.micLabelForMediaStream(mediaStream));
this.props.store.update({ settings: { lastUsedMicDeviceId: micDeviceId } });
this.setState({ micLevelAudioContext, micUpdateInterval });
}
this.setState({ mediaStream });
if (this.state.entryStep == ENTRY_STEPS.mic_grant) {
const { hasAudio } = await this.setMediaStreamToDefault();
if (hasAudio) {
this.goToEntryStep(ENTRY_STEPS.mic_granted);
this.props.hubChannel.sendProfileUpdate();
beginOrSkipAudioSetup = () => {
if (!this.props.forcedVREntryType || !this.props.forcedVREntryType.endsWith("_now")) {
this.goToEntryStep(ENTRY_STEPS.audio_setup);
fetchMicDevices = () => {
return new Promise(resolve => {
navigator.mediaDevices.enumerateDevices().then(mediaDevices => {
this.setState(
{
micDevices: mediaDevices
.filter(d => d.kind === "audioinput")
.map(d => ({ deviceId: d.deviceId, label: d.label }))
},
resolve
);
});
Brian Peiris
committed
});
if (AFRAME.utils.device.isMobile()) return false;
if (!this.state.enterInVR) return false;
if (!this.hasHmdMicrophone()) return false;
return !HMD_MIC_REGEXES.find(r => this.selectedMicLabel().match(r));
};
return !!this.state.micDevices.find(d => HMD_MIC_REGEXES.find(r => d.label.match(r)));
};
micLabelForMediaStream = mediaStream => {
return (mediaStream && mediaStream.getAudioTracks().length > 0 && mediaStream.getAudioTracks()[0].label) || "";
};
return this.micLabelForMediaStream(this.state.mediaStream);
};
micDeviceIdForMicLabel = label => {
return this.state.micDevices.filter(d => d.label === label).map(d => d.deviceId)[0];
return this.micDeviceIdForMicLabel(this.selectedMicLabel());
onAudioReadyButton = () => {
if (AFRAME.utils.device.isMobile() && !this.state.enterInVR && screenfull.enabled) {
screenfull.request();
}
this.props.enterScene(this.state.mediaStream, this.state.enterInVR, this.props.hubId);
if (mediaStream) {
if (mediaStream.getAudioTracks().length > 0) {
console.log(`Using microphone: ${mediaStream.getAudioTracks()[0].label}`);
}
if (mediaStream.getVideoTracks().length > 0) {
}
if (this.state.micLevelAudioContext) {
this.state.micLevelAudioContext.close();
clearInterval(this.state.micUpdateInterval);
}
this.goToEntryStep(ENTRY_STEPS.finished);
attemptLink = async () => {
this.setState({ showLinkDialog: true });
const { code, cancel, onFinished } = await this.props.linkChannel.generateCode();
this.setState({ linkCode: code, linkCodeCancel: cancel });
onFinished.then(() => this.setState({ showLinkDialog: false, linkCode: null, linkCodeCancel: null }));
showInviteDialog = () => {
this.setState({ showInviteDialog: true });
toggleInviteDialog = async () => {
this.setState({ showInviteDialog: !this.state.showInviteDialog });
this.props.scene.emit("add_media", media);
closeDialog = () => {
this.setState({ dialog: null });
};
showHelpDialog() {
this.setState({
dialog: <HelpDialog onClose={this.closeDialog} />
});
}
showSafariDialog() {
this.setState({
dialog: <SafariDialog onClose={this.closeDialog} />
});
}
showInviteTeamDialog() {
this.setState({
dialog: <InviteTeamDialog hubChannel={this.props.hubChannel} onClose={this.closeDialog} />
});
}
showCreateObjectDialog() {
this.setState({
dialog: <CreateObjectDialog onCreate={this.createObject} onClose={this.closeDialog} />
});
}
showWebVRRecommendDialog() {
this.setState({
dialog: <WebVRRecommendDialog onClose={this.closeDialog} />
});
}
onMiniInviteClicked = () => {
const link = "https://hub.link/" + this.props.hubId;
this.setState({ miniInviteActivated: true });
setTimeout(() => {
this.setState({ miniInviteActivated: false });
}, 5000);
if (navigator.share) {
navigator.share({ title: document.title, url: link });
} else {
copy(link);
}
};
sendMessage = e => {
e.preventDefault();
this.props.onSendMessage(this.state.pendingMessage);
this.setState({ pendingMessage: "" });
};
occupantCount = () => {
return this.props.presences ? Object.entries(this.props.presences).length : 0;
};
renderExitedPane = () => {
let subtitle = null;
if (this.props.roomUnavailableReason === "closed") {
// TODO i18n, due to links and markup
subtitle = (
<div>
Sorry, this room is no longer available.
<p />A room may be closed if we receive reports that it violates our{" "}
<WithHoverSound>
<a target="_blank" rel="noreferrer noopener" href="https://github.com/mozilla/hubs/blob/master/TERMS.md">
Terms of Use
</a>
</WithHoverSound>
.<br />
If you have questions, contact us at{" "}
<WithHoverSound>
<a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>
</WithHoverSound>
.<p />
If you'd like to run your own server, hubs's source code is available on{" "}
<WithHoverSound>
<a href="https://github.com/mozilla/hubs">GitHub</a>
</WithHoverSound>
.
</div>
} else if (this.props.platformUnsupportedReason === "no_data_channels") {
// TODO i18n, due to links and markup
subtitle = (
<div>
Your browser does not support{" "}
<WithHoverSound>
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel#Browser_compatibility"
rel="noreferrer noopener"
>
WebRTC Data Channels
</a>
</WithHoverSound>
, which is required to use Hubs.
<br />
If you"d like to use Hubs with Oculus or SteamVR, you can{" "}
<WithHoverSound>
<a href="https://www.mozilla.org/firefox" rel="noreferrer noopener">
Download Firefox
</a>
</WithHoverSound>
.
</div>
} else {
const reason = this.props.roomUnavailableReason || this.props.platformUnsupportedReason;
const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : reason}`;
subtitle = (
<div>
<FormattedMessage id={exitSubtitleId} />
<p />
<div>
You can also{" "}
<WithHoverSound>
</div>
return (
<IntlProvider locale={lang} messages={messages}>
<div className="exited-panel">
<img className="exited-panel__logo" src="../assets/images/logo.svg" />
<div className="exited-panel__subtitle">{subtitle}</div>
</div>
</IntlProvider>
);
};
Brian Peiris
committed
renderBotMode = () => {
return (
<div className="loading-panel">
<img className="loading-panel__logo" src="../assets/images/logo.svg" />
<input type="file" id="bot-audio-input" accept="audio/*" />
<input type="file" id="bot-data-input" accept="application/json" />
</div>
);
};
renderLoader = () => {
return (
<IntlProvider locale={lang} messages={messages}>
<div className="loading-panel">
<div className="loader-wrap">
<div className="loader">
<div className="loader-center" />
</div>
<img className="loading-panel__logo" src="../assets/images/hub-preview-light-no-shadow.png" />
const textRows = this.state.pendingMessage.split("\n").length;
const pendingMessageTextareaHeight = textRows * 28 + "px";
const pendingMessageFieldHeight = textRows * 28 + 20 + "px";
const hasPush = navigator.serviceWorker && "PushManager" in window;
<div className={entryStyles.entryPanel}>
<div className={entryStyles.name}>{this.props.hubName}</div>
<div className={entryStyles.center}>
<WithHoverSound>
<div onClick={() => this.setState({ showProfileEntry: true })} className={entryStyles.profileName}>
<img src="../assets/images/account.svg" className={entryStyles.profileIcon} />
<div title={this.props.store.state.profile.displayName}>{this.props.store.state.profile.displayName}</div>
</WithHoverSound>
<div className={styles.messageEntry} style={{ height: pendingMessageFieldHeight }}>
<textarea
className={classNames([styles.messageEntryInput, "chat-focus-target"])}
rows={textRows}
style={{ height: pendingMessageTextareaHeight }}
onFocus={e => e.target.select()}
onChange={e => this.setState({ pendingMessage: e.target.value })}
onKeyDown={e => {
if (e.keyCode === 13 && !e.shiftKey) {
this.sendMessage(e);
<WithHoverSound>
<input className={styles.messageEntrySubmit} type="submit" value="send" />
</WithHoverSound>
</div>
{hasPush && (
<div className={entryStyles.subscribe}>
<input
id="subscribe"
type="checkbox"
onChange={this.onSubscribeChanged}
checked={this.state.isSubscribed === undefined ? this.props.initialIsSubscribed : this.state.isSubscribed}
/>
<label htmlFor="subscribe">
<FormattedMessage id="entry.notify_me" />
</label>
</div>
)}
<div className={entryStyles.buttonContainer}>
<WithHoverSound>
<button
className={classNames([entryStyles.actionButton, entryStyles.wideButton])}
onClick={() => this.handleStartEntry()}
>
<FormattedMessage id="entry.enter-room" />
</button>
</WithHoverSound>
</div>
</div>
// Only screen sharing in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and will attempt to share your webcam instead!
const isFireFox = /firefox/i.test(navigator.userAgent);
const isNonMobile = !AFRAME.utils.device.isMobile();
const screenSharingCheckbox =
this.props.enableScreenSharing && isNonMobile && isFireFox && this.renderScreensharing();
return (
<div className={entryStyles.entryPanel}>
<div className={entryStyles.title}>
<FormattedMessage id="entry.choose-device" />
</div>
<div className={entryStyles.buttonContainer}>
{this.props.availableVREntryTypes.screen === VR_DEVICE_AVAILABILITY.yes && (
<TwoDEntryButton onClick={this.enter2D} />
)}
{this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && (
<SafariEntryButton onClick={this.showSafariDialog} />
)}
{this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
<GenericEntryButton onClick={this.enterVR} />
)}
{this.props.availableVREntryTypes.daydream === VR_DEVICE_AVAILABILITY.yes && (
<DaydreamEntryButton onClick={this.enterDaydream} subtitle={null} />
)}
<DeviceEntryButton onClick={() => this.attemptLink()} isInHMD={this.props.availableVREntryTypes.isInHMD} />
{this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (
<div className={entryStyles.secondary} onClick={this.enterVR}>
<FormattedMessage id="entry.cardboard" />
)}
{screenSharingCheckbox}
</div>
</div>
);
};
renderScreensharing = () => {
return (
<label className={entryStyles.screenSharing}>
<input
className={entryStyles.checkbox}
type="checkbox"
value={this.state.shareScreen}
onChange={this.setStateAndRequestScreen}
/>
<FormattedMessage id="entry.enable-screen-sharing" />
</label>
);
};
renderMicPanel = () => {
return (
<div className="mic-grant-panel">
<div className="mic-grant-panel__grant-container">
<div className="mic-grant-panel__title">
<FormattedMessage
id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-title" : "audio.granted-title"}
/>
</div>
<div className="mic-grant-panel__subtitle">
<FormattedMessage
id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-subtitle" : "audio.granted-subtitle"}
/>
</div>
<div className="mic-grant-panel__button-container">
{this.state.entryStep == ENTRY_STEPS.mic_grant ? (
<WithHoverSound>
<button className="mic-grant-panel__button" onClick={this.onMicGrantButton}>
<img src="../assets/images/mic_denied.png" srcSet="../assets/images/mic_denied@2x.png 2x" />
</WithHoverSound>
) : (
<WithHoverSound>
<button className="mic-grant-panel__button" onClick={this.onMicGrantButton}>
<img src="../assets/images/mic_granted.png" srcSet="../assets/images/mic_granted@2x.png 2x" />
</button>
</WithHoverSound>
)}
<div className="mic-grant-panel__next-container">
<WithHoverSound>
<button className={classNames("mic-grant-panel__next")} onClick={this.onMicGrantButton}>
<FormattedMessage id="audio.granted-next" />
</button>
</WithHoverSound>
</div>
</div>
</div>
const micClip = {
clip: `rect(${maxLevelHeight - Math.floor(this.state.micLevel * maxLevelHeight)}px, 111px, 111px, 0px)`
};
const speakerClip = { clip: `rect(${this.state.tonePlaying ? 0 : maxLevelHeight}px, 111px, 111px, 0px)` };
const subtitleId = AFRAME.utils.device.isMobile() ? "audio.subtitle-mobile" : "audio.subtitle-desktop";
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
<div className="audio-setup-panel">
<div>
<div className="audio-setup-panel__title">
<FormattedMessage id="audio.title" />
</div>
<div className="audio-setup-panel__subtitle">
{(AFRAME.utils.device.isMobile() || this.state.enterInVR) && <FormattedMessage id={subtitleId} />}
</div>
<div className="audio-setup-panel__levels">
<div className="audio-setup-panel__levels__icon">
<img
src="../assets/images/level_background.png"
srcSet="../assets/images/level_background@2x.png 2x"
className="audio-setup-panel__levels__icon-part"
/>
<img
src="../assets/images/level_fill.png"
srcSet="../assets/images/level_fill@2x.png 2x"
className="audio-setup-panel__levels__icon-part"
style={micClip}
/>
{this.state.audioTrack ? (
<img
src="../assets/images/mic_level.png"
srcSet="../assets/images/mic_level@2x.png 2x"
className="audio-setup-panel__levels__icon-part"
/>
) : (
<img
src="../assets/images/mic_denied.png"
srcSet="../assets/images/mic_denied@2x.png 2x"
className="audio-setup-panel__levels__icon-part"
/>
)}
</div>
<WithHoverSound>
<div className="audio-setup-panel__levels__icon" onClick={this.playTestTone}>
<img
src="../assets/images/level_background.png"
srcSet="../assets/images/level_background@2x.png 2x"
className="audio-setup-panel__levels__icon-part"
/>
<img
src="../assets/images/level_fill.png"
srcSet="../assets/images/level_fill@2x.png 2x"
className="audio-setup-panel__levels__icon-part"
style={speakerClip}
/>
<img
src="../assets/images/speaker_level.png"
srcSet="../assets/images/speaker_level@2x.png 2x"
className="audio-setup-panel__levels__icon-part"
/>
</WithHoverSound>
</div>
{this.state.audioTrack && (
<div className="audio-setup-panel__device-chooser">
<WithHoverSound>
<select
className="audio-setup-panel__device-chooser__dropdown"
value={this.selectedMicDeviceId()}
onChange={this.micDeviceChanged}
{this.state.micDevices.map(d => (
<option key={d.deviceId} value={d.deviceId}>
{d.label}
</option>
))}
</select>
</WithHoverSound>
<img
className="audio-setup-panel__device-chooser__mic-icon"
src="../assets/images/mic_small.png"
srcSet="../assets/images/mic_small@2x.png 2x"
/>
<img
className="audio-setup-panel__device-chooser__dropdown-arrow"
src="../assets/images/dropdown_arrow.png"
srcSet="../assets/images/dropdown_arrow@2x.png 2x"
/>
)}
{this.shouldShowHmdMicWarning() && (
<div className="audio-setup-panel__hmd-mic-warning">
<img
src="../assets/images/warning_icon.png"
srcSet="../assets/images/warning_icon@2x.png 2x"
className="audio-setup-panel__hmd-mic-warning__icon"
/>
<span className="audio-setup-panel__hmd-mic-warning__label">
<FormattedMessage id="audio.hmd-mic-warning" />
</span>