/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set sw=2 ts=8 et ft=cpp : */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsIPrefService.h" #include "nsIPrefBranch.h" #include "CSFLog.h" #include "prenv.h" #include "mozilla/Logging.h" #ifdef XP_WIN #include "mozilla/WindowsVersion.h" #endif static mozilla::LazyLogModule sGetUserMediaLog("GetUserMedia"); #include "MediaEngineWebRTC.h" #include "ImageContainer.h" #include "nsIComponentRegistrar.h" #include "MediaEngineTabVideoSource.h" #include "MediaEngineRemoteVideoSource.h" #include "CamerasChild.h" #include "nsITabSource.h" #include "MediaTrackConstraints.h" #ifdef MOZ_WIDGET_ANDROID #include "AndroidJNIWrapper.h" #include "AndroidBridge.h" #endif #undef LOG #define LOG(args) MOZ_LOG(sGetUserMediaLog, mozilla::LogLevel::Debug, args) namespace mozilla { // statics from AudioInputCubeb nsTArray<int>* AudioInputCubeb::mDeviceIndexes; int AudioInputCubeb::mDefaultDevice = -1; nsTArray<nsCString>* AudioInputCubeb::mDeviceNames; cubeb_device_collection* AudioInputCubeb::mDevices = nullptr; bool AudioInputCubeb::mAnyInUse = false; StaticMutex AudioInputCubeb::sMutex; // AudioDeviceID is an annoying opaque value that's really a string // pointer, and is freed when the cubeb_device_collection is destroyed void AudioInputCubeb::UpdateDeviceList() { cubeb* cubebContext = CubebUtils::GetCubebContext(); if (!cubebContext) { return; } cubeb_device_collection *devices = nullptr; if (CUBEB_OK != cubeb_enumerate_devices(cubebContext, CUBEB_DEVICE_TYPE_INPUT, &devices)) { return; } for (auto& device_index : (*mDeviceIndexes)) { device_index = -1; // unmapped } // We keep all the device names, but wipe the mappings and rebuild them // Calculate translation from existing mDevices to new devices. Note we // never end up with less devices than before, since people have // stashed indexes. // For some reason the "fake" device for automation is marked as DISABLED, // so white-list it. mDefaultDevice = -1; for (uint32_t i = 0; i < devices->count; i++) { LOG(("Cubeb device %u: type 0x%x, state 0x%x, name %s, id %p", i, devices->device[i]->type, devices->device[i]->state, devices->device[i]->friendly_name, devices->device[i]->device_id)); if (devices->device[i]->type == CUBEB_DEVICE_TYPE_INPUT && // paranoia (devices->device[i]->state == CUBEB_DEVICE_STATE_ENABLED || (devices->device[i]->state == CUBEB_DEVICE_STATE_DISABLED && devices->device[i]->friendly_name && strcmp(devices->device[i]->friendly_name, "Sine source at 440 Hz") == 0))) { auto j = mDeviceNames->IndexOf(devices->device[i]->device_id); if (j != nsTArray<nsCString>::NoIndex) { // match! update the mapping (*mDeviceIndexes)[j] = i; } else { // new device, add to the array mDeviceIndexes->AppendElement(i); mDeviceNames->AppendElement(devices->device[i]->device_id); j = mDeviceIndexes->Length()-1; } if (devices->device[i]->preferred & CUBEB_DEVICE_PREF_VOICE) { // There can be only one... we hope NS_ASSERTION(mDefaultDevice == -1, "multiple default cubeb input devices!"); mDefaultDevice = j; } } } LOG(("Cubeb default input device %d", mDefaultDevice)); StaticMutexAutoLock lock(sMutex); // swap state if (mDevices) { cubeb_device_collection_destroy(mDevices); } mDevices = devices; } MediaEngineWebRTC::MediaEngineWebRTC(MediaEnginePrefs &aPrefs) : mMutex("mozilla::MediaEngineWebRTC"), mVoiceEngine(nullptr), mAudioInput(nullptr), mFullDuplex(aPrefs.mFullDuplex), mExtendedFilter(aPrefs.mExtendedFilter), mDelayAgnostic(aPrefs.mDelayAgnostic), mHasTabVideoSource(false) { nsCOMPtr<nsIComponentRegistrar> compMgr; NS_GetComponentRegistrar(getter_AddRefs(compMgr)); if (compMgr) { compMgr->IsContractIDRegistered(NS_TABSOURCESERVICE_CONTRACTID, &mHasTabVideoSource); } // XXX gFarendObserver = new AudioOutputObserver(); camera::GetChildAndCall( &camera::CamerasChild::AddDeviceChangeCallback, this); } void MediaEngineWebRTC::SetFakeDeviceChangeEvents() { camera::GetChildAndCall( &camera::CamerasChild::SetFakeDeviceChangeEvents); } void MediaEngineWebRTC::EnumerateVideoDevices(dom::MediaSourceEnum aMediaSource, nsTArray<RefPtr<MediaEngineVideoSource> >* aVSources) { // We spawn threads to handle gUM runnables, so we must protect the member vars MutexAutoLock lock(mMutex); mozilla::camera::CaptureEngine capEngine = mozilla::camera::InvalidEngine; #ifdef MOZ_WIDGET_ANDROID // get the JVM JavaVM* jvm; JNIEnv* const env = jni::GetEnvForThread(); MOZ_ALWAYS_TRUE(!env->GetJavaVM(&jvm)); if (webrtc::VideoEngine::SetAndroidObjects(jvm) != 0) { LOG(("VieCapture:SetAndroidObjects Failed")); return; } #endif bool scaryKind = false; // flag sources with cross-origin exploit potential switch (aMediaSource) { case dom::MediaSourceEnum::Window: capEngine = mozilla::camera::WinEngine; break; case dom::MediaSourceEnum::Application: capEngine = mozilla::camera::AppEngine; break; case dom::MediaSourceEnum::Screen: capEngine = mozilla::camera::ScreenEngine; scaryKind = true; break; case dom::MediaSourceEnum::Browser: capEngine = mozilla::camera::BrowserEngine; scaryKind = true; break; case dom::MediaSourceEnum::Camera: capEngine = mozilla::camera::CameraEngine; break; default: // BOOM MOZ_CRASH("No valid video engine"); break; } /** * We still enumerate every time, in case a new device was plugged in since * the last call. TODO: Verify that WebRTC actually does deal with hotplugging * new devices (with or without new engine creation) and accordingly adjust. * Enumeration is not neccessary if GIPS reports the same set of devices * for a given instance of the engine. Likewise, if a device was plugged out, * mVideoSources must be updated. */ int num; num = mozilla::camera::GetChildAndCall( &mozilla::camera::CamerasChild::NumberOfCaptureDevices, capEngine); for (int i = 0; i < num; i++) { char deviceName[MediaEngineSource::kMaxDeviceNameLength]; char uniqueId[MediaEngineSource::kMaxUniqueIdLength]; bool scarySource = false; // paranoia deviceName[0] = '\0'; uniqueId[0] = '\0'; int error; error = mozilla::camera::GetChildAndCall( &mozilla::camera::CamerasChild::GetCaptureDevice, capEngine, i, deviceName, sizeof(deviceName), uniqueId, sizeof(uniqueId), &scarySource); if (error) { LOG(("camera:GetCaptureDevice: Failed %d", error )); continue; } #ifdef DEBUG LOG((" Capture Device Index %d, Name %s", i, deviceName)); webrtc::CaptureCapability cap; int numCaps = mozilla::camera::GetChildAndCall( &mozilla::camera::CamerasChild::NumberOfCapabilities, capEngine, uniqueId); LOG(("Number of Capabilities %d", numCaps)); for (int j = 0; j < numCaps; j++) { if (mozilla::camera::GetChildAndCall( &mozilla::camera::CamerasChild::GetCaptureCapability, capEngine, uniqueId, j, cap) != 0) { break; } LOG(("type=%d width=%d height=%d maxFPS=%d", cap.rawType, cap.width, cap.height, cap.maxFPS )); } #endif if (uniqueId[0] == '\0') { // In case a device doesn't set uniqueId! strncpy(uniqueId, deviceName, sizeof(uniqueId)); uniqueId[sizeof(uniqueId)-1] = '\0'; // strncpy isn't safe } RefPtr<MediaEngineVideoSource> vSource; NS_ConvertUTF8toUTF16 uuid(uniqueId); if (mVideoSources.Get(uuid, getter_AddRefs(vSource))) { // We've already seen this device, just refresh and append. static_cast<MediaEngineRemoteVideoSource*>(vSource.get())->Refresh(i); aVSources->AppendElement(vSource.get()); } else { vSource = new MediaEngineRemoteVideoSource(i, capEngine, aMediaSource, scaryKind || scarySource); mVideoSources.Put(uuid, vSource); // Hashtable takes ownership. aVSources->AppendElement(vSource); } } if (mHasTabVideoSource || dom::MediaSourceEnum::Browser == aMediaSource) { aVSources->AppendElement(new MediaEngineTabVideoSource()); } } bool MediaEngineWebRTC::SupportsDuplex() { return mFullDuplex; } void MediaEngineWebRTC::EnumerateAudioDevices(dom::MediaSourceEnum aMediaSource, nsTArray<RefPtr<MediaEngineAudioSource> >* aASources) { ScopedCustomReleasePtr<webrtc::VoEBase> ptrVoEBase; // We spawn threads to handle gUM runnables, so we must protect the member vars MutexAutoLock lock(mMutex); if (aMediaSource == dom::MediaSourceEnum::AudioCapture) { RefPtr<MediaEngineWebRTCAudioCaptureSource> audioCaptureSource = new MediaEngineWebRTCAudioCaptureSource(nullptr); aASources->AppendElement(audioCaptureSource); return; } #ifdef MOZ_WIDGET_ANDROID jobject context = mozilla::AndroidBridge::Bridge()->GetGlobalContextRef(); // get the JVM JavaVM* jvm; JNIEnv* const env = jni::GetEnvForThread(); MOZ_ALWAYS_TRUE(!env->GetJavaVM(&jvm)); if (webrtc::VoiceEngine::SetAndroidObjects(jvm, (void*)context) != 0) { LOG(("VoiceEngine:SetAndroidObjects Failed")); return; } #endif if (!mVoiceEngine) { mConfig.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(mExtendedFilter)); mConfig.Set<webrtc::DelayAgnostic>(new webrtc::DelayAgnostic(mDelayAgnostic)); mVoiceEngine = webrtc::VoiceEngine::Create(mConfig); if (!mVoiceEngine) { return; } } ptrVoEBase = webrtc::VoEBase::GetInterface(mVoiceEngine); if (!ptrVoEBase) { return; } // Always re-init the voice engine, since if we close the last use we // DeInitEngine() and Terminate(), which shuts down Process() - but means // we have to Init() again before using it. Init() when already inited is // just a no-op, so call always. if (ptrVoEBase->Init() < 0) { return; } if (!mAudioInput) { if (SupportsDuplex()) { // The platform_supports_full_duplex. mAudioInput = new mozilla::AudioInputCubeb(mVoiceEngine); } else { mAudioInput = new mozilla::AudioInputWebRTC(mVoiceEngine); } } int nDevices = 0; mAudioInput->GetNumOfRecordingDevices(nDevices); int i; #if defined(MOZ_WIDGET_ANDROID) i = 0; // Bug 1037025 - let the OS handle defaulting for now on android/b2g #else // -1 is "default communications device" depending on OS in webrtc.org code i = -1; #endif for (; i < nDevices; i++) { // We use constants here because GetRecordingDeviceName takes char[128]. char deviceName[128]; char uniqueId[128]; // paranoia; jingle doesn't bother with this deviceName[0] = '\0'; uniqueId[0] = '\0'; int error = mAudioInput->GetRecordingDeviceName(i, deviceName, uniqueId); if (error) { LOG((" VoEHardware:GetRecordingDeviceName: Failed %d", error)); continue; } if (uniqueId[0] == '\0') { // Mac and Linux don't set uniqueId! MOZ_ASSERT(sizeof(deviceName) == sizeof(uniqueId)); // total paranoia strcpy(uniqueId, deviceName); // safe given assert and initialization/error-check } RefPtr<MediaEngineAudioSource> aSource; NS_ConvertUTF8toUTF16 uuid(uniqueId); if (mAudioSources.Get(uuid, getter_AddRefs(aSource))) { // We've already seen this device, just append. aASources->AppendElement(aSource.get()); } else { AudioInput* audioinput = mAudioInput; if (SupportsDuplex()) { // The platform_supports_full_duplex. // For cubeb, it has state (the selected ID) // XXX just use the uniqueID for cubeb and support it everywhere, and get rid of this // XXX Small window where the device list/index could change! audioinput = new mozilla::AudioInputCubeb(mVoiceEngine, i); } aSource = new MediaEngineWebRTCMicrophoneSource(mVoiceEngine, audioinput, i, deviceName, uniqueId); mAudioSources.Put(uuid, aSource); // Hashtable takes ownership. aASources->AppendElement(aSource); } } } void MediaEngineWebRTC::Shutdown() { // This is likely paranoia MutexAutoLock lock(mMutex); if (camera::GetCamerasChildIfExists()) { camera::GetChildAndCall( &camera::CamerasChild::RemoveDeviceChangeCallback, this); } LOG(("%s", __FUNCTION__)); // Shutdown all the sources, since we may have dangling references to the // sources in nsDOMUserMediaStreams waiting for GC/CC for (auto iter = mVideoSources.Iter(); !iter.Done(); iter.Next()) { MediaEngineVideoSource* source = iter.UserData(); if (source) { source->Shutdown(); } } for (auto iter = mAudioSources.Iter(); !iter.Done(); iter.Next()) { MediaEngineAudioSource* source = iter.UserData(); if (source) { source->Shutdown(); } } mVideoSources.Clear(); mAudioSources.Clear(); if (mVoiceEngine) { mVoiceEngine->SetTraceCallback(nullptr); webrtc::VoiceEngine::Delete(mVoiceEngine); } mVoiceEngine = nullptr; mozilla::camera::Shutdown(); AudioInputCubeb::CleanupGlobalData(); } }