diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index 805e6b85360..9ace320c50c 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -949,10 +949,26 @@ GetSources(MediaEngine *engine, dom::MediaSourceEnum aSrcType, } } +template +static bool +AreUnfitSettings(const MediaTrackConstraints &aConstraints, + nsTArray>& aSources) +{ + nsTArray aggregateConstraints; + aggregateConstraints.AppendElement(&aConstraints); + + for (auto& source : aSources) { + if (source->GetBestFitnessDistance(aggregateConstraints) != UINT32_MAX) { + return false; + } + } + return true; +} + // Apply constrains to a supplied list of sources (removes items from the list) template -static void +static const char* SelectSettings(const MediaTrackConstraints &aConstraints, nsTArray>& aSources) { @@ -963,6 +979,7 @@ SelectSettings(const MediaTrackConstraints &aConstraints, // Stack constraintSets that pass, starting with the required one, because the // whole stack must be re-satisfied each time a capability-set is ruled out // (this avoids storing state or pushing algorithm into the lower-level code). + nsTArray> unsatisfactory; nsTArray aggregateConstraints; aggregateConstraints.AppendElement(&c); @@ -971,6 +988,7 @@ SelectSettings(const MediaTrackConstraints &aConstraints, for (uint32_t i = 0; i < aSources.Length();) { uint32_t distance = aSources[i]->GetBestFitnessDistance(aggregateConstraints); if (distance == UINT32_MAX) { + unsatisfactory.AppendElement(aSources[i]); aSources.RemoveElementAt(i); } else { ordered.insert(std::pair>(distance, @@ -978,6 +996,49 @@ SelectSettings(const MediaTrackConstraints &aConstraints, ++i; } } + if (!aSources.Length()) { + // None selected. The spec says to report a constraint that satisfies NONE + // of the sources. Unfortunately, this is a bit laborious to find out, and + // requires updating as new constraints are added! + + if (c.mDeviceId.IsConstrainDOMStringParameters()) { + MediaTrackConstraints fresh; + fresh.mDeviceId = c.mDeviceId; + if (AreUnfitSettings(fresh, unsatisfactory)) { + return "deviceId"; + } + } + if (c.mWidth.IsConstrainLongRange()) { + MediaTrackConstraints fresh; + fresh.mWidth = c.mWidth; + if (AreUnfitSettings(fresh, unsatisfactory)) { + return "width"; + } + } + if (c.mHeight.IsConstrainLongRange()) { + MediaTrackConstraints fresh; + fresh.mHeight = c.mHeight; + if (AreUnfitSettings(fresh, unsatisfactory)) { + return "height"; + } + } + if (c.mFrameRate.IsConstrainDoubleRange()) { + MediaTrackConstraints fresh; + fresh.mFrameRate = c.mFrameRate; + if (AreUnfitSettings(fresh, unsatisfactory)) { + return "frameRate"; + } + } + if (c.mFacingMode.IsConstrainDOMStringParameters()) { + MediaTrackConstraints fresh; + fresh.mFacingMode = c.mFacingMode; + if (AreUnfitSettings(fresh, unsatisfactory)) { + return "facingMode"; + } + } + return ""; + } + // Order devices by shortest distance for (auto& ordinal : ordered) { aSources.RemoveElement(ordinal.second); @@ -1006,9 +1067,10 @@ SelectSettings(const MediaTrackConstraints &aConstraints, } } } + return nullptr; } -static bool +static const char* SelectSettings(MediaStreamConstraints &aConstraints, nsTArray>& aSources) { @@ -1016,7 +1078,6 @@ SelectSettings(MediaStreamConstraints &aConstraints, // a candidate set is overconstrained (zero members), we must split up the // list into videos and audios, and put it back together again at the end. - bool overconstrained = false; nsTArray> videos; nsTArray> audios; @@ -1032,25 +1093,21 @@ SelectSettings(MediaStreamConstraints &aConstraints, aSources.Clear(); MOZ_ASSERT(!aSources.Length()); + const char* badConstraint = nullptr; + if (IsOn(aConstraints.mVideo)) { - SelectSettings(GetInvariant(aConstraints.mVideo), videos); - if (!videos.Length()) { - overconstrained = true; - } + badConstraint = SelectSettings(GetInvariant(aConstraints.mVideo), videos); for (auto& video : videos) { aSources.AppendElement(video); } } - if (IsOn(aConstraints.mAudio)) { - SelectSettings(GetInvariant(aConstraints.mAudio), audios); - if (!audios.Length()) { - overconstrained = true; - } + if (audios.Length() && IsOn(aConstraints.mAudio)) { + badConstraint = SelectSettings(GetInvariant(aConstraints.mAudio), audios); for (auto& audio : audios) { aSources.AppendElement(audio); } } - return !overconstrained; + return badConstraint; } /** @@ -1697,7 +1754,7 @@ MediaManager::GetUserMedia(nsPIDOMWindow* aWindow, auto& vc = c.mVideo.GetAsMediaTrackConstraints(); videoType = StringToEnum(dom::MediaSourceEnumValues::strings, vc.mMediaSource, - videoType); + dom::MediaSourceEnum::Other); switch (videoType) { case dom::MediaSourceEnum::Camera: break; @@ -1740,7 +1797,10 @@ MediaManager::GetUserMedia(nsPIDOMWindow* aWindow, case dom::MediaSourceEnum::Other: default: { nsRefPtr error = - new MediaStreamError(aWindow, NS_LITERAL_STRING("NotFoundError")); + new MediaStreamError(aWindow, + NS_LITERAL_STRING("OverconstrainedError"), + NS_LITERAL_STRING(""), + NS_LITERAL_STRING("mediaSource")); onFailure->OnError(error); return NS_OK; } @@ -1785,7 +1845,7 @@ MediaManager::GetUserMedia(nsPIDOMWindow* aWindow, auto& ac = c.mAudio.GetAsMediaTrackConstraints(); audioType = StringToEnum(dom::MediaSourceEnumValues::strings, ac.mMediaSource, - audioType); + dom::MediaSourceEnum::Other); // Work around WebIDL default since spec uses same dictionary w/audio & video. if (audioType == dom::MediaSourceEnum::Camera) { audioType = dom::MediaSourceEnum::Microphone; @@ -1812,7 +1872,10 @@ MediaManager::GetUserMedia(nsPIDOMWindow* aWindow, case dom::MediaSourceEnum::Other: default: { nsRefPtr error = - new MediaStreamError(aWindow, NS_LITERAL_STRING("NotFoundError")); + new MediaStreamError(aWindow, + NS_LITERAL_STRING("OverconstrainedError"), + NS_LITERAL_STRING(""), + NS_LITERAL_STRING("mediaSource")); onFailure->OnError(error); return NS_OK; } @@ -1905,8 +1968,19 @@ MediaManager::GetUserMedia(nsPIDOMWindow* aWindow, } // Apply any constraints. This modifies the list. - - if (!SelectSettings(c, *devices)) { + const char* badConstraint = SelectSettings(c, *devices); + if (badConstraint) { + nsString constraint; + constraint.AssignASCII(badConstraint); + nsRefPtr error = + new MediaStreamError(window, + NS_LITERAL_STRING("OverconstrainedError"), + NS_LITERAL_STRING(""), + constraint); + onFailure->OnError(error); + return; + } + if (!devices->Length()) { nsRefPtr error = new MediaStreamError(window, NS_LITERAL_STRING("NotFoundError")); onFailure->OnError(error); diff --git a/dom/media/MediaStreamError.cpp b/dom/media/MediaStreamError.cpp index 75e7d43115a..44d45b25023 100644 --- a/dom/media/MediaStreamError.cpp +++ b/dom/media/MediaStreamError.cpp @@ -28,8 +28,9 @@ BaseMediaMgrError::BaseMediaMgrError(const nsAString& aName, } else if (mName.EqualsLiteral("InternalError")) { mMessage.AssignLiteral("Internal error."); } else if (mName.EqualsLiteral("NotSupportedError")) { - mMessage.AssignLiteral("Constraints with no audio or video in it are not " - "supported"); + mMessage.AssignLiteral("The operation is not supported."); + } else if (mName.EqualsLiteral("OverconstrainedError")) { + mMessage.AssignLiteral("Constraints could be not satisfied."); } } } diff --git a/dom/media/tests/mochitest/test_enumerateDevices.html b/dom/media/tests/mochitest/test_enumerateDevices.html index 6b623d1b798..06565e26abb 100644 --- a/dom/media/tests/mochitest/test_enumerateDevices.html +++ b/dom/media/tests/mochitest/test_enumerateDevices.html @@ -16,9 +16,13 @@ function mustSucceed(msg, f) { e => is(e.name, null, msg + " must succeed: " + e.message)); } -function mustFailWith(msg, reason, f) { - return f().then(() => ok(false, msg + " must fail"), - e => is(e.name, reason, msg + " must fail: " + e.message)); +function mustFailWith(msg, reason, constraint, f) { + return f().then(() => ok(false, msg + " must fail"), e => { + is(e.name, reason, msg + " must fail: " + e.message); + if (constraint) { + is(e.constraint, constraint, msg + " must fail w/correct constraint."); + } + }); } var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r)); @@ -49,13 +53,15 @@ runTest(() => audio: { deviceId: "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=" }, fake: true, }))) - .then(() => mustFailWith("unknown exact deviceId on video", "NotFoundError", - () => navigator.mediaDevices.getUserMedia({ + .then(() => mustFailWith("unknown exact deviceId on video", + "OverconstrainedError", "deviceId", + () => navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=" } }, fake: true, }))) - .then(() => mustFailWith("unknown exact deviceId on audio", "NotFoundError", - () => navigator.mediaDevices.getUserMedia({ + .then(() => mustFailWith("unknown exact deviceId on audio", + "OverconstrainedError", "deviceId", + () => navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=" } }, fake: true, }))) diff --git a/dom/media/tests/mochitest/test_getUserMedia_constraints.html b/dom/media/tests/mochitest/test_getUserMedia_constraints.html index f0dc97cc870..e076060914d 100644 --- a/dom/media/tests/mochitest/test_getUserMedia_constraints.html +++ b/dom/media/tests/mochitest/test_getUserMedia_constraints.html @@ -39,9 +39,14 @@ var tests = [ { message: "browser screensharing requires permission", constraints: { video: { mediaSource: 'browser' } }, error: "PermissionDeniedError" }, - { message: "unknown mediaSource fails", + { message: "unknown mediaSource in video fails", constraints: { video: { mediaSource: 'uncle' } }, - error: "NotFoundError" }, + error: "OverconstrainedError", + constraint: "mediaSource" }, + { message: "unknown mediaSource in audio fails", + constraints: { audio: { mediaSource: 'uncle' } }, + error: "OverconstrainedError", + constraint: "mediaSource" }, { message: "emtpy constraint fails", constraints: { }, error: "NotSupportedError" }, @@ -106,8 +111,13 @@ runTest(function() { return tests.reduce((p, test) => p.then(() => navigator.mediaDevices.getUserMedia(test.constraints)) - .then(() => is(null, test.error, test.message), - e => is(e.name, test.error, test.message + ": " + e.message)), p); + .then(() => is(null, test.error, test.message), e => { + is(e.name, test.error, test.message + ": " + e.message); + if (test.constraint) { + is(e.constraint, test.constraint, + test.message + " w/correct constraint."); + } + }), p); });