diff --git a/lib/audioPlayer.js b/lib/audioPlayer.js index eadb35f..0d234a1 100644 --- a/lib/audioPlayer.js +++ b/lib/audioPlayer.js @@ -38,12 +38,13 @@ module.exports = initialized = true; }, - /** - * Play a sound - * @param {String} name - Sound name - * @param {[Float]} relativeVolume - Relative volume (0.0 - 1.0) - */ - play(name, relativeVolume) + /** + * Play a sound + * @param {String} name - Sound name + * @param {String} deviceId - DeviceId to use for audio output + * @param {[Float]} relativeVolume - Relative volume (0.0 - 1.0) + */ + play(name, deviceId, relativeVolume) { this.initialize(); @@ -62,6 +63,11 @@ module.exports = sound.audio.pause(); sound.audio.currentTime = 0.0; sound.audio.volume = (sound.volume || 1.0) * relativeVolume; + + if (deviceId && sound.audio.setSinkId) { + sound.audio.setSinkId(deviceId); + } + sound.audio.play(); } catch (error) diff --git a/lib/components/Phone.jsx b/lib/components/Phone.jsx index c99258b..7937230 100644 --- a/lib/components/Phone.jsx +++ b/lib/components/Phone.jsx @@ -100,6 +100,7 @@ export default class Phone extends React.Component
{state.session ? @@ -351,13 +353,15 @@ export default class Phone extends React.Component { logger.debug('handleOutgoingCall() [uri:"%s"]', uri); + const mediaSettings = this.props.settings.media; + let session = this._ua.call(uri, { pcConfig : this.props.settings.pcConfig || { iceServers: [] }, mediaConstraints : { - audio : true, - video : true + audio: mediaSettings.audioInput ? {deviceId: {exact: mediaSettings.audioInput}} : true, + video : mediaSettings.videoInput ? {deviceId: {exact: mediaSettings.videoInput}} : true }, rtcOfferConstraints : { @@ -373,13 +377,13 @@ export default class Phone extends React.Component session.on('progress', () => { - audioPlayer.play('ringback'); + audioPlayer.play('ringback', mediaSettings.audioOutput); }); session.on('failed', (data) => { audioPlayer.stop('ringback'); - audioPlayer.play('rejected'); + audioPlayer.play('rejected', mediaSettings.audioOutput); this.setState({ session: null }); this.props.onNotify( @@ -399,7 +403,7 @@ export default class Phone extends React.Component session.on('accepted', () => { audioPlayer.stop('ringback'); - audioPlayer.play('answered'); + audioPlayer.play('answered', mediaSettings.audioOutput); }); } diff --git a/lib/components/Session.jsx b/lib/components/Session.jsx index 76315a0..59c252e 100644 --- a/lib/components/Session.jsx +++ b/lib/components/Session.jsx @@ -112,6 +112,8 @@ export default class Session extends React.Component let localVideo = this.refs.localVideo; let session = this.props.session; + let mediaSettings = this.props.settings.media; + let peerconnection = session.connection; let localStream = peerconnection.getLocalStreams()[0]; let remoteStream = peerconnection.getRemoteStreams()[0]; @@ -123,7 +125,7 @@ export default class Session extends React.Component this._localClonedStream = localStream.clone(); // Display local stream - localVideo.srcObject = this._localClonedStream; + this._attachStreamToElement(localVideo, this._localClonedStream, mediaSettings.audioOutput); setTimeout(() => { @@ -140,7 +142,7 @@ export default class Session extends React.Component { logger.debug('already have a remote stream'); - this._handleRemoteStream(remoteStream); + this._handleRemoteStream(remoteStream); } if (session.isEstablished()) @@ -309,9 +311,10 @@ export default class Session extends React.Component logger.debug('_handleRemoteStream() [stream:%o]', stream); let remoteVideo = this.refs.remoteVideo; + let mediaSettings = this.props.settings.media; // Display remote stream - remoteVideo.srcObject = stream; + this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput); this._checkRemoteVideo(stream); @@ -325,7 +328,7 @@ export default class Session extends React.Component logger.debug('remote stream "addtrack" event [track:%o]', track); // Refresh remote video - remoteVideo.srcObject = stream; + this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput); this._checkRemoteVideo(stream); @@ -343,7 +346,7 @@ export default class Session extends React.Component logger.debug('remote stream "removetrack" event'); // Refresh remote video - remoteVideo.srcObject = stream; + this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput); this._checkRemoteVideo(stream); }); @@ -362,11 +365,34 @@ export default class Session extends React.Component this.setState({ remoteHasVideo: !!videoTrack }); } + + /** + * Re/Attach stream to element with deviceId + * @param {HTMLMediaElement} element - Target audio/video element + * @param {Stream} stream - Stream to attach + * @param {String} deviceId - DeviceId to use for audio output + */ + _attachStreamToElement(element, stream, deviceId) { + // Pause stream before device change + element.pause(); + + // Redirect audio output to exact device + if (deviceId && element.setSinkId) { + element.setSinkId(deviceId); + } + + // ReAttach stream + element.srcObject = stream; + + // Continue on new device + element.play(); + } } Session.propTypes = { + settings : PropTypes.object.isRequired, session : PropTypes.object.isRequired, onNotify : PropTypes.func.isRequired, - onHideNotification : PropTypes.func.isRequired, + onHideNotification : PropTypes.func.isRequired }; diff --git a/lib/components/Settings.jsx b/lib/components/Settings.jsx index da82646..2adc565 100644 --- a/lib/components/Settings.jsx +++ b/lib/components/Settings.jsx @@ -24,13 +24,39 @@ export default class Settings extends React.Component this.state = { - settings : clone(settings, false) + settings : clone(settings, false), + devices: [] }; } + componentDidMount() { + if ( + navigator && + navigator.mediaDevices && + navigator.mediaDevices.getUserMedia && + navigator.mediaDevices.enumerateDevices && + (window.AudioContext || window.webkitAudioContext) + ) { + // TODO: Detect device change + navigator.mediaDevices.getUserMedia({audio:true,video:true}) + .then(() => { + navigator.mediaDevices.enumerateDevices().then(devices => { + console.log('Loaded devices', devices); + this.setState({devices}); + }) + }); + + } else { + console.warn('MediaDevices API is missing!'); + } + } + render() { - let settings = this.state.settings; + const { + devices, + settings + } = this.state; return ( @@ -195,6 +221,64 @@ export default class Settings extends React.Component
+
+ + + {devices.filter(x => x.kind === 'audioinput').map(x => ( + + ))} + +
+ +
+ + + {devices.filter(x => x.kind === 'audiooutput').map(x => ( + + ))} + +
+ +
+ + + {devices.filter(x => x.kind === 'audiooutput').map(x => ( + + ))} + +
+ +
+ + + {devices.filter(x => x.kind === 'videoinput').map(x => ( + + ))} + +
+ +
+