Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic media selection #12

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions lib/audioPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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)
Expand Down
16 changes: 10 additions & 6 deletions lib/components/Phone.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export default class Phone extends React.Component
<div className='content'>
{state.session ?
<Session
settings={props.settings}
session={state.session}
onNotify={props.onNotify}
onHideNotification={props.onHideNotification}
Expand Down Expand Up @@ -129,6 +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;

if (settings.socket.via_transport !== 'auto')
socket.via_transport = settings.socket.via_transport;
Expand Down Expand Up @@ -266,7 +268,7 @@ export default class Phone extends React.Component
return;
}

audioPlayer.play('ringing');
audioPlayer.play('ringing', mediaSettings.ringing);
this.setState({ incomingSession: session });

session.on('failed', () =>
Expand Down Expand Up @@ -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 :
{
Expand All @@ -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(
Expand All @@ -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);
});
}

Expand Down
38 changes: 32 additions & 6 deletions lib/components/Session.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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(() =>
{
Expand All @@ -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())
Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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);
});
Expand All @@ -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
};
126 changes: 123 additions & 3 deletions lib/components/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<TransitionAppear duration={250}>
Expand Down Expand Up @@ -195,6 +221,64 @@ export default class Settings extends React.Component

<div className='separator'/>

<div className='item'>
<SelectField
floatingLabelText='Audio input'
value={settings.media.audioInput || null}
fullWidth
onChange={this.handleChangeAudioInput.bind(this)}
>
<MenuItem />
{devices.filter(x => x.kind === 'audioinput').map(x => (
<MenuItem value={x.deviceId} primaryText={x.label} key={x.deviceId} />
))}
</SelectField>
</div>

<div className='item'>
<SelectField
floatingLabelText='Audio output'
value={settings.media.audioOutput || null}
fullWidth
onChange={this.handleChangeAudioOutput.bind(this)}
>
<MenuItem />
{devices.filter(x => x.kind === 'audiooutput').map(x => (
<MenuItem value={x.deviceId} primaryText={x.label} key={x.deviceId} />
))}
</SelectField>
</div>

<div className='item'>
<SelectField
floatingLabelText='Audio ringing'
value={settings.media.audioRinging || null}
fullWidth
onChange={this.handleChangeAudioOutputRinging.bind(this)}
>
<MenuItem />
{devices.filter(x => x.kind === 'audiooutput').map(x => (
<MenuItem value={x.deviceId} primaryText={x.label} key={x.deviceId} />
))}
</SelectField>
</div>

<div className='item'>
<SelectField
floatingLabelText='Video input'
value={settings.media.videoInput || null}
fullWidth
onChange={this.handleChangeVideoInput.bind(this)}
>
<MenuItem />
{devices.filter(x => x.kind === 'videoinput').map(x => (
<MenuItem value={x.deviceId} primaryText={x.label} key={x.deviceId} />
))}
</SelectField>
</div>

<div className='separator'/>

<div className='buttons'>
<RaisedButton
label='Cancel'
Expand Down Expand Up @@ -235,7 +319,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 });
}

Expand Down Expand Up @@ -319,6 +403,42 @@ export default class Settings extends React.Component
this.setState({ settings });
}

handleChangeAudioInput(event, key, value)
{
let settings = this.state.settings;

settings.media.audioInput = value;

this.setState({ settings });
}

handleChangeAudioOutput(event, key, value)
{
let settings = this.state.settings;

settings.media.audioOutput = value;

this.setState({ settings });
}

handleChangeAudioOutputRinging(event, key, value)
{
let settings = this.state.settings;

settings.media.audioRinging = value;

this.setState({ settings });
}

handleChangeVideoInput(event, key, value)
{
let settings = this.state.settings;

settings.media.videoInput = value;

this.setState({ settings });
}

handleSubmit()
{
logger.debug('handleSubmit()');
Expand Down
7 changes: 7 additions & 0 deletions lib/settingsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ const DEFAULT_SETTINGS =
enabled : false,
AppID : null,
AppSecret : null
},
media:
{
audioInput: null,
audioOutput: null,
audioRinging: null,
videoInput: null
}
};

Expand Down