michael@0: /* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "MediaManager.h" michael@0: michael@0: #include "MediaStreamGraph.h" michael@0: #include "GetUserMediaRequest.h" michael@0: #include "nsHashPropertyBag.h" michael@0: #ifdef MOZ_WIDGET_GONK michael@0: #include "nsIAudioManager.h" michael@0: #endif michael@0: #include "nsIDOMFile.h" michael@0: #include "nsIEventTarget.h" michael@0: #include "nsIUUIDGenerator.h" michael@0: #include "nsIScriptGlobalObject.h" michael@0: #include "nsIPermissionManager.h" michael@0: #include "nsIPopupWindowManager.h" michael@0: #include "nsISupportsArray.h" michael@0: #include "nsIDocShell.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsISupportsPrimitives.h" michael@0: #include "nsIInterfaceRequestorUtils.h" michael@0: #include "mozilla/Types.h" michael@0: #include "mozilla/dom/ContentChild.h" michael@0: #include "mozilla/dom/MediaStreamBinding.h" michael@0: #include "mozilla/dom/MediaStreamTrackBinding.h" michael@0: #include "mozilla/dom/GetUserMediaRequestBinding.h" michael@0: #include "MediaTrackConstraints.h" michael@0: michael@0: #include "Latency.h" michael@0: michael@0: // For PR_snprintf michael@0: #include "prprf.h" michael@0: michael@0: #include "nsJSUtils.h" michael@0: #include "nsDOMFile.h" michael@0: #include "nsGlobalWindow.h" michael@0: michael@0: /* Using WebRTC backend on Desktops (Mac, Windows, Linux), otherwise default */ michael@0: #include "MediaEngineDefault.h" michael@0: #if defined(MOZ_WEBRTC) michael@0: #include "MediaEngineWebRTC.h" michael@0: #endif michael@0: michael@0: #ifdef MOZ_B2G michael@0: #include "MediaPermissionGonk.h" michael@0: #endif michael@0: michael@0: // GetCurrentTime is defined in winbase.h as zero argument macro forwarding to michael@0: // GetTickCount() and conflicts with MediaStream::GetCurrentTime. michael@0: #ifdef GetCurrentTime michael@0: #undef GetCurrentTime michael@0: #endif michael@0: michael@0: namespace mozilla { michael@0: michael@0: #ifdef LOG michael@0: #undef LOG michael@0: #endif michael@0: michael@0: #ifdef PR_LOGGING michael@0: PRLogModuleInfo* michael@0: GetMediaManagerLog() michael@0: { michael@0: static PRLogModuleInfo *sLog; michael@0: if (!sLog) michael@0: sLog = PR_NewLogModule("MediaManager"); michael@0: return sLog; michael@0: } michael@0: #define LOG(msg) PR_LOG(GetMediaManagerLog(), PR_LOG_DEBUG, msg) michael@0: #else michael@0: #define LOG(msg) michael@0: #endif michael@0: michael@0: using dom::MediaStreamConstraints; // Outside API (contains JSObject) michael@0: using dom::MediaTrackConstraintSet; // Mandatory or optional constraints michael@0: using dom::MediaTrackConstraints; // Raw mMandatory (as JSObject) michael@0: using dom::GetUserMediaRequest; michael@0: using dom::Sequence; michael@0: using dom::OwningBooleanOrMediaTrackConstraints; michael@0: using dom::SupportedAudioConstraints; michael@0: using dom::SupportedVideoConstraints; michael@0: michael@0: ErrorCallbackRunnable::ErrorCallbackRunnable( michael@0: nsCOMPtr& aSuccess, michael@0: nsCOMPtr& aError, michael@0: const nsAString& aErrorMsg, uint64_t aWindowID) michael@0: : mErrorMsg(aErrorMsg) michael@0: , mWindowID(aWindowID) michael@0: , mManager(MediaManager::GetInstance()) michael@0: { michael@0: mSuccess.swap(aSuccess); michael@0: mError.swap(aError); michael@0: } michael@0: michael@0: ErrorCallbackRunnable::~ErrorCallbackRunnable() michael@0: { michael@0: MOZ_ASSERT(!mSuccess && !mError); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: ErrorCallbackRunnable::Run() michael@0: { michael@0: // Only run if the window is still active. michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: michael@0: nsCOMPtr success = mSuccess.forget(); michael@0: nsCOMPtr error = mError.forget(); michael@0: michael@0: if (!(mManager->IsWindowStillActive(mWindowID))) { michael@0: return NS_OK; michael@0: } michael@0: // This is safe since we're on main-thread, and the windowlist can only michael@0: // be invalidated from the main-thread (see OnNavigation) michael@0: error->OnError(mErrorMsg); michael@0: return NS_OK; michael@0: } michael@0: michael@0: /** michael@0: * Invoke the "onSuccess" callback in content. The callback will take a michael@0: * DOMBlob in the case of {picture:true}, and a MediaStream in the case of michael@0: * {audio:true} or {video:true}. There is a constructor available for each michael@0: * form. Do this only on the main thread. michael@0: */ michael@0: class SuccessCallbackRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: SuccessCallbackRunnable( michael@0: nsCOMPtr& aSuccess, michael@0: nsCOMPtr& aError, michael@0: nsIDOMFile* aFile, uint64_t aWindowID) michael@0: : mFile(aFile) michael@0: , mWindowID(aWindowID) michael@0: , mManager(MediaManager::GetInstance()) michael@0: { michael@0: mSuccess.swap(aSuccess); michael@0: mError.swap(aError); michael@0: } michael@0: michael@0: NS_IMETHOD michael@0: Run() michael@0: { michael@0: // Only run if the window is still active. michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: michael@0: nsCOMPtr success = mSuccess.forget(); michael@0: nsCOMPtr error = mError.forget(); michael@0: michael@0: if (!(mManager->IsWindowStillActive(mWindowID))) { michael@0: return NS_OK; michael@0: } michael@0: // This is safe since we're on main-thread, and the windowlist can only michael@0: // be invalidated from the main-thread (see OnNavigation) michael@0: success->OnSuccess(mFile); michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: nsCOMPtr mSuccess; michael@0: nsCOMPtr mError; michael@0: nsCOMPtr mFile; michael@0: uint64_t mWindowID; michael@0: nsRefPtr mManager; // get ref to this when creating the runnable michael@0: }; michael@0: michael@0: /** michael@0: * Invoke the GetUserMediaDevices success callback. Wrapped in a runnable michael@0: * so that it may be called on the main thread. The error callback is also michael@0: * passed so it can be released correctly. michael@0: */ michael@0: class DeviceSuccessCallbackRunnable: public nsRunnable michael@0: { michael@0: public: michael@0: DeviceSuccessCallbackRunnable( michael@0: uint64_t aWindowID, michael@0: nsCOMPtr& aSuccess, michael@0: nsCOMPtr& aError, michael@0: nsTArray >* aDevices) michael@0: : mDevices(aDevices) michael@0: , mWindowID(aWindowID) michael@0: , mManager(MediaManager::GetInstance()) michael@0: { michael@0: mSuccess.swap(aSuccess); michael@0: mError.swap(aError); michael@0: } michael@0: michael@0: NS_IMETHOD michael@0: Run() michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: michael@0: // Only run if window is still on our active list. michael@0: if (!mManager->IsWindowStillActive(mWindowID)) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsCOMPtr devices = michael@0: do_CreateInstance("@mozilla.org/variant;1"); michael@0: michael@0: int32_t len = mDevices->Length(); michael@0: if (len == 0) { michael@0: // XXX michael@0: // We should in the future return an empty array, and dynamically add michael@0: // devices to the dropdowns if things are hotplugged while the michael@0: // requester is up. michael@0: mError->OnError(NS_LITERAL_STRING("NO_DEVICES_FOUND")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsTArray tmp(len); michael@0: for (int32_t i = 0; i < len; i++) { michael@0: tmp.AppendElement(mDevices->ElementAt(i)); michael@0: } michael@0: michael@0: devices->SetAsArray(nsIDataType::VTYPE_INTERFACE, michael@0: &NS_GET_IID(nsIMediaDevice), michael@0: mDevices->Length(), michael@0: const_cast( michael@0: static_cast(tmp.Elements()) michael@0: )); michael@0: michael@0: mSuccess->OnSuccess(devices); michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: nsCOMPtr mSuccess; michael@0: nsCOMPtr mError; michael@0: nsAutoPtr > > mDevices; michael@0: uint64_t mWindowID; michael@0: nsRefPtr mManager; michael@0: }; michael@0: michael@0: // Handle removing GetUserMediaCallbackMediaStreamListener from main thread michael@0: class GetUserMediaListenerRemove: public nsRunnable michael@0: { michael@0: public: michael@0: GetUserMediaListenerRemove(uint64_t aWindowID, michael@0: GetUserMediaCallbackMediaStreamListener *aListener) michael@0: : mWindowID(aWindowID) michael@0: , mListener(aListener) {} michael@0: michael@0: NS_IMETHOD michael@0: Run() michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: nsRefPtr manager(MediaManager::GetInstance()); michael@0: manager->RemoveFromWindowList(mWindowID, mListener); michael@0: return NS_OK; michael@0: } michael@0: michael@0: protected: michael@0: uint64_t mWindowID; michael@0: nsRefPtr mListener; michael@0: }; michael@0: michael@0: /** michael@0: * nsIMediaDevice implementation. michael@0: */ michael@0: NS_IMPL_ISUPPORTS(MediaDevice, nsIMediaDevice) michael@0: michael@0: MediaDevice* MediaDevice::Create(MediaEngineVideoSource* source) { michael@0: return new VideoDevice(source); michael@0: } michael@0: michael@0: MediaDevice* MediaDevice::Create(MediaEngineAudioSource* source) { michael@0: return new AudioDevice(source); michael@0: } michael@0: michael@0: MediaDevice::MediaDevice(MediaEngineSource* aSource) michael@0: : mHasFacingMode(false) michael@0: , mSource(aSource) { michael@0: mSource->GetName(mName); michael@0: mSource->GetUUID(mID); michael@0: } michael@0: michael@0: VideoDevice::VideoDevice(MediaEngineVideoSource* aSource) michael@0: : MediaDevice(aSource) { michael@0: #ifdef MOZ_B2G_CAMERA michael@0: if (mName.EqualsLiteral("back")) { michael@0: mHasFacingMode = true; michael@0: mFacingMode = dom::VideoFacingModeEnum::Environment; michael@0: } else if (mName.EqualsLiteral("front")) { michael@0: mHasFacingMode = true; michael@0: mFacingMode = dom::VideoFacingModeEnum::User; michael@0: } michael@0: #endif // MOZ_B2G_CAMERA michael@0: michael@0: // Kludge to test user-facing cameras on OSX. michael@0: if (mName.Find(NS_LITERAL_STRING("Face")) != -1) { michael@0: mHasFacingMode = true; michael@0: mFacingMode = dom::VideoFacingModeEnum::User; michael@0: } michael@0: } michael@0: michael@0: AudioDevice::AudioDevice(MediaEngineAudioSource* aSource) michael@0: : MediaDevice(aSource) {} michael@0: michael@0: NS_IMETHODIMP michael@0: MediaDevice::GetName(nsAString& aName) michael@0: { michael@0: aName.Assign(mName); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaDevice::GetType(nsAString& aType) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: VideoDevice::GetType(nsAString& aType) michael@0: { michael@0: aType.Assign(NS_LITERAL_STRING("video")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: AudioDevice::GetType(nsAString& aType) michael@0: { michael@0: aType.Assign(NS_LITERAL_STRING("audio")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaDevice::GetId(nsAString& aID) michael@0: { michael@0: aID.Assign(mID); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaDevice::GetFacingMode(nsAString& aFacingMode) michael@0: { michael@0: if (mHasFacingMode) { michael@0: aFacingMode.Assign(NS_ConvertUTF8toUTF16( michael@0: dom::VideoFacingModeEnumValues::strings[uint32_t(mFacingMode)].value)); michael@0: } else { michael@0: aFacingMode.Truncate(0); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: MediaEngineVideoSource* michael@0: VideoDevice::GetSource() michael@0: { michael@0: return static_cast(&*mSource); michael@0: } michael@0: michael@0: MediaEngineAudioSource* michael@0: AudioDevice::GetSource() michael@0: { michael@0: return static_cast(&*mSource); michael@0: } michael@0: michael@0: /** michael@0: * A subclass that we only use to stash internal pointers to MediaStreamGraph objects michael@0: * that need to be cleaned up. michael@0: */ michael@0: class nsDOMUserMediaStream : public DOMLocalMediaStream michael@0: { michael@0: public: michael@0: static already_AddRefed michael@0: CreateTrackUnionStream(nsIDOMWindow* aWindow, michael@0: MediaEngineSource *aAudioSource, michael@0: MediaEngineSource *aVideoSource) michael@0: { michael@0: DOMMediaStream::TrackTypeHints hints = michael@0: (aAudioSource ? DOMMediaStream::HINT_CONTENTS_AUDIO : 0) | michael@0: (aVideoSource ? DOMMediaStream::HINT_CONTENTS_VIDEO : 0); michael@0: michael@0: nsRefPtr stream = new nsDOMUserMediaStream(aAudioSource); michael@0: stream->InitTrackUnionStream(aWindow, hints); michael@0: return stream.forget(); michael@0: } michael@0: michael@0: nsDOMUserMediaStream(MediaEngineSource *aAudioSource) : michael@0: mAudioSource(aAudioSource), michael@0: mEchoOn(true), michael@0: mAgcOn(false), michael@0: mNoiseOn(true), michael@0: #ifdef MOZ_WEBRTC michael@0: mEcho(webrtc::kEcDefault), michael@0: mAgc(webrtc::kAgcDefault), michael@0: mNoise(webrtc::kNsDefault), michael@0: #else michael@0: mEcho(0), michael@0: mAgc(0), michael@0: mNoise(0), michael@0: #endif michael@0: mPlayoutDelay(20) michael@0: {} michael@0: michael@0: virtual ~nsDOMUserMediaStream() michael@0: { michael@0: Stop(); michael@0: michael@0: if (mPort) { michael@0: mPort->Destroy(); michael@0: } michael@0: if (mSourceStream) { michael@0: mSourceStream->Destroy(); michael@0: } michael@0: } michael@0: michael@0: virtual void Stop() michael@0: { michael@0: if (mSourceStream) { michael@0: mSourceStream->EndAllTrackAndFinish(); michael@0: } michael@0: } michael@0: michael@0: // Allow getUserMedia to pass input data directly to PeerConnection/MediaPipeline michael@0: virtual bool AddDirectListener(MediaStreamDirectListener *aListener) MOZ_OVERRIDE michael@0: { michael@0: if (mSourceStream) { michael@0: mSourceStream->AddDirectListener(aListener); michael@0: return true; // application should ignore NotifyQueuedTrackData michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: virtual void michael@0: AudioConfig(bool aEchoOn, uint32_t aEcho, michael@0: bool aAgcOn, uint32_t aAgc, michael@0: bool aNoiseOn, uint32_t aNoise, michael@0: int32_t aPlayoutDelay) michael@0: { michael@0: mEchoOn = aEchoOn; michael@0: mEcho = aEcho; michael@0: mAgcOn = aAgcOn; michael@0: mAgc = aAgc; michael@0: mNoiseOn = aNoiseOn; michael@0: mNoise = aNoise; michael@0: mPlayoutDelay = aPlayoutDelay; michael@0: } michael@0: michael@0: virtual void RemoveDirectListener(MediaStreamDirectListener *aListener) MOZ_OVERRIDE michael@0: { michael@0: if (mSourceStream) { michael@0: mSourceStream->RemoveDirectListener(aListener); michael@0: } michael@0: } michael@0: michael@0: // let us intervene for direct listeners when someone does track.enabled = false michael@0: virtual void SetTrackEnabled(TrackID aID, bool aEnabled) MOZ_OVERRIDE michael@0: { michael@0: // We encapsulate the SourceMediaStream and TrackUnion into one entity, so michael@0: // we can handle the disabling at the SourceMediaStream michael@0: michael@0: // We need to find the input track ID for output ID aID, so we let the TrackUnion michael@0: // forward the request to the source and translate the ID michael@0: GetStream()->AsProcessedStream()->ForwardTrackEnabled(aID, aEnabled); michael@0: } michael@0: michael@0: // The actual MediaStream is a TrackUnionStream. But these resources need to be michael@0: // explicitly destroyed too. michael@0: nsRefPtr mSourceStream; michael@0: nsRefPtr mPort; michael@0: nsRefPtr mAudioSource; // so we can turn on AEC michael@0: bool mEchoOn; michael@0: bool mAgcOn; michael@0: bool mNoiseOn; michael@0: uint32_t mEcho; michael@0: uint32_t mAgc; michael@0: uint32_t mNoise; michael@0: uint32_t mPlayoutDelay; michael@0: }; michael@0: michael@0: /** michael@0: * Creates a MediaStream, attaches a listener and fires off a success callback michael@0: * to the DOM with the stream. We also pass in the error callback so it can michael@0: * be released correctly. michael@0: * michael@0: * All of this must be done on the main thread! michael@0: * michael@0: * Note that the various GetUserMedia Runnable classes currently allow for michael@0: * two streams. If we ever need to support getting more than two streams michael@0: * at once, we could convert everything to nsTArray >'s, michael@0: * though that would complicate the constructors some. Currently the michael@0: * GetUserMedia spec does not allow for more than 2 streams to be obtained in michael@0: * one call, to simplify handling of constraints. michael@0: */ michael@0: class GetUserMediaStreamRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: GetUserMediaStreamRunnable( michael@0: nsCOMPtr& aSuccess, michael@0: nsCOMPtr& aError, michael@0: uint64_t aWindowID, michael@0: GetUserMediaCallbackMediaStreamListener* aListener, michael@0: MediaEngineSource* aAudioSource, michael@0: MediaEngineSource* aVideoSource) michael@0: : mAudioSource(aAudioSource) michael@0: , mVideoSource(aVideoSource) michael@0: , mWindowID(aWindowID) michael@0: , mListener(aListener) michael@0: , mManager(MediaManager::GetInstance()) michael@0: { michael@0: mSuccess.swap(aSuccess); michael@0: mError.swap(aError); michael@0: } michael@0: michael@0: ~GetUserMediaStreamRunnable() {} michael@0: michael@0: class TracksAvailableCallback : public DOMMediaStream::OnTracksAvailableCallback michael@0: { michael@0: public: michael@0: TracksAvailableCallback(MediaManager* aManager, michael@0: nsIDOMGetUserMediaSuccessCallback* aSuccess, michael@0: uint64_t aWindowID, michael@0: DOMMediaStream* aStream) michael@0: : mWindowID(aWindowID), mSuccess(aSuccess), mManager(aManager), michael@0: mStream(aStream) {} michael@0: virtual void NotifyTracksAvailable(DOMMediaStream* aStream) MOZ_OVERRIDE michael@0: { michael@0: // We're in the main thread, so no worries here. michael@0: if (!(mManager->IsWindowStillActive(mWindowID))) { michael@0: return; michael@0: } michael@0: michael@0: // Start currentTime from the point where this stream was successfully michael@0: // returned. michael@0: aStream->SetLogicalStreamStartTime(aStream->GetStream()->GetCurrentTime()); michael@0: michael@0: // This is safe since we're on main-thread, and the windowlist can only michael@0: // be invalidated from the main-thread (see OnNavigation) michael@0: LOG(("Returning success for getUserMedia()")); michael@0: mSuccess->OnSuccess(aStream); michael@0: } michael@0: uint64_t mWindowID; michael@0: nsCOMPtr mSuccess; michael@0: nsRefPtr mManager; michael@0: // Keep the DOMMediaStream alive until the NotifyTracksAvailable callback michael@0: // has fired, otherwise we might immediately destroy the DOMMediaStream and michael@0: // shut down the underlying MediaStream prematurely. michael@0: // This creates a cycle which is broken when NotifyTracksAvailable michael@0: // is fired (which will happen unless the browser shuts down, michael@0: // since we only add this callback when we've successfully appended michael@0: // the desired tracks in the MediaStreamGraph) or when michael@0: // DOMMediaStream::NotifyMediaStreamGraphShutdown is called. michael@0: nsRefPtr mStream; michael@0: }; michael@0: michael@0: NS_IMETHOD michael@0: Run() michael@0: { michael@0: #ifdef MOZ_WEBRTC michael@0: int32_t aec = (int32_t) webrtc::kEcUnchanged; michael@0: int32_t agc = (int32_t) webrtc::kAgcUnchanged; michael@0: int32_t noise = (int32_t) webrtc::kNsUnchanged; michael@0: #else michael@0: int32_t aec = 0, agc = 0, noise = 0; michael@0: #endif michael@0: bool aec_on = false, agc_on = false, noise_on = false; michael@0: int32_t playout_delay = 0; michael@0: michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: nsPIDOMWindow *window = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(mWindowID)); michael@0: michael@0: // We're on main-thread, and the windowlist can only michael@0: // be invalidated from the main-thread (see OnNavigation) michael@0: StreamListeners* listeners = mManager->GetWindowListeners(mWindowID); michael@0: if (!listeners || !window || !window->GetExtantDoc()) { michael@0: // This window is no longer live. mListener has already been removed michael@0: return NS_OK; michael@0: } michael@0: michael@0: #ifdef MOZ_WEBRTC michael@0: // Right now these configs are only of use if webrtc is available michael@0: nsresult rv; michael@0: nsCOMPtr prefs = do_GetService("@mozilla.org/preferences-service;1", &rv); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsCOMPtr branch = do_QueryInterface(prefs); michael@0: michael@0: if (branch) { michael@0: branch->GetBoolPref("media.getusermedia.aec_enabled", &aec_on); michael@0: branch->GetIntPref("media.getusermedia.aec", &aec); michael@0: branch->GetBoolPref("media.getusermedia.agc_enabled", &agc_on); michael@0: branch->GetIntPref("media.getusermedia.agc", &agc); michael@0: branch->GetBoolPref("media.getusermedia.noise_enabled", &noise_on); michael@0: branch->GetIntPref("media.getusermedia.noise", &noise); michael@0: branch->GetIntPref("media.getusermedia.playout_delay", &playout_delay); michael@0: } michael@0: } michael@0: #endif michael@0: // Create a media stream. michael@0: nsRefPtr trackunion = michael@0: nsDOMUserMediaStream::CreateTrackUnionStream(window, mAudioSource, michael@0: mVideoSource); michael@0: if (!trackunion) { michael@0: nsCOMPtr error = mError.forget(); michael@0: LOG(("Returning error for getUserMedia() - no stream")); michael@0: error->OnError(NS_LITERAL_STRING("NO_STREAM")); michael@0: return NS_OK; michael@0: } michael@0: trackunion->AudioConfig(aec_on, (uint32_t) aec, michael@0: agc_on, (uint32_t) agc, michael@0: noise_on, (uint32_t) noise, michael@0: playout_delay); michael@0: michael@0: michael@0: MediaStreamGraph* gm = MediaStreamGraph::GetInstance(); michael@0: nsRefPtr stream = gm->CreateSourceStream(nullptr); michael@0: michael@0: // connect the source stream to the track union stream to avoid us blocking michael@0: trackunion->GetStream()->AsProcessedStream()->SetAutofinish(true); michael@0: nsRefPtr port = trackunion->GetStream()->AsProcessedStream()-> michael@0: AllocateInputPort(stream, MediaInputPort::FLAG_BLOCK_OUTPUT); michael@0: trackunion->mSourceStream = stream; michael@0: trackunion->mPort = port.forget(); michael@0: // Log the relationship between SourceMediaStream and TrackUnion stream michael@0: // Make sure logger starts before capture michael@0: AsyncLatencyLogger::Get(true); michael@0: LogLatency(AsyncLatencyLogger::MediaStreamCreate, michael@0: reinterpret_cast(stream.get()), michael@0: reinterpret_cast(trackunion->GetStream())); michael@0: michael@0: trackunion->CombineWithPrincipal(window->GetExtantDoc()->NodePrincipal()); michael@0: michael@0: // The listener was added at the begining in an inactive state. michael@0: // Activate our listener. We'll call Start() on the source when get a callback michael@0: // that the MediaStream has started consuming. The listener is freed michael@0: // when the page is invalidated (on navigation or close). michael@0: mListener->Activate(stream.forget(), mAudioSource, mVideoSource); michael@0: michael@0: // Note: includes JS callbacks; must be released on MainThread michael@0: TracksAvailableCallback* tracksAvailableCallback = michael@0: new TracksAvailableCallback(mManager, mSuccess, mWindowID, trackunion); michael@0: michael@0: mListener->AudioConfig(aec_on, (uint32_t) aec, michael@0: agc_on, (uint32_t) agc, michael@0: noise_on, (uint32_t) noise, michael@0: playout_delay); michael@0: michael@0: // Dispatch to the media thread to ask it to start the sources, michael@0: // because that can take a while. michael@0: // Pass ownership of trackunion to the MediaOperationRunnable michael@0: // to ensure it's kept alive until the MediaOperationRunnable runs (at least). michael@0: nsIThread *mediaThread = MediaManager::GetThread(); michael@0: nsRefPtr runnable( michael@0: new MediaOperationRunnable(MEDIA_START, mListener, trackunion, michael@0: tracksAvailableCallback, michael@0: mAudioSource, mVideoSource, false, mWindowID, michael@0: mError.forget())); michael@0: mediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL); michael@0: michael@0: // We won't need mError now. michael@0: mError = nullptr; michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: nsCOMPtr mSuccess; michael@0: nsCOMPtr mError; michael@0: nsRefPtr mAudioSource; michael@0: nsRefPtr mVideoSource; michael@0: uint64_t mWindowID; michael@0: nsRefPtr mListener; michael@0: nsRefPtr mManager; // get ref to this when creating the runnable michael@0: }; michael@0: michael@0: static bool michael@0: IsOn(const OwningBooleanOrMediaTrackConstraints &aUnion) { michael@0: return !aUnion.IsBoolean() || aUnion.GetAsBoolean(); michael@0: } michael@0: michael@0: static const MediaTrackConstraints& michael@0: GetInvariant(const OwningBooleanOrMediaTrackConstraints &aUnion) { michael@0: static const MediaTrackConstraints empty; michael@0: return aUnion.IsMediaTrackConstraints() ? michael@0: aUnion.GetAsMediaTrackConstraints() : empty; michael@0: } michael@0: michael@0: /** michael@0: * Helper functions that implement the constraints algorithm from michael@0: * http://dev.w3.org/2011/webrtc/editor/getusermedia.html#methods-5 michael@0: */ michael@0: michael@0: // Reminder: add handling for new constraints both here and in GetSources below! michael@0: michael@0: static bool SatisfyConstraintSet(const MediaEngineVideoSource *, michael@0: const MediaTrackConstraintSet &aConstraints, michael@0: nsIMediaDevice &aCandidate) michael@0: { michael@0: if (aConstraints.mFacingMode.WasPassed()) { michael@0: nsString s; michael@0: aCandidate.GetFacingMode(s); michael@0: if (!s.EqualsASCII(dom::VideoFacingModeEnumValues::strings[ michael@0: uint32_t(aConstraints.mFacingMode.Value())].value)) { michael@0: return false; michael@0: } michael@0: } michael@0: // TODO: Add more video-specific constraints michael@0: return true; michael@0: } michael@0: michael@0: static bool SatisfyConstraintSet(const MediaEngineAudioSource *, michael@0: const MediaTrackConstraintSet &aConstraints, michael@0: nsIMediaDevice &aCandidate) michael@0: { michael@0: // TODO: Add audio-specific constraints michael@0: return true; michael@0: } michael@0: michael@0: typedef nsTArray > SourceSet; michael@0: michael@0: // Source getter that constrains list returned michael@0: michael@0: template michael@0: static SourceSet * michael@0: GetSources(MediaEngine *engine, michael@0: ConstraintsType &aConstraints, michael@0: void (MediaEngine::* aEnumerate)(nsTArray >*), michael@0: char* media_device_name = nullptr) michael@0: { michael@0: ScopedDeletePtr result(new SourceSet); michael@0: michael@0: const SourceType * const type = nullptr; michael@0: nsString deviceName; michael@0: // First collect sources michael@0: SourceSet candidateSet; michael@0: { michael@0: nsTArray > sources; michael@0: (engine->*aEnumerate)(&sources); michael@0: /** michael@0: * We're allowing multiple tabs to access the same camera for parity michael@0: * with Chrome. See bug 811757 for some of the issues surrounding michael@0: * this decision. To disallow, we'd filter by IsAvailable() as we used michael@0: * to. michael@0: */ michael@0: for (uint32_t len = sources.Length(), i = 0; i < len; i++) { michael@0: #ifdef DEBUG michael@0: sources[i]->GetName(deviceName); michael@0: if (media_device_name && strlen(media_device_name) > 0) { michael@0: if (deviceName.EqualsASCII(media_device_name)) { michael@0: candidateSet.AppendElement(MediaDevice::Create(sources[i])); michael@0: break; michael@0: } michael@0: } else { michael@0: #endif michael@0: candidateSet.AppendElement(MediaDevice::Create(sources[i])); michael@0: #ifdef DEBUG michael@0: } michael@0: #endif michael@0: } michael@0: } michael@0: michael@0: // Apply constraints to the list of sources. michael@0: michael@0: auto& c = aConstraints; michael@0: if (c.mUnsupportedRequirement) { michael@0: // Check upfront the names of required constraints that are unsupported for michael@0: // this media-type. The spec requires these to fail, so getting them out of michael@0: // the way early provides a necessary invariant for the remaining algorithm michael@0: // which maximizes code-reuse by ignoring constraints of the other type michael@0: // (specifically, SatisfyConstraintSet is reused for the advanced algorithm michael@0: // where the spec requires it to ignore constraints of the other type) michael@0: return result.forget(); michael@0: } michael@0: michael@0: // Now on to the actual algorithm: First apply required constraints. michael@0: michael@0: for (uint32_t i = 0; i < candidateSet.Length();) { michael@0: // Overloading instead of template specialization keeps things local michael@0: if (!SatisfyConstraintSet(type, c.mRequired, *candidateSet[i])) { michael@0: candidateSet.RemoveElementAt(i); michael@0: } else { michael@0: ++i; michael@0: } michael@0: } michael@0: michael@0: // TODO(jib): Proper non-ordered handling of nonrequired constraints (907352) michael@0: // michael@0: // For now, put nonrequired constraints at tail of Advanced list. michael@0: // This isn't entirely accurate, as order will matter, but few will notice michael@0: // the difference until we get camera selection and a few more constraints. michael@0: if (c.mNonrequired.Length()) { michael@0: if (!c.mAdvanced.WasPassed()) { michael@0: c.mAdvanced.Construct(); michael@0: } michael@0: c.mAdvanced.Value().MoveElementsFrom(c.mNonrequired); michael@0: } michael@0: michael@0: // Then apply advanced (formerly known as optional) constraints. michael@0: // michael@0: // These are only effective when there are multiple sources to pick from. michael@0: // Spec as-of-this-writing says to run algorithm on "all possible tracks michael@0: // of media type T that the browser COULD RETURN" (emphasis added). michael@0: // michael@0: // We think users ultimately control which devices we could return, so after michael@0: // determining the webpage's preferred list, we add the remaining choices michael@0: // to the tail, reasoning that they would all have passed individually, michael@0: // i.e. if the user had any one of them as their sole device (enabled). michael@0: // michael@0: // This avoids users having to unplug/disable devices should a webpage pick michael@0: // the wrong one (UX-fail). Webpage-preferred devices will be listed first. michael@0: michael@0: SourceSet tailSet; michael@0: michael@0: if (c.mAdvanced.WasPassed()) { michael@0: auto &array = c.mAdvanced.Value(); michael@0: michael@0: for (int i = 0; i < int(array.Length()); i++) { michael@0: SourceSet rejects; michael@0: for (uint32_t j = 0; j < candidateSet.Length();) { michael@0: if (!SatisfyConstraintSet(type, array[i], *candidateSet[j])) { michael@0: rejects.AppendElement(candidateSet[j]); michael@0: candidateSet.RemoveElementAt(j); michael@0: } else { michael@0: ++j; michael@0: } michael@0: } michael@0: (candidateSet.Length()? tailSet : candidateSet).MoveElementsFrom(rejects); michael@0: } michael@0: } michael@0: michael@0: // TODO: Proper non-ordered handling of nonrequired constraints (Bug 907352) michael@0: michael@0: result->MoveElementsFrom(candidateSet); michael@0: result->MoveElementsFrom(tailSet); michael@0: return result.forget(); michael@0: } michael@0: michael@0: /** michael@0: * Runs on a seperate thread and is responsible for enumerating devices. michael@0: * Depending on whether a picture or stream was asked for, either michael@0: * ProcessGetUserMedia or ProcessGetUserMediaSnapshot is called, and the results michael@0: * are sent back to the DOM. michael@0: * michael@0: * Do not run this on the main thread. The success and error callbacks *MUST* michael@0: * be dispatched on the main thread! michael@0: */ michael@0: class GetUserMediaRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: GetUserMediaRunnable( michael@0: const MediaStreamConstraints& aConstraints, michael@0: already_AddRefed aSuccess, michael@0: already_AddRefed aError, michael@0: uint64_t aWindowID, GetUserMediaCallbackMediaStreamListener *aListener, michael@0: MediaEnginePrefs &aPrefs) michael@0: : mConstraints(aConstraints) michael@0: , mSuccess(aSuccess) michael@0: , mError(aError) michael@0: , mWindowID(aWindowID) michael@0: , mListener(aListener) michael@0: , mPrefs(aPrefs) michael@0: , mDeviceChosen(false) michael@0: , mBackend(nullptr) michael@0: , mManager(MediaManager::GetInstance()) michael@0: {} michael@0: michael@0: /** michael@0: * The caller can also choose to provide their own backend instead of michael@0: * using the one provided by MediaManager::GetBackend. michael@0: */ michael@0: GetUserMediaRunnable( michael@0: const MediaStreamConstraints& aConstraints, michael@0: already_AddRefed aSuccess, michael@0: already_AddRefed aError, michael@0: uint64_t aWindowID, GetUserMediaCallbackMediaStreamListener *aListener, michael@0: MediaEnginePrefs &aPrefs, michael@0: MediaEngine* aBackend) michael@0: : mConstraints(aConstraints) michael@0: , mSuccess(aSuccess) michael@0: , mError(aError) michael@0: , mWindowID(aWindowID) michael@0: , mListener(aListener) michael@0: , mPrefs(aPrefs) michael@0: , mDeviceChosen(false) michael@0: , mBackend(aBackend) michael@0: , mManager(MediaManager::GetInstance()) michael@0: {} michael@0: michael@0: ~GetUserMediaRunnable() { michael@0: } michael@0: michael@0: void michael@0: Fail(const nsAString& aMessage) { michael@0: nsRefPtr runnable = michael@0: new ErrorCallbackRunnable(mSuccess, mError, aMessage, mWindowID); michael@0: // These should be empty now michael@0: MOZ_ASSERT(!mSuccess); michael@0: MOZ_ASSERT(!mError); michael@0: michael@0: NS_DispatchToMainThread(runnable); michael@0: } michael@0: michael@0: NS_IMETHOD michael@0: Run() michael@0: { michael@0: NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread"); michael@0: MOZ_ASSERT(mSuccess); michael@0: MOZ_ASSERT(mError); michael@0: michael@0: MediaEngine* backend = mBackend; michael@0: // Was a backend provided? michael@0: if (!backend) { michael@0: backend = mManager->GetBackend(mWindowID); michael@0: } michael@0: michael@0: // Was a device provided? michael@0: if (!mDeviceChosen) { michael@0: nsresult rv = SelectDevice(backend); michael@0: if (rv != NS_OK) { michael@0: return rv; michael@0: } michael@0: } michael@0: michael@0: // It is an error if audio or video are requested along with picture. michael@0: if (mConstraints.mPicture && michael@0: (IsOn(mConstraints.mAudio) || IsOn(mConstraints.mVideo))) { michael@0: Fail(NS_LITERAL_STRING("NOT_SUPPORTED_ERR")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (mConstraints.mPicture) { michael@0: ProcessGetUserMediaSnapshot(mVideoDevice->GetSource(), 0); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // There's a bug in the permission code that can leave us with mAudio but no audio device michael@0: ProcessGetUserMedia(((IsOn(mConstraints.mAudio) && mAudioDevice) ? michael@0: mAudioDevice->GetSource() : nullptr), michael@0: ((IsOn(mConstraints.mVideo) && mVideoDevice) ? michael@0: mVideoDevice->GetSource() : nullptr)); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: Denied(const nsAString& aErrorMsg) michael@0: { michael@0: MOZ_ASSERT(mSuccess); michael@0: MOZ_ASSERT(mError); michael@0: michael@0: // We add a disabled listener to the StreamListeners array until accepted michael@0: // If this was the only active MediaStream, remove the window from the list. michael@0: if (NS_IsMainThread()) { michael@0: // This is safe since we're on main-thread, and the window can only michael@0: // be invalidated from the main-thread (see OnNavigation) michael@0: nsCOMPtr success = mSuccess.forget(); michael@0: nsCOMPtr error = mError.forget(); michael@0: error->OnError(aErrorMsg); michael@0: michael@0: // Should happen *after* error runs for consistency, but may not matter michael@0: nsRefPtr manager(MediaManager::GetInstance()); michael@0: manager->RemoveFromWindowList(mWindowID, mListener); michael@0: } else { michael@0: // This will re-check the window being alive on main-thread michael@0: // Note: we must remove the listener on MainThread as well michael@0: Fail(aErrorMsg); michael@0: michael@0: // MUST happen after ErrorCallbackRunnable Run()s, as it checks the active window list michael@0: NS_DispatchToMainThread(new GetUserMediaListenerRemove(mWindowID, mListener)); michael@0: } michael@0: michael@0: MOZ_ASSERT(!mSuccess); michael@0: MOZ_ASSERT(!mError); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: SetContraints(const MediaStreamConstraints& aConstraints) michael@0: { michael@0: mConstraints = aConstraints; michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: SetAudioDevice(AudioDevice* aAudioDevice) michael@0: { michael@0: mAudioDevice = aAudioDevice; michael@0: mDeviceChosen = true; michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: SetVideoDevice(VideoDevice* aVideoDevice) michael@0: { michael@0: mVideoDevice = aVideoDevice; michael@0: mDeviceChosen = true; michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: SelectDevice(MediaEngine* backend) michael@0: { michael@0: MOZ_ASSERT(mSuccess); michael@0: MOZ_ASSERT(mError); michael@0: if (mConstraints.mPicture || IsOn(mConstraints.mVideo)) { michael@0: VideoTrackConstraintsN constraints(GetInvariant(mConstraints.mVideo)); michael@0: ScopedDeletePtr sources (GetSources(backend, constraints, michael@0: &MediaEngine::EnumerateVideoDevices)); michael@0: michael@0: if (!sources->Length()) { michael@0: Fail(NS_LITERAL_STRING("NO_DEVICES_FOUND")); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: // Pick the first available device. michael@0: mVideoDevice = do_QueryObject((*sources)[0]); michael@0: LOG(("Selected video device")); michael@0: } michael@0: michael@0: if (IsOn(mConstraints.mAudio)) { michael@0: AudioTrackConstraintsN constraints(GetInvariant(mConstraints.mAudio)); michael@0: ScopedDeletePtr sources (GetSources(backend, constraints, michael@0: &MediaEngine::EnumerateAudioDevices)); michael@0: michael@0: if (!sources->Length()) { michael@0: Fail(NS_LITERAL_STRING("NO_DEVICES_FOUND")); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: // Pick the first available device. michael@0: mAudioDevice = do_QueryObject((*sources)[0]); michael@0: LOG(("Selected audio device")); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /** michael@0: * Allocates a video or audio device and returns a MediaStream via michael@0: * a GetUserMediaStreamRunnable. Runs off the main thread. michael@0: */ michael@0: void michael@0: ProcessGetUserMedia(MediaEngineAudioSource* aAudioSource, michael@0: MediaEngineVideoSource* aVideoSource) michael@0: { michael@0: MOZ_ASSERT(mSuccess); michael@0: MOZ_ASSERT(mError); michael@0: nsresult rv; michael@0: if (aAudioSource) { michael@0: rv = aAudioSource->Allocate(GetInvariant(mConstraints.mAudio), mPrefs); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("Failed to allocate audiosource %d",rv)); michael@0: Fail(NS_LITERAL_STRING("HARDWARE_UNAVAILABLE")); michael@0: return; michael@0: } michael@0: } michael@0: if (aVideoSource) { michael@0: rv = aVideoSource->Allocate(GetInvariant(mConstraints.mVideo), mPrefs); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("Failed to allocate videosource %d\n",rv)); michael@0: if (aAudioSource) { michael@0: aAudioSource->Deallocate(); michael@0: } michael@0: Fail(NS_LITERAL_STRING("HARDWARE_UNAVAILABLE")); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: NS_DispatchToMainThread(new GetUserMediaStreamRunnable( michael@0: mSuccess, mError, mWindowID, mListener, aAudioSource, aVideoSource michael@0: )); michael@0: michael@0: MOZ_ASSERT(!mSuccess); michael@0: MOZ_ASSERT(!mError); michael@0: michael@0: return; michael@0: } michael@0: michael@0: /** michael@0: * Allocates a video device, takes a snapshot and returns a DOMFile via michael@0: * a SuccessRunnable or an error via the ErrorRunnable. Off the main thread. michael@0: */ michael@0: void michael@0: ProcessGetUserMediaSnapshot(MediaEngineVideoSource* aSource, int aDuration) michael@0: { michael@0: MOZ_ASSERT(mSuccess); michael@0: MOZ_ASSERT(mError); michael@0: nsresult rv = aSource->Allocate(GetInvariant(mConstraints.mVideo), mPrefs); michael@0: if (NS_FAILED(rv)) { michael@0: Fail(NS_LITERAL_STRING("HARDWARE_UNAVAILABLE")); michael@0: return; michael@0: } michael@0: michael@0: /** michael@0: * Display picture capture UI here before calling Snapshot() - Bug 748835. michael@0: */ michael@0: nsCOMPtr file; michael@0: aSource->Snapshot(aDuration, getter_AddRefs(file)); michael@0: aSource->Deallocate(); michael@0: michael@0: NS_DispatchToMainThread(new SuccessCallbackRunnable( michael@0: mSuccess, mError, file, mWindowID michael@0: )); michael@0: michael@0: MOZ_ASSERT(!mSuccess); michael@0: MOZ_ASSERT(!mError); michael@0: michael@0: return; michael@0: } michael@0: michael@0: private: michael@0: MediaStreamConstraints mConstraints; michael@0: michael@0: nsCOMPtr mSuccess; michael@0: nsCOMPtr mError; michael@0: uint64_t mWindowID; michael@0: nsRefPtr mListener; michael@0: nsRefPtr mAudioDevice; michael@0: nsRefPtr mVideoDevice; michael@0: MediaEnginePrefs mPrefs; michael@0: michael@0: bool mDeviceChosen; michael@0: michael@0: RefPtr mBackend; michael@0: nsRefPtr mManager; // get ref to this when creating the runnable michael@0: }; michael@0: michael@0: /** michael@0: * Similar to GetUserMediaRunnable, but used for the chrome-only michael@0: * GetUserMediaDevices function. Enumerates a list of audio & video devices, michael@0: * wraps them up in nsIMediaDevice objects and returns it to the success michael@0: * callback. michael@0: */ michael@0: class GetUserMediaDevicesRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: GetUserMediaDevicesRunnable( michael@0: const MediaStreamConstraints& aConstraints, michael@0: already_AddRefed aSuccess, michael@0: already_AddRefed aError, michael@0: uint64_t aWindowId, char* aAudioLoopbackDev, char* aVideoLoopbackDev) michael@0: : mConstraints(aConstraints) michael@0: , mSuccess(aSuccess) michael@0: , mError(aError) michael@0: , mManager(MediaManager::GetInstance()) michael@0: , mWindowId(aWindowId) michael@0: , mLoopbackAudioDevice(aAudioLoopbackDev) michael@0: , mLoopbackVideoDevice(aVideoLoopbackDev) {} michael@0: michael@0: NS_IMETHOD michael@0: Run() michael@0: { michael@0: NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread"); michael@0: michael@0: nsRefPtr backend; michael@0: if (mConstraints.mFake) michael@0: backend = new MediaEngineDefault(); michael@0: else michael@0: backend = mManager->GetBackend(mWindowId); michael@0: michael@0: ScopedDeletePtr final(new SourceSet); michael@0: if (IsOn(mConstraints.mVideo)) { michael@0: VideoTrackConstraintsN constraints(GetInvariant(mConstraints.mVideo)); michael@0: ScopedDeletePtr s(GetSources(backend, constraints, michael@0: &MediaEngine::EnumerateVideoDevices, michael@0: mLoopbackVideoDevice)); michael@0: final->MoveElementsFrom(*s); michael@0: } michael@0: if (IsOn(mConstraints.mAudio)) { michael@0: AudioTrackConstraintsN constraints(GetInvariant(mConstraints.mAudio)); michael@0: ScopedDeletePtr s (GetSources(backend, constraints, michael@0: &MediaEngine::EnumerateAudioDevices, michael@0: mLoopbackAudioDevice)); michael@0: final->MoveElementsFrom(*s); michael@0: } michael@0: NS_DispatchToMainThread(new DeviceSuccessCallbackRunnable(mWindowId, michael@0: mSuccess, mError, michael@0: final.forget())); michael@0: // DeviceSuccessCallbackRunnable should have taken these. michael@0: MOZ_ASSERT(!mSuccess && !mError); michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: MediaStreamConstraints mConstraints; michael@0: nsCOMPtr mSuccess; michael@0: nsCOMPtr mError; michael@0: nsRefPtr mManager; michael@0: uint64_t mWindowId; michael@0: const nsString mCallId; michael@0: // Audio & Video loopback devices to be used based on michael@0: // the preference settings. This is currently used for michael@0: // automated media tests only. michael@0: char* mLoopbackAudioDevice; michael@0: char* mLoopbackVideoDevice; michael@0: }; michael@0: michael@0: MediaManager::MediaManager() michael@0: : mMediaThread(nullptr) michael@0: , mMutex("mozilla::MediaManager") michael@0: , mBackend(nullptr) { michael@0: mPrefs.mWidth = 0; // adaptive default michael@0: mPrefs.mHeight = 0; // adaptive default michael@0: mPrefs.mFPS = MediaEngine::DEFAULT_VIDEO_FPS; michael@0: mPrefs.mMinFPS = MediaEngine::DEFAULT_VIDEO_MIN_FPS; michael@0: michael@0: nsresult rv; michael@0: nsCOMPtr prefs = do_GetService("@mozilla.org/preferences-service;1", &rv); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsCOMPtr branch = do_QueryInterface(prefs); michael@0: if (branch) { michael@0: GetPrefs(branch, nullptr); michael@0: } michael@0: } michael@0: LOG(("%s: default prefs: %dx%d @%dfps (min %d)", __FUNCTION__, michael@0: mPrefs.mWidth, mPrefs.mHeight, mPrefs.mFPS, mPrefs.mMinFPS)); michael@0: } michael@0: michael@0: NS_IMPL_ISUPPORTS(MediaManager, nsIMediaManagerService, nsIObserver) michael@0: michael@0: /* static */ StaticRefPtr MediaManager::sSingleton; michael@0: michael@0: // NOTE: never Dispatch(....,NS_DISPATCH_SYNC) to the MediaManager michael@0: // thread from the MainThread, as we NS_DISPATCH_SYNC to MainThread michael@0: // from MediaManager thread. michael@0: /* static */ MediaManager* michael@0: MediaManager::Get() { michael@0: if (!sSingleton) { michael@0: sSingleton = new MediaManager(); michael@0: michael@0: NS_NewNamedThread("MediaManager", getter_AddRefs(sSingleton->mMediaThread)); michael@0: LOG(("New Media thread for gum")); michael@0: michael@0: NS_ASSERTION(NS_IsMainThread(), "Only create MediaManager on main thread"); michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: if (obs) { michael@0: obs->AddObserver(sSingleton, "xpcom-shutdown", false); michael@0: obs->AddObserver(sSingleton, "getUserMedia:response:allow", false); michael@0: obs->AddObserver(sSingleton, "getUserMedia:response:deny", false); michael@0: obs->AddObserver(sSingleton, "getUserMedia:revoke", false); michael@0: obs->AddObserver(sSingleton, "phone-state-changed", false); michael@0: } michael@0: // else MediaManager won't work properly and will leak (see bug 837874) michael@0: nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); michael@0: if (prefs) { michael@0: prefs->AddObserver("media.navigator.video.default_width", sSingleton, false); michael@0: prefs->AddObserver("media.navigator.video.default_height", sSingleton, false); michael@0: prefs->AddObserver("media.navigator.video.default_fps", sSingleton, false); michael@0: prefs->AddObserver("media.navigator.video.default_minfps", sSingleton, false); michael@0: } michael@0: } michael@0: return sSingleton; michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: MediaManager::GetInstance() michael@0: { michael@0: // so we can have non-refcounted getters michael@0: nsRefPtr service = MediaManager::Get(); michael@0: return service.forget(); michael@0: } michael@0: michael@0: /* static */ nsresult michael@0: MediaManager::NotifyRecordingStatusChange(nsPIDOMWindow* aWindow, michael@0: const nsString& aMsg, michael@0: const bool& aIsAudio, michael@0: const bool& aIsVideo) michael@0: { michael@0: NS_ENSURE_ARG(aWindow); michael@0: michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: if (!obs) { michael@0: NS_WARNING("Could not get the Observer service for GetUserMedia recording notification."); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: nsRefPtr props = new nsHashPropertyBag(); michael@0: props->SetPropertyAsBool(NS_LITERAL_STRING("isAudio"), aIsAudio); michael@0: props->SetPropertyAsBool(NS_LITERAL_STRING("isVideo"), aIsVideo); michael@0: michael@0: bool isApp = false; michael@0: nsString requestURL; michael@0: michael@0: if (nsCOMPtr docShell = aWindow->GetDocShell()) { michael@0: nsresult rv = docShell->GetIsApp(&isApp); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (isApp) { michael@0: rv = docShell->GetAppManifestURL(requestURL); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: } michael@0: michael@0: if (!isApp) { michael@0: nsCString pageURL; michael@0: nsCOMPtr docURI = aWindow->GetDocumentURI(); michael@0: NS_ENSURE_TRUE(docURI, NS_ERROR_FAILURE); michael@0: michael@0: nsresult rv = docURI->GetSpec(pageURL); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: requestURL = NS_ConvertUTF8toUTF16(pageURL); michael@0: } michael@0: michael@0: props->SetPropertyAsBool(NS_LITERAL_STRING("isApp"), isApp); michael@0: props->SetPropertyAsAString(NS_LITERAL_STRING("requestURL"), requestURL); michael@0: michael@0: obs->NotifyObservers(static_cast(props), michael@0: "recording-device-events", michael@0: aMsg.get()); michael@0: michael@0: // Forward recording events to parent process. michael@0: // The events are gathered in chrome process and used for recording indicator michael@0: if (XRE_GetProcessType() != GeckoProcessType_Default) { michael@0: unused << michael@0: dom::ContentChild::GetSingleton()->SendRecordingDeviceEvents(aMsg, michael@0: requestURL, michael@0: aIsAudio, michael@0: aIsVideo); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /** michael@0: * The entry point for this file. A call from Navigator::mozGetUserMedia michael@0: * will end up here. MediaManager is a singleton that is responsible michael@0: * for handling all incoming getUserMedia calls from every window. michael@0: */ michael@0: nsresult michael@0: MediaManager::GetUserMedia(bool aPrivileged, michael@0: nsPIDOMWindow* aWindow, const MediaStreamConstraints& aConstraints, michael@0: nsIDOMGetUserMediaSuccessCallback* aOnSuccess, michael@0: nsIDOMGetUserMediaErrorCallback* aOnError) michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: michael@0: NS_ENSURE_TRUE(aWindow, NS_ERROR_NULL_POINTER); michael@0: NS_ENSURE_TRUE(aOnError, NS_ERROR_NULL_POINTER); michael@0: NS_ENSURE_TRUE(aOnSuccess, NS_ERROR_NULL_POINTER); michael@0: michael@0: nsCOMPtr onSuccess(aOnSuccess); michael@0: nsCOMPtr onError(aOnError); michael@0: michael@0: MediaStreamConstraints c(aConstraints); // copy michael@0: michael@0: /** michael@0: * If we were asked to get a picture, before getting a snapshot, we check if michael@0: * the calling page is allowed to open a popup. We do this because michael@0: * {picture:true} will open a new "window" to let the user preview or select michael@0: * an image, on Android. The desktop UI for {picture:true} is TBD, at which michael@0: * may point we can decide whether to extend this test there as well. michael@0: */ michael@0: #if !defined(MOZ_WEBRTC) michael@0: if (c.mPicture && !aPrivileged) { michael@0: if (aWindow->GetPopupControlState() > openControlled) { michael@0: nsCOMPtr pm = michael@0: do_GetService(NS_POPUPWINDOWMANAGER_CONTRACTID); michael@0: if (!pm) { michael@0: return NS_OK; michael@0: } michael@0: uint32_t permission; michael@0: nsCOMPtr doc = aWindow->GetExtantDoc(); michael@0: pm->TestPermission(doc->NodePrincipal(), &permission); michael@0: if (permission == nsIPopupWindowManager::DENY_POPUP) { michael@0: nsGlobalWindow::FirePopupBlockedEvent( michael@0: doc, aWindow, nullptr, EmptyString(), EmptyString() michael@0: ); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: static bool created = false; michael@0: if (!created) { michael@0: // Force MediaManager to startup before we try to access it from other threads michael@0: // Hack: should init singleton earlier unless it's expensive (mem or CPU) michael@0: (void) MediaManager::Get(); michael@0: #ifdef MOZ_B2G michael@0: // Initialize MediaPermissionManager before send out any permission request. michael@0: (void) MediaPermissionManager::GetInstance(); michael@0: #endif //MOZ_B2G michael@0: } michael@0: michael@0: // Store the WindowID in a hash table and mark as active. The entry is removed michael@0: // when this window is closed or navigated away from. michael@0: uint64_t windowID = aWindow->WindowID(); michael@0: // This is safe since we're on main-thread, and the windowlist can only michael@0: // be invalidated from the main-thread (see OnNavigation) michael@0: StreamListeners* listeners = GetActiveWindows()->Get(windowID); michael@0: if (!listeners) { michael@0: listeners = new StreamListeners; michael@0: GetActiveWindows()->Put(windowID, listeners); michael@0: } michael@0: michael@0: // Ensure there's a thread for gum to proxy to off main thread michael@0: nsIThread *mediaThread = MediaManager::GetThread(); michael@0: michael@0: // Create a disabled listener to act as a placeholder michael@0: GetUserMediaCallbackMediaStreamListener* listener = michael@0: new GetUserMediaCallbackMediaStreamListener(mediaThread, windowID); michael@0: michael@0: // No need for locking because we always do this in the main thread. michael@0: listeners->AppendElement(listener); michael@0: michael@0: // Developer preference for turning off permission check. michael@0: if (Preferences::GetBool("media.navigator.permission.disabled", false)) { michael@0: aPrivileged = true; michael@0: } michael@0: if (!Preferences::GetBool("media.navigator.video.enabled", true)) { michael@0: c.mVideo.SetAsBoolean() = false; michael@0: } michael@0: michael@0: #if defined(ANDROID) || defined(MOZ_WIDGET_GONK) michael@0: // Be backwards compatible only on mobile and only for facingMode. michael@0: if (c.mVideo.IsMediaTrackConstraints()) { michael@0: auto& tc = c.mVideo.GetAsMediaTrackConstraints(); michael@0: if (!tc.mRequire.WasPassed() && michael@0: tc.mMandatory.mFacingMode.WasPassed() && !tc.mFacingMode.WasPassed()) { michael@0: tc.mFacingMode.Construct(tc.mMandatory.mFacingMode.Value()); michael@0: tc.mRequire.Construct().AppendElement(NS_LITERAL_STRING("facingMode")); michael@0: } michael@0: if (tc.mOptional.WasPassed() && !tc.mAdvanced.WasPassed()) { michael@0: tc.mAdvanced.Construct(); michael@0: for (uint32_t i = 0; i < tc.mOptional.Value().Length(); i++) { michael@0: if (tc.mOptional.Value()[i].mFacingMode.WasPassed()) { michael@0: MediaTrackConstraintSet n; michael@0: n.mFacingMode.Construct(tc.mOptional.Value()[i].mFacingMode.Value()); michael@0: tc.mAdvanced.Value().AppendElement(n); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: // Pass callbacks and MediaStreamListener along to GetUserMediaRunnable. michael@0: nsRefPtr runnable; michael@0: if (c.mFake) { michael@0: // Fake stream from default backend. michael@0: runnable = new GetUserMediaRunnable(c, onSuccess.forget(), michael@0: onError.forget(), windowID, listener, mPrefs, new MediaEngineDefault()); michael@0: } else { michael@0: // Stream from default device from WebRTC backend. michael@0: runnable = new GetUserMediaRunnable(c, onSuccess.forget(), michael@0: onError.forget(), windowID, listener, mPrefs); michael@0: } michael@0: michael@0: #ifdef MOZ_B2G_CAMERA michael@0: if (mCameraManager == nullptr) { michael@0: mCameraManager = nsDOMCameraManager::CreateInstance(aWindow); michael@0: } michael@0: #endif michael@0: michael@0: #if defined(ANDROID) && !defined(MOZ_WIDGET_GONK) michael@0: if (c.mPicture) { michael@0: // ShowFilePickerForMimeType() must run on the Main Thread! (on Android) michael@0: NS_DispatchToMainThread(runnable); michael@0: return NS_OK; michael@0: } michael@0: #endif michael@0: // XXX No full support for picture in Desktop yet (needs proper UI) michael@0: if (aPrivileged || michael@0: (c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) { michael@0: mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL); michael@0: } else { michael@0: bool isHTTPS = false; michael@0: nsIURI* docURI = aWindow->GetDocumentURI(); michael@0: if (docURI) { michael@0: docURI->SchemeIs("https", &isHTTPS); michael@0: } michael@0: michael@0: // Check if this site has persistent permissions. michael@0: nsresult rv; michael@0: nsCOMPtr permManager = michael@0: do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: uint32_t audioPerm = nsIPermissionManager::UNKNOWN_ACTION; michael@0: if (IsOn(c.mAudio)) { michael@0: rv = permManager->TestExactPermissionFromPrincipal( michael@0: aWindow->GetExtantDoc()->NodePrincipal(), "microphone", &audioPerm); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: uint32_t videoPerm = nsIPermissionManager::UNKNOWN_ACTION; michael@0: if (IsOn(c.mVideo)) { michael@0: rv = permManager->TestExactPermissionFromPrincipal( michael@0: aWindow->GetExtantDoc()->NodePrincipal(), "camera", &videoPerm); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: if ((!IsOn(c.mAudio) || audioPerm == nsIPermissionManager::DENY_ACTION) && michael@0: (!IsOn(c.mVideo) || videoPerm == nsIPermissionManager::DENY_ACTION)) { michael@0: return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED")); michael@0: } michael@0: michael@0: // Ask for user permission, and dispatch runnable (or not) when a response michael@0: // is received via an observer notification. Each call is paired with its michael@0: // runnable by a GUID. michael@0: nsCOMPtr uuidgen = michael@0: do_GetService("@mozilla.org/uuid-generator;1", &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // Generate a call ID. michael@0: nsID id; michael@0: rv = uuidgen->GenerateUUIDInPlace(&id); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: char buffer[NSID_LENGTH]; michael@0: id.ToProvidedString(buffer); michael@0: NS_ConvertUTF8toUTF16 callID(buffer); michael@0: michael@0: // Store the current unarmed runnable w/callbacks. michael@0: mActiveCallbacks.Put(callID, runnable); michael@0: michael@0: // Add a WindowID cross-reference so OnNavigation can tear things down michael@0: nsTArray* array; michael@0: if (!mCallIds.Get(windowID, &array)) { michael@0: array = new nsTArray(); michael@0: array->AppendElement(callID); michael@0: mCallIds.Put(windowID, array); michael@0: } else { michael@0: array->AppendElement(callID); michael@0: } michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: nsRefPtr req = new GetUserMediaRequest(aWindow, michael@0: callID, c, isHTTPS); michael@0: obs->NotifyObservers(req, "getUserMedia:request", nullptr); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: MediaManager::GetUserMediaDevices(nsPIDOMWindow* aWindow, michael@0: const MediaStreamConstraints& aConstraints, michael@0: nsIGetUserMediaDevicesSuccessCallback* aOnSuccess, michael@0: nsIDOMGetUserMediaErrorCallback* aOnError, michael@0: uint64_t aInnerWindowID) michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: michael@0: NS_ENSURE_TRUE(aOnError, NS_ERROR_NULL_POINTER); michael@0: NS_ENSURE_TRUE(aOnSuccess, NS_ERROR_NULL_POINTER); michael@0: michael@0: nsCOMPtr onSuccess(aOnSuccess); michael@0: nsCOMPtr onError(aOnError); michael@0: char* loopbackAudioDevice = nullptr; michael@0: char* loopbackVideoDevice = nullptr; michael@0: michael@0: #ifdef DEBUG michael@0: nsresult rv; michael@0: michael@0: // Check if the preference for using loopback devices is enabled. michael@0: nsCOMPtr prefs = do_GetService("@mozilla.org/preferences-service;1", &rv); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsCOMPtr branch = do_QueryInterface(prefs); michael@0: if (branch) { michael@0: branch->GetCharPref("media.audio_loopback_dev", &loopbackAudioDevice); michael@0: branch->GetCharPref("media.video_loopback_dev", &loopbackVideoDevice); michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: nsCOMPtr gUMDRunnable = new GetUserMediaDevicesRunnable( michael@0: aConstraints, onSuccess.forget(), onError.forget(), michael@0: (aInnerWindowID ? aInnerWindowID : aWindow->WindowID()), michael@0: loopbackAudioDevice, loopbackVideoDevice); michael@0: michael@0: mMediaThread->Dispatch(gUMDRunnable, NS_DISPATCH_NORMAL); michael@0: return NS_OK; michael@0: } michael@0: michael@0: MediaEngine* michael@0: MediaManager::GetBackend(uint64_t aWindowId) michael@0: { michael@0: // Plugin backends as appropriate. The default engine also currently michael@0: // includes picture support for Android. michael@0: // This IS called off main-thread. michael@0: MutexAutoLock lock(mMutex); michael@0: if (!mBackend) { michael@0: #if defined(MOZ_WEBRTC) michael@0: mBackend = new MediaEngineWebRTC(mPrefs); michael@0: #else michael@0: mBackend = new MediaEngineDefault(); michael@0: #endif michael@0: } michael@0: return mBackend; michael@0: } michael@0: michael@0: static void michael@0: StopSharingCallback(MediaManager *aThis, michael@0: uint64_t aWindowID, michael@0: StreamListeners *aListeners, michael@0: void *aData) michael@0: { michael@0: if (aListeners) { michael@0: auto length = aListeners->Length(); michael@0: for (size_t i = 0; i < length; ++i) { michael@0: GetUserMediaCallbackMediaStreamListener *listener = aListeners->ElementAt(i); michael@0: michael@0: if (listener->Stream()) { // aka HasBeenActivate()ed michael@0: listener->Invalidate(); michael@0: } michael@0: listener->Remove(); michael@0: } michael@0: aListeners->Clear(); michael@0: aThis->RemoveWindowID(aWindowID); michael@0: } michael@0: } michael@0: michael@0: michael@0: void michael@0: MediaManager::OnNavigation(uint64_t aWindowID) michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "OnNavigation called off main thread"); michael@0: LOG(("OnNavigation for %llu", aWindowID)); michael@0: michael@0: // Invalidate this window. The runnables check this value before making michael@0: // a call to content. michael@0: michael@0: nsTArray* callIds; michael@0: if (mCallIds.Get(aWindowID, &callIds)) { michael@0: for (int i = 0, len = callIds->Length(); i < len; ++i) { michael@0: mActiveCallbacks.Remove((*callIds)[i]); michael@0: } michael@0: mCallIds.Remove(aWindowID); michael@0: } michael@0: michael@0: // This is safe since we're on main-thread, and the windowlist can only michael@0: // be added to from the main-thread michael@0: nsPIDOMWindow *window = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(aWindowID)); michael@0: if (window) { michael@0: IterateWindowListeners(window, StopSharingCallback, nullptr); michael@0: } else { michael@0: RemoveWindowID(aWindowID); michael@0: } michael@0: } michael@0: michael@0: void michael@0: MediaManager::RemoveFromWindowList(uint64_t aWindowID, michael@0: GetUserMediaCallbackMediaStreamListener *aListener) michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "RemoveFromWindowList called off main thread"); michael@0: michael@0: // This is defined as safe on an inactive GUMCMSListener michael@0: aListener->Remove(); // really queues the remove michael@0: michael@0: StreamListeners* listeners = GetWindowListeners(aWindowID); michael@0: if (!listeners) { michael@0: return; michael@0: } michael@0: listeners->RemoveElement(aListener); michael@0: if (listeners->Length() == 0) { michael@0: RemoveWindowID(aWindowID); michael@0: // listeners has been deleted here michael@0: michael@0: // get outer windowID michael@0: nsPIDOMWindow *window = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(aWindowID)); michael@0: if (window) { michael@0: nsPIDOMWindow *outer = window->GetOuterWindow(); michael@0: if (outer) { michael@0: uint64_t outerID = outer->WindowID(); michael@0: michael@0: // Notify the UI that this window no longer has gUM active michael@0: char windowBuffer[32]; michael@0: PR_snprintf(windowBuffer, sizeof(windowBuffer), "%llu", outerID); michael@0: nsAutoString data; michael@0: data.Append(NS_ConvertUTF8toUTF16(windowBuffer)); michael@0: michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: obs->NotifyObservers(nullptr, "recording-window-ended", data.get()); michael@0: LOG(("Sent recording-window-ended for window %llu (outer %llu)", michael@0: aWindowID, outerID)); michael@0: } else { michael@0: LOG(("No outer window for inner %llu", aWindowID)); michael@0: } michael@0: } else { michael@0: LOG(("No inner window for %llu", aWindowID)); michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: MediaManager::GetPref(nsIPrefBranch *aBranch, const char *aPref, michael@0: const char *aData, int32_t *aVal) michael@0: { michael@0: int32_t temp; michael@0: if (aData == nullptr || strcmp(aPref,aData) == 0) { michael@0: if (NS_SUCCEEDED(aBranch->GetIntPref(aPref, &temp))) { michael@0: *aVal = temp; michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: MediaManager::GetPrefBool(nsIPrefBranch *aBranch, const char *aPref, michael@0: const char *aData, bool *aVal) michael@0: { michael@0: bool temp; michael@0: if (aData == nullptr || strcmp(aPref,aData) == 0) { michael@0: if (NS_SUCCEEDED(aBranch->GetBoolPref(aPref, &temp))) { michael@0: *aVal = temp; michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: MediaManager::GetPrefs(nsIPrefBranch *aBranch, const char *aData) michael@0: { michael@0: GetPref(aBranch, "media.navigator.video.default_width", aData, &mPrefs.mWidth); michael@0: GetPref(aBranch, "media.navigator.video.default_height", aData, &mPrefs.mHeight); michael@0: GetPref(aBranch, "media.navigator.video.default_fps", aData, &mPrefs.mFPS); michael@0: GetPref(aBranch, "media.navigator.video.default_minfps", aData, &mPrefs.mMinFPS); michael@0: } michael@0: michael@0: nsresult michael@0: MediaManager::Observe(nsISupports* aSubject, const char* aTopic, michael@0: const char16_t* aData) michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "Observer invoked off the main thread"); michael@0: nsCOMPtr obs = services::GetObserverService(); michael@0: michael@0: if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { michael@0: nsCOMPtr branch( do_QueryInterface(aSubject) ); michael@0: if (branch) { michael@0: GetPrefs(branch,NS_ConvertUTF16toUTF8(aData).get()); michael@0: LOG(("%s: %dx%d @%dfps (min %d)", __FUNCTION__, michael@0: mPrefs.mWidth, mPrefs.mHeight, mPrefs.mFPS, mPrefs.mMinFPS)); michael@0: } michael@0: } else if (!strcmp(aTopic, "xpcom-shutdown")) { michael@0: obs->RemoveObserver(this, "xpcom-shutdown"); michael@0: obs->RemoveObserver(this, "getUserMedia:response:allow"); michael@0: obs->RemoveObserver(this, "getUserMedia:response:deny"); michael@0: obs->RemoveObserver(this, "getUserMedia:revoke"); michael@0: michael@0: nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); michael@0: if (prefs) { michael@0: prefs->RemoveObserver("media.navigator.video.default_width", this); michael@0: prefs->RemoveObserver("media.navigator.video.default_height", this); michael@0: prefs->RemoveObserver("media.navigator.video.default_fps", this); michael@0: prefs->RemoveObserver("media.navigator.video.default_minfps", this); michael@0: } michael@0: michael@0: // Close off any remaining active windows. michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: GetActiveWindows()->Clear(); michael@0: mActiveCallbacks.Clear(); michael@0: mCallIds.Clear(); michael@0: LOG(("Releasing MediaManager singleton and thread")); michael@0: // Note: won't be released immediately as the Observer has a ref to us michael@0: sSingleton = nullptr; michael@0: mBackend = nullptr; michael@0: } michael@0: michael@0: return NS_OK; michael@0: michael@0: } else if (!strcmp(aTopic, "getUserMedia:response:allow")) { michael@0: nsString key(aData); michael@0: nsRefPtr runnable; michael@0: if (!mActiveCallbacks.Get(key, getter_AddRefs(runnable))) { michael@0: return NS_OK; michael@0: } michael@0: mActiveCallbacks.Remove(key); michael@0: michael@0: if (aSubject) { michael@0: // A particular device or devices were chosen by the user. michael@0: // NOTE: does not allow setting a device to null; assumes nullptr michael@0: nsCOMPtr array(do_QueryInterface(aSubject)); michael@0: MOZ_ASSERT(array); michael@0: uint32_t len = 0; michael@0: array->Count(&len); michael@0: MOZ_ASSERT(len); michael@0: if (!len) { michael@0: // neither audio nor video were selected michael@0: runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED")); michael@0: return NS_OK; michael@0: } michael@0: for (uint32_t i = 0; i < len; i++) { michael@0: nsCOMPtr supports; michael@0: array->GetElementAt(i,getter_AddRefs(supports)); michael@0: nsCOMPtr device(do_QueryInterface(supports)); michael@0: MOZ_ASSERT(device); // shouldn't be returning anything else... michael@0: if (device) { michael@0: nsString type; michael@0: device->GetType(type); michael@0: if (type.EqualsLiteral("video")) { michael@0: runnable->SetVideoDevice(static_cast(device.get())); michael@0: } else if (type.EqualsLiteral("audio")) { michael@0: runnable->SetAudioDevice(static_cast(device.get())); michael@0: } else { michael@0: NS_WARNING("Unknown device type in getUserMedia"); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Reuse the same thread to save memory. michael@0: mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL); michael@0: return NS_OK; michael@0: michael@0: } else if (!strcmp(aTopic, "getUserMedia:response:deny")) { michael@0: nsString errorMessage(NS_LITERAL_STRING("PERMISSION_DENIED")); michael@0: michael@0: if (aSubject) { michael@0: nsCOMPtr msg(do_QueryInterface(aSubject)); michael@0: MOZ_ASSERT(msg); michael@0: msg->GetData(errorMessage); michael@0: if (errorMessage.IsEmpty()) michael@0: errorMessage.Assign(NS_LITERAL_STRING("UNKNOWN_ERROR")); michael@0: } michael@0: michael@0: nsString key(aData); michael@0: nsRefPtr runnable; michael@0: if (!mActiveCallbacks.Get(key, getter_AddRefs(runnable))) { michael@0: return NS_OK; michael@0: } michael@0: mActiveCallbacks.Remove(key); michael@0: runnable->Denied(errorMessage); michael@0: return NS_OK; michael@0: michael@0: } else if (!strcmp(aTopic, "getUserMedia:revoke")) { michael@0: nsresult rv; michael@0: uint64_t windowID = nsString(aData).ToInteger64(&rv); michael@0: MOZ_ASSERT(NS_SUCCEEDED(rv)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: LOG(("Revoking MediaCapture access for window %llu",windowID)); michael@0: OnNavigation(windowID); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: #ifdef MOZ_WIDGET_GONK michael@0: else if (!strcmp(aTopic, "phone-state-changed")) { michael@0: nsString state(aData); michael@0: if (atoi((const char*)state.get()) == nsIAudioManager::PHONE_STATE_IN_CALL) { michael@0: StopMediaStreams(); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: #endif michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: static PLDHashOperator michael@0: WindowsHashToArrayFunc (const uint64_t& aId, michael@0: StreamListeners* aData, michael@0: void *userArg) michael@0: { michael@0: nsISupportsArray *array = michael@0: static_cast(userArg); michael@0: nsPIDOMWindow *window = static_cast michael@0: (nsGlobalWindow::GetInnerWindowWithId(aId)); michael@0: michael@0: MOZ_ASSERT(window); michael@0: if (window) { michael@0: // mActiveWindows contains both windows that have requested device michael@0: // access and windows that are currently capturing media. We want michael@0: // to return only the latter. See bug 975177. michael@0: bool capturing = false; michael@0: if (aData) { michael@0: uint32_t length = aData->Length(); michael@0: for (uint32_t i = 0; i < length; ++i) { michael@0: nsRefPtr listener = michael@0: aData->ElementAt(i); michael@0: if (listener->CapturingVideo() || listener->CapturingAudio()) { michael@0: capturing = true; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (capturing) michael@0: array->AppendElement(window); michael@0: } michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: MediaManager::GetActiveMediaCaptureWindows(nsISupportsArray **aArray) michael@0: { michael@0: MOZ_ASSERT(aArray); michael@0: nsISupportsArray *array; michael@0: nsresult rv = NS_NewISupportsArray(&array); // AddRefs michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: mActiveWindows.EnumerateRead(WindowsHashToArrayFunc, array); michael@0: michael@0: *aArray = array; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // XXX flags might be better... michael@0: struct CaptureWindowStateData { michael@0: bool *mVideo; michael@0: bool *mAudio; michael@0: }; michael@0: michael@0: static void michael@0: CaptureWindowStateCallback(MediaManager *aThis, michael@0: uint64_t aWindowID, michael@0: StreamListeners *aListeners, michael@0: void *aData) michael@0: { michael@0: struct CaptureWindowStateData *data = (struct CaptureWindowStateData *) aData; michael@0: michael@0: if (aListeners) { michael@0: auto length = aListeners->Length(); michael@0: for (size_t i = 0; i < length; ++i) { michael@0: GetUserMediaCallbackMediaStreamListener *listener = aListeners->ElementAt(i); michael@0: michael@0: if (listener->CapturingVideo()) { michael@0: *data->mVideo = true; michael@0: } michael@0: if (listener->CapturingAudio()) { michael@0: *data->mAudio = true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: michael@0: NS_IMETHODIMP michael@0: MediaManager::MediaCaptureWindowState(nsIDOMWindow* aWindow, bool* aVideo, michael@0: bool* aAudio) michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: struct CaptureWindowStateData data; michael@0: data.mVideo = aVideo; michael@0: data.mAudio = aAudio; michael@0: michael@0: *aVideo = false; michael@0: *aAudio = false; michael@0: michael@0: nsCOMPtr piWin = do_QueryInterface(aWindow); michael@0: if (piWin) { michael@0: IterateWindowListeners(piWin, CaptureWindowStateCallback, &data); michael@0: } michael@0: #ifdef DEBUG michael@0: LOG(("%s: window %lld capturing %s %s", __FUNCTION__, piWin ? piWin->WindowID() : -1, michael@0: *aVideo ? "video" : "", *aAudio ? "audio" : "")); michael@0: #endif michael@0: return NS_OK; michael@0: } michael@0: michael@0: // lets us do all sorts of things to the listeners michael@0: void michael@0: MediaManager::IterateWindowListeners(nsPIDOMWindow *aWindow, michael@0: WindowListenerCallback aCallback, michael@0: void *aData) michael@0: { michael@0: // Iterate the docshell tree to find all the child windows, and for each michael@0: // invoke the callback michael@0: nsCOMPtr piWin = do_QueryInterface(aWindow); michael@0: if (piWin) { michael@0: if (piWin->IsInnerWindow() || piWin->GetCurrentInnerWindow()) { michael@0: uint64_t windowID; michael@0: if (piWin->IsInnerWindow()) { michael@0: windowID = piWin->WindowID(); michael@0: } else { michael@0: windowID = piWin->GetCurrentInnerWindow()->WindowID(); michael@0: } michael@0: StreamListeners* listeners = GetActiveWindows()->Get(windowID); michael@0: // pass listeners so it can modify/delete the list michael@0: (*aCallback)(this, windowID, listeners, aData); michael@0: } michael@0: michael@0: // iterate any children of *this* window (iframes, etc) michael@0: nsCOMPtr docShell = piWin->GetDocShell(); michael@0: if (docShell) { michael@0: int32_t i, count; michael@0: docShell->GetChildCount(&count); michael@0: for (i = 0; i < count; ++i) { michael@0: nsCOMPtr item; michael@0: docShell->GetChildAt(i, getter_AddRefs(item)); michael@0: nsCOMPtr win = do_GetInterface(item); michael@0: michael@0: if (win) { michael@0: IterateWindowListeners(win, aCallback, aData); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: MediaManager::StopMediaStreams() michael@0: { michael@0: nsCOMPtr array; michael@0: GetActiveMediaCaptureWindows(getter_AddRefs(array)); michael@0: uint32_t len; michael@0: array->Count(&len); michael@0: for (uint32_t i = 0; i < len; i++) { michael@0: nsCOMPtr window; michael@0: array->GetElementAt(i, getter_AddRefs(window)); michael@0: nsCOMPtr win(do_QueryInterface(window)); michael@0: if (win) { michael@0: OnNavigation(win->WindowID()); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Can be invoked from EITHER MainThread or MSG thread michael@0: void michael@0: GetUserMediaCallbackMediaStreamListener::Invalidate() michael@0: { michael@0: michael@0: nsRefPtr runnable; michael@0: // We can't take a chance on blocking here, so proxy this to another michael@0: // thread. michael@0: // Pass a ref to us (which is threadsafe) so it can query us for the michael@0: // source stream info. michael@0: runnable = new MediaOperationRunnable(MEDIA_STOP, michael@0: this, nullptr, nullptr, michael@0: mAudioSource, mVideoSource, michael@0: mFinished, mWindowID, nullptr); michael@0: mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL); michael@0: } michael@0: michael@0: // Called from the MediaStreamGraph thread michael@0: void michael@0: GetUserMediaCallbackMediaStreamListener::NotifyFinished(MediaStreamGraph* aGraph) michael@0: { michael@0: mFinished = true; michael@0: Invalidate(); // we know it's been activated michael@0: NS_DispatchToMainThread(new GetUserMediaListenerRemove(mWindowID, this)); michael@0: } michael@0: michael@0: // Called from the MediaStreamGraph thread michael@0: // this can be in response to our own RemoveListener() (via ::Remove()), or michael@0: // because the DOM GC'd the DOMLocalMediaStream/etc we're attached to. michael@0: void michael@0: GetUserMediaCallbackMediaStreamListener::NotifyRemoved(MediaStreamGraph* aGraph) michael@0: { michael@0: { michael@0: MutexAutoLock lock(mLock); // protect access to mRemoved michael@0: MM_LOG(("Listener removed by DOM Destroy(), mFinished = %d", (int) mFinished)); michael@0: mRemoved = true; michael@0: } michael@0: if (!mFinished) { michael@0: NotifyFinished(aGraph); michael@0: } michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: GetUserMediaNotificationEvent::Run() michael@0: { michael@0: NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); michael@0: // Make sure mStream is cleared and our reference to the DOMMediaStream michael@0: // is dropped on the main thread, no matter what happens in this method. michael@0: // Otherwise this object might be destroyed off the main thread, michael@0: // releasing DOMMediaStream off the main thread, which is not allowed. michael@0: nsRefPtr stream = mStream.forget(); michael@0: michael@0: nsString msg; michael@0: switch (mStatus) { michael@0: case STARTING: michael@0: msg = NS_LITERAL_STRING("starting"); michael@0: stream->OnTracksAvailable(mOnTracksAvailableCallback.forget()); michael@0: break; michael@0: case STOPPING: michael@0: msg = NS_LITERAL_STRING("shutdown"); michael@0: if (mListener) { michael@0: mListener->SetStopped(); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: nsCOMPtr window = nsGlobalWindow::GetInnerWindowWithId(mWindowID); michael@0: NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); michael@0: michael@0: return MediaManager::NotifyRecordingStatusChange(window, msg, mIsAudio, mIsVideo); michael@0: } michael@0: michael@0: } // namespace mozilla