diff options
Diffstat (limited to 'src/core/media_capture_devices_dispatcher.cpp')
-rw-r--r-- | src/core/media_capture_devices_dispatcher.cpp | 511 |
1 files changed, 511 insertions, 0 deletions
diff --git a/src/core/media_capture_devices_dispatcher.cpp b/src/core/media_capture_devices_dispatcher.cpp new file mode 100644 index 000000000..2997576ff --- /dev/null +++ b/src/core/media_capture_devices_dispatcher.cpp @@ -0,0 +1,511 @@ +/**************************************************************************** +** +** 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<content::MediaStreamUI> getDevicesForDesktopCapture(content::MediaStreamDevices &devices, content::DesktopMediaID mediaId + , bool captureAudio, bool /*display_notification*/, base::string16 /*application_title*/) +{ + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + scoped_ptr<content::MediaStreamUI> 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<content::WebContents*, RequestsQueue>::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<content::MediaStreamUI>()); +} + + + +MediaCaptureDevicesDispatcher *MediaCaptureDevicesDispatcher::GetInstance() +{ + return Singleton<MediaCaptureDevicesDispatcher>::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<content::WebContents>(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<content::MediaStreamUI> 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<void(bool, const base::string16&)> 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<content::MediaStreamUI>()); +} + +void MediaCaptureDevicesDispatcher::handleScreenCaptureAccessRequest(content::WebContents *webContents, bool userAccepted, const base::string16 &) +{ + content::MediaStreamDevices devices; + scoped_ptr<content::MediaStreamUI> 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<content::WebContents*, RequestsQueue>::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<content::WebContents*, RequestsQueue>::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; + } + } +} |