| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/media/webrtc/display_media_access_handler.h" |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/types/expected.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/bad_message.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/media/capture_access_handler_base.h" |
| #include "chrome/browser/media/webrtc/capture_policy_utils.h" |
| #include "chrome/browser/media/webrtc/desktop_capture_devices_util.h" |
| #include "chrome/browser/media/webrtc/desktop_media_picker_factory_impl.h" |
| #include "chrome/browser/media/webrtc/native_desktop_media_list.h" |
| #include "chrome/browser/media/webrtc/tab_desktop_media_list.h" |
| #include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/url_identity.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/buildflags.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/desktop_media_id.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" |
| #include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h" |
| |
| #if defined(TOOLKIT_VIEWS) |
| #include "chrome/browser/ui/views/frame/browser_frame.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #endif // defined(TOOLKIT_VIEWS) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/chromeos/policy/dlp/dlp_content_manager.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "chrome/browser/media/webrtc/system_media_capture_permissions_mac.h" |
| #endif // BUILDFLAG(IS_MAC) |
| |
| #if BUILDFLAG(SAFE_BROWSING_AVAILABLE) |
| #include "chrome/browser/safe_browsing/user_interaction_observer.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_GLIC) |
| #include "chrome/browser/glic/host/guest_util.h" |
| #endif |
| |
| namespace { |
| using ::blink::mojom::MediaStreamRequestResult; |
| using ::content::DesktopMediaID; |
| using ::content::WebContents; |
| using ::content::WebContentsMediaCaptureId; |
| |
| constexpr UrlIdentity::TypeSet allowed_types = { |
| UrlIdentity::Type::kDefault, UrlIdentity::Type::kIsolatedWebApp, |
| UrlIdentity::Type::kFile, UrlIdentity::Type::kChromeExtension}; |
| |
| constexpr UrlIdentity::FormatOptions options = { |
| .default_options = { |
| UrlIdentity::DefaultFormatOptions::kOmitCryptographicScheme}}; |
| |
| // Helper function to get the title of the calling application. |
| std::u16string GetApplicationTitle(WebContents* web_contents) { |
| DCHECK(web_contents); |
| GURL content_origin = |
| web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().GetURL(); |
| UrlIdentity url_identity = UrlIdentity::CreateFromUrl( |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()), |
| content_origin, allowed_types, options); |
| return url_identity.name; |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| |
| bool IsGlicWebUI(const WebContents* web_contents) { |
| #if BUILDFLAG(ENABLE_GLIC) |
| return glic::IsGlicWebUI(web_contents); |
| #else |
| return false; |
| #endif |
| } |
| |
| // If bypassing the media selection dialog is allowed for this request, this |
| // returns the `DesktopMediaId` to use. Returns a null ID otherwise. |
| DesktopMediaID GetMediaForSelectionDialogBypass( |
| const HostContentSettingsMap& content_settings, |
| WebContents* web_contents, |
| const content::MediaStreamRequest& request) { |
| // Only bypass for chrome:// URLs. |
| if (web_contents->GetLastCommittedURL().scheme() != |
| content::kChromeUIScheme) { |
| return DesktopMediaID(); |
| } |
| |
| const GURL& origin_url = web_contents->GetLastCommittedURL(); |
| // Special behavior for chrome://glic: skip tab capture dialog. |
| if (request.video_type == |
| blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_THIS_TAB && |
| IsGlicWebUI(web_contents)) { |
| DesktopMediaID media_id( |
| DesktopMediaID::TYPE_WEB_CONTENTS, DesktopMediaID::kNullId, |
| WebContentsMediaCaptureId( |
| web_contents->GetPrimaryMainFrame() |
| ->GetProcess() |
| ->GetDeprecatedID(), |
| web_contents->GetPrimaryMainFrame()->GetRoutingID())); |
| media_id.audio_share = |
| request.audio_type != blink::mojom::MediaStreamType::NO_SERVICE; |
| return media_id; |
| } else if (request.video_type == blink::mojom::MediaStreamType::NO_SERVICE && |
| request.audio_type != blink::mojom::MediaStreamType::NO_SERVICE && |
| !request.exclude_system_audio && |
| content_settings.GetContentSetting( |
| origin_url, origin_url, |
| ContentSettingsType::DISPLAY_MEDIA_SYSTEM_AUDIO) == |
| ContentSetting::CONTENT_SETTING_ALLOW) { |
| return DesktopMediaID(DesktopMediaID::TYPE_SCREEN, DesktopMediaID::kNullId, |
| /*audio_share=*/true); |
| } |
| return DesktopMediaID(); |
| } |
| |
| #endif |
| |
| } // namespace |
| |
| DisplayMediaAccessHandler::DisplayMediaAccessHandler() |
| : picker_factory_(new DesktopMediaPickerFactoryImpl()), |
| web_contents_collection_(this) {} |
| |
| DisplayMediaAccessHandler::DisplayMediaAccessHandler( |
| std::unique_ptr<DesktopMediaPickerFactory> picker_factory, |
| bool display_notification) |
| : display_notification_(display_notification), |
| picker_factory_(std::move(picker_factory)), |
| web_contents_collection_(this) {} |
| |
| DisplayMediaAccessHandler::~DisplayMediaAccessHandler() = default; |
| |
| bool DisplayMediaAccessHandler::SupportsStreamType( |
| content::RenderFrameHost* render_frame_host, |
| const blink::mojom::MediaStreamType stream_type, |
| const extensions::Extension* extension) { |
| return stream_type == blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE || |
| stream_type == blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE || |
| stream_type == |
| blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_THIS_TAB || |
| stream_type == |
| blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET; |
| // This class handles MEDIA_DISPLAY_AUDIO_CAPTURE as well, but only if it is |
| // accompanied by MEDIA_DISPLAY_VIDEO_CAPTURE request as per spec. |
| // https://w3c.github.io/mediacapture-screen-share/#mediadevices-additions |
| // 5.1 MediaDevices Additions |
| // "The user agent MUST reject audio-only requests." |
| } |
| |
| bool DisplayMediaAccessHandler::CheckMediaAccessPermission( |
| content::RenderFrameHost* render_frame_host, |
| const url::Origin& security_origin, |
| blink::mojom::MediaStreamType type, |
| const extensions::Extension* extension) { |
| return false; |
| } |
| |
| void DisplayMediaAccessHandler::HandleRequest( |
| WebContents* web_contents, |
| const content::MediaStreamRequest& request, |
| content::MediaResponseCallback callback, |
| const extensions::Extension* extension) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(web_contents); |
| |
| if (capture_policy::GetAllowedCaptureLevel(request.security_origin, |
| web_contents) == |
| AllowedScreenCaptureLevel::kDisallowed) { |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::PERMISSION_DENIED, |
| /*ui=*/nullptr); |
| return; |
| } |
| |
| #if BUILDFLAG(SAFE_BROWSING_AVAILABLE) |
| // SafeBrowsing Delayed Warnings experiment can delay some SafeBrowsing |
| // warnings until user interaction. If the current page has a delayed warning, |
| // it'll have a user interaction observer attached. Show the warning |
| // immediately in that case. |
| safe_browsing::SafeBrowsingUserInteractionObserver* observer = |
| safe_browsing::SafeBrowsingUserInteractionObserver::FromWebContents( |
| web_contents); |
| if (observer) { |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::PERMISSION_DENIED, |
| /*ui=*/nullptr); |
| observer->OnDesktopCaptureRequest(); |
| return; |
| } |
| #endif |
| |
| #if BUILDFLAG(IS_MAC) |
| // Do not allow picker UI to be shown on a page that isn't in the foreground |
| // in Mac, because the UI implementation in Mac pops a window over any content |
| // which might be confusing for the users. See https://crbug.com/1407733 for |
| // details. |
| // |
| // If the page isn't in the foreground, but the page has a document |
| // picture-in-picture window, then we will still allow it as the picker will |
| // be displayed on the document picture-in-picture window. |
| // |
| // TODO(emircan): Remove this once Mac UI doesn't use a window. |
| if (web_contents->GetVisibility() != content::Visibility::VISIBLE && |
| !web_contents->HasPictureInPictureDocument() && |
| request.request_type != blink::MEDIA_DEVICE_UPDATE) { |
| LOG(ERROR) << "Do not allow getDisplayMedia() on a backgrounded page."; |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::INVALID_STATE, |
| /*ui=*/nullptr); |
| return; |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| |
| if (request.request_type == blink::MEDIA_DEVICE_UPDATE) { |
| CHECK(!request.requested_video_device_ids.empty()); |
| CHECK(!request.requested_video_device_ids.front().empty()); |
| ProcessChangeSourceRequest(web_contents, request, std::move(callback)); |
| return; |
| } |
| |
| if (request.video_type == |
| blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_THIS_TAB || |
| request.video_type == |
| blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE || |
| request.video_type == |
| blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET) { |
| // Repeat the permission test from the render process. |
| content::RenderFrameHost* rfh = content::RenderFrameHost::FromID( |
| request.render_process_id, request.render_frame_id); |
| if (!rfh) { |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::INVALID_STATE, |
| /*ui=*/nullptr); |
| return; |
| } |
| |
| // If the display-capture permissions-policy disallows capture, the render |
| // process was not supposed to send this message. |
| if (!rfh->IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kDisplayCapture)) { |
| bad_message::ReceivedBadMessage( |
| rfh->GetProcess(), bad_message::BadMessageReason:: |
| RFH_DISPLAY_CAPTURE_PERMISSION_MISSING); |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::PERMISSION_DENIED, |
| /*ui=*/nullptr); |
| return; |
| } |
| |
| // Renderer process should already check for transient user activation |
| // before sending IPC, but just to be sure double check here as well. This |
| // is not treated as a BadMessage because it is possible for the transient |
| // user activation to expire between the renderer side check and this check. |
| // |
| // TODO(crbug.com/416448339): Introduce and use a new result value, |
| // MediaStreamRequestResult::NO_TRANSIENT_ACTIVATION. In JS, it should map |
| // to `InvalidStateError`, not to `NotAllowedError`. |
| if (!rfh->HasTransientUserActivation() && |
| capture_policy::IsTransientActivationRequiredForGetDisplayMedia( |
| web_contents)) { |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::PERMISSION_DENIED, |
| /*ui=*/nullptr); |
| return; |
| } |
| } |
| |
| // Screen capture is not supported on Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| HostContentSettingsMap* content_settings = |
| HostContentSettingsMapFactory::GetForProfile( |
| web_contents->GetBrowserContext()); |
| CHECK(content_settings); |
| DesktopMediaID media_id = GetMediaForSelectionDialogBypass( |
| *content_settings, web_contents, request); |
| if (!media_id.is_null()) { |
| BypassMediaSelectionDialog(web_contents, request, media_id, |
| std::move(callback)); |
| return; |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // Except for the case when DISPLAY_MEDIA_SYSTEM_AUDIO is allowed, all |
| // requests should contain video stream. |
| if (request.video_type == blink::mojom::MediaStreamType::NO_SERVICE) { |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::NOT_SUPPORTED, |
| /*ui=*/nullptr); |
| return; |
| } |
| |
| ShowMediaSelectionDialog(web_contents, request, std::move(callback)); |
| } |
| |
| void DisplayMediaAccessHandler::UpdateMediaRequestState( |
| int render_process_id, |
| int render_frame_id, |
| int page_request_id, |
| blink::mojom::MediaStreamType stream_type, |
| content::MediaRequestState state) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (state != content::MEDIA_REQUEST_STATE_DONE && |
| state != content::MEDIA_REQUEST_STATE_CLOSING) { |
| return; |
| } |
| |
| if (state == content::MEDIA_REQUEST_STATE_CLOSING) { |
| DeletePendingAccessRequest(render_process_id, render_frame_id, |
| page_request_id); |
| } |
| CaptureAccessHandlerBase::UpdateMediaRequestState( |
| render_process_id, render_frame_id, page_request_id, stream_type, state); |
| |
| // This method only gets called with the above checked states when all |
| // requests are to be canceled. Therefore, we don't need to process the |
| // next queued request. |
| } |
| |
| void DisplayMediaAccessHandler::ShowMediaSelectionDialog( |
| WebContents* web_contents, |
| const content::MediaStreamRequest& request, |
| content::MediaResponseCallback callback) { |
| std::unique_ptr<DesktopMediaPicker> picker = |
| picker_factory_->CreatePicker(&request); |
| if (!picker) { |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::INVALID_STATE, |
| /*ui=*/nullptr); |
| return; |
| } |
| |
| // Ensure we are observing the deletion of |web_contents|. |
| web_contents_collection_.StartObserving(web_contents); |
| |
| RequestsQueue& queue = pending_requests_[web_contents]; |
| |
| queue.push_back(std::make_unique<PendingAccessRequest>( |
| std::move(picker), request, std::move(callback), |
| GetApplicationTitle(web_contents), display_notification_, |
| /*is_allowlisted_extension=*/false)); |
| // If this is the only request then pop picker UI. |
| if (queue.size() == 1) { |
| ProcessQueuedAccessRequest(queue, web_contents); |
| } |
| } |
| |
| bool DisplayMediaAccessHandler::IsRequestFirstInQueue( |
| const RequestsQueue& queue, |
| const content::MediaStreamRequest& request) const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| const PendingAccessRequest& first_pending_request = *queue.front(); |
| // If requests are different, then this is a stale request. |
| return first_pending_request.request.render_process_id == |
| request.render_process_id && |
| first_pending_request.request.render_frame_id == |
| request.render_frame_id && |
| first_pending_request.request.page_request_id == |
| request.page_request_id; |
| } |
| |
| void DisplayMediaAccessHandler::BypassMediaSelectionDialog( |
| WebContents* web_contents, |
| const content::MediaStreamRequest& request, |
| const DesktopMediaID& media_id, |
| content::MediaResponseCallback callback) { |
| if (web_contents->GetLastCommittedURL().scheme() != |
| content::kChromeUIScheme) { |
| std::move(callback).Run(blink::mojom::StreamDevicesSet(), |
| MediaStreamRequestResult::NOT_SUPPORTED, |
| /*ui=*/nullptr); |
| return; |
| } |
| |
| // base::Unretained(this) is safe because DisplayMediaAccessHandler is owned |
| // by MediaCaptureDevicesDispatcher, which is a lazy singleton which is |
| // destroyed when the browser process terminates. |
| GetDevicesForDesktopCapture( |
| web_contents, media_id, request.video_type, request.audio_type, |
| request.security_origin, media_id.audio_share, request.disable_local_echo, |
| request.suppress_local_audio_playback, request.restrict_own_audio, |
| /*display_notification=*/false, GetApplicationTitle(web_contents), |
| request.captured_surface_control_active, |
| base::BindOnce( |
| &DisplayMediaAccessHandler:: |
| OnDesktopCaptureDevicesObtainedAfterBypassMediaSelectionDialog, |
| base::Unretained(this), web_contents->GetWeakPtr(), request, |
| std::move(callback))); |
| } |
| |
| void DisplayMediaAccessHandler:: |
| OnDesktopCaptureDevicesObtainedAfterBypassMediaSelectionDialog( |
| base::WeakPtr<WebContents> web_contents, |
| content::MediaStreamRequest request, |
| content::MediaResponseCallback callback, |
| blink::mojom::StreamDevices devices, |
| std::unique_ptr<content::MediaStreamUI> ui) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!web_contents) { |
| return; |
| } |
| |
| blink::mojom::StreamDevicesSet stream_devices_set; |
| stream_devices_set.stream_devices.emplace_back( |
| blink::mojom::StreamDevices::New()); |
| *(stream_devices_set.stream_devices[0]) = std::move(devices); |
| std::move(callback).Run(stream_devices_set, MediaStreamRequestResult::OK, |
| std::move(ui)); |
| } |
| |
| void DisplayMediaAccessHandler::ProcessChangeSourceRequest( |
| WebContents* web_contents, |
| const content::MediaStreamRequest& request, |
| content::MediaResponseCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(web_contents); |
| |
| // Ensure we are observing the deletion of |web_contents|. |
| web_contents_collection_.StartObserving(web_contents); |
| |
| RequestsQueue& queue = pending_requests_[web_contents]; |
| queue.push_back(std::make_unique<PendingAccessRequest>( |
| /*picker=*/nullptr, request, std::move(callback), |
| GetApplicationTitle(web_contents), display_notification_, |
| /*is_allowlisted_extension=*/false)); |
| // If this is the only request then pop it. Otherwise, there is already a task |
| // scheduled to pop the next request. |
| if (queue.size() == 1) { |
| ProcessQueuedAccessRequest(queue, web_contents); |
| } |
| } |
| |
| void DisplayMediaAccessHandler::ProcessQueuedAccessRequest( |
| const RequestsQueue& queue, |
| WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(web_contents); |
| |
| const PendingAccessRequest& pending_request = *queue.front(); |
| UpdateTrusted(pending_request.request, false /* is_trusted */); |
| |
| const GURL& request_origin = pending_request.request.security_origin; |
| AllowedScreenCaptureLevel capture_level = |
| capture_policy::GetAllowedCaptureLevel(request_origin, web_contents); |
| |
| // If Capture is not allowed, then reject. |
| if (capture_level == AllowedScreenCaptureLevel::kDisallowed) { |
| RejectRequest(web_contents, MediaStreamRequestResult::PERMISSION_DENIED); |
| return; |
| } |
| |
| if (pending_request.request.request_type == blink::MEDIA_DEVICE_UPDATE) { |
| ProcessQueuedChangeSourceRequest(pending_request.request, web_contents); |
| } else { |
| ProcessQueuedPickerRequest(pending_request, web_contents, capture_level, |
| request_origin); |
| } |
| } |
| |
| void DisplayMediaAccessHandler::ProcessQueuedPickerRequest( |
| const PendingAccessRequest& pending_request, |
| WebContents* web_contents, |
| AllowedScreenCaptureLevel capture_level, |
| const GURL& request_origin) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(web_contents); |
| |
| WebContents* ui_web_contents = web_contents; |
| |
| #if defined(TOOLKIT_VIEWS) |
| // If `web_contents` is the opener of a Document Picture in Picture window, |
| // and if the pip window currently has the focus, then show the request in the |
| // pip window instead. |
| if (WebContents* const child_web_contents = |
| web_contents->HasPictureInPictureDocument() |
| ? PictureInPictureWindowManager::GetInstance() |
| ->GetChildWebContents() |
| : nullptr) { |
| // There should not be more than one pip window. If `web_contents` |
| // believes that it is a document pip opener, then make sure that the |
| // window manager agrees with it. |
| CHECK_EQ(PictureInPictureWindowManager::GetInstance()->GetWebContents(), |
| web_contents); |
| |
| // The media-picker prompt will be associated with the PiP window if the |
| // user's last interaction was with the PiP. (This heuristic could in the |
| // future be replaced with an explicit control surface exposed to the |
| // app.) |
| // |
| // Note that `RenderWidgetHostView::HasFocus()` does not work as expected |
| // on Mac; it always returns true. The Widget's activation state is what |
| // tracks the state we care about. It's not 100% accurate either as a |
| // proxy for "the user's last interaction", but it's good enough. |
| if (gfx::NativeWindow native_window = |
| child_web_contents->GetTopLevelNativeWindow()) { |
| if (auto* browser_view = |
| BrowserView::GetBrowserViewForNativeWindow(native_window)) { |
| if (browser_view->frame()->IsActive()) { |
| ui_web_contents = child_web_contents; |
| } |
| } |
| } |
| } |
| #endif // defined(TOOLKIT_VIEWS) |
| |
| std::vector<DesktopMediaList::Type> media_types{ |
| DesktopMediaList::Type::kWebContents, DesktopMediaList::Type::kWindow}; |
| if (!pending_request.request.exclude_monitor_type_surfaces) { |
| media_types.push_back(DesktopMediaList::Type::kScreen); |
| } |
| |
| capture_policy::FilterMediaList(media_types, capture_level); |
| |
| auto includable_web_contents_filter = |
| capture_policy::GetIncludableWebContentsFilter(request_origin, |
| capture_level); |
| if (pending_request.request.exclude_self_browser_surface) { |
| includable_web_contents_filter = DesktopMediaList::ExcludeWebContents( |
| std::move(includable_web_contents_filter), web_contents); |
| } |
| |
| auto source_lists = picker_factory_->CreateMediaList( |
| media_types, web_contents, includable_web_contents_filter); |
| |
| // base::Unretained(this) is safe because DisplayMediaAccessHandler is owned |
| // by MediaCaptureDevicesDispatcher, which is a lazy singleton which is |
| // destroyed when the browser process terminates. |
| DesktopMediaPicker::DoneCallback done_callback = |
| base::BindOnce(&DisplayMediaAccessHandler::OnDisplaySurfaceSelected, |
| base::Unretained(this), web_contents->GetWeakPtr()); |
| DesktopMediaPicker::Params picker_params( |
| DesktopMediaPicker::Params::RequestSource::kGetDisplayMedia); |
| picker_params.web_contents = ui_web_contents; |
| gfx::NativeWindow parent_window = ui_web_contents->GetTopLevelNativeWindow(); |
| picker_params.context = parent_window; |
| picker_params.parent = parent_window; |
| picker_params.app_name = GetApplicationTitle(web_contents); |
| picker_params.target_name = picker_params.app_name; |
| picker_params.request_audio = |
| pending_request.request.audio_type == |
| blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE; |
| picker_params.exclude_system_audio = |
| pending_request.request.exclude_system_audio; |
| picker_params.suppress_local_audio_playback = |
| pending_request.request.suppress_local_audio_playback; |
| picker_params.restrict_own_audio = pending_request.request.restrict_own_audio; |
| picker_params.restricted_by_policy = |
| (capture_level != AllowedScreenCaptureLevel::kUnrestricted); |
| picker_params.preferred_display_surface = |
| pending_request.request.preferred_display_surface; |
| pending_request.picker->Show(picker_params, std::move(source_lists), |
| std::move(done_callback)); |
| } |
| |
| void DisplayMediaAccessHandler::ProcessQueuedChangeSourceRequest( |
| const content::MediaStreamRequest& request, |
| WebContents* web_contents) { |
| DCHECK(web_contents); |
| DCHECK(!request.requested_video_device_ids.empty()); |
| |
| WebContentsMediaCaptureId web_contents_id; |
| if (!WebContentsMediaCaptureId::Parse( |
| request.requested_video_device_ids.front(), &web_contents_id)) { |
| RejectRequest(web_contents, MediaStreamRequestResult::INVALID_STATE); |
| return; |
| } |
| DesktopMediaID media_id(DesktopMediaID::TYPE_WEB_CONTENTS, |
| DesktopMediaID::kNullId, web_contents_id); |
| media_id.audio_share = |
| request.audio_type != blink::mojom::MediaStreamType::NO_SERVICE; |
| OnDisplaySurfaceSelected(web_contents->GetWeakPtr(), media_id); |
| } |
| |
| void DisplayMediaAccessHandler::RejectRequest(WebContents* web_contents, |
| MediaStreamRequestResult result) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(web_contents); |
| |
| auto it = pending_requests_.find(web_contents); |
| if (it == pending_requests_.end()) { |
| return; |
| } |
| RequestsQueue& mutable_queue = it->second; |
| if (mutable_queue.empty()) { |
| return; |
| } |
| PendingAccessRequest& mutable_request = *mutable_queue.front(); |
| std::move(mutable_request.callback) |
| .Run(blink::mojom::StreamDevicesSet(), result, /*ui=*/nullptr); |
| mutable_queue.pop_front(); |
| if (!mutable_queue.empty()) { |
| ProcessQueuedAccessRequest(mutable_queue, web_contents); |
| } |
| } |
| |
| void DisplayMediaAccessHandler::AcceptRequest(WebContents* web_contents, |
| const DesktopMediaID& media_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(web_contents); |
| |
| auto it = pending_requests_.find(web_contents); |
| if (it == pending_requests_.end()) { |
| return; |
| } |
| RequestsQueue& queue = it->second; |
| if (queue.empty()) { |
| // UpdateMediaRequestState() called with MEDIA_REQUEST_STATE_CLOSING. Don't |
| // need to do anything. |
| return; |
| } |
| PendingAccessRequest& pending_request = *queue.front(); |
| |
| const bool disable_local_echo = |
| (media_id.type == DesktopMediaID::TYPE_WEB_CONTENTS) && |
| media_id.web_contents_id.disable_local_echo; |
| |
| GetDevicesForDesktopCapture( |
| web_contents, media_id, pending_request.request.video_type, |
| pending_request.request.audio_type, |
| pending_request.request.security_origin, media_id.audio_share, |
| disable_local_echo, pending_request.request.suppress_local_audio_playback, |
| pending_request.request.restrict_own_audio, display_notification_, |
| GetApplicationTitle(web_contents), |
| pending_request.request.captured_surface_control_active, |
| base::BindOnce(&DisplayMediaAccessHandler:: |
| OnDesktopCaptureDevicesObtainedAfterAcceptRequest, |
| base::Unretained(this), web_contents->GetWeakPtr(), |
| pending_request.request, media_id)); |
| } |
| |
| void DisplayMediaAccessHandler:: |
| OnDesktopCaptureDevicesObtainedAfterAcceptRequest( |
| base::WeakPtr<WebContents> web_contents, |
| content::MediaStreamRequest request, |
| const DesktopMediaID& media_id, |
| blink::mojom::StreamDevices devices, |
| std::unique_ptr<content::MediaStreamUI> ui) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!web_contents) { |
| return; |
| } |
| |
| auto it = pending_requests_.find(web_contents.get()); |
| if (it == pending_requests_.end()) { |
| return; |
| } |
| RequestsQueue& queue = it->second; |
| if (queue.empty()) { |
| // UpdateMediaRequestState() called with MEDIA_REQUEST_STATE_CLOSING. Don't |
| // need to do anything. |
| return; |
| } |
| |
| // Only do something if the request is still pending. |
| if (IsRequestFirstInQueue(queue, request)) { |
| PendingAccessRequest& pending_request = *(queue.front()); |
| UpdateTarget(pending_request.request, media_id); |
| |
| blink::mojom::StreamDevicesSet stream_devices_set; |
| stream_devices_set.stream_devices.emplace_back( |
| blink::mojom::StreamDevices::New()); |
| *(stream_devices_set.stream_devices[0]) = std::move(devices); |
| std::move(pending_request.callback) |
| .Run(stream_devices_set, MediaStreamRequestResult::OK, std::move(ui)); |
| queue.pop_front(); |
| } |
| |
| if (!queue.empty()) { |
| ProcessQueuedAccessRequest(queue, web_contents.get()); |
| } |
| } |
| |
| void DisplayMediaAccessHandler::OnDisplaySurfaceSelected( |
| base::WeakPtr<WebContents> web_contents, |
| base::expected<DesktopMediaID, MediaStreamRequestResult> result) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!web_contents) { |
| // WebContentsDestroyed() will evict the entry from `pending_requests_`, |
| // which was associated with `web_contents` before it got null-out. |
| return; |
| } |
| |
| if (!result.has_value()) { |
| RejectRequest(web_contents.get(), result.error()); |
| return; |
| } |
| |
| const DesktopMediaID media_id = result.value(); |
| CHECK(!media_id.is_null()); |
| |
| // If the media id is tied to a tab, check that it hasn't been destroyed. |
| if (media_id.type == DesktopMediaID::TYPE_WEB_CONTENTS && |
| !WebContents::FromRenderFrameHost(content::RenderFrameHost::FromID( |
| media_id.web_contents_id.render_process_id, |
| media_id.web_contents_id.main_render_frame_id))) { |
| RejectRequest(web_contents.get(), |
| MediaStreamRequestResult::TAB_CAPTURE_FAILURE); |
| return; |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| // Check screen capture permissions on Mac if necessary. |
| // Do not check screen capture permissions when window_id is populated. The |
| // presence of the window_id indicates the window to be captured is a Chromium |
| // window which will be captured internally, the macOS screen capture APIs |
| // will not be used. |
| if (system_media_permissions::ScreenCaptureNeedsSystemLevelPermissions() && |
| (media_id.type == DesktopMediaID::TYPE_SCREEN || |
| (media_id.type == DesktopMediaID::TYPE_WINDOW && !media_id.window_id)) && |
| system_media_permissions::CheckSystemScreenCapturePermission() != |
| system_permission_settings::SystemPermission::kAllowed) { |
| RejectRequest(web_contents.get(), |
| MediaStreamRequestResult::PERMISSION_DENIED_BY_SYSTEM); |
| return; |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Check Data Leak Prevention restrictions on Chrome. |
| // base::Unretained(this) is safe because DisplayMediaAccessHandler is owned |
| // by MediaCaptureDevicesDispatcher, which is a lazy singleton which is |
| // destroyed when the browser process terminates. |
| policy::DlpContentManager::Get()->CheckScreenShareRestriction( |
| media_id, GetApplicationTitle(web_contents.get()), |
| base::BindOnce(&DisplayMediaAccessHandler::OnDlpRestrictionChecked, |
| base::Unretained(this), web_contents, media_id)); |
| #else // BUILDFLAG(IS_CHROMEOS) |
| AcceptRequest(web_contents.get(), media_id); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| void DisplayMediaAccessHandler::OnDlpRestrictionChecked( |
| base::WeakPtr<WebContents> web_contents, |
| const DesktopMediaID& media_id, |
| bool is_dlp_allowed) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!web_contents) { |
| return; |
| } |
| |
| if (!is_dlp_allowed) { |
| RejectRequest(web_contents.get(), |
| MediaStreamRequestResult::PERMISSION_DENIED); |
| } |
| AcceptRequest(web_contents.get(), media_id); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| void DisplayMediaAccessHandler::DeletePendingAccessRequest( |
| int render_process_id, |
| int render_frame_id, |
| int page_request_id) { |
| for (auto& queue_it : pending_requests_) { |
| RequestsQueue& queue = queue_it.second; |
| for (auto it = queue.begin(); it != queue.end(); ++it) { |
| const PendingAccessRequest& pending_request = **it; |
| if (pending_request.request.render_process_id == render_process_id && |
| pending_request.request.render_frame_id == render_frame_id && |
| pending_request.request.page_request_id == page_request_id) { |
| queue.erase(it); |
| return; |
| } |
| } |
| } |
| } |
| |
| void DisplayMediaAccessHandler::WebContentsDestroyed( |
| WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| pending_requests_.erase(web_contents); |
| } |