Bug 1033885 - return MediaStreamError instead of error strings, to spec. r=bz, r=jesup

This commit is contained in:
Jan-Ivar Bruaroey 2014-10-27 15:42:56 -04:00
parent d7d2397713
commit 54d43d8dad
14 changed files with 180 additions and 64 deletions

View File

@ -43,7 +43,7 @@ function runNext() {
ok(false, 'unexpected success, permission request should be denied');
runNext();
}, function failure(err) {
is(err.toLowerCase(), 'permission denied', 'expected permission denied');
is(err.name, 'PermissionDeniedError', 'expected permission denied');
runNext();
});
} else {

View File

@ -223,6 +223,8 @@ function checkNotSharing() {
assertWebRTCIndicatorStatus(null);
}
const permissionError = "error: PermissionDeniedError: The user did not grant permission for the operation.";
let gTests = [
{
@ -381,7 +383,7 @@ let gTests = [
enableDevice("Camera", false);
enableDevice("Microphone", false);
yield promiseMessage("error: PERMISSION_DENIED", () => {
yield promiseMessage(permissionError, () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -405,7 +407,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
yield promiseMessage("error: PERMISSION_DENIED", () => {
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
@ -482,7 +484,7 @@ let gTests = [
enableDevice("Camera", aAllowVideo || aNever);
let expectedMessage =
(aAllowVideo || aAllowAudio) ? "ok" : "error: PERMISSION_DENIED";
(aAllowVideo || aAllowAudio) ? "ok" : permissionError;
yield promiseMessage(expectedMessage, () => {
activateSecondaryAction(aNever ? kActionNever : kActionAlways);
});
@ -587,14 +589,14 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
// Deny the request to cleanup...
yield promiseMessage("error: PERMISSION_DENIED", () => {
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
expectObserverCalled("getUserMedia:response:deny");
expectObserverCalled("recording-window-ended");
}
else {
let expectedMessage = aExpectStream ? "ok" : "error: PERMISSION_DENIED";
let expectedMessage = aExpectStream ? "ok" : permissionError;
yield promiseMessage(expectedMessage, gum);
if (expectedMessage == "ok") {

View File

@ -64,7 +64,7 @@ function handleRequest(aSubject, aTopic, aData) {
constraints, devices, secure);
},
function (error) {
// bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND
// bug 827146 -- In the future, the UI should catch NotFoundError
// and allow the user to plug in a device, instead of immediately failing.
denyRequest({callID: aSubject.callID}, error);
},
@ -108,7 +108,7 @@ function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSec
requestTypes.push("Microphone");
if (!requestTypes.length) {
denyRequest({callID: aCallID}, "NO_DEVICES_FOUND");
denyRequest({callID: aCallID}, "NotFoundError");
return;
}

View File

@ -102,6 +102,7 @@ GetMediaManagerLog()
using dom::MediaStreamConstraints; // Outside API (contains JSObject)
using dom::MediaTrackConstraintSet; // Mandatory or optional constraints
using dom::MediaTrackConstraints; // Raw mMandatory (as JSObject)
using dom::MediaStreamError;
using dom::GetUserMediaRequest;
using dom::Sequence;
using dom::OwningBooleanOrMediaTrackConstraints;
@ -196,8 +197,9 @@ HostHasPermission(nsIURI &docURI)
ErrorCallbackRunnable::ErrorCallbackRunnable(
nsCOMPtr<nsIDOMGetUserMediaSuccessCallback>& aOnSuccess,
nsCOMPtr<nsIDOMGetUserMediaErrorCallback>& aOnFailure,
const nsAString& aErrorMsg, uint64_t aWindowID)
: mErrorMsg(aErrorMsg)
MediaMgrError& aError,
uint64_t aWindowID)
: mError(&aError)
, mWindowID(aWindowID)
, mManager(MediaManager::GetInstance())
{
@ -224,7 +226,11 @@ ErrorCallbackRunnable::Run()
}
// This is safe since we're on main-thread, and the windowlist can only
// be invalidated from the main-thread (see OnNavigation)
onFailure->OnError(mErrorMsg);
nsGlobalWindow* window = nsGlobalWindow::GetInnerWindowWithId(mWindowID);
if (window) {
nsRefPtr<MediaStreamError> error = new MediaStreamError(window, *mError);
onFailure->OnError(error);
}
return NS_OK;
}
@ -315,7 +321,12 @@ public:
// We should in the future return an empty array, and dynamically add
// devices to the dropdowns if things are hotplugged while the
// requester is up.
mOnFailure->OnError(NS_LITERAL_STRING("NO_DEVICES_FOUND"));
nsGlobalWindow* window = nsGlobalWindow::GetInnerWindowWithId(mWindowID);
if (window) {
nsRefPtr<MediaStreamError> error = new MediaStreamError(window,
NS_LITERAL_STRING("NotFoundError"));
mOnFailure->OnError(error);
}
return NS_OK;
}
@ -830,9 +841,16 @@ public:
nsDOMUserMediaStream::CreateTrackUnionStream(window, mListener,
mAudioSource, mVideoSource);
if (!trackunion) {
nsCOMPtr<nsIDOMGetUserMediaErrorCallback> error = mOnFailure.forget();
nsCOMPtr<nsIDOMGetUserMediaErrorCallback> onFailure = mOnFailure.forget();
LOG(("Returning error for getUserMedia() - no stream"));
error->OnError(NS_LITERAL_STRING("NO_STREAM"));
nsGlobalWindow* window = nsGlobalWindow::GetInnerWindowWithId(mWindowID);
if (window) {
nsRefPtr<MediaStreamError> error = new MediaStreamError(window,
NS_LITERAL_STRING("InternalError"),
NS_LITERAL_STRING("No stream."));
onFailure->OnError(error);
}
return NS_OK;
}
trackunion->AudioConfig(aec_on, (uint32_t) aec,
@ -1097,9 +1115,11 @@ public:
}
void
Fail(const nsAString& aMessage) {
Fail(const nsAString& aName,
const nsAString& aMessage = EmptyString()) {
nsRefPtr<MediaMgrError> error = new MediaMgrError(aName, aMessage);
nsRefPtr<ErrorCallbackRunnable> runnable =
new ErrorCallbackRunnable(mOnSuccess, mOnFailure, aMessage, mWindowID);
new ErrorCallbackRunnable(mOnSuccess, mOnFailure, *error, mWindowID);
// These should be empty now
MOZ_ASSERT(!mOnSuccess);
MOZ_ASSERT(!mOnFailure);
@ -1136,7 +1156,8 @@ public:
}
nsresult
Denied(const nsAString& aErrorMsg)
Denied(const nsAString& aName,
const nsAString& aMessage = EmptyString())
{
MOZ_ASSERT(mOnSuccess);
MOZ_ASSERT(mOnFailure);
@ -1148,15 +1169,20 @@ public:
// be invalidated from the main-thread (see OnNavigation)
nsCOMPtr<nsIDOMGetUserMediaSuccessCallback> onSuccess = mOnSuccess.forget();
nsCOMPtr<nsIDOMGetUserMediaErrorCallback> onFailure = mOnFailure.forget();
onFailure->OnError(aErrorMsg);
nsGlobalWindow* window = nsGlobalWindow::GetInnerWindowWithId(mWindowID);
if (window) {
nsRefPtr<MediaStreamError> error = new MediaStreamError(window,
aName, aMessage);
onFailure->OnError(error);
}
// Should happen *after* error runs for consistency, but may not matter
nsRefPtr<MediaManager> manager(MediaManager::GetInstance());
manager->RemoveFromWindowList(mWindowID, mListener);
} else {
// This will re-check the window being alive on main-thread
// Note: we must remove the listener on MainThread as well
Fail(aErrorMsg);
Fail(aName, aMessage);
// MUST happen after ErrorCallbackRunnable Run()s, as it checks the active window list
NS_DispatchToMainThread(new GetUserMediaListenerRemove(mWindowID, mListener));
@ -1202,7 +1228,7 @@ public:
GetSources(backend, constraints, &MediaEngine::EnumerateVideoDevices, sources);
if (!sources.Length()) {
Fail(NS_LITERAL_STRING("NO_DEVICES_FOUND"));
Fail(NS_LITERAL_STRING("NotFoundError"));
return NS_ERROR_FAILURE;
}
// Pick the first available device.
@ -1215,7 +1241,7 @@ public:
GetSources(backend, constraints, &MediaEngine::EnumerateAudioDevices, sources);
if (!sources.Length()) {
Fail(NS_LITERAL_STRING("NO_DEVICES_FOUND"));
Fail(NS_LITERAL_STRING("NotFoundError"));
return NS_ERROR_FAILURE;
}
// Pick the first available device.
@ -1241,7 +1267,8 @@ public:
rv = aAudioSource->Allocate(GetInvariant(mConstraints.mAudio), mPrefs);
if (NS_FAILED(rv)) {
LOG(("Failed to allocate audiosource %d",rv));
Fail(NS_LITERAL_STRING("HARDWARE_UNAVAILABLE"));
Fail(NS_LITERAL_STRING("SourceUnavailableError"),
NS_LITERAL_STRING("Failed to allocate audiosource"));
return;
}
}
@ -1252,7 +1279,8 @@ public:
if (aAudioSource) {
aAudioSource->Deallocate();
}
Fail(NS_LITERAL_STRING("HARDWARE_UNAVAILABLE"));
Fail(NS_LITERAL_STRING("SourceUnavailableError"),
NS_LITERAL_STRING("Failed to allocate videosource"));
return;
}
}
@ -1655,10 +1683,10 @@ MediaManager::GetUserMedia(
if (tc.mMediaSource != dom::MediaSourceEnum::Camera) {
if (tc.mMediaSource == dom::MediaSourceEnum::Browser) {
if (!Preferences::GetBool("media.getusermedia.browser.enabled", false)) {
return task->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
return task->Denied(NS_LITERAL_STRING("PermissionDeniedError"));
}
} else if (!Preferences::GetBool("media.getusermedia.screensharing.enabled", false)) {
return task->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
return task->Denied(NS_LITERAL_STRING("PermissionDeniedError"));
}
/* Deny screensharing if the requesting document is not from a host
on the whitelist. */
@ -1676,7 +1704,7 @@ MediaManager::GetUserMedia(
) ||
#endif
(!privileged && !HostHasPermission(*docURI))) {
return task->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
return task->Denied(NS_LITERAL_STRING("PermissionDeniedError"));
}
}
}
@ -1730,7 +1758,7 @@ MediaManager::GetUserMedia(
if ((!IsOn(c.mAudio) || audioPerm == nsIPermissionManager::DENY_ACTION) &&
(!IsOn(c.mVideo) || videoPerm == nsIPermissionManager::DENY_ACTION)) {
return task->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
return task->Denied(NS_LITERAL_STRING("PermissionDeniedError"));
}
// Ask for user permission, and dispatch task (or not) when a response
@ -2020,7 +2048,7 @@ MediaManager::Observe(nsISupports* aSubject, const char* aTopic,
MOZ_ASSERT(len);
if (!len) {
// neither audio nor video were selected
task->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
task->Denied(NS_LITERAL_STRING("PermissionDeniedError"));
return NS_OK;
}
for (uint32_t i = 0; i < len; i++) {
@ -2047,14 +2075,14 @@ MediaManager::Observe(nsISupports* aSubject, const char* aTopic,
return NS_OK;
} else if (!strcmp(aTopic, "getUserMedia:response:deny")) {
nsString errorMessage(NS_LITERAL_STRING("PERMISSION_DENIED"));
nsString errorMessage(NS_LITERAL_STRING("PermissionDeniedError"));
if (aSubject) {
nsCOMPtr<nsISupportsString> msg(do_QueryInterface(aSubject));
MOZ_ASSERT(msg);
msg->GetData(errorMessage);
if (errorMessage.IsEmpty())
errorMessage.AssignLiteral(MOZ_UTF16("UNKNOWN_ERROR"));
errorMessage.AssignLiteral(MOZ_UTF16("InternalError"));
}
nsString key(aData);

View File

@ -26,6 +26,7 @@
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/MediaStreamBinding.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "mozilla/dom/MediaStreamError.h"
#include "prlog.h"
#include "DOMMediaStream.h"
@ -298,7 +299,7 @@ class MediaManager;
class GetUserMediaTask;
/**
* Send an error back to content. The error is the form a string.
* Send an error back to content.
* Do this only on the main thread. The onSuccess callback is also passed here
* so it can be released correctly.
*/
@ -306,16 +307,16 @@ class ErrorCallbackRunnable : public nsRunnable
{
public:
ErrorCallbackRunnable(
nsCOMPtr<nsIDOMGetUserMediaSuccessCallback>& aSuccess,
nsCOMPtr<nsIDOMGetUserMediaErrorCallback>& aError,
const nsAString& aErrorMsg, uint64_t aWindowID);
nsCOMPtr<nsIDOMGetUserMediaSuccessCallback>& aOnSuccess,
nsCOMPtr<nsIDOMGetUserMediaErrorCallback>& aOnFailure,
MediaMgrError& aError, uint64_t aWindowID);
NS_IMETHOD Run();
private:
~ErrorCallbackRunnable();
nsCOMPtr<nsIDOMGetUserMediaSuccessCallback> mOnSuccess;
nsCOMPtr<nsIDOMGetUserMediaErrorCallback> mOnFailure;
const nsString mErrorMsg;
nsRefPtr<MediaMgrError> mError;
uint64_t mWindowID;
nsRefPtr<MediaManager> mManager; // get ref to this when creating the runnable
};
@ -373,10 +374,12 @@ public:
mOnTracksAvailableCallback.forget()));
nsString log;
log.AssignASCII(errorLog, strlen(errorLog));
log.AssignASCII(errorLog);
nsCOMPtr<nsIDOMGetUserMediaSuccessCallback> onSuccess;
nsRefPtr<MediaMgrError> error = new MediaMgrError(
NS_LITERAL_STRING("InternalError"), log);
NS_DispatchToMainThread(new ErrorCallbackRunnable(onSuccess, mOnFailure,
log, mWindowID));
*error, mWindowID));
}
void

View File

@ -17,6 +17,7 @@
#include "GetUserMediaRequest.h"
#include "mozilla/dom/PBrowserChild.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "mozilla/dom/MediaStreamError.h"
#include "nsISupportsPrimitives.h"
#include "nsServiceManagerUtils.h"
#include "nsArrayUtils.h"
@ -224,7 +225,7 @@ MediaPermissionRequest::Cancel()
{
nsString callID;
mRequest->GetCallID(callID);
NotifyPermissionDeny(callID, NS_LITERAL_STRING("Permission Denied"));
NotifyPermissionDeny(callID, NS_LITERAL_STRING("PermissionDeniedError"));
return NS_OK;
}
@ -393,9 +394,17 @@ NS_IMPL_ISUPPORTS(MediaDeviceErrorCallback, nsIDOMGetUserMediaErrorCallback)
// nsIDOMGetUserMediaErrorCallback method
NS_IMETHODIMP
MediaDeviceErrorCallback::OnError(const nsAString &aError)
MediaDeviceErrorCallback::OnError(nsISupports* aError)
{
return NotifyPermissionDeny(mCallID, aError);
MediaStreamError *error = nullptr;
nsresult rv = CallQueryInterface(aError, &error);
if (NS_FAILED(rv)) {
return rv;
}
nsString name;
error->GetName(name);
return NotifyPermissionDeny(mCallID, name);
}
} // namespace anonymous

View File

@ -9,6 +9,31 @@
#include "nsContentUtils.h"
namespace mozilla {
BaseMediaMgrError::BaseMediaMgrError(const nsAString& aName,
const nsAString& aMessage,
const nsAString& aConstraintName)
: mName(aName)
, mMessage(aMessage)
, mConstraintName(aConstraintName)
{
if (mMessage.IsEmpty()) {
if (mName.EqualsLiteral("NotFoundError")) {
mMessage.AssignLiteral("The object can not be found here.");
} else if (mName.EqualsLiteral("PermissionDeniedError")) {
mMessage.AssignLiteral("The user did not grant permission for the operation.");
} else if (mName.EqualsLiteral("SourceUnavailableError")) {
mMessage.AssignLiteral("The source of the MediaStream could not be "
"accessed due to a hardware error (e.g. lock from another process).");
} else if (mName.EqualsLiteral("InternalError")) {
mMessage.AssignLiteral("Internal error.");
}
}
}
NS_IMPL_ISUPPORTS0(MediaMgrError)
namespace dom {
MediaStreamError::MediaStreamError(
@ -16,10 +41,8 @@ MediaStreamError::MediaStreamError(
const nsAString& aName,
const nsAString& aMessage,
const nsAString& aConstraintName)
: mParent(aParent)
, mName(aName)
, mMessage(aMessage)
, mConstraintName(aConstraintName) {}
: BaseMediaMgrError(aName, aMessage, aConstraintName)
, mParent(aParent) {}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaStreamError, mParent)
NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaStreamError)

View File

@ -21,16 +21,59 @@
namespace mozilla {
namespace dom {
class MediaStreamError : public nsISupports, public nsWrapperCache
#define MOZILLA_DOM_MEDIASTREAMERROR_IMPLEMENTATION_IID \
{ 0x95fa29aa, 0x0cc2, 0x4698, \
{ 0x9d, 0xa9, 0xf2, 0xeb, 0x03, 0x91, 0x0b, 0xd1 } }
class MediaStreamError;
}
class BaseMediaMgrError
{
friend class dom::MediaStreamError;
protected:
BaseMediaMgrError(const nsAString& aName,
const nsAString& aMessage,
const nsAString& aConstraintName);
const nsString mName;
nsString mMessage;
const nsString mConstraintName;
};
class MediaMgrError MOZ_FINAL : public nsISupports,
public BaseMediaMgrError
{
public:
MediaMgrError(const nsAString& aName,
const nsAString& aMessage = EmptyString(),
const nsAString& aConstraintName = EmptyString())
: BaseMediaMgrError(aName, aMessage, aConstraintName) {}
NS_DECL_THREADSAFE_ISUPPORTS
private:
~MediaMgrError() {}
};
namespace dom {
class MediaStreamError MOZ_FINAL : public nsISupports,
public BaseMediaMgrError,
public nsWrapperCache
{
public:
MediaStreamError(nsPIDOMWindow* aParent,
const nsAString& aName,
const nsAString& aMessage,
const nsAString& aConstraintName);
const nsAString& aMessage = EmptyString(),
const nsAString& aConstraintName = EmptyString());
MediaStreamError(nsPIDOMWindow* aParent,
const BaseMediaMgrError& aOther)
: BaseMediaMgrError(aOther.mName, aOther.mMessage, aOther.mConstraintName)
, mParent(aParent) {}
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MediaStreamError)
NS_DECLARE_STATIC_IID_ACCESSOR(MOZILLA_DOM_MEDIASTREAMERROR_IMPLEMENTATION_IID)
virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
@ -46,11 +89,10 @@ private:
virtual ~MediaStreamError() {}
nsRefPtr<nsPIDOMWindow> mParent;
const nsString mName;
const nsString mMessage;
const nsString mConstraintName;
};
NS_DEFINE_STATIC_IID_ACCESSOR(MediaStreamError,
MOZILLA_DOM_MEDIASTREAMERROR_IMPLEMENTATION_IID)
} // namespace dom
} // namespace mozilla

View File

@ -34,5 +34,5 @@ interface nsIDOMGetUserMediaSuccessCallback : nsISupports
[scriptable, function, uuid(2614bbcf-85cc-43e5-8740-964f52bdc7ca)]
interface nsIDOMGetUserMediaErrorCallback : nsISupports
{
void onError(in DOMString error);
void onError(in nsISupports error);
};

View File

@ -14,19 +14,19 @@ var common_tests = [
{ message: "unknown required constraint on video fails",
constraints: { video: { somethingUnknown:0, require:["somethingUnknown"] },
fake: true },
error: "NO_DEVICES_FOUND" },
error: "NotFoundError" },
{ message: "unknown required constraint on audio fails",
constraints: { audio: { somethingUnknown:0, require:["somethingUnknown"] },
fake: true },
error: "NO_DEVICES_FOUND" },
error: "NotFoundError" },
{ message: "video overconstrained by facingMode fails",
constraints: { video: { facingMode:'left', require:["facingMode"] },
fake: true },
error: "NO_DEVICES_FOUND" },
error: "NotFoundError" },
{ message: "audio overconstrained by facingMode fails",
constraints: { audio: { facingMode:'left', require:["facingMode"] },
fake: true },
error: "NO_DEVICES_FOUND" },
error: "NotFoundError" },
{ message: "Success-path: optional video facingMode + audio ignoring facingMode",
constraints: { fake: true,
audio: { facingMode:'left',
@ -61,8 +61,8 @@ function testConstraints(tests) {
next();
}
function Failure(err) {
ok(tests[i].error? (err === tests[i].error) : false,
tests[i].message + " (err=" + err + ")");
ok(tests[i].error? (err.name == tests[i].error) : false,
tests[i].message + " (err=" + err.name + ")");
i++;
next();
}

View File

@ -26,7 +26,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=882145
var mobile_tests = [
{ message: "legacy facingMode overconstrains video (mobile)",
constraints: { video: { mandatory: { facingMode:'left' } }, fake: true },
error: "NO_DEVICES_FOUND" },
error: "NotFoundError" },
];
runTest(function () {

View File

@ -11,6 +11,7 @@
#include "mozilla/dom/SpeechRecognitionBinding.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "mozilla/dom/MediaStreamError.h"
#include "mozilla/MediaManager.h"
#include "mozilla/Services.h"
@ -950,19 +951,26 @@ SpeechRecognition::GetUserMediaSuccessCallback::OnSuccess(nsISupports* aStream)
NS_IMPL_ISUPPORTS(SpeechRecognition::GetUserMediaErrorCallback, nsIDOMGetUserMediaErrorCallback)
NS_IMETHODIMP
SpeechRecognition::GetUserMediaErrorCallback::OnError(const nsAString& aError)
SpeechRecognition::GetUserMediaErrorCallback::OnError(nsISupports* aError)
{
nsRefPtr<MediaStreamError> error = do_QueryObject(aError);
if (!error) {
return NS_OK;
}
SpeechRecognitionErrorCode errorCode;
if (aError.EqualsLiteral("PERMISSION_DENIED")) {
nsString name;
error->GetName(name);
if (name.EqualsLiteral("PERMISSION_DENIED")) {
errorCode = SpeechRecognitionErrorCode::Not_allowed;
} else {
errorCode = SpeechRecognitionErrorCode::Audio_capture;
}
nsString message;
error->GetMessage(message);
mRecognition->DispatchError(SpeechRecognition::EVENT_AUDIO_ERROR, errorCode,
aError);
message);
return NS_OK;
}

View File

@ -354,7 +354,7 @@ partial interface Navigator {
#ifdef MOZ_MEDIA_NAVIGATOR
callback NavigatorUserMediaSuccessCallback = void (MediaStream stream);
callback NavigatorUserMediaErrorCallback = void (DOMString error);
callback NavigatorUserMediaErrorCallback = void (MediaStreamError error);
partial interface Navigator {
[Throws, Func="Navigator::HasUserMediaSupport"]

View File

@ -27,7 +27,8 @@ function test() {
loadWebapp("getUserMedia.webapp", undefined, function onLoad() {
let msg = gAppBrowser.contentDocument.getElementById("msg");
mutObserver = new MutationObserver(function(mutations) {
is(msg.textContent, "PERMISSION_DENIED", "getUserMedia permission denied.");
is(msg.textContent, "PermissionDeniedError",
"getUserMedia permission denied.");
ok(getUserMediaDialogOpened, "Prompt shown.");
finish();
});