Skip to content
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
44 changes: 43 additions & 1 deletion src/main/js/media_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export class MediaManager {
*/
this.volumeMeterUrl = 'volume-meter-processor.js';

/**
* If camera permission is denied during initialization but microphone is granted,
* fallback to audio-only initialization automatically
*/
this.fallbackToAudioIfVideoPermissionDenied = true;

/**
* The values of the above fields are provided as user parameters by the constructor.
* TODO: Also some other hidden parameters may be passed here
Expand Down Expand Up @@ -261,7 +267,27 @@ export class MediaManager {
this.checkWebRTCPermissions();
if (typeof this.mediaConstraints.video != "undefined" && this.mediaConstraints.video != false)
{
return this.openStream(this.mediaConstraints);
return this.openStream(this.mediaConstraints).catch(error => {
// If camera access is denied but audio is allowed, optionally fallback to audio-only
if (this.fallbackToAudioIfVideoPermissionDenied && this.isCameraPermissionError(error)) {
// if audio is requested (true or constraints object), try audio-only
if (typeof this.mediaConstraints.audio == "undefined" || this.mediaConstraints.audio == false) {
// no audio requested, cannot fallback
throw error;
}

const retryConstraints = { video: false, audio: this.mediaConstraints.audio };
return this.navigatorUserMedia(retryConstraints).then((stream) => {
return this.gotStream(stream).then(() => {
this.callback("audio_only_fallback", { reason: error.name });
});
}).catch((e) => {
// propagate original error
throw error;
});
}
throw error;
});
}
else if (typeof this.mediaConstraints.audio != "undefined" && this.mediaConstraints.audio != false)
{
Expand Down Expand Up @@ -299,6 +325,21 @@ export class MediaManager {
}
}

/**
* Returns true if the error corresponds to camera permission/availability issues
*/
isCameraPermissionError(error) {
if (!error) {
return false;
}
const name = error.name || "";
return name == "NotAllowedError" ||
name == "NotFoundError" ||
name == "NotReadableError" ||
name == "OverconstrainedError" ||
name == "SecurityError";
}

/*
* Called to get the available video and audio devices on the system
*/
Expand Down Expand Up @@ -679,6 +720,7 @@ export class MediaManager {
this.getDevices()
} else {
this.callbackError(error.name, error.message);
throw error;
}
});
}
Expand Down
74 changes: 74 additions & 0 deletions src/test/js/media_manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,4 +631,78 @@ describe("MediaManager", function () {

});

it("fallbacks to audio-only when camera permission denied", async function () {
var adaptor = new WebRTCAdaptor({
websocketURL: "ws://example.com",
initializeComponents: false,
mediaConstraints: {
video: true,
audio: true
}
});

// create a real audio track via WebAudio
var audioContext = new (window.AudioContext || window.webkitAudioContext)();
var oscillator = audioContext.createOscillator();
oscillator.type = "sine";
oscillator.frequency.value = 440;
var destination = audioContext.createMediaStreamDestination();
oscillator.connect(destination);
oscillator.start();
var audioTrack = destination.stream.getAudioTracks()[0];

// stub getUserMedia: reject when video requested, resolve when audio-only
var originalGetUserMedia = navigator.mediaDevices.getUserMedia;
navigator.mediaDevices.getUserMedia = async (constraints) => {
if (constraints && constraints.video !== false) {
throw new DOMException("Permission denied", "NotAllowedError");
}
return new MediaStream([audioTrack]);
};

var fallbackEventFired = false;
adaptor.addEventListener((info, obj) => {
if (info === "audio_only_fallback") {
fallbackEventFired = true;
}
});

await adaptor.mediaManager.initLocalStream();

expect(fallbackEventFired).to.be.true;
expect(adaptor.mediaManager.localStream.getAudioTracks().length).to.be.equal(1);
expect(adaptor.mediaManager.localStream.getVideoTracks().length).to.be.equal(0);

navigator.mediaDevices.getUserMedia = originalGetUserMedia;
});

it("does not fallback when disabled and camera permission denied", async function () {
var adaptor = new WebRTCAdaptor({
websocketURL: "ws://example.com",
initializeComponents: false,
mediaConstraints: {
video: true,
audio: true
},
fallbackToAudioIfVideoPermissionDenied: false
});

var originalGetUserMedia = navigator.mediaDevices.getUserMedia;
navigator.mediaDevices.getUserMedia = async (constraints) => {
throw new DOMException("Permission denied", "NotAllowedError");
};

let failed = false;
try {
await adaptor.mediaManager.initLocalStream();
} catch (e) {
failed = true;
expect(e.name).to.be.equal("NotAllowedError");
}

expect(failed).to.be.true;

navigator.mediaDevices.getUserMedia = originalGetUserMedia;
});

});