Created
March 9, 2026 11:02
-
-
Save SteffenDE/e804240076aa6a173abe8a774250a5ce to your computer and use it in GitHub Desktop.
Chrome PulseAudio web app + title integration, use default browser in web apps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc | |
| index 77c8dba45e..b57b5b21e6 100644 | |
| --- a/chrome/browser/chrome_content_browser_client.cc | |
| +++ b/chrome/browser/chrome_content_browser_client.cc | |
| @@ -554,6 +554,7 @@ | |
| #include "chrome/browser/web_applications/web_app_helpers.h" | |
| #include "chrome/browser/web_applications/web_app_provider.h" | |
| #include "chrome/browser/web_applications/web_app_registrar.h" | |
| +#include "chrome/browser/web_applications/web_app_tab_helper.h" | |
| #include "chrome/browser/web_applications/web_app_utils.h" | |
| #include "chrome/browser/webauthn/authenticator_request_scheduler.h" | |
| #include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h" | |
| @@ -595,6 +596,10 @@ | |
| #include "components/crash/core/app/crashpad.h" | |
| #endif | |
| +#if BUILDFLAG(IS_LINUX) | |
| +#include "chrome/browser/web_applications/os_integration/web_app_shortcut_linux.h" | |
| +#endif | |
| + | |
| #if BUILDFLAG(IS_ANDROID) | |
| #include "base/android/device_info.h" | |
| #include "components/crash/content/browser/crash_handler_host_linux.h" | |
| @@ -7029,6 +7034,36 @@ ChromeContentBrowserClient::GetAutoPipInfo( | |
| #endif // BUILDFLAG(IS_ANDROID) | |
| } | |
| +content::ContentBrowserClient::WebAppAudioStreamInfo | |
| +ChromeContentBrowserClient::GetWebAppInfoForAudioStream( | |
| + content::WebContents& web_contents) const { | |
| +#if BUILDFLAG(IS_ANDROID) | |
| + return {}; | |
| +#else | |
| + const webapps::AppId* app_id = | |
| + web_app::WebAppTabHelper::GetAppId(&web_contents); | |
| + if (!app_id) | |
| + return {}; | |
| + | |
| + Profile* profile = | |
| + Profile::FromBrowserContext(web_contents.GetBrowserContext()); | |
| + web_app::WebAppProvider* provider = | |
| + web_app::WebAppProvider::GetForWebApps(profile); | |
| + if (!provider) | |
| + return {}; | |
| + | |
| + content::ContentBrowserClient::WebAppAudioStreamInfo info; | |
| + info.app_name = provider->registrar_unsafe().GetAppShortName(*app_id); | |
| +#if BUILDFLAG(IS_LINUX) | |
| + info.xdg_icon_name = | |
| + web_app::GetAppDesktopShortcutFilename(profile->GetPath(), *app_id) | |
| + .RemoveFinalExtension() | |
| + .value(); | |
| +#endif | |
| + return info; | |
| +#endif // BUILDFLAG(IS_ANDROID) | |
| +} | |
| + | |
| void ChromeContentBrowserClient::RegisterRendererPreferenceWatcher( | |
| content::BrowserContext* browser_context, | |
| mojo::PendingRemote<blink::mojom::RendererPreferenceWatcher> watcher) { | |
| diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h | |
| index 6aac7e8ab4..d14ea29e15 100644 | |
| --- a/chrome/browser/chrome_content_browser_client.h | |
| +++ b/chrome/browser/chrome_content_browser_client.h | |
| @@ -842,6 +842,8 @@ class ChromeContentBrowserClient : public content::ContentBrowserClient { | |
| content::WebContents* web_contents) override; | |
| media::PictureInPictureEventsInfo::AutoPipInfo GetAutoPipInfo( | |
| const content::WebContents& web_contents) const override; | |
| + WebAppAudioStreamInfo GetWebAppInfoForAudioStream( | |
| + content::WebContents& web_contents) const override; | |
| void RegisterRendererPreferenceWatcher( | |
| content::BrowserContext* browser_context, | |
| mojo::PendingRemote<blink::mojom::RendererPreferenceWatcher> watcher) | |
| diff --git a/content/browser/media/audio_output_stream_broker.cc b/content/browser/media/audio_output_stream_broker.cc | |
| index 847d33aac9..5131161674 100644 | |
| --- a/content/browser/media/audio_output_stream_broker.cc | |
| +++ b/content/browser/media/audio_output_stream_broker.cc | |
| @@ -18,6 +18,7 @@ | |
| #include "content/public/browser/media_observer.h" | |
| #include "content/public/common/content_client.h" | |
| #include "media/audio/audio_device_description.h" | |
| +#include "media/mojo/mojom/audio_stream_factory.mojom.h" | |
| #include "media/audio/audio_logging.h" | |
| #include "media/mojo/mojom/audio_data_pipe.mojom.h" | |
| #include "media/mojo/mojom/audio_output_stream.mojom.h" | |
| @@ -138,8 +139,28 @@ void AudioOutputStreamBroker::CreateStream( | |
| TRACE_EVENT_BEGIN("audio", "CreateStream", perfetto::Track::FromPointer(this), | |
| "device id", output_device_id_); | |
| + factory_ = factory; | |
| stream_creation_start_time_ = base::TimeTicks::Now(); | |
| + // Resolve stream metadata (title, web app identity) before creating the | |
| + // stream so that PulseAudio sees the correct values at stream creation time. | |
| + ResolveAudioStreamInfo( | |
| + render_process_id(), render_frame_id(), | |
| + base::BindOnce(&AudioOutputStreamBroker::OnAudioStreamInfoResolved, | |
| + weak_ptr_factory_.GetWeakPtr())); | |
| +} | |
| + | |
| +void AudioOutputStreamBroker::OnAudioStreamInfoResolved( | |
| + AudioStreamInfo info) { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| + if (!factory_) | |
| + return; | |
| + | |
| + auto metadata = media::mojom::AudioStreamMetadata::New(); | |
| + metadata->title = std::move(info.title); | |
| + metadata->app_name = std::move(info.app_name); | |
| + metadata->xdg_icon_name = std::move(info.xdg_icon_name); | |
| + | |
| // Set up observer ptr. Unretained is safe because |this| owns | |
| // |observer_receiver_|. | |
| mojo::PendingAssociatedRemote<media::mojom::AudioOutputStreamObserver> | |
| @@ -151,37 +172,30 @@ void AudioOutputStreamBroker::CreateStream( | |
| mojo::PendingRemote<media::mojom::AudioOutputStream> stream; | |
| auto stream_receiver = stream.InitWithNewPipeAndPassReceiver(); | |
| - // Note that the component id for AudioLog is used to differentiate between | |
| - // several users of the same audio log. Since this audio log is for a single | |
| - // stream, the component id used doesn't matter. | |
| constexpr int log_component_id = 0; | |
| if (MediaStreamManager::GetPreferredOutputManagerInstance() && | |
| media::AudioDeviceDescription::IsDefaultDevice(output_device_id_)) { | |
| - // Register the device switcher with PreferredAudioOutputDeviceManager. | |
| - // `output_device_id_` will be updated by the `SwitchAudioOutputDeviceId`, | |
| - // which is called by the PreferredAudioOutputDeviceManager during | |
| - // `AddSwitcher()`. | |
| MediaStreamManager::GetPreferredOutputManagerInstance()->AddSwitcher( | |
| main_frame_token_, this); | |
| - factory->CreateSwitchableOutputStream( | |
| + factory_->CreateSwitchableOutputStream( | |
| std::move(stream_receiver), | |
| device_switch_interface_.BindNewPipeAndPassReceiver(), | |
| std::move(observer), | |
| MediaInternals::GetInstance()->CreateMojoAudioLog( | |
| media::AudioLogFactory::AudioComponent::kAudioOuputController, | |
| log_component_id, render_process_id(), render_frame_id()), | |
| - output_device_id_, params_, group_id_, | |
| + output_device_id_, params_, group_id_, std::move(metadata), | |
| base::BindOnce(&AudioOutputStreamBroker::StreamCreated, | |
| weak_ptr_factory_.GetWeakPtr(), std::move(stream))); | |
| } else { | |
| - factory->CreateOutputStream( | |
| + factory_->CreateOutputStream( | |
| std::move(stream_receiver), std::move(observer), | |
| MediaInternals::GetInstance()->CreateMojoAudioLog( | |
| media::AudioLogFactory::AudioComponent::kAudioOuputController, | |
| log_component_id, render_process_id(), render_frame_id()), | |
| - output_device_id_, params_, group_id_, | |
| + output_device_id_, params_, group_id_, std::move(metadata), | |
| base::BindOnce(&AudioOutputStreamBroker::StreamCreated, | |
| weak_ptr_factory_.GetWeakPtr(), std::move(stream))); | |
| } | |
| @@ -191,13 +205,11 @@ void AudioOutputStreamBroker::StreamCreated( | |
| mojo::PendingRemote<media::mojom::AudioOutputStream> stream, | |
| media::mojom::ReadWriteAudioDataPipePtr data_pipe) { | |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| - // End "CreateStream" trace event. | |
| TRACE_EVENT_END("audio", perfetto::Track::FromPointer(this), "success", | |
| !!data_pipe); | |
| stream_creation_start_time_ = base::TimeTicks(); | |
| if (!data_pipe) { | |
| - // Stream creation failed. Signal error. | |
| client_.ResetWithReason( | |
| static_cast<uint32_t>(DisconnectReason::kPlatformError), std::string()); | |
| Cleanup(DisconnectReason::kStreamCreationFailed); | |
| diff --git a/content/browser/media/audio_output_stream_broker.h b/content/browser/media/audio_output_stream_broker.h | |
| index d9d9d12d15..37bf924882 100644 | |
| --- a/content/browser/media/audio_output_stream_broker.h | |
| +++ b/content/browser/media/audio_output_stream_broker.h | |
| @@ -14,6 +14,7 @@ | |
| #include "base/unguessable_token.h" | |
| #include "content/browser/renderer_host/media/audio_output_stream_observer_impl.h" | |
| #include "content/browser/renderer_host/media/preferred_audio_output_device_manager.h" | |
| +#include "content/browser/media/audio_stream_broker_helper.h" | |
| #include "content/common/content_export.h" | |
| #include "content/public/browser/audio_stream_broker.h" | |
| #include "media/base/audio_parameters.h" | |
| @@ -72,6 +73,7 @@ class CONTENT_EXPORT AudioOutputStreamBroker final | |
| void StreamCreated( | |
| mojo::PendingRemote<media::mojom::AudioOutputStream> stream, | |
| media::mojom::ReadWriteAudioDataPipePtr data_pipe); | |
| + void OnAudioStreamInfoResolved(AudioStreamInfo info); | |
| void ObserverBindingLost(uint32_t reason, const std::string& description); | |
| void Cleanup(DisconnectReason reason); | |
| bool AwaitingCreated() const; | |
| @@ -95,6 +97,10 @@ class CONTENT_EXPORT AudioOutputStreamBroker final | |
| observer_receiver_; | |
| mojo::Remote<media::mojom::DeviceSwitchInterface> device_switch_interface_; | |
| + // Stored from CreateStream() so we can create the stream after resolving | |
| + // metadata asynchronously. | |
| + raw_ptr<media::mojom::AudioStreamFactory> factory_ = nullptr; | |
| + | |
| DisconnectReason disconnect_reason_ = DisconnectReason::kDocumentDestroyed; | |
| base::WeakPtrFactory<AudioOutputStreamBroker> weak_ptr_factory_{this}; | |
| diff --git a/content/browser/media/audio_output_stream_broker_unittest.cc b/content/browser/media/audio_output_stream_broker_unittest.cc | |
| index b11eaf31d1..d66ddc25c5 100644 | |
| --- a/content/browser/media/audio_output_stream_broker_unittest.cc | |
| +++ b/content/browser/media/audio_output_stream_broker_unittest.cc | |
| @@ -160,6 +160,7 @@ class MockStreamFactory final : public audio::FakeStreamFactory { | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) final { | |
| // No way to cleanly exit the test here in case of failure, so use CHECK. | |
| CHECK(stream_request_data_); | |
| @@ -183,10 +184,11 @@ class MockStreamFactory final : public audio::FakeStreamFactory { | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) final { | |
| CreateOutputStream(std::move(stream_receiver), std::move(observer), | |
| std::move(log), output_device_id, params, group_id, | |
| - std::move(created_callback)); | |
| + std::move(metadata), std::move(created_callback)); | |
| } | |
| raw_ptr<StreamRequestData> stream_request_data_; | |
| diff --git a/content/browser/media/audio_stream_broker_helper.cc b/content/browser/media/audio_stream_broker_helper.cc | |
| index 0c21518dbc..f3e885a027 100644 | |
| --- a/content/browser/media/audio_stream_broker_helper.cc | |
| +++ b/content/browser/media/audio_stream_broker_helper.cc | |
| @@ -4,10 +4,16 @@ | |
| #include "content/browser/media/audio_stream_broker_helper.h" | |
| +#include <string> | |
| + | |
| #include "base/functional/bind.h" | |
| #include "base/location.h" | |
| +#include "base/strings/utf_string_conversions.h" | |
| #include "content/browser/renderer_host/render_frame_host_impl.h" | |
| #include "content/public/browser/browser_thread.h" | |
| +#include "content/public/browser/content_browser_client.h" | |
| +#include "content/public/browser/web_contents.h" | |
| +#include "content/public/common/content_client.h" | |
| namespace content { | |
| @@ -50,4 +56,30 @@ void NotifyFrameHostOfAudioStreamStopped(int render_process_id, | |
| base::BindOnce(impl, render_process_id, render_frame_id, is_capturing)); | |
| } | |
| +void ResolveAudioStreamInfo( | |
| + int render_process_id, | |
| + int render_frame_id, | |
| + base::OnceCallback<void(AudioStreamInfo)> callback) { | |
| + auto resolve_on_ui = [](int render_process_id, int render_frame_id, | |
| + base::OnceCallback<void(AudioStreamInfo)> callback) { | |
| + DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
| + AudioStreamInfo info; | |
| + if (auto* host = | |
| + RenderFrameHostImpl::FromID(render_process_id, render_frame_id)) { | |
| + if (auto* web_contents = WebContents::FromRenderFrameHost(host)) { | |
| + info.title = base::UTF16ToUTF8(web_contents->GetTitle()); | |
| + auto app_info = GetContentClient()->browser() | |
| + ->GetWebAppInfoForAudioStream(*web_contents); | |
| + info.app_name = std::move(app_info.app_name); | |
| + info.xdg_icon_name = std::move(app_info.xdg_icon_name); | |
| + } | |
| + } | |
| + GetIOThreadTaskRunner({})->PostTask( | |
| + FROM_HERE, base::BindOnce(std::move(callback), std::move(info))); | |
| + }; | |
| + GetUIThreadTaskRunner({})->PostTask( | |
| + FROM_HERE, base::BindOnce(resolve_on_ui, render_process_id, | |
| + render_frame_id, std::move(callback))); | |
| +} | |
| + | |
| } // namespace content | |
| diff --git a/content/browser/media/audio_stream_broker_helper.h b/content/browser/media/audio_stream_broker_helper.h | |
| index ca6aa1c8ac..00069f16a6 100644 | |
| --- a/content/browser/media/audio_stream_broker_helper.h | |
| +++ b/content/browser/media/audio_stream_broker_helper.h | |
| @@ -5,6 +5,10 @@ | |
| #ifndef CONTENT_BROWSER_MEDIA_AUDIO_STREAM_BROKER_HELPER_H_ | |
| #define CONTENT_BROWSER_MEDIA_AUDIO_STREAM_BROKER_HELPER_H_ | |
| +#include <string> | |
| + | |
| +#include "base/functional/callback.h" | |
| + | |
| namespace content { | |
| // Thread-safe utility that notifies the RenderFrameHost identified by | |
| @@ -20,6 +24,21 @@ void NotifyFrameHostOfAudioStreamStopped(int render_process_id, | |
| int render_frame_id, | |
| bool is_capturing); | |
| +struct AudioStreamInfo { | |
| + std::string title; | |
| + std::string app_name; | |
| + std::string xdg_icon_name; | |
| +}; | |
| + | |
| +// Resolves audio stream metadata for the WebContents associated with the given | |
| +// render frame. Posts to the UI thread to look up title (via GetTitle()) and | |
| +// web-app identity (via ContentBrowserClient::GetWebAppInfoForAudioStream()), | |
| +// then runs |callback| on the IO thread. | |
| +void ResolveAudioStreamInfo( | |
| + int render_process_id, | |
| + int render_frame_id, | |
| + base::OnceCallback<void(AudioStreamInfo)> callback); | |
| + | |
| } // namespace content | |
| #endif // CONTENT_BROWSER_MEDIA_AUDIO_STREAM_BROKER_HELPER_H_ | |
| diff --git a/content/browser/media/forwarding_audio_stream_factory.cc b/content/browser/media/forwarding_audio_stream_factory.cc | |
| index c3fb32fe98..e96706389d 100644 | |
| --- a/content/browser/media/forwarding_audio_stream_factory.cc | |
| +++ b/content/browser/media/forwarding_audio_stream_factory.cc | |
| @@ -7,6 +7,7 @@ | |
| #include <utility> | |
| #include "base/check_op.h" | |
| +#include "base/strings/utf_string_conversions.h" | |
| #include "base/functional/bind.h" | |
| #include "base/functional/callback_helpers.h" | |
| #include "base/location.h" | |
| @@ -174,6 +175,14 @@ void ForwardingAudioStreamFactory::Core::SetMuted(bool muted) { | |
| muter_->Connect(remote_factory_.get()); | |
| } | |
| +void ForwardingAudioStreamFactory::Core::SetStreamLabel( | |
| + const std::string& label) { | |
| + DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
| + if (outputs_.empty()) | |
| + return; | |
| + GetFactory()->SetStreamLabelForGroup(group_id_, label); | |
| +} | |
| + | |
| void ForwardingAudioStreamFactory::Core::AddLoopbackSink( | |
| AudioStreamBroker::LoopbackSink* sink) { | |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
| @@ -295,6 +304,15 @@ void ForwardingAudioStreamFactory::RenderFrameDeleted( | |
| render_frame_host->GetRoutingID())); | |
| } | |
| +void ForwardingAudioStreamFactory::TitleWasSet(NavigationEntry* entry) { | |
| + DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
| + std::string title = base::UTF16ToUTF8(web_contents()->GetTitle()); | |
| + GetIOThreadTaskRunner({})->PostTask( | |
| + FROM_HERE, | |
| + base::BindOnce(&Core::SetStreamLabel, base::Unretained(core_.get()), | |
| + std::move(title))); | |
| +} | |
| + | |
| void ForwardingAudioStreamFactory::OverrideAudioStreamFactoryBinderForTesting( | |
| AudioStreamFactoryBinder binder) { | |
| GetAudioStreamFactoryBinderOverride() = std::move(binder); | |
| diff --git a/content/browser/media/forwarding_audio_stream_factory.h b/content/browser/media/forwarding_audio_stream_factory.h | |
| index 0a12aaa7ee..193ec2985e 100644 | |
| --- a/content/browser/media/forwarding_audio_stream_factory.h | |
| +++ b/content/browser/media/forwarding_audio_stream_factory.h | |
| @@ -108,6 +108,10 @@ class CONTENT_EXPORT ForwardingAudioStreamFactory final | |
| // factory. | |
| void SetMuted(bool muted); | |
| + // Updates the stream label for all output streams in this group. | |
| + void SetStreamLabel(const std::string& label); | |
| + | |
| + | |
| // AudioStreamLoopback::Source implementation | |
| void AddLoopbackSink(AudioStreamBroker::LoopbackSink* sink) final; | |
| void RemoveLoopbackSink(AudioStreamBroker::LoopbackSink* sink) final; | |
| @@ -206,6 +210,7 @@ class CONTENT_EXPORT ForwardingAudioStreamFactory final | |
| // WebContentsObserver implementation. We observe these events so that we can | |
| // clean up streams belonging to a frame when that frame is destroyed. | |
| void RenderFrameDeleted(RenderFrameHost* render_frame_host) final; | |
| + void TitleWasSet(NavigationEntry* entry) final; | |
| Core* core() { return core_.get(); } | |
| diff --git a/content/browser/renderer_host/media/render_frame_audio_output_stream_factory_unittest.cc b/content/browser/renderer_host/media/render_frame_audio_output_stream_factory_unittest.cc | |
| index ae1eb8a444..fd492ea047 100644 | |
| --- a/content/browser/renderer_host/media/render_frame_audio_output_stream_factory_unittest.cc | |
| +++ b/content/browser/renderer_host/media/render_frame_audio_output_stream_factory_unittest.cc | |
| @@ -98,6 +98,7 @@ class RenderFrameAudioOutputStreamFactoryTest | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) override { | |
| last_created_callback_ = std::move(created_callback); | |
| } | |
| @@ -112,6 +113,7 @@ class RenderFrameAudioOutputStreamFactoryTest | |
| const std::string& device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) override { | |
| last_created_callback_ = std::move(created_callback); | |
| } | |
| diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc | |
| index 368d4b51b5..840a4d0598 100644 | |
| --- a/content/public/browser/content_browser_client.cc | |
| +++ b/content/public/browser/content_browser_client.cc | |
| @@ -1390,6 +1390,12 @@ ContentBrowserClient::GetAutoPipInfo(const WebContents& web_contents) const { | |
| return media::PictureInPictureEventsInfo::AutoPipInfo(); | |
| } | |
| +ContentBrowserClient::WebAppAudioStreamInfo | |
| +ContentBrowserClient::GetWebAppInfoForAudioStream( | |
| + WebContents& web_contents) const { | |
| + return {}; | |
| +} | |
| + | |
| void ContentBrowserClient::RegisterRendererPreferenceWatcher( | |
| BrowserContext* browser_context, | |
| mojo::PendingRemote<blink::mojom::RendererPreferenceWatcher> watcher) { | |
| diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h | |
| index aff5ce9518..c7cfdba248 100644 | |
| --- a/content/public/browser/content_browser_client.h | |
| +++ b/content/public/browser/content_browser_client.h | |
| @@ -2567,6 +2567,16 @@ class CONTENT_EXPORT ContentBrowserClient { | |
| virtual media::PictureInPictureEventsInfo::AutoPipInfo GetAutoPipInfo( | |
| const WebContents& web_contents) const; | |
| + struct WebAppAudioStreamInfo { | |
| + std::string app_name; | |
| + std::string xdg_icon_name; | |
| + }; | |
| + | |
| + // Called on the UI thread to retrieve web-app identity for audio stream | |
| + // labelling (e.g. PulseAudio PA_PROP_APPLICATION_NAME / ICON_NAME). | |
| + virtual WebAppAudioStreamInfo GetWebAppInfoForAudioStream( | |
| + WebContents& web_contents) const; | |
| + | |
| // Registers the watcher to observe updates in RendererPreferences. | |
| virtual void RegisterRendererPreferenceWatcher( | |
| BrowserContext* browser_context, | |
| diff --git a/media/audio/audio_io.h b/media/audio/audio_io.h | |
| index b9708113eb..fd800e579a 100644 | |
| --- a/media/audio/audio_io.h | |
| +++ b/media/audio/audio_io.h | |
| @@ -7,6 +7,8 @@ | |
| #include <stdint.h> | |
| +#include <string> | |
| + | |
| #include "base/time/time.h" | |
| #include "media/base/audio_glitch_info.h" | |
| #include "media/base/media_export.h" | |
| @@ -134,6 +136,16 @@ class MEDIA_EXPORT AudioOutputStream { | |
| // playing. (i.e. called after Stop or Open) | |
| virtual void Flush() = 0; | |
| + // Sets a human-readable label for this stream (e.g. tab title). | |
| + // Used on platforms that support per-stream naming (e.g. PulseAudio). | |
| + // Default implementation is a no-op. | |
| + virtual void SetStreamLabel(const std::string& label) {} | |
| + | |
| + // Sets the application identity for this stream. | |
| + // |xdg_icon_name| is the freedesktop icon name (Linux only). | |
| + virtual void SetStreamAppIdentity(const std::string& app_name, | |
| + const std::string& xdg_icon_name) {} | |
| + | |
| // Constrains a timedelta representing a delay to between 0 and 10 seconds. | |
| // This is used by OS implementations to prevent miscalculated delay values | |
| // from creating large amounts of noise in the delay stats. | |
| diff --git a/media/audio/audio_output_dispatcher.h b/media/audio/audio_output_dispatcher.h | |
| index 50a8b0b916..053d6b9bfa 100644 | |
| --- a/media/audio/audio_output_dispatcher.h | |
| +++ b/media/audio/audio_output_dispatcher.h | |
| @@ -18,8 +18,11 @@ | |
| #ifndef MEDIA_AUDIO_AUDIO_OUTPUT_DISPATCHER_H_ | |
| #define MEDIA_AUDIO_AUDIO_OUTPUT_DISPATCHER_H_ | |
| +#include <string> | |
| + | |
| #include "base/memory/raw_ptr.h" | |
| #include "media/audio/audio_io.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| namespace media { | |
| class AudioManager; | |
| @@ -41,8 +44,10 @@ class MEDIA_EXPORT AudioOutputDispatcher { | |
| virtual AudioOutputProxy* CreateStreamProxy() = 0; | |
| // Called by AudioOutputProxy to open the stream. | |
| - // Returns false, if it fails to open it. | |
| - virtual bool OpenStream() = 0; | |
| + // Returns false, if it fails to open it. |metadata| is set on the physical | |
| + // stream before Open() so that platform backends (e.g. PulseAudio) can use | |
| + // it during stream creation. | |
| + virtual bool OpenStream(const AudioStreamMetadata& metadata) = 0; | |
| // Called by AudioOutputProxy when the stream is started. | |
| // Uses |callback| to get source data and report errors, if any. | |
| @@ -66,6 +71,15 @@ class MEDIA_EXPORT AudioOutputDispatcher { | |
| // called when a stream is stopped. | |
| virtual void FlushStream(AudioOutputProxy* stream_proxy) = 0; | |
| + // Called by AudioOutputProxy to set a human-readable label on the | |
| + // underlying physical stream (e.g. tab title for PulseAudio). | |
| + virtual void SetStreamLabel(AudioOutputProxy* stream_proxy, | |
| + const std::string& label) {} | |
| + | |
| + virtual void SetStreamAppIdentity(AudioOutputProxy* stream_proxy, | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) {} | |
| + | |
| protected: | |
| AudioManager* audio_manager() const { return audio_manager_; } | |
| diff --git a/media/audio/audio_output_dispatcher_impl.cc b/media/audio/audio_output_dispatcher_impl.cc | |
| index a02fc285d3..09f56a8687 100644 | |
| --- a/media/audio/audio_output_dispatcher_impl.cc | |
| +++ b/media/audio/audio_output_dispatcher_impl.cc | |
| @@ -59,12 +59,16 @@ AudioOutputProxy* AudioOutputDispatcherImpl::CreateStreamProxy() { | |
| return new AudioOutputProxy(weak_factory_.GetWeakPtr()); | |
| } | |
| -bool AudioOutputDispatcherImpl::OpenStream() { | |
| +bool AudioOutputDispatcherImpl::OpenStream( | |
| + const AudioStreamMetadata& metadata) { | |
| DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| - // Ensure that there is at least one open stream. | |
| - if (idle_streams_.empty() && !CreateAndOpenStream()) { | |
| - return false; | |
| + // When metadata is set (e.g. for a PWA), always create a fresh stream so the | |
| + // platform backend sees the correct identity at creation time. Don't reuse | |
| + // idle streams that were created with different (or no) metadata. | |
| + if (!metadata.empty() || idle_streams_.empty()) { | |
| + if (!CreateAndOpenStream(metadata)) | |
| + return false; | |
| } | |
| ++idle_proxies_; | |
| @@ -78,7 +82,7 @@ bool AudioOutputDispatcherImpl::StartStream( | |
| DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| DCHECK(!proxy_to_physical_map_.contains(stream_proxy)); | |
| - if (idle_streams_.empty() && !CreateAndOpenStream()) { | |
| + if (idle_streams_.empty() && !CreateAndOpenStream(AudioStreamMetadata())) { | |
| return false; | |
| } | |
| @@ -91,6 +95,12 @@ bool AudioOutputDispatcherImpl::StartStream( | |
| double volume = 0; | |
| stream_proxy->GetVolume(&volume); | |
| physical_stream->SetVolume(volume); | |
| + // Update the label on the reused physical stream to match the new proxy's | |
| + // tab title. App identity is not updated here because streams with custom | |
| + // app identity (PWAs) are never pooled — they are closed in StopStream(). | |
| + if (!stream_proxy->stream_label().empty()) { | |
| + physical_stream->SetStreamLabel(stream_proxy->stream_label()); | |
| + } | |
| DCHECK(audio_logs_.contains(physical_stream)); | |
| AudioLog* const audio_log = audio_logs_[physical_stream].get(); | |
| audio_log->OnSetVolume(volume); | |
| @@ -106,7 +116,23 @@ void AudioOutputDispatcherImpl::StopStream(AudioOutputProxy* stream_proxy) { | |
| DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| auto it = proxy_to_physical_map_.find(stream_proxy); | |
| CHECK(it != proxy_to_physical_map_.end()); | |
| - StopPhysicalStream(it->second); | |
| + | |
| + AudioOutputStream* physical_stream = it->second; | |
| + | |
| + // Streams with custom app identity can't be reused for other apps, so close | |
| + // them immediately instead of returning to the idle pool. | |
| + if (!stream_proxy->app_name().empty()) { | |
| + physical_stream->Stop(); | |
| + DCHECK(audio_logs_.contains(physical_stream)); | |
| + audio_logs_[physical_stream]->OnStopped(); | |
| + // Close() must be called before erasing the log, because Close() calls | |
| + // SendLogMessage() which uses a callback capturing the log via Unretained. | |
| + physical_stream->Close(); | |
| + audio_logs_[physical_stream]->OnClosed(); | |
| + audio_logs_.erase(physical_stream); | |
| + } else { | |
| + StopPhysicalStream(physical_stream); | |
| + } | |
| proxy_to_physical_map_.erase(it); | |
| ++idle_proxies_; | |
| } | |
| @@ -138,6 +164,26 @@ void AudioOutputDispatcherImpl::CloseStream(AudioOutputProxy* stream_proxy) { | |
| // StopStream(). | |
| void AudioOutputDispatcherImpl::FlushStream(AudioOutputProxy* stream_proxy) {} | |
| +void AudioOutputDispatcherImpl::SetStreamLabel(AudioOutputProxy* stream_proxy, | |
| + const std::string& label) { | |
| + DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| + auto it = proxy_to_physical_map_.find(stream_proxy); | |
| + if (it != proxy_to_physical_map_.end()) { | |
| + it->second->SetStreamLabel(label); | |
| + } | |
| +} | |
| + | |
| +void AudioOutputDispatcherImpl::SetStreamAppIdentity( | |
| + AudioOutputProxy* stream_proxy, | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) { | |
| + DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| + auto it = proxy_to_physical_map_.find(stream_proxy); | |
| + if (it != proxy_to_physical_map_.end()) { | |
| + it->second->SetStreamAppIdentity(app_name, xdg_icon_name); | |
| + } | |
| +} | |
| + | |
| void AudioOutputDispatcherImpl::OnDeviceChange() { | |
| DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| @@ -154,7 +200,8 @@ bool AudioOutputDispatcherImpl::HasOutputProxies() const { | |
| return idle_proxies_ || !proxy_to_physical_map_.empty(); | |
| } | |
| -bool AudioOutputDispatcherImpl::CreateAndOpenStream() { | |
| +bool AudioOutputDispatcherImpl::CreateAndOpenStream( | |
| + const AudioStreamMetadata& metadata) { | |
| DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| const int stream_id = audio_stream_id_++; | |
| std::unique_ptr<AudioLog> audio_log = audio_manager()->CreateAudioLog( | |
| @@ -167,6 +214,11 @@ bool AudioOutputDispatcherImpl::CreateAndOpenStream() { | |
| return false; | |
| } | |
| + if (!metadata.title.empty()) | |
| + stream->SetStreamLabel(metadata.title); | |
| + if (!metadata.app_name.empty()) | |
| + stream->SetStreamAppIdentity(metadata.app_name, metadata.xdg_icon_name); | |
| + | |
| if (!stream->Open()) { | |
| stream->Close(); | |
| return false; | |
| diff --git a/media/audio/audio_output_dispatcher_impl.h b/media/audio/audio_output_dispatcher_impl.h | |
| index a28096cc4d..d70fa9c519 100644 | |
| --- a/media/audio/audio_output_dispatcher_impl.h | |
| +++ b/media/audio/audio_output_dispatcher_impl.h | |
| @@ -49,13 +49,18 @@ class MEDIA_EXPORT AudioOutputDispatcherImpl | |
| // AudioOutputDispatcher implementation. | |
| AudioOutputProxy* CreateStreamProxy() override; | |
| - bool OpenStream() override; | |
| + bool OpenStream(const AudioStreamMetadata& metadata) override; | |
| bool StartStream(AudioOutputStream::AudioSourceCallback* callback, | |
| AudioOutputProxy* stream_proxy) override; | |
| void StopStream(AudioOutputProxy* stream_proxy) override; | |
| void StreamVolumeSet(AudioOutputProxy* stream_proxy, double volume) override; | |
| void CloseStream(AudioOutputProxy* stream_proxy) override; | |
| void FlushStream(AudioOutputProxy* stream_proxy) override; | |
| + void SetStreamLabel(AudioOutputProxy* stream_proxy, | |
| + const std::string& label) override; | |
| + void SetStreamAppIdentity(AudioOutputProxy* stream_proxy, | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) override; | |
| // AudioDeviceListener implementation. | |
| void OnDeviceChange() override; | |
| @@ -70,7 +75,7 @@ class MEDIA_EXPORT AudioOutputDispatcherImpl | |
| // Creates a new physical output stream, opens it and pushes to | |
| // |idle_streams_|. Returns false if the stream couldn't be created or | |
| // opened. | |
| - bool CreateAndOpenStream(); | |
| + bool CreateAndOpenStream(const AudioStreamMetadata& metadata); | |
| // Similar to CloseAllIdleStreams(), but keeps |keep_alive| streams alive. | |
| void CloseIdleStreams(size_t keep_alive); | |
| diff --git a/media/audio/audio_output_proxy.cc b/media/audio/audio_output_proxy.cc | |
| index 5f17fe6f80..cf59f88daf 100644 | |
| --- a/media/audio/audio_output_proxy.cc | |
| +++ b/media/audio/audio_output_proxy.cc | |
| @@ -7,6 +7,7 @@ | |
| #include "base/check_op.h" | |
| #include "media/audio/audio_manager.h" | |
| #include "media/audio/audio_output_dispatcher.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| namespace media { | |
| @@ -25,7 +26,11 @@ bool AudioOutputProxy::Open() { | |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | |
| DCHECK_EQ(state_, kCreated); | |
| - if (!dispatcher_ || !dispatcher_->OpenStream()) { | |
| + AudioStreamMetadata metadata; | |
| + metadata.title = stream_label_; | |
| + metadata.app_name = app_name_; | |
| + metadata.xdg_icon_name = xdg_icon_name_; | |
| + if (!dispatcher_ || !dispatcher_->OpenStream(metadata)) { | |
| state_ = kOpenError; | |
| return false; | |
| } | |
| @@ -99,4 +104,20 @@ void AudioOutputProxy::Flush() { | |
| dispatcher_->FlushStream(this); | |
| } | |
| +void AudioOutputProxy::SetStreamLabel(const std::string& label) { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | |
| + stream_label_ = label; | |
| + if (dispatcher_) | |
| + dispatcher_->SetStreamLabel(this, label); | |
| +} | |
| + | |
| +void AudioOutputProxy::SetStreamAppIdentity(const std::string& app_name, | |
| + const std::string& xdg_icon_name) { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | |
| + app_name_ = app_name; | |
| + xdg_icon_name_ = xdg_icon_name; | |
| + if (dispatcher_) | |
| + dispatcher_->SetStreamAppIdentity(this, app_name, xdg_icon_name); | |
| +} | |
| + | |
| } // namespace media | |
| diff --git a/media/audio/audio_output_proxy.h b/media/audio/audio_output_proxy.h | |
| index 118677fa54..56c79d16ea 100644 | |
| --- a/media/audio/audio_output_proxy.h | |
| +++ b/media/audio/audio_output_proxy.h | |
| @@ -39,6 +39,12 @@ class MEDIA_EXPORT AudioOutputProxy : public AudioOutputStream { | |
| void GetVolume(double* volume) override; | |
| void Close() override; | |
| void Flush() override; | |
| + void SetStreamLabel(const std::string& label) override; | |
| + const std::string& stream_label() const { return stream_label_; } | |
| + void SetStreamAppIdentity(const std::string& app_name, | |
| + const std::string& xdg_icon_name) override; | |
| + const std::string& app_name() const { return app_name_; } | |
| + const std::string& xdg_icon_name() const { return xdg_icon_name_; } | |
| AudioOutputDispatcher* get_dispatcher_for_testing() const { | |
| return dispatcher_.get(); | |
| @@ -63,6 +69,15 @@ class MEDIA_EXPORT AudioOutputProxy : public AudioOutputStream { | |
| // is stopped, and then started again. | |
| double volume_; | |
| + // Need to save the stream label so it can be re-applied when a new physical | |
| + // stream is assigned (e.g. after pause/resume). | |
| + std::string stream_label_; | |
| + | |
| + // Need to save the app identity so it can be re-applied when a new physical | |
| + // stream is assigned (e.g. after pause/resume). | |
| + std::string app_name_; | |
| + std::string xdg_icon_name_; | |
| + | |
| SEQUENCE_CHECKER(sequence_checker_); | |
| }; | |
| diff --git a/media/audio/audio_output_resampler.cc b/media/audio/audio_output_resampler.cc | |
| index 114ed6ac87..c266a26213 100644 | |
| --- a/media/audio/audio_output_resampler.cc | |
| +++ b/media/audio/audio_output_resampler.cc | |
| @@ -280,7 +280,7 @@ AudioOutputProxy* AudioOutputResampler::CreateStreamProxy() { | |
| return new AudioOutputProxy(weak_factory_.GetWeakPtr()); | |
| } | |
| -bool AudioOutputResampler::OpenStream() { | |
| +bool AudioOutputResampler::OpenStream(const AudioStreamMetadata& metadata) { | |
| DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| bool first_stream = false; | |
| @@ -299,7 +299,7 @@ bool AudioOutputResampler::OpenStream() { | |
| constexpr char kOpenLowLatencyOffloadHistogramName[] = | |
| "Media.AudioOutputResampler.OpenLowLatencyStream2.Offload"; | |
| - if (dispatcher_->OpenStream()) { | |
| + if (dispatcher_->OpenStream(metadata)) { | |
| // Only record the UMA statistic if we didn't fallback during construction | |
| // and only for the first stream we open. | |
| if (original_output_params_.format() == | |
| @@ -356,7 +356,7 @@ bool AudioOutputResampler::OpenStream() { | |
| output_params_); | |
| if (output_params_.IsValid()) { | |
| dispatcher_ = MakeDispatcher(device_id_, output_params_); | |
| - if (dispatcher_->OpenStream()) { | |
| + if (dispatcher_->OpenStream(AudioStreamMetadata())) { | |
| base::UmaHistogramEnumeration( | |
| kOpenLowLatencyHistogramName, | |
| OpenStreamResult::kFallbackToLowLatencySuccess); | |
| @@ -373,7 +373,7 @@ bool AudioOutputResampler::OpenStream() { | |
| output_params_ = GetFallbackHighLatencyOutputParams(original_output_params_); | |
| const std::string fallback_device_id = ""; | |
| dispatcher_ = MakeDispatcher(fallback_device_id, output_params_); | |
| - if (dispatcher_->OpenStream()) { | |
| + if (dispatcher_->OpenStream(AudioStreamMetadata())) { | |
| base::UmaHistogramEnumeration(kOpenLowLatencyHistogramName, | |
| OpenStreamResult::kFallbackToLinear); | |
| return true; | |
| @@ -387,7 +387,7 @@ bool AudioOutputResampler::OpenStream() { | |
| output_params_ = input_params_; | |
| output_params_.set_format(AudioParameters::AUDIO_FAKE); | |
| dispatcher_ = MakeDispatcher(device_id_, output_params_); | |
| - if (dispatcher_->OpenStream()) { | |
| + if (dispatcher_->OpenStream(AudioStreamMetadata())) { | |
| base::UmaHistogramEnumeration(kOpenLowLatencyHistogramName, | |
| OpenStreamResult::kFallbackToFake); | |
| return true; | |
| @@ -470,6 +470,22 @@ void AudioOutputResampler::FlushStream(AudioOutputProxy* stream_proxy) { | |
| dispatcher_->FlushStream(stream_proxy); | |
| } | |
| +void AudioOutputResampler::SetStreamLabel(AudioOutputProxy* stream_proxy, | |
| + const std::string& label) { | |
| + DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| + if (dispatcher_) | |
| + dispatcher_->SetStreamLabel(stream_proxy, label); | |
| +} | |
| + | |
| +void AudioOutputResampler::SetStreamAppIdentity( | |
| + AudioOutputProxy* stream_proxy, | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) { | |
| + DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| + if (dispatcher_) | |
| + dispatcher_->SetStreamAppIdentity(stream_proxy, app_name, xdg_icon_name); | |
| +} | |
| + | |
| void AudioOutputResampler::StopStreamInternal( | |
| const CallbackMap::value_type& item) { | |
| DCHECK(audio_manager()->GetTaskRunner()->BelongsToCurrentThread()); | |
| diff --git a/media/audio/audio_output_resampler.h b/media/audio/audio_output_resampler.h | |
| index 96aab4a95a..8141ff5275 100644 | |
| --- a/media/audio/audio_output_resampler.h | |
| +++ b/media/audio/audio_output_resampler.h | |
| @@ -51,13 +51,18 @@ class MEDIA_EXPORT AudioOutputResampler : public AudioOutputDispatcher { | |
| // AudioOutputDispatcher interface. | |
| AudioOutputProxy* CreateStreamProxy() override; | |
| - bool OpenStream() override; | |
| + bool OpenStream(const AudioStreamMetadata& metadata) override; | |
| bool StartStream(AudioOutputStream::AudioSourceCallback* callback, | |
| AudioOutputProxy* stream_proxy) override; | |
| void StopStream(AudioOutputProxy* stream_proxy) override; | |
| void StreamVolumeSet(AudioOutputProxy* stream_proxy, double volume) override; | |
| void CloseStream(AudioOutputProxy* stream_proxy) override; | |
| void FlushStream(AudioOutputProxy* stream_proxy) override; | |
| + void SetStreamLabel(AudioOutputProxy* stream_proxy, | |
| + const std::string& label) override; | |
| + void SetStreamAppIdentity(AudioOutputProxy* stream_proxy, | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) override; | |
| private: | |
| using CallbackMap = | |
| diff --git a/media/audio/audio_stream_metadata.h b/media/audio/audio_stream_metadata.h | |
| new file mode 100644 | |
| index 0000000000..3a55567527 | |
| --- /dev/null | |
| +++ b/media/audio/audio_stream_metadata.h | |
| @@ -0,0 +1,33 @@ | |
| +// Copyright 2025 The Chromium Authors | |
| +// Use of this source code is governed by a BSD-style license that can be | |
| +// found in the LICENSE file. | |
| + | |
| +#ifndef MEDIA_AUDIO_AUDIO_STREAM_METADATA_H_ | |
| +#define MEDIA_AUDIO_AUDIO_STREAM_METADATA_H_ | |
| + | |
| +#include <string> | |
| + | |
| +#include "media/base/media_export.h" | |
| + | |
| +namespace media { | |
| + | |
| +struct MEDIA_EXPORT AudioStreamMetadata { | |
| + // Tab/page title, used as the PulseAudio stream name. | |
| + std::string title; | |
| + | |
| + // Application name override (e.g. PWA name). | |
| + // Sets PA_PROP_APPLICATION_NAME on PulseAudio. | |
| + std::string app_name; | |
| + | |
| + // Freedesktop icon name for the application (Linux only). | |
| + // Sets PA_PROP_APPLICATION_ICON_NAME on PulseAudio. | |
| + std::string xdg_icon_name; | |
| + | |
| + bool empty() const { | |
| + return title.empty() && app_name.empty() && xdg_icon_name.empty(); | |
| + } | |
| +}; | |
| + | |
| +} // namespace media | |
| + | |
| +#endif // MEDIA_AUDIO_AUDIO_STREAM_METADATA_H_ | |
| diff --git a/media/audio/pulse/pulse.sigs b/media/audio/pulse/pulse.sigs | |
| index 2edcd1152f..b5a4c2d482 100644 | |
| --- a/media/audio/pulse/pulse.sigs | |
| +++ b/media/audio/pulse/pulse.sigs | |
| @@ -55,6 +55,7 @@ int pa_stream_peek(pa_stream* p, const void** data, size_t* nbytes); | |
| void pa_stream_set_read_callback(pa_stream* p, pa_stream_request_cb_t cb, void* userdata); | |
| void pa_stream_set_state_callback(pa_stream* s, pa_stream_notify_cb_t cb, void* userdata); | |
| int pa_stream_write(pa_stream* p, const void* data, size_t nbytes, pa_free_cb_t free_cb, int64_t offset, pa_seek_mode_t seek); | |
| +pa_operation* pa_stream_set_name(pa_stream *s, const char *name, pa_stream_success_cb_t cb, void *userdata); | |
| void pa_stream_set_write_callback(pa_stream *p, pa_stream_request_cb_t cb, void *userdata); | |
| void pa_stream_unref(pa_stream* s); | |
| int pa_context_errno(const_pa_context_ptr c); | |
| diff --git a/media/audio/pulse/pulse_output.cc b/media/audio/pulse/pulse_output.cc | |
| index 6783fd712b..f85d3cd86d 100644 | |
| --- a/media/audio/pulse/pulse_output.cc | |
| +++ b/media/audio/pulse/pulse_output.cc | |
| @@ -88,8 +88,7 @@ bool PulseAudioOutputStream::Open() { | |
| SendLogMessage("%s()", __func__); | |
| bool result = pulse::CreateOutputStream( | |
| &pa_mainloop_, &pa_context_, &pa_stream_, params_, device_id_, | |
| - AudioManager::GetGlobalAppName(), &StreamNotifyCallback, | |
| - &StreamRequestCallback, this); | |
| + pending_metadata_, &StreamNotifyCallback, &StreamRequestCallback, this); | |
| if (!result) { | |
| SendLogMessage("%s => (ERROR: failed to open PA stream)", __func__); | |
| } | |
| @@ -331,4 +330,27 @@ void PulseAudioOutputStream::GetVolume(double* volume) { | |
| *volume = volume_; | |
| } | |
| +void PulseAudioOutputStream::SetStreamLabel(const std::string& label) { | |
| + DCHECK(thread_checker_.CalledOnValidThread()); | |
| + | |
| + if (!pa_stream_) { | |
| + pending_metadata_.title = label; | |
| + return; | |
| + } | |
| + | |
| + AutoPulseLock auto_lock(pa_mainloop_); | |
| + pa_operation* operation = pa_stream_set_name( | |
| + pa_stream_, label.c_str(), &pulse::StreamSuccessCallback, pa_mainloop_); | |
| + WaitForOperationCompletion(pa_mainloop_, operation, pa_context_, pa_stream_); | |
| +} | |
| + | |
| +void PulseAudioOutputStream::SetStreamAppIdentity( | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) { | |
| + DCHECK(thread_checker_.CalledOnValidThread()); | |
| + DCHECK(!pa_stream_) << "App identity must be set before Open()"; | |
| + pending_metadata_.app_name = app_name; | |
| + pending_metadata_.xdg_icon_name = xdg_icon_name; | |
| +} | |
| + | |
| } // namespace media | |
| diff --git a/media/audio/pulse/pulse_output.h b/media/audio/pulse/pulse_output.h | |
| index 40ab823e38..bb9908c5bd 100644 | |
| --- a/media/audio/pulse/pulse_output.h | |
| +++ b/media/audio/pulse/pulse_output.h | |
| @@ -29,6 +29,7 @@ | |
| #include "base/threading/thread_checker.h" | |
| #include "media/audio/audio_io.h" | |
| #include "media/audio/audio_manager.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| #include "media/base/amplitude_peak_detector.h" | |
| #include "media/base/audio_parameters.h" | |
| @@ -59,6 +60,9 @@ class PulseAudioOutputStream : public AudioOutputStream { | |
| void Stop() override; | |
| void SetVolume(double volume) override; | |
| void GetVolume(double* volume) override; | |
| + void SetStreamLabel(const std::string& label) override; | |
| + void SetStreamAppIdentity(const std::string& app_name, | |
| + const std::string& xdg_icon_name) override; | |
| private: | |
| // Helper method used for sending native logs to the registered client. | |
| @@ -110,6 +114,9 @@ class PulseAudioOutputStream : public AudioOutputStream { | |
| AmplitudePeakDetector peak_detector_; | |
| + // Metadata set before Open() to be used during stream creation. | |
| + AudioStreamMetadata pending_metadata_; | |
| + | |
| base::ThreadChecker thread_checker_; | |
| }; | |
| diff --git a/media/audio/pulse/pulse_util.cc b/media/audio/pulse/pulse_util.cc | |
| index a08e42a464..e23ab68137 100644 | |
| --- a/media/audio/pulse/pulse_util.cc | |
| +++ b/media/audio/pulse/pulse_util.cc | |
| @@ -19,6 +19,7 @@ | |
| #include "base/synchronization/waitable_event.h" | |
| #include "build/branding_buildflags.h" | |
| #include "media/audio/audio_device_description.h" | |
| +#include "media/audio/audio_manager.h" | |
| #include "media/base/audio_timestamp_helper.h" | |
| #if defined(DLOPEN_PULSEAUDIO) | |
| @@ -514,7 +515,7 @@ bool CreateOutputStream(raw_ptr<pa_threaded_mainloop>* mainloop, | |
| raw_ptr<pa_stream>* stream, | |
| const AudioParameters& params, | |
| const std::string& device_id, | |
| - const std::string& app_name, | |
| + const AudioStreamMetadata& metadata, | |
| pa_stream_notify_cb_t stream_callback, | |
| pa_stream_request_cb_t write_callback, | |
| void* user_data) { | |
| @@ -525,8 +526,12 @@ bool CreateOutputStream(raw_ptr<pa_threaded_mainloop>* mainloop, | |
| RETURN_ON_FAILURE(*mainloop, "Failed to create PulseAudio main loop."); | |
| pa_mainloop_api* pa_mainloop_api = pa_threaded_mainloop_get_api(*mainloop); | |
| - *context = pa_context_new( | |
| - pa_mainloop_api, app_name.empty() ? PRODUCT_STRING : app_name.c_str()); | |
| + std::string context_name = metadata.app_name; | |
| + if (context_name.empty()) | |
| + context_name = AudioManager::GetGlobalAppName(); | |
| + if (context_name.empty()) | |
| + context_name = PRODUCT_STRING; | |
| + *context = pa_context_new(pa_mainloop_api, context_name.c_str()); | |
| RETURN_ON_FAILURE(*context, "Failed to create PulseAudio context."); | |
| // A state callback must be set before calling pa_threaded_mainloop_lock() or | |
| @@ -571,13 +576,21 @@ bool CreateOutputStream(raw_ptr<pa_threaded_mainloop>* mainloop, | |
| map = &source_channel_map; | |
| } | |
| - // Open playback stream and | |
| - // tell PulseAudio what the stream icon should be. | |
| + // Open playback stream with application identity and icon. | |
| + // Always set APPLICATION_NAME explicitly — pa_stream_new_with_proplist does | |
| + // not inherit it from the context, so omitting it results in an empty name. | |
| ScopedPropertyList property_list; | |
| + pa_proplist_sets(property_list.get(), PA_PROP_APPLICATION_NAME, | |
| + context_name.c_str()); | |
| pa_proplist_sets(property_list.get(), PA_PROP_APPLICATION_ICON_NAME, | |
| - kBrowserDisplayName); | |
| + metadata.xdg_icon_name.empty() | |
| + ? kBrowserDisplayName | |
| + : metadata.xdg_icon_name.c_str()); | |
| + const char* pa_stream_name = | |
| + metadata.title.empty() ? "Playback" : metadata.title.c_str(); | |
| *stream = pa_stream_new_with_proplist( | |
| - *context, "Playback", &sample_specifications, map, property_list.get()); | |
| + *context, pa_stream_name, &sample_specifications, map, | |
| + property_list.get()); | |
| RETURN_ON_FAILURE(*stream, "failed to create PA playback stream"); | |
| pa_stream_set_state_callback(*stream, stream_callback, user_data); | |
| diff --git a/media/audio/pulse/pulse_util.h b/media/audio/pulse/pulse_util.h | |
| index b11fba1e30..a04fdc4c11 100644 | |
| --- a/media/audio/pulse/pulse_util.h | |
| +++ b/media/audio/pulse/pulse_util.h | |
| @@ -12,6 +12,7 @@ | |
| #include "base/memory/raw_ptr.h" | |
| #include "base/time/time.h" | |
| #include "media/audio/audio_device_name.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| #include "media/base/audio_parameters.h" | |
| #include "media/base/channel_layout.h" | |
| @@ -93,7 +94,7 @@ bool CreateOutputStream(raw_ptr<pa_threaded_mainloop>* mainloop, | |
| raw_ptr<pa_stream>* stream, | |
| const AudioParameters& params, | |
| const std::string& device_id, | |
| - const std::string& app_name, | |
| + const AudioStreamMetadata& metadata, | |
| pa_stream_notify_cb_t stream_callback, | |
| pa_stream_request_cb_t write_callback, | |
| void* user_data); | |
| diff --git a/media/mojo/mojom/audio_stream_factory.mojom b/media/mojo/mojom/audio_stream_factory.mojom | |
| index e240fbade7..2bea1c30a4 100644 | |
| --- a/media/mojo/mojom/audio_stream_factory.mojom | |
| +++ b/media/mojo/mojom/audio_stream_factory.mojom | |
| @@ -13,6 +13,14 @@ import "media/mojo/mojom/audio_processing.mojom"; | |
| import "mojo/public/mojom/base/unguessable_token.mojom"; | |
| import "sandbox/policy/mojom/context.mojom"; | |
| +// Metadata for an audio output stream, set at creation time so that platform | |
| +// backends (e.g. PulseAudio) can use it during stream setup. | |
| +struct AudioStreamMetadata { | |
| + string title; // Tab/page title (stream name). | |
| + string app_name; // Application name override (e.g. PWA name). | |
| + string xdg_icon_name; // Freedesktop icon name (Linux only). | |
| +}; | |
| + | |
| // Mutes a group of AudioOutputStreams while at least one binding to an instance | |
| // exists. Once the last binding is dropped, all streams in the group are | |
| // un-muted. | |
| @@ -73,7 +81,8 @@ interface AudioStreamFactory { | |
| pending_associated_remote<media.mojom.AudioOutputStreamObserver>? observer, | |
| pending_remote<media.mojom.AudioLog>? log, | |
| string device_id, media.mojom.AudioParameters params, | |
| - mojo_base.mojom.UnguessableToken group_id) | |
| + mojo_base.mojom.UnguessableToken group_id, | |
| + AudioStreamMetadata metadata) | |
| => (media.mojom.ReadWriteAudioDataPipe? data_pipe); | |
| // Same as CreateOutputStream(), but the output device of the resulting | |
| @@ -84,7 +93,8 @@ interface AudioStreamFactory { | |
| pending_associated_remote<media.mojom.AudioOutputStreamObserver>? observer, | |
| pending_remote<media.mojom.AudioLog>? log, | |
| string device_id, media.mojom.AudioParameters params, | |
| - mojo_base.mojom.UnguessableToken group_id) | |
| + mojo_base.mojom.UnguessableToken group_id, | |
| + AudioStreamMetadata metadata) | |
| => (media.mojom.ReadWriteAudioDataPipe? data_pipe); | |
| // Binds the request to the LocalMuter associated with the given |group_id|. | |
| @@ -99,6 +109,11 @@ interface AudioStreamFactory { | |
| BindMuter(pending_associated_receiver<LocalMuter> receiver, | |
| mojo_base.mojom.UnguessableToken group_id); | |
| + // Updates the stream label (e.g. tab title shown in PulseAudio) for all | |
| + // output streams belonging to the given |group_id|. | |
| + SetStreamLabelForGroup(mojo_base.mojom.UnguessableToken group_id, | |
| + string label); | |
| + | |
| // Creates an AudioInputStream that provides the result of looping-back and | |
| // mixing-together all current and future AudioOutputStreams tagged with the | |
| // given |group_id|. The loopback re-mixes audio, if necessary, so that the | |
| diff --git a/services/audio/device_listener_output_stream.cc b/services/audio/device_listener_output_stream.cc | |
| index 784fc172fa..df2335c6c3 100644 | |
| --- a/services/audio/device_listener_output_stream.cc | |
| +++ b/services/audio/device_listener_output_stream.cc | |
| @@ -72,6 +72,16 @@ void DeviceListenerOutputStream::Flush() { | |
| stream_->Flush(); | |
| } | |
| +void DeviceListenerOutputStream::SetStreamLabel(const std::string& label) { | |
| + stream_->SetStreamLabel(label); | |
| +} | |
| + | |
| +void DeviceListenerOutputStream::SetStreamAppIdentity( | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) { | |
| + stream_->SetStreamAppIdentity(app_name, xdg_icon_name); | |
| +} | |
| + | |
| void DeviceListenerOutputStream::OnDeviceChange() { | |
| DCHECK(task_runner_->BelongsToCurrentThread()); | |
| std::move(on_device_change_callback_).Run(); | |
| diff --git a/services/audio/device_listener_output_stream.h b/services/audio/device_listener_output_stream.h | |
| index e2ca6056b8..af1b6986d4 100644 | |
| --- a/services/audio/device_listener_output_stream.h | |
| +++ b/services/audio/device_listener_output_stream.h | |
| @@ -43,6 +43,9 @@ class DeviceListenerOutputStream final | |
| void GetVolume(double* volume) final; | |
| void Close() final; | |
| void Flush() final; | |
| + void SetStreamLabel(const std::string& label) final; | |
| + void SetStreamAppIdentity(const std::string& app_name, | |
| + const std::string& xdg_icon_name) final; | |
| private: | |
| ~DeviceListenerOutputStream() final; | |
| diff --git a/services/audio/output_controller.cc b/services/audio/output_controller.cc | |
| index 1acd08a61a..b7ab982c0a 100644 | |
| --- a/services/audio/output_controller.cc | |
| +++ b/services/audio/output_controller.cc | |
| @@ -297,6 +297,13 @@ void OutputController::RecreateStream(OutputController::RecreateReason reason) { | |
| return; | |
| } | |
| + // Set metadata before Open() so that the platform backend (e.g. PulseAudio) | |
| + // can use it during stream creation for correct rule matching. | |
| + if (!metadata_.title.empty()) | |
| + stream_->SetStreamLabel(metadata_.title); | |
| + if (!metadata_.app_name.empty()) | |
| + stream_->SetStreamAppIdentity(metadata_.app_name, metadata_.xdg_icon_name); | |
| + | |
| if (!stream_->Open()) { | |
| SendLogMessage("%s => (ERROR: failed to open the created output stream)", | |
| __func__); | |
| @@ -449,6 +456,19 @@ void OutputController::SetVolume(double volume) { | |
| } | |
| } | |
| +void OutputController::SetStreamMetadata( | |
| + const media::AudioStreamMetadata& metadata) { | |
| + DCHECK(task_runner_->BelongsToCurrentThread()); | |
| + metadata_ = metadata; | |
| +} | |
| + | |
| +void OutputController::SetStreamLabel(const std::string& label) { | |
| + DCHECK(task_runner_->BelongsToCurrentThread()); | |
| + metadata_.title = label; | |
| + if (stream_) | |
| + stream_->SetStreamLabel(label); | |
| +} | |
| + | |
| int OutputController::OnMoreData(base::TimeDelta delay, | |
| base::TimeTicks delay_timestamp, | |
| const media::AudioGlitchInfo& glitch_info, | |
| diff --git a/services/audio/output_controller.h b/services/audio/output_controller.h | |
| index d43f09fc63..4ffa1a328d 100644 | |
| --- a/services/audio/output_controller.h | |
| +++ b/services/audio/output_controller.h | |
| @@ -25,6 +25,7 @@ | |
| #include "base/unguessable_token.h" | |
| #include "media/audio/audio_io.h" | |
| #include "media/audio/audio_manager.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| #include "media/base/audio_power_monitor.h" | |
| #include "services/audio/loopback_source.h" | |
| @@ -166,6 +167,13 @@ class OutputController : public media::AudioOutputStream::AudioSourceCallback, | |
| // Sets the volume of the audio output stream. | |
| void SetVolume(double volume); | |
| + // Sets stream metadata (title, app name, icon) that will be applied to the | |
| + // stream at creation time. Must be called before CreateStream(). | |
| + void SetStreamMetadata(const media::AudioStreamMetadata& metadata); | |
| + | |
| + // Updates the stream label (e.g. tab title) on the underlying stream. | |
| + void SetStreamLabel(const std::string& label); | |
| + | |
| // AudioSourceCallback implementation. | |
| int OnMoreData(base::TimeDelta delay, | |
| base::TimeTicks delay_timestamp, | |
| @@ -326,6 +334,10 @@ class OutputController : public media::AudioOutputStream::AudioSourceCallback, | |
| // `SwitchAudioOutputDeviceId()`, which will recreate the stream. | |
| std::string output_device_id_; | |
| + // Metadata for the audio stream (title, app name, icon), applied to the | |
| + // platform stream before Open() so that PulseAudio rules see correct values. | |
| + media::AudioStreamMetadata metadata_; | |
| + | |
| raw_ptr<media::AudioOutputStream, DanglingUntriaged> stream_; | |
| // When true, local audio output should be muted; either by having audio | |
| diff --git a/services/audio/output_device_mixer_impl.cc b/services/audio/output_device_mixer_impl.cc | |
| index 32d79c8d4f..d2c9cf44a0 100644 | |
| --- a/services/audio/output_device_mixer_impl.cc | |
| +++ b/services/audio/output_device_mixer_impl.cc | |
| @@ -119,7 +119,7 @@ class OutputDeviceMixerImpl::MixTrack final | |
| static_cast<void*>(this)); | |
| DCHECK(!rendering_stream_); | |
| rendering_stream_.reset( | |
| - mixer_->CreateAndOpenDeviceStream(graph_input_->GetParams())); | |
| + mixer_->CreateAndOpenDeviceStream(graph_input_->GetParams(), metadata_)); | |
| if (rendering_stream_) { | |
| rendering_stream_->SetVolume(volume_); | |
| @@ -175,6 +175,20 @@ class OutputDeviceMixerImpl::MixTrack final | |
| audio_source_callback_->OnError(ErrorType::kUnknown); | |
| } | |
| + void SetStreamLabel(const std::string& label) { | |
| + metadata_.title = label; | |
| + if (rendering_stream_) | |
| + rendering_stream_->SetStreamLabel(label); | |
| + } | |
| + | |
| + void SetStreamAppIdentity(const std::string& app_name, | |
| + const std::string& xdg_icon_name) { | |
| + metadata_.app_name = app_name; | |
| + metadata_.xdg_icon_name = xdg_icon_name; | |
| + if (rendering_stream_) | |
| + rendering_stream_->SetStreamAppIdentity(app_name, xdg_icon_name); | |
| + } | |
| + | |
| void OnDeviceChange() { | |
| TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"), "MixTrack::OnDeviceChange", | |
| "this", static_cast<void*>(this)); | |
| @@ -201,6 +215,7 @@ class OutputDeviceMixerImpl::MixTrack final | |
| } | |
| double volume_ = kDefaultVolume; | |
| + media::AudioStreamMetadata metadata_; | |
| const raw_ptr<OutputDeviceMixerImpl> mixer_; | |
| @@ -307,6 +322,19 @@ class OutputDeviceMixerImpl::MixableOutputStream final | |
| void Flush() final {} | |
| + void SetStreamLabel(const std::string& label) final { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| + if (mixer_) | |
| + mixer_->SetStreamLabel(mix_track_, label); | |
| + } | |
| + | |
| + void SetStreamAppIdentity(const std::string& app_name, | |
| + const std::string& xdg_icon_name) final { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| + if (mixer_) | |
| + mixer_->SetStreamAppIdentity(mix_track_, app_name, xdg_icon_name); | |
| + } | |
| + | |
| private: | |
| SEQUENCE_CHECKER(owning_sequence_); | |
| // OutputDeviceMixerImpl will release all the resources and invalidate its | |
| @@ -616,8 +644,29 @@ void OutputDeviceMixerImpl::CloseStream(MixTrack* mix_track) { | |
| mix_tracks_.erase(iter); | |
| } | |
| +void OutputDeviceMixerImpl::SetStreamLabel(MixTrack* mix_track, | |
| + const std::string& label) { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| + // Forward to the track's independent rendering stream. | |
| + mix_track->SetStreamLabel(label); | |
| + // Also forward to the shared mixing output stream if mixing is active. | |
| + if (mixing_graph_output_stream_) | |
| + mixing_graph_output_stream_->SetStreamLabel(label); | |
| +} | |
| + | |
| +void OutputDeviceMixerImpl::SetStreamAppIdentity( | |
| + MixTrack* mix_track, | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name) { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| + mix_track->SetStreamAppIdentity(app_name, xdg_icon_name); | |
| + if (mixing_graph_output_stream_) | |
| + mixing_graph_output_stream_->SetStreamAppIdentity(app_name, xdg_icon_name); | |
| +} | |
| + | |
| media::AudioOutputStream* OutputDeviceMixerImpl::CreateAndOpenDeviceStream( | |
| - const media::AudioParameters& params) { | |
| + const media::AudioParameters& params, | |
| + const media::AudioStreamMetadata& metadata) { | |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| DCHECK(params.IsValid()); | |
| DCHECK_EQ(params.format(), media::AudioParameters::AUDIO_PCM_LOW_LATENCY); | |
| @@ -627,6 +676,11 @@ media::AudioOutputStream* OutputDeviceMixerImpl::CreateAndOpenDeviceStream( | |
| if (!stream) | |
| return nullptr; | |
| + if (!metadata.title.empty()) | |
| + stream->SetStreamLabel(metadata.title); | |
| + if (!metadata.app_name.empty()) | |
| + stream->SetStreamAppIdentity(metadata.app_name, metadata.xdg_icon_name); | |
| + | |
| if (!stream->Open()) { | |
| LOG(ERROR) << "Failed to open stream"; | |
| stream->Close(); | |
| diff --git a/services/audio/output_device_mixer_impl.h b/services/audio/output_device_mixer_impl.h | |
| index 0702826e2f..0b6eb070a6 100644 | |
| --- a/services/audio/output_device_mixer_impl.h | |
| +++ b/services/audio/output_device_mixer_impl.h | |
| @@ -18,6 +18,7 @@ | |
| #include "base/synchronization/lock.h" | |
| #include "base/timer/timer.h" | |
| #include "media/audio/audio_io.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| #include "media/base/audio_parameters.h" | |
| #include "media/base/reentrancy_checker.h" | |
| #include "services/audio/mixing_graph.h" | |
| @@ -110,10 +111,15 @@ class OutputDeviceMixerImpl final : public OutputDeviceMixer { | |
| media::AudioOutputStream::AudioSourceCallback* callback); | |
| void StopStream(MixTrack* mix_track); | |
| void CloseStream(MixTrack* mix_track); | |
| + void SetStreamLabel(MixTrack* mix_track, const std::string& label); | |
| + void SetStreamAppIdentity(MixTrack* mix_track, | |
| + const std::string& app_name, | |
| + const std::string& xdg_icon_name); | |
| // Helper to create physical audio streams. | |
| media::AudioOutputStream* CreateAndOpenDeviceStream( | |
| - const media::AudioParameters& params); | |
| + const media::AudioParameters& params, | |
| + const media::AudioStreamMetadata& metadata = media::AudioStreamMetadata()); | |
| // Delivers audio to listeners; provided as a callback to MixingGraph. | |
| void BroadcastToListeners(const media::AudioBus& audio_bus, | |
| diff --git a/services/audio/output_stream.cc b/services/audio/output_stream.cc | |
| index 5223fd57da..8b7145db2f 100644 | |
| --- a/services/audio/output_stream.cc | |
| +++ b/services/audio/output_stream.cc | |
| @@ -173,7 +173,8 @@ OutputStream::OutputStream( | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| LoopbackCoordinator* coordinator, | |
| - const base::UnguessableToken& loopback_group_id) | |
| + const base::UnguessableToken& loopback_group_id, | |
| + const media::AudioStreamMetadata& metadata) | |
| : delete_callback_(std::move(delete_callback)), | |
| receiver_(this, std::move(stream_receiver)), | |
| device_switch_receiver_(this, std::move(device_switch_receiver)), | |
| @@ -222,6 +223,7 @@ OutputStream::OutputStream( | |
| log_->OnCreated(params, output_device_id); | |
| coordinator_->AddMember(loopback_group_id_, &controller_); | |
| + controller_.SetStreamMetadata(metadata); | |
| if (!reader_.IsValid() || !controller_.CreateStream()) { | |
| // Either SyncReader initialization failed or the controller failed to | |
| // create the stream. In the latter case, the controller will have called | |
| @@ -305,6 +307,12 @@ void OutputStream::SetVolume(double volume) { | |
| log_->OnSetVolume(volume); | |
| } | |
| + | |
| +void OutputStream::SetStreamLabel(const std::string& label) { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| + controller_.SetStreamLabel(label); | |
| +} | |
| + | |
| void OutputStream::SwitchAudioOutputDeviceId( | |
| const std::string& output_device_id) { | |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| diff --git a/services/audio/output_stream.h b/services/audio/output_stream.h | |
| index 8d2470519f..a391b02e51 100644 | |
| --- a/services/audio/output_stream.h | |
| +++ b/services/audio/output_stream.h | |
| @@ -27,6 +27,7 @@ | |
| #include "mojo/public/cpp/system/handle.h" | |
| #include "mojo/public/cpp/system/platform_handle.h" | |
| #include "services/audio/loopback_coordinator.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| #include "services/audio/output_controller.h" | |
| #include "services/audio/sync_reader.h" | |
| @@ -65,7 +66,8 @@ class OutputStream final : public media::mojom::AudioOutputStream, | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| LoopbackCoordinator* coordinator, | |
| - const base::UnguessableToken& loopback_group_id); | |
| + const base::UnguessableToken& loopback_group_id, | |
| + const media::AudioStreamMetadata& metadata); | |
| OutputStream(const OutputStream&) = delete; | |
| OutputStream& operator=(const OutputStream&) = delete; | |
| @@ -81,6 +83,12 @@ class OutputStream final : public media::mojom::AudioOutputStream, | |
| // media::mojom::DeviceSwitchInterface implementation. | |
| void SwitchAudioOutputDeviceId(const std::string& output_device_id) final; | |
| + const base::UnguessableToken& loopback_group_id() const { | |
| + return loopback_group_id_; | |
| + } | |
| + | |
| + void SetStreamLabel(const std::string& label); | |
| + | |
| // OutputController::EventHandler implementation. | |
| void OnControllerPlaying() final; | |
| void OnControllerPaused() final; | |
| diff --git a/services/audio/output_stream_unittest.cc b/services/audio/output_stream_unittest.cc | |
| index b5a9d4a4fc..26d7b08ce2 100644 | |
| --- a/services/audio/output_stream_unittest.cc | |
| +++ b/services/audio/output_stream_unittest.cc | |
| @@ -158,7 +158,8 @@ class TestEnvironment { | |
| remote_stream.InitWithNewPipeAndPassReceiver(), observer_.MakeRemote(), | |
| log_.MakeRemote(), "", | |
| media::AudioParameters::UnavailableDeviceParams(), | |
| - base::UnguessableToken::Create(), created_callback_.Get()); | |
| + base::UnguessableToken::Create(), | |
| + media::mojom::AudioStreamMetadata::New(), created_callback_.Get()); | |
| return remote_stream; | |
| } | |
| @@ -169,7 +170,8 @@ class TestEnvironment { | |
| remote_stream.InitWithNewPipeAndPassReceiver(), | |
| mojo::NullAssociatedRemote(), log_.MakeRemote(), "", | |
| media::AudioParameters::UnavailableDeviceParams(), | |
| - base::UnguessableToken::Create(), created_callback_.Get()); | |
| + base::UnguessableToken::Create(), | |
| + media::mojom::AudioStreamMetadata::New(), created_callback_.Get()); | |
| return remote_stream; | |
| } | |
| @@ -180,7 +182,8 @@ class TestEnvironment { | |
| remote_stream.InitWithNewPipeAndPassReceiver(), observer_.MakeRemote(), | |
| mojo::NullRemote(), "", | |
| media::AudioParameters::UnavailableDeviceParams(), | |
| - base::UnguessableToken::Create(), created_callback_.Get()); | |
| + base::UnguessableToken::Create(), | |
| + media::mojom::AudioStreamMetadata::New(), created_callback_.Get()); | |
| return remote_stream; | |
| } | |
| @@ -192,7 +195,8 @@ class TestEnvironment { | |
| device_switch_interface_.BindNewPipeAndPassReceiver(), | |
| observer_.MakeRemote(), log_.MakeRemote(), device_id, | |
| media::AudioParameters::UnavailableDeviceParams(), | |
| - base::UnguessableToken::Create(), created_callback_.Get()); | |
| + base::UnguessableToken::Create(), | |
| + media::mojom::AudioStreamMetadata::New(), created_callback_.Get()); | |
| return remote_stream; | |
| } | |
| diff --git a/services/audio/public/cpp/fake_stream_factory.h b/services/audio/public/cpp/fake_stream_factory.h | |
| index 6ac239329a..c68050a633 100644 | |
| --- a/services/audio/public/cpp/fake_stream_factory.h | |
| +++ b/services/audio/public/cpp/fake_stream_factory.h | |
| @@ -70,6 +70,7 @@ class FakeStreamFactory : public media::mojom::AudioStreamFactory { | |
| const std::string& device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) override {} | |
| void CreateSwitchableOutputStream( | |
| mojo::PendingReceiver<media::mojom::AudioOutputStream> stream_receiver, | |
| @@ -81,6 +82,7 @@ class FakeStreamFactory : public media::mojom::AudioStreamFactory { | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) override {} | |
| void BindMuter( | |
| mojo::PendingAssociatedReceiver<media::mojom::LocalMuter> receiver, | |
| diff --git a/services/audio/public/cpp/output_device.cc b/services/audio/public/cpp/output_device.cc | |
| index 8670782afe..61a0c6acc7 100644 | |
| --- a/services/audio/public/cpp/output_device.cc | |
| +++ b/services/audio/public/cpp/output_device.cc | |
| @@ -31,6 +31,7 @@ OutputDevice::OutputDevice( | |
| stream_factory_->CreateOutputStream( | |
| stream_.BindNewPipeAndPassReceiver(), mojo::NullAssociatedRemote(), | |
| mojo::NullRemote(), device_id, params, base::UnguessableToken::Create(), | |
| + media::mojom::AudioStreamMetadata::New(), | |
| base::BindOnce(&OutputDevice::StreamCreated, weak_factory_.GetWeakPtr())); | |
| stream_.set_disconnect_handler(base::BindOnce( | |
| &OutputDevice::OnConnectionError, weak_factory_.GetWeakPtr())); | |
| diff --git a/services/audio/public/cpp/output_device_unittest.cc b/services/audio/public/cpp/output_device_unittest.cc | |
| index f0e668fd4a..3cea21b6a4 100644 | |
| --- a/services/audio/public/cpp/output_device_unittest.cc | |
| +++ b/services/audio/public/cpp/output_device_unittest.cc | |
| @@ -110,6 +110,7 @@ class FakeOutputStreamFactory final : public audio::FakeStreamFactory { | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) final { | |
| EXPECT_FALSE(observer); | |
| EXPECT_FALSE(log); | |
| diff --git a/services/audio/stream_factory.cc b/services/audio/stream_factory.cc | |
| index 940d248ba0..ab9e88eee9 100644 | |
| --- a/services/audio/stream_factory.cc | |
| +++ b/services/audio/stream_factory.cc | |
| @@ -16,6 +16,7 @@ | |
| #include "base/unguessable_token.h" | |
| #include "build/chromecast_buildflags.h" | |
| #include "media/audio/audio_device_description.h" | |
| +#include "media/audio/audio_stream_metadata.h" | |
| #include "media/base/media_switches.h" | |
| #include "services/audio/input_stream.h" | |
| #include "services/audio/local_muter.h" | |
| @@ -165,6 +166,7 @@ void StreamFactory::CreateOutputStream( | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) { | |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| TRACE_EVENT_INSTANT( | |
| @@ -174,7 +176,7 @@ void StreamFactory::CreateOutputStream( | |
| CreateOutputStreamInternal(std::move(stream_receiver), mojo::NullReceiver(), | |
| std::move(observer), std::move(log), | |
| output_device_id, params, group_id, | |
| - std::move(created_callback)); | |
| + std::move(metadata), std::move(created_callback)); | |
| } | |
| void StreamFactory::CreateSwitchableOutputStream( | |
| @@ -187,6 +189,7 @@ void StreamFactory::CreateSwitchableOutputStream( | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) { | |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| TRACE_EVENT_INSTANT("audio", "CreateSwitchableOutputStream", | |
| @@ -198,7 +201,7 @@ void StreamFactory::CreateSwitchableOutputStream( | |
| CreateOutputStreamInternal( | |
| std::move(stream_receiver), std::move(device_switch_receiver), | |
| std::move(observer), std::move(log), output_device_id, params, group_id, | |
| - std::move(created_callback)); | |
| + std::move(metadata), std::move(created_callback)); | |
| } | |
| void StreamFactory::BindMuter( | |
| @@ -343,6 +346,7 @@ void StreamFactory::CreateOutputStreamInternal( | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) { | |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| TRACE_EVENT_INSTANT( | |
| @@ -387,12 +391,30 @@ void StreamFactory::CreateOutputStreamInternal( | |
| } | |
| #endif | |
| + media::AudioStreamMetadata stream_metadata; | |
| + if (metadata) { | |
| + stream_metadata.title = std::move(metadata->title); | |
| + stream_metadata.app_name = std::move(metadata->app_name); | |
| + stream_metadata.xdg_icon_name = std::move(metadata->xdg_icon_name); | |
| + } | |
| + | |
| output_streams_.insert(std::make_unique<OutputStream>( | |
| std::move(created_callback), std::move(deleter_callback), | |
| std::move(managed_device_output_stream_create_callback), | |
| std::move(stream_receiver), std::move(device_switch_receiver), | |
| std::move(observer), std::move(log), audio_manager_, | |
| - device_id_or_group_id, params, &coordinator_, group_id)); | |
| + device_id_or_group_id, params, &coordinator_, group_id, | |
| + stream_metadata)); | |
| +} | |
| + | |
| +void StreamFactory::SetStreamLabelForGroup( | |
| + const base::UnguessableToken& group_id, | |
| + const std::string& label) { | |
| + DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); | |
| + for (const auto& stream : output_streams_) { | |
| + if (stream->loopback_group_id() == group_id) | |
| + stream->SetStreamLabel(label); | |
| + } | |
| } | |
| #if BUILDFLAG(CHROME_WIDE_ECHO_CANCELLATION) | |
| diff --git a/services/audio/stream_factory.h b/services/audio/stream_factory.h | |
| index fb60e68e73..ab50a87d62 100644 | |
| --- a/services/audio/stream_factory.h | |
| +++ b/services/audio/stream_factory.h | |
| @@ -94,6 +94,7 @@ class StreamFactory final : public media::mojom::AudioStreamFactory { | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) final; | |
| void CreateSwitchableOutputStream( | |
| mojo::PendingReceiver<media::mojom::AudioOutputStream> receiver, | |
| @@ -105,7 +106,10 @@ class StreamFactory final : public media::mojom::AudioStreamFactory { | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback) final; | |
| + void SetStreamLabelForGroup(const base::UnguessableToken& group_id, | |
| + const std::string& label) final; | |
| void BindMuter( | |
| mojo::PendingAssociatedReceiver<media::mojom::LocalMuter> receiver, | |
| const base::UnguessableToken& group_id) final; | |
| @@ -143,6 +147,7 @@ class StreamFactory final : public media::mojom::AudioStreamFactory { | |
| const std::string& output_device_id, | |
| const media::AudioParameters& params, | |
| const base::UnguessableToken& group_id, | |
| + media::mojom::AudioStreamMetadataPtr metadata, | |
| CreateOutputStreamCallback created_callback); | |
| SEQUENCE_CHECKER(owning_sequence_); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/chrome/browser/ui/browser_navigator.cc b/chrome/browser/ui/browser_navigator.cc | |
| index afd65467c7..d4ee2a6f5c 100644 | |
| --- a/chrome/browser/ui/browser_navigator.cc | |
| +++ b/chrome/browser/ui/browser_navigator.cc | |
| @@ -657,6 +657,17 @@ base::WeakPtr<content::NavigationHandle> Navigate(NavigateParams* params) { | |
| params->browser = override_params->browser(); | |
| singleton_index = override_params->tab_index().value_or(-1); | |
| } else { | |
| + // If the navigation originates from a web app window and the URL is outside | |
| + // the app's scope, open it in the OS default browser via xdg-open (or | |
| + // platform equivalent) instead of in a Chromium window. | |
| + if (source_browser && source_browser->is_type_app() && | |
| + source_browser->app_controller() && | |
| + !source_browser->app_controller()->IsUrlInAppScope(params->url) && | |
| + params->url.SchemeIsHTTPOrHTTPS()) { | |
| + platform_util::OpenExternal(params->url); | |
| + return nullptr; | |
| + } | |
| + | |
| std::tuple<BrowserWindowInterface*, int> browser_and_index = | |
| GetBrowserAndTabForDisposition(*params); | |
| params->browser = |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment