| // Copyright 2018 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 "chrome/browser/media/cast_mirroring_service_host.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/logging.h" |
| #include "base/memory/read_only_shared_memory_region.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/optional.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/media/cast_remoting_connector.h" |
| #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" |
| #include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" |
| #include "chrome/browser/net/system_network_context_manager.h" |
| #include "components/mirroring/browser/single_client_video_capture_host.h" |
| #include "components/mirroring/mojom/cast_message_channel.mojom.h" |
| #include "components/mirroring/mojom/session_observer.mojom.h" |
| #include "components/mirroring/mojom/session_parameters.mojom.h" |
| #include "content/public/browser/audio_service.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/desktop_streams_registry.h" |
| #include "content/public/browser/gpu_client.h" |
| #include "content/public/browser/network_service_instance.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/service_process_host.h" |
| #include "content/public/browser/video_capture_device_launcher.h" |
| #include "content/public/browser/web_contents.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/network_service.mojom.h" |
| #include "services/viz/public/mojom/gpu.mojom.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "url/origin.h" |
| |
| using content::BrowserThread; |
| |
| namespace mirroring { |
| |
| namespace { |
| |
| using media::mojom::AudioInputStream; |
| using media::mojom::AudioInputStreamClient; |
| using media::mojom::AudioInputStreamObserver; |
| |
| // Default resolution constraint. |
| constexpr gfx::Size kMaxResolution(1920, 1080); |
| |
| void CreateVideoCaptureHostOnIO( |
| const std::string& device_id, |
| blink::mojom::MediaStreamType type, |
| mojo::PendingReceiver<media::mojom::VideoCaptureHost> receiver) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| scoped_refptr<base::SingleThreadTaskRunner> device_task_runner = |
| base::ThreadPool::CreateSingleThreadTaskRunner( |
| {base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::SingleThreadTaskRunnerThreadMode::DEDICATED); |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<SingleClientVideoCaptureHost>( |
| device_id, type, |
| base::BindRepeating(&content::VideoCaptureDeviceLauncher:: |
| CreateInProcessVideoCaptureDeviceLauncher, |
| std::move(device_task_runner))), |
| std::move(receiver)); |
| } |
| |
| blink::mojom::MediaStreamType ConvertVideoStreamType( |
| content::DesktopMediaID::Type type) { |
| switch (type) { |
| case content::DesktopMediaID::TYPE_NONE: |
| return blink::mojom::MediaStreamType::NO_SERVICE; |
| case content::DesktopMediaID::TYPE_WEB_CONTENTS: |
| return blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE; |
| case content::DesktopMediaID::TYPE_SCREEN: |
| case content::DesktopMediaID::TYPE_WINDOW: |
| return blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE; |
| } |
| |
| // To suppress compiler warning on Windows. |
| return blink::mojom::MediaStreamType::NO_SERVICE; |
| } |
| |
| // Get the content::WebContents associated with the given |id|. |
| content::WebContents* GetContents( |
| const content::WebContentsMediaCaptureId& id) { |
| return content::WebContents::FromRenderFrameHost( |
| content::RenderFrameHost::FromID(id.render_process_id, |
| id.main_render_frame_id)); |
| } |
| |
| content::DesktopMediaID BuildMediaIdForWebContents( |
| content::WebContents* contents) { |
| content::DesktopMediaID media_id; |
| if (!contents) |
| return media_id; |
| media_id.type = content::DesktopMediaID::TYPE_WEB_CONTENTS; |
| media_id.web_contents_id = content::WebContentsMediaCaptureId( |
| contents->GetMainFrame()->GetProcess()->GetID(), |
| contents->GetMainFrame()->GetRoutingID(), |
| true /* enable_auto_throttling */, true /* disable_local_echo */); |
| return media_id; |
| } |
| |
| // Returns the size of the primary display in pixels, or base::nullopt if it |
| // cannot be determined. |
| base::Optional<gfx::Size> GetScreenResolution() { |
| display::Screen* screen = display::Screen::GetScreen(); |
| if (!screen) { |
| DVLOG(1) << "Cannot get the Screen object."; |
| return base::nullopt; |
| } |
| return screen->GetPrimaryDisplay().GetSizeInPixel(); |
| } |
| |
| } // namespace |
| |
| // static |
| void CastMirroringServiceHost::GetForTab( |
| content::WebContents* target_contents, |
| mojo::PendingReceiver<mojom::MirroringServiceHost> receiver) { |
| if (target_contents) { |
| const content::DesktopMediaID media_id = |
| BuildMediaIdForWebContents(target_contents); |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<CastMirroringServiceHost>(media_id), |
| std::move(receiver)); |
| } |
| } |
| |
| // static |
| void CastMirroringServiceHost::GetForDesktop( |
| content::WebContents* initiator_contents, |
| const std::string& desktop_stream_id, |
| mojo::PendingReceiver<mojom::MirroringServiceHost> receiver) { |
| DCHECK(!desktop_stream_id.empty()); |
| if (initiator_contents) { |
| std::string original_extension_name; |
| const content::DesktopMediaID media_id = |
| content::DesktopStreamsRegistry::GetInstance()->RequestMediaForStreamId( |
| desktop_stream_id, |
| initiator_contents->GetMainFrame()->GetProcess()->GetID(), |
| initiator_contents->GetMainFrame()->GetRoutingID(), |
| url::Origin::Create(initiator_contents->GetVisibleURL()), |
| &original_extension_name, content::kRegistryStreamTypeDesktop); |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<CastMirroringServiceHost>(media_id), |
| std::move(receiver)); |
| } |
| } |
| |
| // static |
| void CastMirroringServiceHost::GetForDesktop( |
| const content::DesktopMediaID& media_id, |
| mojo::PendingReceiver<mojom::MirroringServiceHost> receiver) { |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<CastMirroringServiceHost>(media_id), |
| std::move(receiver)); |
| } |
| |
| // static |
| void CastMirroringServiceHost::GetForOffscreenTab( |
| content::BrowserContext* context, |
| const GURL& presentation_url, |
| const std::string& presentation_id, |
| mojo::PendingReceiver<mojom::MirroringServiceHost> receiver) { |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| auto host = |
| std::make_unique<CastMirroringServiceHost>(content::DesktopMediaID()); |
| host->OpenOffscreenTab(context, presentation_url, presentation_id); |
| mojo::MakeSelfOwnedReceiver(std::move(host), std::move(receiver)); |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| } |
| |
| CastMirroringServiceHost::CastMirroringServiceHost( |
| content::DesktopMediaID source_media_id) |
| : source_media_id_(source_media_id), |
| gpu_client_(nullptr, base::OnTaskRunnerDeleter(nullptr)) { |
| // Observe the target WebContents for Tab mirroring. |
| if (source_media_id_.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) |
| Observe(GetContents(source_media_id_.web_contents_id)); |
| } |
| |
| CastMirroringServiceHost::~CastMirroringServiceHost() {} |
| |
| void CastMirroringServiceHost::Start( |
| mojom::SessionParametersPtr session_params, |
| mojo::PendingRemote<mojom::SessionObserver> observer, |
| mojo::PendingRemote<mojom::CastMessageChannel> outbound_channel, |
| mojo::PendingReceiver<mojom::CastMessageChannel> inbound_channel) { |
| // Start() should not be called in the middle of a mirroring session. |
| if (mirroring_service_) { |
| LOG(WARNING) << "Unexpected Start() call during an active" |
| << "mirroring session"; |
| return; |
| } |
| |
| // Launch and connect to the Mirroring Service. The process will run until |
| // |mirroring_service_| is reset. |
| content::ServiceProcessHost::Launch( |
| mirroring_service_.BindNewPipeAndPassReceiver(), |
| content::ServiceProcessHost::Options() |
| .WithDisplayName("Mirroring Service") |
| .Pass()); |
| mojo::PendingRemote<mojom::ResourceProvider> provider; |
| resource_provider_receiver.Bind(provider.InitWithNewPipeAndPassReceiver()); |
| mirroring_service_->Start( |
| std::move(session_params), GetCaptureResolutionConstraint(), |
| std::move(observer), std::move(provider), std::move(outbound_channel), |
| std::move(inbound_channel)); |
| |
| ShowCaptureIndicator(); |
| } |
| |
| // static |
| gfx::Size CastMirroringServiceHost::GetCaptureResolutionConstraint() { |
| base::Optional<gfx::Size> screen_resolution = GetScreenResolution(); |
| if (screen_resolution) { |
| return GetClampedResolution(screen_resolution.value()); |
| } else { |
| return kMaxResolution; |
| } |
| } |
| |
| // static |
| gfx::Size CastMirroringServiceHost::GetClampedResolution( |
| gfx::Size screen_resolution) { |
| // Use landscape mode dimensions for screens in portrait mode. |
| if (screen_resolution.height() > screen_resolution.width()) { |
| screen_resolution = |
| gfx::Size(screen_resolution.height(), screen_resolution.width()); |
| } |
| const int width_step = 160; |
| const int height_step = 90; |
| int clamped_width = 0; |
| int clamped_height = 0; |
| if (kMaxResolution.height() * screen_resolution.width() < |
| kMaxResolution.width() * screen_resolution.height()) { |
| clamped_width = std::min(kMaxResolution.width(), screen_resolution.width()); |
| clamped_width = clamped_width - (clamped_width % width_step); |
| clamped_height = clamped_width * height_step / width_step; |
| } else { |
| clamped_height = |
| std::min(kMaxResolution.height(), screen_resolution.height()); |
| clamped_height = clamped_height - (clamped_height % height_step); |
| clamped_width = clamped_height * width_step / height_step; |
| } |
| |
| clamped_width = std::max(clamped_width, width_step); |
| clamped_height = std::max(clamped_height, height_step); |
| return gfx::Size(clamped_width, clamped_height); |
| } |
| |
| void CastMirroringServiceHost::BindGpu( |
| mojo::PendingReceiver<viz::mojom::Gpu> receiver) { |
| gpu_client_ = content::CreateGpuClient(std::move(receiver), base::DoNothing(), |
| content::GetIOThreadTaskRunner({})); |
| } |
| |
| void CastMirroringServiceHost::GetVideoCaptureHost( |
| mojo::PendingReceiver<media::mojom::VideoCaptureHost> receiver) { |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&CreateVideoCaptureHostOnIO, source_media_id_.ToString(), |
| ConvertVideoStreamType(source_media_id_.type), |
| std::move(receiver))); |
| } |
| |
| void CastMirroringServiceHost::GetNetworkContext( |
| mojo::PendingReceiver<network::mojom::NetworkContext> receiver) { |
| network::mojom::NetworkContextParamsPtr network_context_params = |
| g_browser_process->system_network_context_manager() |
| ->CreateDefaultNetworkContextParams(); |
| network_context_params->context_name = "mirroring"; |
| content::GetNetworkService()->CreateNetworkContext( |
| std::move(receiver), std::move(network_context_params)); |
| } |
| |
| void CastMirroringServiceHost::CreateAudioStream( |
| mojo::PendingRemote<mojom::AudioStreamCreatorClient> requestor, |
| const media::AudioParameters& params, |
| uint32_t total_segments) { |
| if (!audio_stream_factory_) { |
| content::GetAudioService().BindStreamFactory( |
| audio_stream_factory_.BindNewPipeAndPassReceiver()); |
| } |
| |
| if (source_media_id_.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) { |
| content::WebContents* const contents = web_contents(); |
| if (!contents) { |
| VLOG(1) << "Failed to create audio stream: Invalid source."; |
| return; |
| } |
| const base::UnguessableToken group_id = contents->GetAudioGroupId(); |
| |
| // Fix for regression: https://crbug.com/1111026 |
| // |
| // Muting of the browser tab's local audio output starts when the first |
| // WebContents loopback capture stream is requested. The mute is held so |
| // that switching audio capture on/off (between mirroring and remoting |
| // modes) does not cause ~1 second blips of audio to bother the user. When |
| // this CastMirroringServiceHost is destroyed, the "Muter" will go away, |
| // restoring local audio output. |
| // |
| // There may be other browser features that also mute the same tab (before |
| // or during mirroring). The Audio Service allows multiple Muters for the |
| // same tab, and so the mute state will remain in-place if requested by |
| // those other features. |
| if (!web_contents_audio_muter_) { |
| audio_stream_factory_->BindMuter( |
| web_contents_audio_muter_.BindNewEndpointAndPassReceiver(), group_id); |
| } |
| |
| CreateAudioStreamForTab(std::move(requestor), params, total_segments, |
| group_id); |
| } else { |
| CreateAudioStreamForDesktop(std::move(requestor), params, total_segments); |
| } |
| } |
| |
| void CastMirroringServiceHost::CreateAudioStreamForTab( |
| mojo::PendingRemote<mojom::AudioStreamCreatorClient> requestor, |
| const media::AudioParameters& params, |
| uint32_t total_segments, |
| const base::UnguessableToken& group_id) { |
| // Stream control message pipes. The pipe endpoints will end up at the Audio |
| // Service and the Mirroring Service, not here. |
| mojo::MessagePipe pipe_to_audio_service; |
| mojo::MessagePipe pipe_to_mirroring_service; |
| |
| // The Audio Service's CreateLoopbackStream() API requires an observer, but |
| // CastMirroringServiceHost does not care about any of the events. Also, the |
| // Audio Service requires that something has to be bound to the receive end of |
| // the message pipe or it will kill the stream. Thus, a dummy is provided |
| // here. |
| class DummyObserver final : public AudioInputStreamObserver { |
| void DidStartRecording() final {} |
| }; |
| mojo::MessagePipe observer_pipe; |
| mojo::MakeSelfOwnedReceiver(std::make_unique<DummyObserver>(), |
| mojo::PendingReceiver<AudioInputStreamObserver>( |
| std::move(observer_pipe.handle1))); |
| |
| // The following insane glob of code asks the Audio Service to create a |
| // loopback stream using the |group_id| as the selector for the tab's audio |
| // outputs. One end of the message pipes is passed to the Audio Service via |
| // the CreateLoopbackStream() call. Then, when the reply comes back, the other |
| // end of the message pipes is passed to the Mirroring Service (the |
| // |requestor|), along with the audio data pipe. |
| audio_stream_factory_->CreateLoopbackStream( |
| mojo::PendingReceiver<AudioInputStream>( |
| std::move(pipe_to_audio_service.handle1)), |
| mojo::PendingRemote<AudioInputStreamClient>( |
| std::move(pipe_to_mirroring_service.handle0), 0), |
| mojo::PendingRemote<AudioInputStreamObserver>( |
| std::move(observer_pipe.handle0), 0), |
| params, total_segments, group_id, |
| base::BindOnce( |
| [](mojo::PendingRemote<mojom::AudioStreamCreatorClient> requestor, |
| mojo::PendingRemote<AudioInputStream> stream, |
| mojo::PendingReceiver<AudioInputStreamClient> client, |
| media::mojom::ReadOnlyAudioDataPipePtr data_pipe) { |
| mojo::Remote<mojom::AudioStreamCreatorClient>(std::move(requestor)) |
| ->StreamCreated(std::move(stream), std::move(client), |
| std::move(data_pipe)); |
| }, |
| std::move(requestor), |
| mojo::PendingRemote<AudioInputStream>( |
| std::move(pipe_to_audio_service.handle0), 0), |
| mojo::PendingReceiver<AudioInputStreamClient>( |
| std::move(pipe_to_mirroring_service.handle1)))); |
| } |
| |
| void CastMirroringServiceHost::CreateAudioStreamForDesktop( |
| mojo::PendingRemote<mojom::AudioStreamCreatorClient> requestor, |
| const media::AudioParameters& params, |
| uint32_t total_segments) { |
| // Stream control message pipes. The pipe endpoints will end up at the Audio |
| // Service and the Mirroring Service, not here. |
| mojo::MessagePipe pipe_to_audio_service; |
| mojo::MessagePipe pipe_to_mirroring_service; |
| |
| // This does the mostly the same thing as the similar insane glob of code in |
| // the CreateAudioStreamForTab() method. Here, system-wide audio is requested |
| // from the platform, and so the CreateInputStream() API is used instead of |
| // CreateLoopbackStream(). CreateInputStream() is more complex, having a |
| // number of optional parameters that people seem to just keep adding more of |
| // over time, with little consideration for maintainable code structure, and |
| // add to the fun we're having here. |
| // |
| // See if you can spot all 6 unused fields! :P |
| audio_stream_factory_->CreateInputStream( |
| mojo::PendingReceiver<AudioInputStream>( |
| std::move(pipe_to_audio_service.handle1)), |
| mojo::PendingRemote<AudioInputStreamClient>( |
| std::move(pipe_to_mirroring_service.handle0), 0), |
| mojo::NullRemote(), mojo::NullRemote(), |
| media::AudioDeviceDescription::kLoopbackWithMuteDeviceId, params, |
| total_segments, false, base::ReadOnlySharedMemoryRegion(), |
| base::BindOnce( |
| [](mojo::PendingRemote<mojom::AudioStreamCreatorClient> requestor, |
| mojo::PendingRemote<AudioInputStream> stream, |
| mojo::PendingReceiver<AudioInputStreamClient> client, |
| media::mojom::ReadOnlyAudioDataPipePtr data_pipe, bool, |
| const base::Optional<base::UnguessableToken>&) { |
| mojo::Remote<mojom::AudioStreamCreatorClient>(std::move(requestor)) |
| ->StreamCreated(std::move(stream), std::move(client), |
| std::move(data_pipe)); |
| }, |
| std::move(requestor), |
| mojo::PendingRemote<AudioInputStream>( |
| std::move(pipe_to_audio_service.handle0), 0), |
| mojo::PendingReceiver<AudioInputStreamClient>( |
| std::move(pipe_to_mirroring_service.handle1)))); |
| } |
| |
| void CastMirroringServiceHost::ConnectToRemotingSource( |
| mojo::PendingRemote<media::mojom::Remoter> remoter, |
| mojo::PendingReceiver<media::mojom::RemotingSource> receiver) { |
| if (source_media_id_.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) { |
| content::WebContents* source_contents = web_contents(); |
| if (source_contents) { |
| CastRemotingConnector::Get(source_contents) |
| ->ConnectWithMediaRemoter(std::move(remoter), std::move(receiver)); |
| } |
| } |
| } |
| |
| void CastMirroringServiceHost::WebContentsDestroyed() { |
| mirroring_service_.reset(); |
| web_contents_audio_muter_.reset(); |
| audio_stream_factory_.reset(); |
| gpu_client_.reset(); |
| } |
| |
| void CastMirroringServiceHost::ShowCaptureIndicator() { |
| if (source_media_id_.type != content::DesktopMediaID::TYPE_WEB_CONTENTS || |
| !web_contents()) { |
| return; |
| } |
| const blink::MediaStreamDevice device( |
| ConvertVideoStreamType(source_media_id_.type), |
| source_media_id_.ToString(), /* name */ std::string()); |
| media_stream_ui_ = MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetMediaStreamCaptureIndicator() |
| ->RegisterMediaStream(web_contents(), {device}); |
| media_stream_ui_->OnStarted( |
| base::OnceClosure(), content::MediaStreamUI::SourceCallback(), |
| /*label=*/std::string(), /*screen_capture_ids=*/{}, |
| content::MediaStreamUI::StateChangeCallback()); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| void CastMirroringServiceHost::RequestMediaAccessPermission( |
| const content::MediaStreamRequest& request, |
| content::MediaResponseCallback callback) { |
| // This should not be called when mirroring an OffscreenTab through the |
| // mirroring service. |
| NOTREACHED(); |
| } |
| |
| void CastMirroringServiceHost::DestroyTab(OffscreenTab* tab) { |
| if (offscreen_tab_ && (offscreen_tab_.get() == tab)) |
| offscreen_tab_.reset(); |
| } |
| |
| void CastMirroringServiceHost::OpenOffscreenTab( |
| content::BrowserContext* context, |
| const GURL& presentation_url, |
| const std::string& presentation_id) { |
| DCHECK(!offscreen_tab_); |
| offscreen_tab_ = std::make_unique<OffscreenTab>(this, context); |
| offscreen_tab_->Start(presentation_url, GetCaptureResolutionConstraint(), |
| presentation_id); |
| source_media_id_ = BuildMediaIdForWebContents(offscreen_tab_->web_contents()); |
| DCHECK_EQ(content::DesktopMediaID::TYPE_WEB_CONTENTS, source_media_id_.type); |
| Observe(offscreen_tab_->web_contents()); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| } // namespace mirroring |