refactor(audio): use preloaded audio stream if provided

Avoids a surplus gUM with local echo test et al
This commit is contained in:
prlanzarin 2022-04-11 22:18:40 +00:00
parent 1e37924e41
commit f4ba6dd9a2
7 changed files with 66 additions and 9 deletions

View File

@ -297,7 +297,7 @@ export default class FullAudioBridge extends BaseAudioBridge {
async _startBroker(options) {
try {
const { isListenOnly, extension } = options;
const { isListenOnly, extension, inputStream } = options;
this.inEchoTest = !!extension;
this.isListenOnly = isListenOnly;
@ -314,6 +314,7 @@ export default class FullAudioBridge extends BaseAudioBridge {
mediaServer: getMediaServerAdapter(),
constraints: getAudioConstraints({ deviceId: this.inputDeviceId }),
forceRelay: shouldForceRelay(),
stream: (inputStream && inputStream.active) ? inputStream : undefined,
};
this.broker = new FullAudioBroker(

View File

@ -88,6 +88,8 @@ class SIPSession {
this._reconnecting = false;
this._currentSessionState = null;
this._ignoreCallState = false;
this.mediaStreamFactory = this.mediaStreamFactory.bind(this)
}
get inputStream() {
@ -224,6 +226,7 @@ class SIPSession {
inputDeviceId,
outputDeviceId,
validIceCandidates,
inputStream,
}, managerCallback) {
return new Promise((resolve, reject) => {
const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
@ -259,6 +262,7 @@ class SIPSession {
isListenOnly,
inputDeviceId,
outputDeviceId,
inputStream,
}).catch((reason) => {
reject(reason);
});
@ -287,10 +291,14 @@ class SIPSession {
isListenOnly,
inputDeviceId,
outputDeviceId,
inputStream,
} = options;
this.inputDeviceId = inputDeviceId;
this.outputDeviceId = outputDeviceId;
// If a valid MediaStream was provided it means it was preloaded somewhere
// else - let's use it so we don't call gUM needlessly
if (inputStream && inputStream.active) this.preloadedInputStream = inputStream;
const {
userId,
@ -423,6 +431,17 @@ class SIPSession {
return this.stopUserAgent();
}
mediaStreamFactory(constraints) {
if (this.preloadedInputStream && this.preloadedInputStream.active) {
return Promise.resolve(this.preloadedInputStream);
}
// The rest of this mimicks the default factory behavior.
if (!constraints.audio && !constraints.video) {
return Promise.resolve(new MediaStream());
}
return navigator.mediaDevices.getUserMedia(constraints);
}
createUserAgent(iceServers) {
return new Promise((resolve, reject) => {
if (this.userRequestedHangup === true) reject();
@ -465,6 +484,9 @@ class SIPSession {
let userAgentConnected = false;
const token = `sessionToken=${sessionToken}`;
// Create session description handler factory
const customSDHFactory = SIP.Web.defaultSessionDescriptionHandlerFactory(this.mediaStreamFactory);
this.userAgent = new SIP.UserAgent({
uri: SIP.UserAgent.makeURI(`sip:${encodeURIComponent(callerIdName)}@${hostname}`),
transportOptions: {
@ -474,6 +496,7 @@ class SIPSession {
keepAliveDebounce: WEBSOCKET_KEEP_ALIVE_DEBOUNCE,
traceSip: TRACE_SIP,
},
sessionDescriptionHandlerFactory: customSDHFactory,
sessionDescriptionHandlerFactoryOptions: {
peerConnectionConfiguration: {
iceServers,
@ -1280,7 +1303,12 @@ export default class SIPBridge extends BaseAudioBridge {
return this.activeSession ? this.activeSession.ignoreCallState : false;
}
joinAudio({ isListenOnly, extension, validIceCandidates }, managerCallback) {
joinAudio({
isListenOnly,
extension,
validIceCandidates,
inputStream,
}, managerCallback) {
const hasFallbackDomain = typeof IPV4_FALLBACK_DOMAIN === 'string' && IPV4_FALLBACK_DOMAIN !== '';
return new Promise((resolve, reject) => {
@ -1319,6 +1347,7 @@ export default class SIPBridge extends BaseAudioBridge {
inputDeviceId,
outputDeviceId,
validIceCandidates,
inputStream,
}, callback)
.then((value) => {
this.changeOutputDevice(outputDeviceId, true);
@ -1339,6 +1368,7 @@ export default class SIPBridge extends BaseAudioBridge {
inputDeviceId,
outputDeviceId,
validIceCandidates,
inputStream,
}, callback)
.then((value) => {
this.changeOutputDevice(outputDeviceId, true);

View File

@ -135,7 +135,7 @@ class AudioModal extends Component {
this.handleRetryGoToEchoTest = this.handleRetryGoToEchoTest.bind(this);
this.handleGoToEchoTest = this.handleGoToEchoTest.bind(this);
this.handleJoinMicrophone = this.handleJoinMicrophone.bind(this);
this.handleJoinSimplifiedEcho = this.handleJoinSimplifiedEcho.bind(this);
this.handleJoinLocalEcho = this.handleJoinLocalEcho.bind(this);
this.handleJoinListenOnly = this.handleJoinListenOnly.bind(this);
this.skipAudioOptions = this.skipAudioOptions.bind(this);
@ -336,12 +336,14 @@ class AudioModal extends Component {
});
}
handleJoinSimplifiedEcho() {
handleJoinLocalEcho(inputStream) {
const { changeInputStream } = this.props;
// Reset the modal to a connecting state - this kind of sucks?
// FIXME - prlanzarin Apr 04 2022
// prlanzarin Apr 04 2022
this.setState({
content: null,
});
if (inputStream) changeInputStream(inputStream);
this.handleJoinMicrophone();
}
@ -510,7 +512,7 @@ class AudioModal extends Component {
const confirmationCallback = !localEchoEnabled
? this.handleRetryGoToEchoTest
: this.handleJoinSimplifiedEcho;
: this.handleJoinLocalEcho;
const handleGUMFailure = () => {
this.setState({

View File

@ -71,6 +71,7 @@ export default lockContextContainer(withModalMounter(withTracker(({ userLocks })
leaveEchoTest,
changeInputDevice: (inputDeviceId) => Service
.changeInputDevice(inputDeviceId),
changeInputStream: (inputStream) => Service.changeInputStream(inputStream),
changeOutputDevice: (outputDeviceId, isLive) => Service
.changeOutputDevice(outputDeviceId, isLive),
joinEchoTest: () => Service.joinEchoTest(),

View File

@ -71,6 +71,7 @@ class AudioSettings extends React.Component {
this.handleInputChange = this.handleInputChange.bind(this);
this.handleOutputChange = this.handleOutputChange.bind(this);
this.handleConfirmationClick = this.handleConfirmationClick.bind(this);
this.state = {
inputDeviceId,
@ -202,12 +203,32 @@ class AudioSettings extends React.Component {
) : null
}
handleConfirmationClick () {
const {
withEcho,
handleConfirmation,
} = this.props;
const {
stream,
} = this.state;
// The local echo mode is not enabled or there isn't any stream in this:
// just run the provided callback
if (!withEcho || !stream) return handleConfirmation();
// Local echo mode was enabled and there is a valid input stream => call
// the confirmation callback with the input stream as arg so it can be used
// in upstream components. The rationale is not surplus gUM calls.
// We're cloning it because the original will be cleaned up on unmount here.
const inputStream = stream.clone();
return handleConfirmation(inputStream);
}
render() {
const {
isConnecting,
intl,
handleBack,
handleConfirmation,
} = this.props;
const { inputDeviceId, outputDeviceId } = this.state;
@ -269,7 +290,7 @@ class AudioSettings extends React.Component {
size="md"
color="primary"
label={intl.formatMessage(intlMessages.retryLabel)}
onClick={handleConfirmation}
onClick={this.handleConfirmationClick}
/>
</Styled.EnterAudio>
</Styled.FormWrapper>

View File

@ -110,6 +110,7 @@ export default {
joinEchoTest: () => AudioManager.joinEchoTest(),
toggleMuteMicrophone: debounce(toggleMuteMicrophone, 500, { leading: true, trailing: false }),
changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId),
changeInputStream: (newInputStream) => AudioManager.inputStream = newInputStream,
liveChangeInputDevice: inputDeviceId => AudioManager.liveChangeInputDevice(inputDeviceId),
changeOutputDevice: (outputDeviceId, isLive) => {
if (AudioManager.outputDeviceId !== outputDeviceId) {

View File

@ -18,7 +18,7 @@ class FullAudioBroker extends BaseBroker {
this.offering = true;
// Optional parameters are: caleeName, iceServers, offering,
// mediaServer, extension, constraints
// mediaServer, extension, constraints, stream
Object.assign(this, options);
}
@ -62,6 +62,7 @@ class FullAudioBroker extends BaseBroker {
joinAudio() {
return new Promise((resolve, reject) => {
const options = {
audioStream: this.stream,
mediaConstraints: {
audio: this.constraints ? this.constraints : true,
video: false,