From 387b11c429af0229b72de77bf2badef9d4a65332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kal=C3=A1bek?= Date: Tue, 9 Jan 2018 16:10:11 +0100 Subject: [PATCH 1/5] Add settings --- lib/components/Settings.jsx | 119 +++++++++++++++++++++++++++++++++++- lib/settingsManager.js | 7 +++ 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/lib/components/Settings.jsx b/lib/components/Settings.jsx index da82646..60d26d3 100644 --- a/lib/components/Settings.jsx +++ b/lib/components/Settings.jsx @@ -24,13 +24,34 @@ 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.enumerateDevices && + (window.AudioContext || window.webkitAudioContext) + ) { + // TODO: Detect device change + 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 +216,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 => ( + + ))} + +
+ +
+
Date: Tue, 9 Jan 2018 16:12:15 +0100 Subject: [PATCH 2/5] Modify input devices --- lib/components/Phone.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/components/Phone.jsx b/lib/components/Phone.jsx index c99258b..434c791 100644 --- a/lib/components/Phone.jsx +++ b/lib/components/Phone.jsx @@ -351,13 +351,14 @@ 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 : { From 1b706c1f42cdebc1f4bccf8f91d2c758d27d51e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kal=C3=A1bek?= Date: Tue, 9 Jan 2018 16:14:23 +0100 Subject: [PATCH 3/5] Modify output devices TODO: Detect old/disconnected devices TODO: Switch devices on fly? --- lib/audioPlayer.js | 18 +++++++++++------ lib/components/Phone.jsx | 11 +++++++---- lib/components/Session.jsx | 40 +++++++++++++++++++++++++++++++------- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/lib/audioPlayer.js b/lib/audioPlayer.js index eadb35f..a700d85 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 434c791..e85606a 100644 --- a/lib/components/Phone.jsx +++ b/lib/components/Phone.jsx @@ -100,6 +100,7 @@ export default class Phone extends React.Component
{state.session ? @@ -352,6 +354,7 @@ 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: [] }, @@ -374,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( @@ -400,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..68fe256 100644 --- a/lib/components/Session.jsx +++ b/lib/components/Session.jsx @@ -111,7 +111,9 @@ export default class Session extends React.Component this._mounted = true; let localVideo = this.refs.localVideo; - let session = this.props.session; + 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 }; From 5de5be336693b784ec5145c30b1acac4cd245745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kal=C3=A1bek?= Date: Wed, 10 Jan 2018 14:10:05 +0100 Subject: [PATCH 4/5] Make linter happy --- lib/audioPlayer.js | 2 +- lib/components/Phone.jsx | 4 ++-- lib/components/Session.jsx | 20 ++++++++--------- lib/components/Settings.jsx | 44 ++++++++++++++++++------------------- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/audioPlayer.js b/lib/audioPlayer.js index a700d85..0d234a1 100644 --- a/lib/audioPlayer.js +++ b/lib/audioPlayer.js @@ -65,7 +65,7 @@ module.exports = sound.audio.volume = (sound.volume || 1.0) * relativeVolume; if (deviceId && sound.audio.setSinkId) { - sound.audio.setSinkId(deviceId) + sound.audio.setSinkId(deviceId); } sound.audio.play(); diff --git a/lib/components/Phone.jsx b/lib/components/Phone.jsx index e85606a..7937230 100644 --- a/lib/components/Phone.jsx +++ b/lib/components/Phone.jsx @@ -130,7 +130,7 @@ export default class Phone extends React.Component let settings = this.props.settings; let socket = new JsSIP.WebSocketInterface(settings.socket.uri); - let mediaSettings = settings.media + let mediaSettings = settings.media; if (settings.socket.via_transport !== 'auto') socket.via_transport = settings.socket.via_transport; @@ -353,7 +353,7 @@ export default class Phone extends React.Component { logger.debug('handleOutgoingCall() [uri:"%s"]', uri); - const mediaSettings = this.props.settings.media + const mediaSettings = this.props.settings.media; let session = this._ua.call(uri, { diff --git a/lib/components/Session.jsx b/lib/components/Session.jsx index 68fe256..59c252e 100644 --- a/lib/components/Session.jsx +++ b/lib/components/Session.jsx @@ -111,8 +111,8 @@ export default class Session extends React.Component this._mounted = true; let localVideo = this.refs.localVideo; - let session = this.props.session - let mediaSettings = this.props.settings.media + let session = this.props.session; + let mediaSettings = this.props.settings.media; let peerconnection = session.connection; let localStream = peerconnection.getLocalStreams()[0]; @@ -125,7 +125,7 @@ export default class Session extends React.Component this._localClonedStream = localStream.clone(); // Display local stream - this._attachStreamToElement(localVideo, this._localClonedStream, mediaSettings.audioOutput) + this._attachStreamToElement(localVideo, this._localClonedStream, mediaSettings.audioOutput); setTimeout(() => { @@ -142,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()) @@ -311,10 +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 + let mediaSettings = this.props.settings.media; // Display remote stream - this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput) + this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput); this._checkRemoteVideo(stream); @@ -328,7 +328,7 @@ export default class Session extends React.Component logger.debug('remote stream "addtrack" event [track:%o]', track); // Refresh remote video - this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput) + this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput); this._checkRemoteVideo(stream); @@ -346,7 +346,7 @@ export default class Session extends React.Component logger.debug('remote stream "removetrack" event'); // Refresh remote video - this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput) + this._attachStreamToElement(remoteVideo, stream, mediaSettings.audioOutput); this._checkRemoteVideo(stream); }); @@ -378,14 +378,14 @@ export default class Session extends React.Component // Redirect audio output to exact device if (deviceId && element.setSinkId) { - element.setSinkId(deviceId) + element.setSinkId(deviceId); } // ReAttach stream element.srcObject = stream; // Continue on new device - element.play() + element.play(); } } diff --git a/lib/components/Settings.jsx b/lib/components/Settings.jsx index 60d26d3..76c2757 100644 --- a/lib/components/Settings.jsx +++ b/lib/components/Settings.jsx @@ -38,11 +38,11 @@ export default class Settings extends React.Component ) { // TODO: Detect device change navigator.mediaDevices.enumerateDevices().then(devices => { - console.log('Loaded devices', devices) - this.setState({devices}) - }) + console.log('Loaded devices', devices); + this.setState({devices}); + }); } else { - console.warn('MediaDevices API is missing!') + console.warn('MediaDevices API is missing!'); } } @@ -51,7 +51,7 @@ export default class Settings extends React.Component const { devices, settings - } = this.state + } = this.state; return ( @@ -216,19 +216,19 @@ 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 => ( + ))}
@@ -252,8 +252,8 @@ export default class Settings extends React.Component onChange={this.handleChangeAudioOutputRinging.bind(this)} > - {devices.filter(x => x.kind === 'audiooutput').map(x => ( - + {devices.filter(x => x.kind === 'audiooutput').map(x => ( + ))}
@@ -266,8 +266,8 @@ export default class Settings extends React.Component onChange={this.handleChangeVideoInput.bind(this)} > - {devices.filter(x => x.kind === 'videoinput').map(x => ( - + {devices.filter(x => x.kind === 'videoinput').map(x => ( + ))}
@@ -314,7 +314,7 @@ export default class Settings extends React.Component { let settings = this.state.settings; - settings.socket.uri = event.target.value;; + settings.socket.uri = event.target.value; this.setState({ settings }); } From de1000c685ecf2fbd472ad84354d3150fcb8fbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kal=C3=A1bek?= Date: Wed, 10 Jan 2018 15:27:40 +0100 Subject: [PATCH 5/5] Request permissions first --- lib/components/Settings.jsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/components/Settings.jsx b/lib/components/Settings.jsx index 76c2757..2adc565 100644 --- a/lib/components/Settings.jsx +++ b/lib/components/Settings.jsx @@ -33,14 +33,19 @@ export default class Settings extends React.Component if ( navigator && navigator.mediaDevices && + navigator.mediaDevices.getUserMedia && navigator.mediaDevices.enumerateDevices && (window.AudioContext || window.webkitAudioContext) ) { // TODO: Detect device change - navigator.mediaDevices.enumerateDevices().then(devices => { - console.log('Loaded devices', devices); - this.setState({devices}); - }); + 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!'); }