Skip to content

Instantly share code, notes, and snippets.

@SteffenDE
Created March 9, 2026 11:02
Show Gist options
  • Select an option

  • Save SteffenDE/e804240076aa6a173abe8a774250a5ce to your computer and use it in GitHub Desktop.

Select an option

Save SteffenDE/e804240076aa6a173abe8a774250a5ce to your computer and use it in GitHub Desktop.
Chrome PulseAudio web app + title integration, use default browser in web apps
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_);
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