| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** 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 The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/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 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.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.Chromium file. |
| |
| #include "media_capture_devices_dispatcher.h" |
| |
| #include "javascript_dialog_manager_qt.h" |
| #include "type_conversion.h" |
| #include "web_contents_delegate_qt.h" |
| #include "web_contents_view_qt.h" |
| #include "web_engine_settings.h" |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/desktop_media_id.h" |
| #include "content/public/browser/desktop_streams_registry.h" |
| #include "content/public/browser/media_capture_devices.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/render_process_host.h" |
| #include "content/public/common/origin_util.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/audio/audio_manager_base.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if QT_CONFIG(webengine_webrtc) |
| #include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h" |
| #include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h" |
| #endif |
| |
| #include <QtCore/qcoreapplication.h> |
| |
| namespace QtWebEngineCore { |
| |
| using content::BrowserThread; |
| using blink::mojom::MediaStreamRequestResult; |
| using blink::mojom::MediaStreamType; |
| |
| namespace { |
| |
| const blink::MediaStreamDevice *findDeviceWithId(const blink::MediaStreamDevices &devices, const std::string &deviceId) |
| { |
| blink::MediaStreamDevices::const_iterator iter = devices.begin(); |
| for (; iter != devices.end(); ++iter) { |
| if (iter->id == deviceId) { |
| return &(*iter); |
| } |
| } |
| return 0; |
| } |
| |
| // Based on chrome/browser/media/webrtc/desktop_capture_devices_util.cc: |
| void getDevicesForDesktopCapture(blink::MediaStreamDevices *devices, |
| content::DesktopMediaID mediaId, |
| bool captureAudio, |
| MediaStreamType videoType, |
| MediaStreamType audioType) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // Add selected desktop source to the list. |
| devices->push_back(blink::MediaStreamDevice(videoType, mediaId.ToString(), mediaId.ToString())); |
| if (captureAudio) { |
| if (mediaId.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) { |
| devices->push_back( |
| blink::MediaStreamDevice(audioType, mediaId.ToString(), "Tab audio")); |
| } else { |
| // Use the special loopback device ID for system audio capture. |
| devices->push_back(blink::MediaStreamDevice( |
| audioType, |
| media::AudioDeviceDescription::kLoopbackInputDeviceId, |
| "System Audio")); |
| } |
| } |
| } |
| |
| content::DesktopMediaID getDefaultScreenId() |
| { |
| // While this function is executing another thread may also want to create a |
| // DesktopCapturer [1]. Unfortunately, creating a DesktopCapturer is not |
| // thread safe on X11 due to the use of webrtc::XErrorTrap. It's safe to |
| // disable this code on X11 since we don't actually need to create a |
| // DesktopCapturer to get the screen id anyway |
| // (ScreenCapturerLinux::GetSourceList always returns 0 as the id). |
| // |
| // [1]: webrtc::InProcessVideoCaptureDeviceLauncher::DoStartDesktopCaptureOnDeviceThread |
| |
| #if QT_CONFIG(webengine_webrtc) && !defined(WEBRTC_USE_X11) |
| // Source id patterns are different across platforms. |
| // On Linux, the hardcoded value "0" is used. |
| // On Windows, the screens are enumerated consecutively in increasing order from 0. |
| // On macOS the source ids are randomish numbers assigned by the OS. |
| |
| // In order to provide a correct screen id, we query for the available screen ids, and |
| // select the first one as the main display id. |
| // The code is based on the file |
| // src/chrome/browser/extensions/api/desktop_capture/desktop_capture_base.cc. |
| webrtc::DesktopCaptureOptions options = |
| webrtc::DesktopCaptureOptions::CreateDefault(); |
| options.set_disable_effects(false); |
| std::unique_ptr<webrtc::DesktopCapturer> screen_capturer( |
| webrtc::DesktopCapturer::CreateScreenCapturer(options)); |
| |
| if (screen_capturer) { |
| webrtc::DesktopCapturer::SourceList screens; |
| if (screen_capturer->GetSourceList(&screens)) { |
| if (screens.size() > 0) { |
| return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, screens[0].id); |
| } |
| } |
| } |
| #endif |
| |
| return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); |
| } |
| |
| WebContentsAdapterClient::MediaRequestFlags mediaRequestFlagsForRequest(const content::MediaStreamRequest &request) |
| { |
| if (request.audio_type == MediaStreamType::DEVICE_AUDIO_CAPTURE && |
| request.video_type == MediaStreamType::DEVICE_VIDEO_CAPTURE) |
| return {WebContentsAdapterClient::MediaAudioCapture, WebContentsAdapterClient::MediaVideoCapture}; |
| |
| if (request.audio_type == MediaStreamType::DEVICE_AUDIO_CAPTURE && |
| request.video_type == MediaStreamType::NO_SERVICE) |
| return {WebContentsAdapterClient::MediaAudioCapture}; |
| |
| if (request.audio_type == MediaStreamType::NO_SERVICE && |
| request.video_type == MediaStreamType::DEVICE_VIDEO_CAPTURE) |
| return {WebContentsAdapterClient::MediaVideoCapture}; |
| |
| if (request.audio_type == MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE && |
| request.video_type == MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE) |
| return {WebContentsAdapterClient::MediaDesktopAudioCapture, WebContentsAdapterClient::MediaDesktopVideoCapture}; |
| |
| if (request.audio_type == MediaStreamType::DISPLAY_AUDIO_CAPTURE && |
| request.video_type == MediaStreamType::DISPLAY_VIDEO_CAPTURE) |
| return {WebContentsAdapterClient::MediaDesktopAudioCapture, WebContentsAdapterClient::MediaDesktopVideoCapture}; |
| |
| if (request.video_type == MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE || |
| request.video_type == MediaStreamType::DISPLAY_VIDEO_CAPTURE) |
| return {WebContentsAdapterClient::MediaDesktopVideoCapture}; |
| |
| return {}; |
| } |
| |
| // Based on MediaStreamCaptureIndicator::UIDelegate |
| class MediaStreamUIQt : public content::MediaStreamUI |
| { |
| public: |
| MediaStreamUIQt(content::WebContents *webContents, const blink::MediaStreamDevices &devices) |
| : m_delegate(static_cast<WebContentsDelegateQt *>(webContents->GetDelegate())->AsWeakPtr()) |
| , m_devices(devices) |
| { |
| DCHECK(!m_devices.empty()); |
| } |
| |
| ~MediaStreamUIQt() override |
| { |
| if (m_started && m_delegate) |
| m_delegate->removeDevices(m_devices); |
| } |
| |
| private: |
| gfx::NativeViewId OnStarted(base::OnceClosure, SourceCallback) override |
| { |
| DCHECK(!m_started); |
| m_started = true; |
| if (m_delegate) |
| m_delegate->addDevices(m_devices); |
| return 0; |
| } |
| |
| base::WeakPtr<WebContentsDelegateQt> m_delegate; |
| const blink::MediaStreamDevices m_devices; |
| bool m_started = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(MediaStreamUIQt); |
| }; |
| |
| } // namespace |
| |
| MediaCaptureDevicesDispatcher::PendingAccessRequest::PendingAccessRequest(const content::MediaStreamRequest &request, |
| const RepeatingMediaResponseCallback &callback) |
| : request(request) |
| , callback(callback) |
| { |
| } |
| |
| MediaCaptureDevicesDispatcher::PendingAccessRequest::~PendingAccessRequest() |
| { |
| } |
| |
| void MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse(content::WebContents *webContents, const QUrl &securityOrigin, WebContentsAdapterClient::MediaRequestFlags authorizationFlags) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| blink::MediaStreamDevices devices; |
| auto it = m_pendingRequests.find(webContents); |
| if (it == m_pendingRequests.end() || it->second.empty()) |
| return; |
| |
| RequestsQueue &queue(it->second); |
| 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())); |
| |
| WebContentsAdapterClient::MediaRequestFlags requestFlags = mediaRequestFlagsForRequest(request); |
| WebContentsAdapterClient::MediaRequestFlags finalFlags = requestFlags & authorizationFlags; |
| |
| bool microphoneRequested = finalFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture); |
| bool webcamRequested = finalFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture); |
| bool desktopAudioRequested = finalFlags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture); |
| bool desktopVideoRequested = finalFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture); |
| |
| if (securityOriginsMatch) { |
| if (microphoneRequested || webcamRequested) { |
| switch (request.request_type) { |
| case blink::MEDIA_OPEN_DEVICE_PEPPER_ONLY: |
| getDefaultDevices("", "", microphoneRequested, webcamRequested, &devices); |
| break; |
| case blink::MEDIA_DEVICE_ACCESS: |
| case blink::MEDIA_DEVICE_UPDATE: |
| case blink::MEDIA_GENERATE_STREAM: |
| getDefaultDevices(request.requested_audio_device_id, request.requested_video_device_id, |
| microphoneRequested, webcamRequested, &devices); |
| break; |
| } |
| } else if (desktopVideoRequested) { |
| getDevicesForDesktopCapture(&devices, getDefaultScreenId(), desktopAudioRequested, |
| request.video_type, request.audio_type); |
| } |
| } |
| |
| content::MediaResponseCallback callback = std::move(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. |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(&MediaCaptureDevicesDispatcher::ProcessQueuedAccessRequest, |
| base::Unretained(this), webContents)); |
| } |
| |
| if (devices.empty()) |
| std::move(callback).Run(devices, MediaStreamRequestResult::INVALID_STATE, |
| std::unique_ptr<content::MediaStreamUI>()); |
| else |
| std::move(callback).Run(devices, MediaStreamRequestResult::OK, |
| std::make_unique<MediaStreamUIQt>(webContents, devices)); |
| } |
| |
| MediaCaptureDevicesDispatcher *MediaCaptureDevicesDispatcher::GetInstance() |
| { |
| return base::Singleton<MediaCaptureDevicesDispatcher>::get(); |
| } |
| |
| MediaCaptureDevicesDispatcher::MediaCaptureDevicesDispatcher() |
| { |
| // 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_CURRENTLY_ON(BrowserThread::UI); |
| m_notificationsRegistrar.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, |
| content::NotificationService::AllSources()); |
| } |
| |
| MediaCaptureDevicesDispatcher::~MediaCaptureDevicesDispatcher() |
| { |
| } |
| |
| void MediaCaptureDevicesDispatcher::Observe(int type, const content::NotificationSource &source, const content::NotificationDetails &details) |
| { |
| DCHECK_CURRENTLY_ON(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, content::MediaResponseCallback callback) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| WebContentsAdapterClient::MediaRequestFlags flags = mediaRequestFlagsForRequest(request); |
| if (!flags) { |
| std::move(callback).Run(blink::MediaStreamDevices(), MediaStreamRequestResult::NOT_SUPPORTED, std::unique_ptr<content::MediaStreamUI>()); |
| return; |
| } |
| |
| if (flags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) { |
| const bool screenCaptureEnabled = |
| adapterClient->webEngineSettings()->testAttribute(WebEngineSettings::ScreenCaptureEnabled); |
| const bool originIsSecure = content::IsOriginSecure(request.security_origin); |
| if (!screenCaptureEnabled || !originIsSecure) { |
| std::move(callback).Run(blink::MediaStreamDevices(), MediaStreamRequestResult::INVALID_STATE, std::unique_ptr<content::MediaStreamUI>()); |
| return; |
| } |
| |
| if (!request.requested_video_device_id.empty()) { |
| // Non-empty device id from the chooseDesktopMedia() extension API. |
| processDesktopCaptureAccessRequest(webContents, request, std::move(callback)); |
| return; |
| } |
| } |
| |
| enqueueMediaAccessRequest(webContents, request, std::move(callback)); |
| // We might not require this approval for pepper requests. |
| adapterClient->runMediaAccessPermissionRequest(toQt(request.security_origin), flags); |
| } |
| |
| void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request, content::MediaResponseCallback callback) |
| { |
| blink::MediaStreamDevices devices; |
| |
| content::WebContents *const web_contents_for_stream = content::WebContents::FromRenderFrameHost( |
| content::RenderFrameHost::FromID(request.render_process_id, request.render_frame_id)); |
| content::RenderFrameHost *const main_frame = web_contents_for_stream ? web_contents_for_stream->GetMainFrame() : NULL; |
| |
| content::DesktopMediaID mediaId; |
| if (main_frame) { |
| // The extension name that the stream is registered with. |
| std::string originalExtensionName; |
| // Resolve DesktopMediaID for the specified device id. |
| mediaId = content::DesktopStreamsRegistry::GetInstance()->RequestMediaForStreamId( |
| request.requested_video_device_id, main_frame->GetProcess()->GetID(), |
| main_frame->GetRoutingID(), url::Origin::Create(request.security_origin), |
| &originalExtensionName, content::kRegistryStreamTypeDesktop); |
| } |
| |
| // Received invalid device id. |
| if (mediaId.type == content::DesktopMediaID::TYPE_NONE) { |
| std::move(callback).Run(devices, MediaStreamRequestResult::INVALID_STATE, std::unique_ptr<content::MediaStreamUI>()); |
| return; |
| } |
| |
| // Audio is only supported for screen capture streams. |
| bool capture_audio = (mediaId.type == content::DesktopMediaID::TYPE_SCREEN && request.audio_type == MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE); |
| |
| getDevicesForDesktopCapture(&devices, mediaId, capture_audio, request.video_type, request.audio_type); |
| |
| if (devices.empty()) |
| std::move(callback).Run(devices, MediaStreamRequestResult::INVALID_STATE, |
| std::unique_ptr<content::MediaStreamUI>()); |
| else |
| std::move(callback).Run(devices, MediaStreamRequestResult::OK, |
| std::make_unique<MediaStreamUIQt>(webContents, devices)); |
| } |
| |
| void MediaCaptureDevicesDispatcher::enqueueMediaAccessRequest(content::WebContents *webContents, |
| const content::MediaStreamRequest &request, |
| content::MediaResponseCallback callback) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| RequestsQueue &queue = m_pendingRequests[webContents]; |
| queue.push_back(PendingAccessRequest(request, base::AdaptCallbackForRepeating(std::move(callback)))); |
| } |
| |
| void MediaCaptureDevicesDispatcher::ProcessQueuedAccessRequest(content::WebContents *webContents) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| auto it = m_pendingRequests.find(webContents); |
| if (it == m_pendingRequests.end() || it->second.empty()) |
| return; |
| |
| RequestsQueue &queue(it->second); |
| content::MediaStreamRequest &request = queue.front().request; |
| |
| WebContentsAdapterClient *adapterClient = WebContentsViewQt::from(static_cast<content::WebContentsImpl *>(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, blink::MediaStreamDevices *devices) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(audio || video); |
| |
| if (audio) { |
| const blink::MediaStreamDevices &audioDevices = content::MediaCaptureDevices::GetInstance()->GetAudioCaptureDevices(); |
| const blink::MediaStreamDevice *device = findDeviceWithId(audioDevices, audioDeviceId); |
| if (!device && !audioDevices.empty()) |
| device = &audioDevices.front(); |
| if (device) |
| devices->push_back(*device); |
| } |
| |
| if (video) { |
| const blink::MediaStreamDevices &videoDevices = content::MediaCaptureDevices::GetInstance()->GetVideoCaptureDevices(); |
| const blink::MediaStreamDevice *device = findDeviceWithId(videoDevices, videoDeviceId); |
| if (!device && !videoDevices.empty()) |
| device = &videoDevices.front(); |
| if (device) |
| devices->push_back(*device); |
| } |
| } |
| |
| void MediaCaptureDevicesDispatcher::OnMediaRequestStateChanged(int render_process_id, int render_frame_id, int page_request_id, const GURL &security_origin, blink::mojom::MediaStreamType stream_type, content::MediaRequestState state) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(&MediaCaptureDevicesDispatcher::updateMediaRequestStateOnUIThread, |
| base::Unretained(this), render_process_id, render_frame_id, |
| page_request_id, security_origin, stream_type, state)); |
| } |
| |
| void MediaCaptureDevicesDispatcher::updateMediaRequestStateOnUIThread(int render_process_id, |
| int render_frame_id, |
| int page_request_id, |
| const GURL & /*security_origin*/, |
| blink::mojom::MediaStreamType /*stream_type*/, |
| content::MediaRequestState state) |
| { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // Cancel the request. |
| if (state == content::MEDIA_REQUEST_STATE_CLOSING) { |
| for (auto &pair : m_pendingRequests) { |
| RequestsQueue &queue = pair.second; |
| for (auto it = queue.begin(); it != queue.end(); ++it) { |
| if (it->request.render_process_id == render_process_id |
| && it->request.render_frame_id == render_frame_id |
| && it->request.page_request_id == page_request_id) { |
| queue.erase(it); |
| return; |
| } |
| } |
| } |
| } |
| } |
| |
| } // namespace QtWebEngineCore |