/**************************************************************************** ** ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtWebEngine module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "media_capture_devices_dispatcher.h" #include "javascript_dialog_manager_qt.h" #include "type_conversion.h" #include "web_contents_view_qt.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/media/desktop_streams_registry.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/desktop_media_id.h" #include "content/public/browser/media_devices_monitor.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/notification_types.h" #include "content/public/browser/web_contents.h" #include "content/public/common/media_stream_request.h" #include "media/audio/audio_manager_base.h" #include "ui/base/l10n/l10n_util.h" using content::BrowserThread; using content::MediaStreamDevices; namespace { const content::MediaStreamDevice *findDeviceWithId(const content::MediaStreamDevices &devices, const std::string &deviceId) { content::MediaStreamDevices::const_iterator iter = devices.begin(); for (; iter != devices.end(); ++iter) { if (iter->id == deviceId) { return &(*iter); } } return 0; } base::string16 getContentsUrl(content::WebContents *webContents) { return UTF8ToUTF16(webContents->GetURL().GetOrigin().spec()); } scoped_ptr getDevicesForDesktopCapture(content::MediaStreamDevices &devices, content::DesktopMediaID mediaId , bool captureAudio, bool /*display_notification*/, base::string16 /*application_title*/) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); scoped_ptr ui; // Add selected desktop source to the list. devices.push_back(content::MediaStreamDevice( content::MEDIA_DESKTOP_VIDEO_CAPTURE, mediaId.ToString(), "Screen")); if (captureAudio) { // Use the special loopback device ID for system audio capture. devices.push_back(content::MediaStreamDevice( content::MEDIA_LOOPBACK_AUDIO_CAPTURE, media::AudioManagerBase::kLoopbackInputDeviceId, "System Audio")); } return ui.Pass(); } WebContentsAdapterClient::MediaRequestFlags mediaRequestFlagsForRequest(const content::MediaStreamRequest &request) { WebContentsAdapterClient::MediaRequestFlags requestFlags; if (request.audio_type == content::MEDIA_DEVICE_AUDIO_CAPTURE) requestFlags |= WebContentsAdapterClient::MediaAudioCapture; if (request.video_type == content::MEDIA_DEVICE_VIDEO_CAPTURE) requestFlags |= WebContentsAdapterClient::MediaVideoCapture; return requestFlags; } } // namespace MediaCaptureDevicesDispatcher::PendingAccessRequest::PendingAccessRequest(const content::MediaStreamRequest &request , const content::MediaResponseCallback &callback) : request(request) , callback(callback) { } MediaCaptureDevicesDispatcher::PendingAccessRequest::~PendingAccessRequest() { } void MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse(content::WebContents *webContents, const QUrl &securityOrigin , WebContentsAdapterClient::MediaRequestFlags authorizationFlags) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); content::MediaStreamDevices devices; std::map::iterator it = m_pendingRequests.find(webContents); if (it == m_pendingRequests.end()) // WebContents has been destroyed. Don't need to do anything. return; RequestsQueue &queue(it->second); if (queue.empty()) return; content::MediaStreamRequest &request = queue.front().request; const QUrl requestSecurityOrigin(toQt(request.security_origin)); bool securityOriginsMatch = (requestSecurityOrigin.host() == securityOrigin.host() && requestSecurityOrigin.scheme() == securityOrigin.scheme() && requestSecurityOrigin.port() == securityOrigin.port()); if (!securityOriginsMatch) qWarning("Security origin mismatch for media access permission: %s requested and %s provided\n", qPrintable(requestSecurityOrigin.toString()) , qPrintable(securityOrigin.toString())); bool microphoneRequested = (request.audio_type && authorizationFlags & WebContentsAdapterClient::MediaAudioCapture); bool webcamRequested = (request.video_type && authorizationFlags & WebContentsAdapterClient::MediaVideoCapture); if (securityOriginsMatch && (microphoneRequested || webcamRequested)) { switch (request.request_type) { case content::MEDIA_OPEN_DEVICE: Q_UNREACHABLE(); // only speculative as this is for Pepper getDefaultDevices("", "", microphoneRequested, webcamRequested, &devices); break; case content::MEDIA_DEVICE_ACCESS: case content::MEDIA_GENERATE_STREAM: case content::MEDIA_ENUMERATE_DEVICES: getDefaultDevices(request.requested_audio_device_id, request.requested_video_device_id, microphoneRequested, webcamRequested, &devices); break; } } content::MediaResponseCallback callback = queue.front().callback; queue.pop_front(); if (!queue.empty()) { // Post a task to process next queued request. It has to be done // asynchronously to make sure that calling infobar is not destroyed until // after this function returns. BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&MediaCaptureDevicesDispatcher::ProcessQueuedAccessRequest, base::Unretained(this), webContents)); } callback.Run(devices, scoped_ptr()); } MediaCaptureDevicesDispatcher *MediaCaptureDevicesDispatcher::GetInstance() { return Singleton::get(); } MediaCaptureDevicesDispatcher::MediaCaptureDevicesDispatcher() : m_devicesEnumerated(false) { // MediaCaptureDevicesDispatcher is a singleton. It should be created on // UI thread. Otherwise, it will not receive // content::NOTIFICATION_WEB_CONTENTS_DESTROYED, and that will result in // possible use after free. DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); m_notificationsRegistrar.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, content::NotificationService::AllSources()); } MediaCaptureDevicesDispatcher::~MediaCaptureDevicesDispatcher() { } const MediaStreamDevices &MediaCaptureDevicesDispatcher::getAudioCaptureDevices() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (!m_devicesEnumerated) { content::EnsureMonitorCaptureDevices(); m_devicesEnumerated = true; } return m_audioDevices; } const MediaStreamDevices &MediaCaptureDevicesDispatcher::getVideoCaptureDevices() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (!m_devicesEnumerated) { content::EnsureMonitorCaptureDevices(); m_devicesEnumerated = true; } return m_videoDevices; } void MediaCaptureDevicesDispatcher::Observe(int type, const content::NotificationSource &source, const content::NotificationDetails &details) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (type == content::NOTIFICATION_WEB_CONTENTS_DESTROYED) { content::WebContents *webContents = content::Source(source).ptr(); m_pendingRequests.erase(webContents); } } void MediaCaptureDevicesDispatcher::processMediaAccessRequest(WebContentsAdapterClient *adapterClient, content::WebContents *webContents , const content::MediaStreamRequest &request , const content::MediaResponseCallback &callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); // Let's not support tab capture for now. if (request.video_type == content::MEDIA_TAB_VIDEO_CAPTURE || request.audio_type == content::MEDIA_TAB_AUDIO_CAPTURE) return; if (request.video_type == content::MEDIA_DESKTOP_VIDEO_CAPTURE || request.audio_type == content::MEDIA_LOOPBACK_AUDIO_CAPTURE) // It's still unclear what to make of screen capture. We can rely on existing javascript dialog infrastructure // to experiment with this without exposing it through our API yet. processDesktopCaptureAccessRequest(webContents, request, callback); else { enqueueMediaAccessRequest(webContents, request, callback); // We might not require this approval for pepper requests. adapterClient->runMediaAccessPermissionRequest(toQt(request.security_origin), mediaRequestFlagsForRequest(request)); } } void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request , const content::MediaResponseCallback &callback) { content::MediaStreamDevices devices; scoped_ptr ui; if (request.video_type != content::MEDIA_DESKTOP_VIDEO_CAPTURE) { callback.Run(devices, ui.Pass()); return; } // If the device id wasn't specified then this is a screen capture request // (i.e. chooseDesktopMedia() API wasn't used to generate device id). if (request.requested_video_device_id.empty()) { processScreenCaptureAccessRequest( webContents, request, callback); return; } // Resolve DesktopMediaID for the specified device id. content::DesktopMediaID mediaId = getDesktopStreamsRegistry()->RequestMediaForStreamId( request.requested_video_device_id, request.render_process_id, request.render_view_id, request.security_origin); // Received invalid device id. if (mediaId.type == content::DesktopMediaID::TYPE_NONE) { callback.Run(devices, ui.Pass()); return; } // Audio is only supported for screen capture streams. bool capture_audio = (mediaId.type == content::DesktopMediaID::TYPE_SCREEN && request.audio_type == content::MEDIA_LOOPBACK_AUDIO_CAPTURE); ui = getDevicesForDesktopCapture( devices, mediaId, capture_audio, true, getContentsUrl(webContents)); callback.Run(devices, ui.Pass()); } void MediaCaptureDevicesDispatcher::processScreenCaptureAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request ,const content::MediaResponseCallback &callback) { DCHECK_EQ(request.video_type, content::MEDIA_DESKTOP_VIDEO_CAPTURE); // FIXME: expose through the settings once we have them const bool screenCaptureEnabled = !qgetenv("QT_WEBENGINE_USE_EXPERIMENTAL_SCREEN_CAPTURE").isNull(); const bool originIsSecure = request.security_origin.SchemeIsSecure(); if (screenCaptureEnabled && originIsSecure) { enqueueMediaAccessRequest(webContents, request, callback); base::Callback dialogCallback = base::Bind(&MediaCaptureDevicesDispatcher::handleScreenCaptureAccessRequest, base::Unretained(this), base::Unretained(webContents)); QUrl securityOrigin(toQt(request.security_origin)); QString message = QObject::tr("Do you want %1 to share your screen?").arg(securityOrigin.toString()); QString title = QObject::tr("%1 Screen Sharing request").arg(securityOrigin.toString()); JavaScriptDialogManagerQt::GetInstance()->runDialogForContents(webContents, WebContentsAdapterClient::InternalAuthorizationDialog, message , QString(), securityOrigin, dialogCallback, title); } else callback.Run(content::MediaStreamDevices(), scoped_ptr()); } void MediaCaptureDevicesDispatcher::handleScreenCaptureAccessRequest(content::WebContents *webContents, bool userAccepted, const base::string16 &) { content::MediaStreamDevices devices; scoped_ptr ui; if (userAccepted) { content::DesktopMediaID screenId = content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); ui = getDevicesForDesktopCapture(devices, screenId, false/*capture_audio*/, false/*display_notification*/, getContentsUrl(webContents)); } std::map::iterator it = m_pendingRequests.find(webContents); if (it == m_pendingRequests.end()) { // WebContents has been destroyed. Don't need to do anything. return; } RequestsQueue &queue(it->second); if (queue.empty()) return; content::MediaResponseCallback callback = queue.front().callback; queue.pop_front(); callback.Run(devices, ui.Pass()); } void MediaCaptureDevicesDispatcher::enqueueMediaAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request ,const content::MediaResponseCallback &callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); RequestsQueue &queue = m_pendingRequests[webContents]; queue.push_back(PendingAccessRequest(request, callback)); } void MediaCaptureDevicesDispatcher::ProcessQueuedAccessRequest(content::WebContents *webContents) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); std::map::iterator it = m_pendingRequests.find(webContents); if (it == m_pendingRequests.end() || it->second.empty()) return; RequestsQueue &queue(it->second); if (queue.empty()) return; content::MediaStreamRequest &request = queue.front().request; DCHECK(!it->second.empty()); WebContentsAdapterClient *adapterClient = WebContentsViewQt::from(webContents->GetView())->client(); adapterClient->runMediaAccessPermissionRequest(toQt(request.security_origin), mediaRequestFlagsForRequest(request)); } void MediaCaptureDevicesDispatcher::getDefaultDevices(const std::string &audioDeviceId, const std::string &videoDeviceId, bool audio, bool video , content::MediaStreamDevices *devices) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(audio || video); if (audio) { const content::MediaStreamDevices &audioDevices = getAudioCaptureDevices(); const content::MediaStreamDevice *device = findDeviceWithId(audioDevices, audioDeviceId); if (!device && !audioDevices.empty()) device = &(*audioDevices.begin()); if (device) devices->push_back(*device); } if (video) { const content::MediaStreamDevices &videoDevices = getVideoCaptureDevices(); const content::MediaStreamDevice *device = findDeviceWithId(videoDevices, videoDeviceId); if (!device && !videoDevices.empty()) device = &(*videoDevices.begin()); if (device) devices->push_back(*device); } } DesktopStreamsRegistry *MediaCaptureDevicesDispatcher::getDesktopStreamsRegistry() { if (!m_desktopStreamsRegistry) m_desktopStreamsRegistry.reset(new DesktopStreamsRegistry()); return m_desktopStreamsRegistry.get(); } void MediaCaptureDevicesDispatcher::OnAudioCaptureDevicesChanged(const content::MediaStreamDevices &devices) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&MediaCaptureDevicesDispatcher::updateAudioDevicesOnUIThread, base::Unretained(this), devices)); } void MediaCaptureDevicesDispatcher::OnVideoCaptureDevicesChanged(const content::MediaStreamDevices &devices) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&MediaCaptureDevicesDispatcher::updateVideoDevicesOnUIThread, base::Unretained(this), devices)); } void MediaCaptureDevicesDispatcher::OnMediaRequestStateChanged(int renderProcessId, int renderViewId, int pageRequestId , const content::MediaStreamDevice &device, content::MediaRequestState state) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind( &MediaCaptureDevicesDispatcher::updateMediaRequestStateOnUIThread, base::Unretained(this), renderProcessId, renderViewId, pageRequestId, device, state)); } void MediaCaptureDevicesDispatcher::OnCreatingAudioStream(int /*renderProcessId*/, int /*renderViewId*/) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); } void MediaCaptureDevicesDispatcher::updateAudioDevicesOnUIThread(const content::MediaStreamDevices &devices) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); m_devicesEnumerated = true; m_audioDevices = devices; } void MediaCaptureDevicesDispatcher::updateVideoDevicesOnUIThread(const content::MediaStreamDevices &devices) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); m_devicesEnumerated = true; m_videoDevices = devices; } void MediaCaptureDevicesDispatcher::updateMediaRequestStateOnUIThread(int renderProcessId, int renderViewId, int pageRequestId , const content::MediaStreamDevice &device, content::MediaRequestState state) { // Track desktop capture sessions. Tracking is necessary to avoid unbalanced // session counts since not all requests will reach MEDIA_REQUEST_STATE_DONE, // but they will all reach MEDIA_REQUEST_STATE_CLOSING. if (device.type == content::MEDIA_DESKTOP_VIDEO_CAPTURE) { if (state == content::MEDIA_REQUEST_STATE_DONE) { DesktopCaptureSession session = { renderProcessId, renderViewId, pageRequestId }; m_desktopCaptureSessions.push_back(session); } else if (state == content::MEDIA_REQUEST_STATE_CLOSING) { for (DesktopCaptureSessions::iterator it = m_desktopCaptureSessions.begin(); it != m_desktopCaptureSessions.end(); ++it) { if (it->render_process_id == renderProcessId && it->render_view_id == renderViewId && it->page_request_id == pageRequestId) { m_desktopCaptureSessions.erase(it); break; } } } } // Cancel the request. if (state == content::MEDIA_REQUEST_STATE_CLOSING) { bool found = false; for (RequestsQueues::iterator rqs_it = m_pendingRequests.begin(); rqs_it != m_pendingRequests.end(); ++rqs_it) { RequestsQueue &queue = rqs_it->second; for (RequestsQueue::iterator it = queue.begin(); it != queue.end(); ++it) { if (it->request.render_process_id == renderProcessId && it->request.render_view_id == renderViewId && it->request.page_request_id == pageRequestId) { queue.erase(it); found = true; break; } } if (found) break; } } }