From d44d2b8ed48ad3c8b901ccd521d65a3e50ea6cdb Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Wed, 17 Jun 2026 18:12:58 +0200 Subject: [PATCH 1/6] media: Add RendererType::kUrlPlayer and buildflag Add use_starboard_url_player GN capability flag, derived from use_starboard_media and tvOS platform detection. Expose it as BUILDFLAG(USE_STARBOARD_URL_PLAYER) through build_config.h and register matching Mojo enabled_features entry. Add RendererType::kUrlPlayer = 13 to the C++ enum, Mojom enum, traits mapping, and histogram. The new enum is gated behind USE_STARBOARD_URL_PLAYER so non-tvOS builds are unaffected. This establishes the capability boundary for all subsequent URL-player CLs. Bug: 512045535 --- build/build_config.h | 6 ++++++ cobalt/build/configs/BUILD.gn | 8 ++++++++ media/base/renderer.cc | 8 +++++++- media/base/renderer.h | 10 ++++++++-- media/mojo/mojom/BUILD.gn | 4 ++++ media/mojo/mojom/media_types.mojom | 2 ++ media/mojo/mojom/media_types_enum_mojom_traits.h | 9 +++++++++ starboard/build/buildflags.gni | 7 +++++++ tools/metrics/histograms/enums.xml | 2 ++ 9 files changed, 53 insertions(+), 3 deletions(-) diff --git a/build/build_config.h b/build/build_config.h index d098eba7d4bd..8d420be31d5a 100644 --- a/build/build_config.h +++ b/build/build_config.h @@ -310,6 +310,12 @@ #define BUILDFLAG_INTERNAL_USE_STARBOARD_MEDIA() (0) #endif +#if defined(ENABLE_BUILDFLAG_USE_STARBOARD_URL_PLAYER) +#define BUILDFLAG_INTERNAL_USE_STARBOARD_URL_PLAYER() (1) +#else +#define BUILDFLAG_INTERNAL_USE_STARBOARD_URL_PLAYER() (0) +#endif + #if defined(ENABLE_BUILDFLAG_USE_EVERGREEN) #define BUILDFLAG_INTERNAL_USE_EVERGREEN() (1) #else diff --git a/cobalt/build/configs/BUILD.gn b/cobalt/build/configs/BUILD.gn index b1d06843dd4e..a359e742d09e 100644 --- a/cobalt/build/configs/BUILD.gn +++ b/cobalt/build/configs/BUILD.gn @@ -111,6 +111,14 @@ config("buildflag_defines") { defines += [ "ENABLE_BUILDFLAG_USE_STARBOARD_MEDIA" ] } + # use_starboard_url_player and BUILDFLAG(USE_STARBOARD_URL_PLAYER) should be + # used to wrap URL-based player functionality (e.g., HLS via AVPlayer on + # tvOS). This is a derived capability that is true only for tvOS builds with + # Starboard media enabled. + if (use_starboard_url_player) { + defines += [ "ENABLE_BUILDFLAG_USE_STARBOARD_URL_PLAYER" ] + } + # use_evergreen and BUILDFLAG(USE_EVERGREEN) should be used for # Evergreen-specific functionality, i.e. the Evergreen loader_app. Do not use # this flag for hermetic build functionality, instead use diff --git a/media/base/renderer.cc b/media/base/renderer.cc index bfa21ffc7cc8..51c40fb39b32 100644 --- a/media/base/renderer.cc +++ b/media/base/renderer.cc @@ -3,7 +3,9 @@ // found in the LICENSE file. #include "media/base/renderer.h" + #include "base/logging.h" +#include "build/build_config.h" namespace media { @@ -34,7 +36,11 @@ std::string GetRendererName(RendererType renderer_type) { #if BUILDFLAG(USE_STARBOARD_MEDIA) case RendererType::kStarboard: return "StarboardRenderer"; -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + case RendererType::kUrlPlayer: + return "UrlPlayerRenderer"; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) } } diff --git a/media/base/renderer.h b/media/base/renderer.h index cdb63a54ac74..6dbe77c31fda 100644 --- a/media/base/renderer.h +++ b/media/base/renderer.h @@ -9,6 +9,7 @@ #include "base/functional/callback.h" #include "base/time/time.h" +#include "build/build_config.h" #include "media/base/buffering_state.h" #include "media/base/demuxer_stream.h" #include "media/base/media_export.h" @@ -39,10 +40,15 @@ enum class RendererType { kTest = 11, // Renderer implementations used in tests #if BUILDFLAG(USE_STARBOARD_MEDIA) kStarboard = 12, // StarboardRendererFactory +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + kUrlPlayer = 13, // UrlPlayerRendererClientFactory + kMaxValue = kUrlPlayer, +#elif BUILDFLAG(USE_STARBOARD_MEDIA) kMaxValue = kStarboard, -#else // BUILDFLAG(USE_STARBOARD_MEDIA) +#else kMaxValue = kTest, -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) }; // Get the name of the Renderer for `renderer_type`. The returned name could be diff --git a/media/mojo/mojom/BUILD.gn b/media/mojo/mojom/BUILD.gn index c15765653f54..0a97c6e23e0f 100644 --- a/media/mojo/mojom/BUILD.gn +++ b/media/mojo/mojom/BUILD.gn @@ -150,6 +150,10 @@ mojom("mojom") { enabled_features += [ "use_starboard_media" ] } + if (is_cobalt && use_starboard_url_player) { + enabled_features += [ "use_starboard_url_player" ] + } + shared_typemaps = [ { types = [ diff --git a/media/mojo/mojom/media_types.mojom b/media/mojo/mojom/media_types.mojom index d649d9e890a2..0621d33fbef5 100644 --- a/media/mojo/mojom/media_types.mojom +++ b/media/mojo/mojom/media_types.mojom @@ -650,6 +650,8 @@ enum RendererType { kTest= 11, // Renderer implementations used in tests [EnableIf=use_starboard_media] kStarboard = 12, // StarboardRendererFactory + [EnableIf=use_starboard_url_player] + kUrlPlayer = 13, // UrlPlayerRendererClientFactory }; // See media/base/demuxer.h for description. diff --git a/media/mojo/mojom/media_types_enum_mojom_traits.h b/media/mojo/mojom/media_types_enum_mojom_traits.h index 20563e88d10e..60d99fa84231 100644 --- a/media/mojo/mojom/media_types_enum_mojom_traits.h +++ b/media/mojo/mojom/media_types_enum_mojom_traits.h @@ -391,6 +391,10 @@ struct EnumTraits { #if BUILDFLAG(USE_STARBOARD_MEDIA) case ::media::RendererType::kStarboard: return media::mojom::RendererType::kStarboard; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + case ::media::RendererType::kUrlPlayer: + return media::mojom::RendererType::kUrlPlayer; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) #endif // BUILDFLAG(USE_STARBOARD_MEDIA) } @@ -436,6 +440,11 @@ struct EnumTraits { case media::mojom::RendererType::kStarboard: *output = ::media::RendererType::kStarboard; return true; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + case media::mojom::RendererType::kUrlPlayer: + *output = ::media::RendererType::kUrlPlayer; + return true; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) #endif // BUILDFLAG(USE_STARBOARD_MEDIA) } diff --git a/starboard/build/buildflags.gni b/starboard/build/buildflags.gni index 0ce582db6d3e..04c2d3ae5fe5 100644 --- a/starboard/build/buildflags.gni +++ b/starboard/build/buildflags.gni @@ -23,3 +23,10 @@ declare_args() { is_cobalt && current_toolchain != host_toolchain && (!is_starboard || current_toolchain != v8_snapshot_toolchain) } + +if (is_ios) { + import("//build/config/apple/mobile_config.gni") + use_starboard_url_player = use_starboard_media && target_platform == "tvos" +} else { + use_starboard_url_player = false +} diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index 0c8b02df7e97..31be098edba6 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -19946,6 +19946,8 @@ from previous Chrome versions. + + From 30d42231fd7b0877e12b130b511cd10e65d76e50 Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Wed, 17 Jun 2026 18:34:10 +0200 Subject: [PATCH 2/6] media: Add InitializeWithUrl transport and URL player renderer Add mojom::Renderer::InitializeWithUrl(client, source_url) to deliver the URL on the renderer Mojo pipe, keeping URL delivery and initialization ordered on the same pipe. Add UrlPlayerRenderer for tvOS URL-based playback via SbPlayerBridge and UrlPlayerRendererWrapper as the Mojo service bridge. The wrapper's InitializeWithUrl() sets the URL on the renderer internally then calls Initialize(). MojoRendererService validates the renderer type and URL before forwarding via static_cast, guarded by GetRendererType() == kUrlPlayer and ReportBadMessage(). Bug: 512045535 --- media/mojo/clients/mojo_renderer.cc | 26 + media/mojo/clients/mojo_renderer.h | 7 + media/mojo/clients/mojo_renderer_wrapper.cc | 11 + media/mojo/clients/mojo_renderer_wrapper.h | 7 + .../starboard_renderer_client_unittest.cc | 8 + media/mojo/mojom/renderer.mojom | 8 + media/mojo/services/BUILD.gn | 10 + media/mojo/services/mojo_renderer_service.cc | 33 ++ media/mojo/services/mojo_renderer_service.h | 8 + .../starboard/url_player_renderer_wrapper.cc | 207 +++++++ .../starboard/url_player_renderer_wrapper.h | 136 +++++ .../url_player_renderer_wrapper_unittest.cc | 353 ++++++++++++ media/starboard/BUILD.gn | 9 + media/starboard/url_player_renderer.cc | 503 ++++++++++++++++++ media/starboard/url_player_renderer.h | 163 ++++++ .../starboard/url_player_renderer_unittest.cc | 277 ++++++++++ 16 files changed, 1766 insertions(+) create mode 100644 media/mojo/services/starboard/url_player_renderer_wrapper.cc create mode 100644 media/mojo/services/starboard/url_player_renderer_wrapper.h create mode 100644 media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc create mode 100644 media/starboard/url_player_renderer.cc create mode 100644 media/starboard/url_player_renderer.h create mode 100644 media/starboard/url_player_renderer_unittest.cc diff --git a/media/mojo/clients/mojo_renderer.cc b/media/mojo/clients/mojo_renderer.cc index 3dee3d43280e..9e996d02b4ba 100644 --- a/media/mojo/clients/mojo_renderer.cc +++ b/media/mojo/clients/mojo_renderer.cc @@ -151,6 +151,32 @@ void MojoRenderer::Initialize(MediaResource* media_resource, base::Unretained(this), client)); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MojoRenderer::InitializeWithUrl(media::RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb) { + DVLOG(1) << __func__; + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(source_url.is_valid()); + + if (encountered_error_) { + task_runner_->PostTask( + FROM_HERE, base::BindOnce(std::move(init_cb), + PIPELINE_ERROR_INITIALIZATION_FAILED)); + return; + } + + init_cb_ = std::move(init_cb); + + BindRemoteRendererIfNeeded(); + + remote_renderer_->InitializeWithUrl( + client_receiver_.BindNewEndpointAndPassRemote(), source_url, + base::BindOnce(&MojoRenderer::OnInitialized, base::Unretained(this), + client)); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void MojoRenderer::SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) { DVLOG(1) << __func__; diff --git a/media/mojo/clients/mojo_renderer.h b/media/mojo/clients/mojo_renderer.h index 00a32abedc09..cfc4cfa176bc 100644 --- a/media/mojo/clients/mojo_renderer.h +++ b/media/mojo/clients/mojo_renderer.h @@ -19,9 +19,11 @@ #include "media/base/renderer.h" #include "media/base/time_delta_interpolator.h" #include "media/mojo/mojom/renderer.mojom.h" +#include "build/build_config.h" #include "mojo/public/cpp/bindings/associated_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" +#include "url/gurl.h" #if BUILDFLAG(USE_STARBOARD_MEDIA) #include "media/mojo/common/starboard/mojo_renderer_bypass_bridge.h" @@ -65,6 +67,11 @@ class MojoRenderer : public Renderer, public mojom::RendererClient { void Initialize(MediaResource* media_resource, media::RendererClient* client, PipelineStatusCallback init_cb) override; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl(media::RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; void SetLatencyHint(std::optional latency_hint) override; void Flush(base::OnceClosure flush_cb) override; diff --git a/media/mojo/clients/mojo_renderer_wrapper.cc b/media/mojo/clients/mojo_renderer_wrapper.cc index 6bdf72389ee9..ce0a1397360e 100644 --- a/media/mojo/clients/mojo_renderer_wrapper.cc +++ b/media/mojo/clients/mojo_renderer_wrapper.cc @@ -6,6 +6,8 @@ #include +#include "base/logging.h" + namespace media { MojoRendererWrapper::MojoRendererWrapper( @@ -20,6 +22,15 @@ void MojoRendererWrapper::Initialize(MediaResource* media_resource, mojo_renderer_->Initialize(media_resource, client, std::move(init_cb)); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MojoRendererWrapper::InitializeWithUrl(RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb) { + DVLOG(1) << __func__; + mojo_renderer_->InitializeWithUrl(client, source_url, std::move(init_cb)); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void MojoRendererWrapper::Flush(base::OnceClosure flush_cb) { mojo_renderer_->Flush(std::move(flush_cb)); } diff --git a/media/mojo/clients/mojo_renderer_wrapper.h b/media/mojo/clients/mojo_renderer_wrapper.h index 0b42900b84de..0b90baeefbc5 100644 --- a/media/mojo/clients/mojo_renderer_wrapper.h +++ b/media/mojo/clients/mojo_renderer_wrapper.h @@ -9,6 +9,8 @@ #include "media/base/renderer.h" #include "media/mojo/clients/mojo_renderer.h" +#include "build/build_config.h" +#include "url/gurl.h" namespace media { @@ -30,6 +32,11 @@ class MojoRendererWrapper : public Renderer { void Initialize(MediaResource* media_resource, RendererClient* client, PipelineStatusCallback init_cb) override; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl(RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; void SetLatencyHint(std::optional latency_hint) override; void Flush(base::OnceClosure flush_cb) override; diff --git a/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc b/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc index 7304375fa7de..d80bf29056f9 100644 --- a/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc +++ b/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc @@ -24,6 +24,7 @@ #include "media/base/mock_media_log.h" #include "media/base/test_helpers.h" #include "media/base/video_renderer_sink.h" +#include "media/media_buildflags.h" #include "media/mojo/mojom/renderer.mojom.h" #include "media/mojo/mojom/renderer_extensions.mojom.h" #include "media/renderers/video_overlay_factory.h" @@ -61,6 +62,13 @@ class FakeMojomRenderer : public mojom::Renderer { InitializeWithBypassBridgeCallback cb) override { std::move(cb).Run(true); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl(mojo::PendingAssociatedRemote, + const GURL& source_url, + InitializeWithUrlCallback cb) override { + std::move(cb).Run(true); + } +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) MOCK_METHOD1(Flush, void(FlushCallback)); void StartPlayingFrom(base::TimeDelta time) override {} MOCK_METHOD1(SetPlaybackRate, void(double)); diff --git a/media/mojo/mojom/renderer.mojom b/media/mojo/mojom/renderer.mojom index 87f9ac58c64e..8d8412d105f3 100644 --- a/media/mojo/mojom/renderer.mojom +++ b/media/mojo/mojom/renderer.mojom @@ -25,6 +25,14 @@ interface Renderer { Initialize(pending_associated_remote client, array>? streams) => (bool success); + // Initializes the tvOS URL player with a `source_url` delivered on the + // renderer pipe. No demuxer streams are needed - the platform handles + // demuxing. Sending the URL on the renderer pipe keeps URL delivery + // and initialization ordered on the same Mojo pipe. + [EnableIf=use_starboard_url_player] + InitializeWithUrl(pending_associated_remote client, + url.mojom.Url source_url) => (bool success); + // Supported only in single-process mode with the feature enabled. [EnableIf=use_starboard_media] InitializeWithBypassBridge(pending_associated_remote client, diff --git a/media/mojo/services/BUILD.gn b/media/mojo/services/BUILD.gn index 251f8c1bde67..a1129380798e 100644 --- a/media/mojo/services/BUILD.gn +++ b/media/mojo/services/BUILD.gn @@ -143,6 +143,13 @@ component("services") { sources += [ "gpu_mojo_media_client_stubs.cc" ] } + if (is_cobalt && use_starboard_url_player) { + sources += [ + "starboard/url_player_renderer_wrapper.cc", + "starboard/url_player_renderer_wrapper.h", + ] + } + if (is_chromeos) { deps += [ "//chromeos/components/cdm_factory_daemon:cdm_factory_daemon_gpu" ] @@ -366,6 +373,9 @@ source_set("unit_tests") { "//starboard/common", ] } + if (is_cobalt && use_starboard_url_player) { + sources += [ "starboard/url_player_renderer_wrapper_unittest.cc" ] + } } mojolpm_fuzzer_test("webrtc_video_perf_mojolpm_fuzzer") { diff --git a/media/mojo/services/mojo_renderer_service.cc b/media/mojo/services/mojo_renderer_service.cc index 40359f29aa83..8aba63e4ae3e 100644 --- a/media/mojo/services/mojo_renderer_service.cc +++ b/media/mojo/services/mojo_renderer_service.cc @@ -21,6 +21,10 @@ #include "media/mojo/common/starboard/mojo_renderer_bypass_bridge.h" #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#include "media/mojo/services/starboard/url_player_renderer_wrapper.h" +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + namespace media { // Time interval to update media time. @@ -135,6 +139,35 @@ void MojoRendererService::Initialize( std::move(callback))); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MojoRendererService::InitializeWithUrl( + mojo::PendingAssociatedRemote client, + const GURL& source_url, + InitializeWithUrlCallback callback) { + DVLOG(1) << __func__ << ": " << source_url; + DCHECK_EQ(state_, STATE_UNINITIALIZED); + + if (renderer_->GetRendererType() != RendererType::kUrlPlayer) { + mojo::ReportBadMessage("InitializeWithUrl requires URL player renderer"); + return; + } + + if (!source_url.is_valid()) { + mojo::ReportBadMessage("InitializeWithUrl requires a valid source URL"); + return; + } + + client_.Bind(std::move(client)); + state_ = STATE_INITIALIZING; + + static_cast(renderer_.get()) + ->InitializeWithUrl( + this, source_url, + base::BindOnce(&MojoRendererService::OnRendererInitializeDone, + weak_this_, std::move(callback))); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + #if BUILDFLAG(USE_STARBOARD_MEDIA) void MojoRendererService::InitializeWithBypassBridge( mojo::PendingAssociatedRemote client, diff --git a/media/mojo/services/mojo_renderer_service.h b/media/mojo/services/mojo_renderer_service.h index 6e056373b104..41734b3132c8 100644 --- a/media/mojo/services/mojo_renderer_service.h +++ b/media/mojo/services/mojo_renderer_service.h @@ -23,11 +23,13 @@ #include "media/base/renderer_client.h" #include "media/mojo/mojom/renderer.mojom.h" #include "media/mojo/services/media_mojo_export.h" +#include "build/build_config.h" #include "mojo/public/cpp/bindings/associated_remote.h" #include "mojo/public/cpp/bindings/pending_associated_remote.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/self_owned_receiver.h" +#include "url/gurl.h" namespace media { @@ -67,6 +69,12 @@ class MEDIA_MOJO_EXPORT MojoRendererService final : public mojom::Renderer, std::optional>> streams, InitializeCallback callback) final; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl( + mojo::PendingAssociatedRemote client, + const GURL& source_url, + InitializeWithUrlCallback callback) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) #if BUILDFLAG(USE_STARBOARD_MEDIA) void InitializeWithBypassBridge( mojo::PendingAssociatedRemote client, diff --git a/media/mojo/services/starboard/url_player_renderer_wrapper.cc b/media/mojo/services/starboard/url_player_renderer_wrapper.cc new file mode 100644 index 000000000000..7502e05d7f85 --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper.cc @@ -0,0 +1,207 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/mojo/services/starboard/url_player_renderer_wrapper.h" + +#include + +#include "base/logging.h" +#include "media/mojo/services/mojo_media_log.h" +#include "ui/gfx/geometry/rect_conversions.h" + +namespace media { + +UrlPlayerRendererTraits::UrlPlayerRendererTraits( + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) + : task_runner(std::move(task_runner)), + media_log_remote(std::move(media_log_remote)), + video_geometry_setter_service(video_geometry_setter_service), + overlay_plane_id(overlay_plane_id), + viewport_size(viewport_size), + renderer_extension_receiver(std::move(renderer_extension_receiver)), + client_extension_remote(std::move(client_extension_remote)) {} + +UrlPlayerRendererTraits::~UrlPlayerRendererTraits() = default; + +UrlPlayerRendererWrapper::UrlPlayerRendererWrapper( + UrlPlayerRendererTraits traits) + : renderer_extension_receiver_( + this, + std::move(traits.renderer_extension_receiver)), + client_extension_remote_(std::move(traits.client_extension_remote), + traits.task_runner), + video_geometry_setter_service_(traits.video_geometry_setter_service), + overlay_plane_id_(traits.overlay_plane_id), + renderer_( + traits.task_runner, + std::make_unique(std::move(traits.media_log_remote), + traits.task_runner), + traits.overlay_plane_id, + traits.viewport_size) { + DETACH_FROM_THREAD(thread_checker_); +} + +UrlPlayerRendererWrapper::~UrlPlayerRendererWrapper() = default; + +void UrlPlayerRendererWrapper::Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + DCHECK(init_cb); + + // Subscribe to video geometry changes. + DCHECK(video_geometry_setter_service_); + video_geometry_setter_service_->GetVideoGeometryChangeSubscriber( + video_geometry_change_subscriber_remote_.BindNewPipeAndPassReceiver()); + DCHECK(video_geometry_change_subscriber_remote_); + video_geometry_change_subscriber_remote_->SubscribeToVideoGeometryChange( + overlay_plane_id_, + video_geometry_change_client_receiver_.BindNewPipeAndPassRemote(), + base::BindOnce( + &UrlPlayerRendererWrapper::OnSubscribeToVideoGeometryChange, + weak_factory_.GetWeakPtr(), media_resource, client, + std::move(init_cb))); + + // Install renderer callbacks for paint, rendering mode, and window handle. + GetRenderer()->SetUrlPlayerRendererCallbacks( + base::BindRepeating(&UrlPlayerRendererWrapper::OnPaintVideoHoleFrame, + weak_factory_.GetWeakPtr()), + base::BindRepeating( + &UrlPlayerRendererWrapper::OnUpdateStarboardRenderingMode, + weak_factory_.GetWeakPtr()), + base::BindRepeating(&UrlPlayerRendererWrapper::OnGetSbWindowHandle, + weak_factory_.GetWeakPtr())); +} + +void UrlPlayerRendererWrapper::InitializeWithUrl( + RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + DCHECK(source_url.is_valid()); + + GetRenderer()->SetSourceUrl(source_url.spec()); + Initialize(/*media_resource=*/nullptr, client, std::move(init_cb)); +} + +void UrlPlayerRendererWrapper::Flush(base::OnceClosure flush_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->Flush(std::move(flush_cb)); +} + +void UrlPlayerRendererWrapper::StartPlayingFrom(base::TimeDelta time) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->StartPlayingFrom(time); +} + +void UrlPlayerRendererWrapper::SetPlaybackRate(double playback_rate) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetPlaybackRate(playback_rate); +} + +void UrlPlayerRendererWrapper::SetVolume(float volume) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetVolume(volume); +} + +void UrlPlayerRendererWrapper::SetCdm(CdmContext* cdm_context, + CdmAttachedCB cdm_attached_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetCdm(cdm_context, std::move(cdm_attached_cb)); +} + +void UrlPlayerRendererWrapper::SetLatencyHint( + std::optional latency_hint) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetLatencyHint(latency_hint); +} + +base::TimeDelta UrlPlayerRendererWrapper::GetMediaTime() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + return GetRenderer()->GetMediaTime(); +} + +RendererType UrlPlayerRendererWrapper::GetRendererType() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + return RendererType::kUrlPlayer; +} + +void UrlPlayerRendererWrapper::OnGpuChannelTokenReady( + mojom::CommandBufferIdPtr command_buffer_id) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + // URL player uses punch-out only; GPU channel is not needed. +} + +void UrlPlayerRendererWrapper::GetCurrentVideoFrame( + GetCurrentVideoFrameCallback callback) { + // URL player uses punch-out only; decode-to-texture is unsupported. + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + std::move(callback).Run(nullptr); +} + +void UrlPlayerRendererWrapper::OnSbWindowHandleReady( + uint64_t sb_window_handle) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->OnSbWindowHandleReady(sb_window_handle); +} + +UrlPlayerRenderer* UrlPlayerRendererWrapper::GetRenderer() { + if (test_renderer_) { + return test_renderer_; + } + return &renderer_; +} + +void UrlPlayerRendererWrapper::OnVideoGeometryChange( + const gfx::RectF& rect_f, + gfx::OverlayTransform /* transform */) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + gfx::Rect new_bounds = gfx::ToEnclosedRect(rect_f); + GetRenderer()->OnVideoGeometryChange(new_bounds); +} + +void UrlPlayerRendererWrapper::OnPaintVideoHoleFrame(const gfx::Size& size) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->PaintVideoHoleFrame(size); +} + +void UrlPlayerRendererWrapper::OnUpdateStarboardRenderingMode( + StarboardRenderingMode mode) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->UpdateStarboardRenderingMode(mode); +} + +void UrlPlayerRendererWrapper::OnGetSbWindowHandle() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->GetSbWindowHandle(); +} + +void UrlPlayerRendererWrapper::OnSubscribeToVideoGeometryChange( + MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + DCHECK(init_cb); + GetRenderer()->Initialize(media_resource, client, std::move(init_cb)); +} + +} // namespace media diff --git a/media/mojo/services/starboard/url_player_renderer_wrapper.h b/media/mojo/services/starboard/url_player_renderer_wrapper.h new file mode 100644 index 000000000000..3215646de0df --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper.h @@ -0,0 +1,136 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEDIA_MOJO_SERVICES_STARBOARD_URL_PLAYER_RENDERER_WRAPPER_H_ +#define MEDIA_MOJO_SERVICES_STARBOARD_URL_PLAYER_RENDERER_WRAPPER_H_ + +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "cobalt/media/service/mojom/video_geometry_setter.mojom.h" +#include "cobalt/media/service/video_geometry_setter_service.h" +#include "media/base/renderer.h" +#include "media/mojo/mojom/renderer_extensions.mojom.h" +#include "media/starboard/url_player_renderer.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "url/gurl.h" + +namespace media { + +struct UrlPlayerRendererTraits { + scoped_refptr task_runner; + mojo::PendingRemote media_log_remote; + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service; + const base::UnguessableToken& overlay_plane_id; + const gfx::Size& viewport_size; + mojo::PendingReceiver + renderer_extension_receiver; + mojo::PendingRemote + client_extension_remote; + + UrlPlayerRendererTraits( + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote); + UrlPlayerRendererTraits(UrlPlayerRendererTraits&& that) = default; + ~UrlPlayerRendererTraits(); +}; + +// Wrapper around UrlPlayerRenderer that bridges to Mojo extension endpoints. +// Mirrors StarboardRendererWrapper but omits GPU factory, decode-target +// infrastructure, and Android overlay support because URL player uses +// punch-out only. +class UrlPlayerRendererWrapper + : public Renderer, + public mojom::StarboardRendererExtension, + public cobalt::media::mojom::VideoGeometryChangeClient { + public: + using RendererExtension = mojom::StarboardRendererExtension; + using ClientExtension = mojom::StarboardRendererClientExtension; + + explicit UrlPlayerRendererWrapper(UrlPlayerRendererTraits traits); + + UrlPlayerRendererWrapper(const UrlPlayerRendererWrapper&) = delete; + UrlPlayerRendererWrapper& operator=(const UrlPlayerRendererWrapper&) = delete; + + ~UrlPlayerRendererWrapper() override; + + // Renderer implementation. + void Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) override; + void InitializeWithUrl(RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb); + void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; + void SetLatencyHint(std::optional latency_hint) override; + void Flush(base::OnceClosure flush_cb) override; + void StartPlayingFrom(base::TimeDelta time) override; + void SetPlaybackRate(double playback_rate) override; + void SetVolume(float volume) override; + base::TimeDelta GetMediaTime() override; + RendererType GetRendererType() override; + + // mojom::StarboardRendererExtension implementation. + void OnGpuChannelTokenReady( + mojom::CommandBufferIdPtr command_buffer_id) override; + void GetCurrentVideoFrame(GetCurrentVideoFrameCallback callback) override; + void OnSbWindowHandleReady(uint64_t sb_window_handle) override; + // cobalt::media::mojom::VideoGeometryChangeClient implementation. + void OnVideoGeometryChange(const gfx::RectF& rect_f, + gfx::OverlayTransform transform) override; + + UrlPlayerRenderer* GetRenderer(); + + void SetRendererForTesting(UrlPlayerRenderer* renderer) { + test_renderer_ = renderer; + } + + private: + void OnPaintVideoHoleFrame(const gfx::Size& size); + void OnUpdateStarboardRenderingMode(StarboardRenderingMode mode); + void OnGetSbWindowHandle(); + void OnSubscribeToVideoGeometryChange(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb); + + mojo::Receiver renderer_extension_receiver_; + mojo::Remote client_extension_remote_; + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service_; + const base::UnguessableToken overlay_plane_id_; + UrlPlayerRenderer renderer_; + + raw_ptr test_renderer_ = nullptr; + + mojo::Remote + video_geometry_change_subscriber_remote_; + mojo::Receiver + video_geometry_change_client_receiver_{this}; + + THREAD_CHECKER(thread_checker_); + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace media + +#endif // MEDIA_MOJO_SERVICES_STARBOARD_URL_PLAYER_RENDERER_WRAPPER_H_ diff --git a/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc b/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc new file mode 100644 index 000000000000..adb96e58fab7 --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc @@ -0,0 +1,353 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/mojo/services/starboard/url_player_renderer_wrapper.h" + +#include +#include + +#include "base/functional/callback_helpers.h" +#include "base/test/gmock_callback_support.h" +#include "base/test/mock_callback.h" +#include "base/test/task_environment.h" +#include "cobalt/media/service/video_geometry_setter_service.h" +#include "media/base/media_util.h" +#include "media/base/mock_filters.h" +#include "media/base/test_helpers.h" +#include "media/starboard/sbplayer_interface.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +struct SbPlayerPrivate {}; + +namespace media { +namespace { + +using ::base::test::RunOnceCallback; +using ::testing::_; +using ::testing::DoAll; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::StrEq; +using ::testing::StrictMock; + +constexpr char kSourceUrl[] = "https://example.test/video.m3u8"; + +class MockUrlPlayerRenderer : public UrlPlayerRenderer { + public: + explicit MockUrlPlayerRenderer( + const scoped_refptr& task_runner) + : UrlPlayerRenderer(task_runner, + std::make_unique(), + base::UnguessableToken::Create(), + gfx::Size()) {} + + void Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) override { + OnInitialize(media_resource, client, init_cb); + } + MOCK_METHOD(void, + OnInitialize, + (MediaResource*, RendererClient*, PipelineStatusCallback&)); + MOCK_METHOD(void, SetCdm, (CdmContext*, CdmAttachedCB), (override)); + MOCK_METHOD(void, + SetLatencyHint, + (std::optional), + (override)); + MOCK_METHOD(void, Flush, (base::OnceClosure), (override)); + MOCK_METHOD(void, StartPlayingFrom, (base::TimeDelta), (override)); + MOCK_METHOD(void, SetPlaybackRate, (double), (override)); + MOCK_METHOD(void, SetVolume, (float), (override)); + MOCK_METHOD(base::TimeDelta, GetMediaTime, (), (override)); +}; + +class MockStarboardRendererClientExtension + : public mojom::StarboardRendererClientExtension { + public: + MOCK_METHOD(void, PaintVideoHoleFrame, (const gfx::Size&), (override)); + MOCK_METHOD(void, + UpdateStarboardRenderingMode, + (StarboardRenderingMode), + (override)); + MOCK_METHOD(void, GetSbWindowHandle, (), (override)); +}; + +class MockSbPlayerInterface : public SbPlayerInterface { + public: + MOCK_METHOD(SbPlayer, + Create, + (SbWindow, + const SbPlayerCreationParam*, + SbPlayerDeallocateSampleFunc, + SbPlayerDecoderStatusFunc, + SbPlayerStatusFunc, + SbPlayerErrorFunc, + void*, + SbDecodeTargetGraphicsContextProvider*), + (override)); + MOCK_METHOD(SbPlayerOutputMode, + GetPreferredOutputMode, + (const SbPlayerCreationParam*), + (override)); + void Destroy(SbPlayer player) override { delete player; } + MOCK_METHOD(void, Seek, (SbPlayer, base::TimeDelta, int), (override)); + MOCK_METHOD(void, + WriteSamples, + (SbPlayer, SbMediaType, const SbPlayerSampleInfo*, int), + (override)); + MOCK_METHOD(int, + GetMaximumNumberOfSamplesPerWrite, + (SbPlayer, SbMediaType), + (override)); + MOCK_METHOD(void, WriteEndOfStream, (SbPlayer, SbMediaType), (override)); + MOCK_METHOD(void, SetBounds, (SbPlayer, int, int, int, int, int), (override)); + MOCK_METHOD(bool, SetPlaybackRate, (SbPlayer, double), (override)); + MOCK_METHOD(void, SetVolume, (SbPlayer, double), (override)); + MOCK_METHOD(void, GetInfo, (SbPlayer, SbPlayerInfo*), (override)); + MOCK_METHOD(SbDecodeTarget, GetCurrentFrame, (SbPlayer), (override)); + MOCK_METHOD(SbPlayer, + CreateUrlPlayer, + (const char*, + SbWindow, + SbPlayerStatusFunc, + SbPlayerEncryptedMediaInitDataEncounteredCB, + SbPlayerErrorFunc, + void*), + (override)); + MOCK_METHOD(void, SetUrlPlayerDrmSystem, (SbPlayer, SbDrmSystem), (override)); + MOCK_METHOD(bool, + GetUrlPlayerOutputModeSupported, + (SbPlayerOutputMode), + (override)); + MOCK_METHOD(void, + GetUrlPlayerExtraInfo, + (SbPlayer, SbUrlPlayerExtraInfo*), + (override)); + MOCK_METHOD(bool, + GetAudioConfiguration, + (SbPlayer, int, SbMediaAudioConfiguration*), + (override)); +}; + +class UrlPlayerRendererWrapperTest : public testing::Test { + protected: + UrlPlayerRendererWrapperTest() + : mock_renderer_(std::make_unique>( + task_environment_.GetMainThreadTaskRunner())), + client_extension_receiver_(&client_extension_) { + mojo::PendingRemote + client_extension_remote; + client_extension_receiver_.Bind( + client_extension_remote.InitWithNewPipeAndPassReceiver()); + + auto media_log_remote = media_log_receiver_.InitWithNewPipeAndPassRemote(); + auto renderer_extension_receiver = + renderer_extension_.BindNewPipeAndPassReceiver(); + UrlPlayerRendererTraits traits( + task_environment_.GetMainThreadTaskRunner(), + std::move(media_log_remote), &video_geometry_setter_service_, + base::UnguessableToken::Create(), gfx::Size(), + std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + renderer_wrapper_ = + std::make_unique(std::move(traits)); + renderer_wrapper_->SetRendererForTesting(mock_renderer_.get()); + } + + ~UrlPlayerRendererWrapperTest() override { + renderer_wrapper_.reset(); + mock_renderer_.reset(); + } + + base::test::TaskEnvironment task_environment_; + cobalt::media::VideoGeometrySetterService video_geometry_setter_service_; + std::unique_ptr> mock_renderer_; + StrictMock client_extension_; + mojo::Receiver + client_extension_receiver_; + mojo::Remote renderer_extension_; + mojo::PendingReceiver media_log_receiver_; + std::unique_ptr renderer_wrapper_; + NiceMock media_resource_; + NiceMock renderer_client_; +}; + +TEST_F(UrlPlayerRendererWrapperTest, InitializesAfterGeometrySubscription) { + base::MockOnceCallback init_cb; + EXPECT_CALL(*mock_renderer_, + OnInitialize(&media_resource_, &renderer_client_, _)) + .WillOnce(RunOnceCallback<2>(PIPELINE_OK)); + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))); + + renderer_wrapper_->Initialize(&media_resource_, &renderer_client_, + init_cb.Get()); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererWrapperTest, DelegatesRendererMethods) { + const base::TimeDelta start_time = base::Seconds(5); + const base::TimeDelta media_time = base::Seconds(42); + base::MockOnceClosure flush_cb; + base::MockOnceCallback cdm_cb; + + EXPECT_CALL(*mock_renderer_, + SetLatencyHint(std::optional(base::Milliseconds(250)))); + renderer_wrapper_->SetLatencyHint(base::Milliseconds(250)); + EXPECT_CALL(*mock_renderer_, StartPlayingFrom(start_time)); + renderer_wrapper_->StartPlayingFrom(start_time); + EXPECT_CALL(*mock_renderer_, SetPlaybackRate(1.25)); + renderer_wrapper_->SetPlaybackRate(1.25); + EXPECT_CALL(*mock_renderer_, SetVolume(0.5f)); + renderer_wrapper_->SetVolume(0.5f); + EXPECT_CALL(*mock_renderer_, GetMediaTime()).WillOnce(Return(media_time)); + EXPECT_EQ(renderer_wrapper_->GetMediaTime(), media_time); + EXPECT_CALL(*mock_renderer_, Flush(_)) + .WillOnce(Invoke([](base::OnceClosure cb) { std::move(cb).Run(); })); + EXPECT_CALL(flush_cb, Run()); + renderer_wrapper_->Flush(flush_cb.Get()); + EXPECT_CALL(*mock_renderer_, SetCdm(nullptr, _)) + .WillOnce(Invoke([](CdmContext*, Renderer::CdmAttachedCB cb) { + std::move(cb).Run(true); + })); + EXPECT_CALL(cdm_cb, Run(true)); + renderer_wrapper_->SetCdm(nullptr, cdm_cb.Get()); +} + +TEST_F(UrlPlayerRendererWrapperTest, ReportsUrlPlayerTypeAndNoGpuFrame) { + EXPECT_EQ(renderer_wrapper_->GetRendererType(), RendererType::kUrlPlayer); + + auto command_buffer_id = mojom::CommandBufferId::New(); + command_buffer_id->channel_token = base::UnguessableToken::Create(); + command_buffer_id->route_id = 7; + renderer_wrapper_->OnGpuChannelTokenReady(std::move(command_buffer_id)); + + base::MockCallback + frame_cb; + EXPECT_CALL(frame_cb, Run(testing::IsNull())); + renderer_wrapper_->GetCurrentVideoFrame(frame_cb.Get()); +} + +TEST_F(UrlPlayerRendererWrapperTest, InitializeWithUrlPassesNullMediaResource) { + base::MockOnceCallback init_cb; + // InitializeWithUrl should call Initialize with nullptr media_resource. + EXPECT_CALL(*mock_renderer_, OnInitialize(nullptr, &renderer_client_, _)) + .WillOnce(RunOnceCallback<2>(PIPELINE_OK)); + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))); + + renderer_wrapper_->InitializeWithUrl(&renderer_client_, GURL(kSourceUrl), + init_cb.Get()); + task_environment_.RunUntilIdle(); +} + +class UrlPlayerRendererWrapperIntegrationTest : public testing::Test { + protected: + UrlPlayerRendererWrapperIntegrationTest() + : overlay_plane_id_(base::UnguessableToken::Create()), + client_extension_receiver_(&client_extension_) { + mojo::PendingRemote + client_extension_remote; + client_extension_receiver_.Bind( + client_extension_remote.InitWithNewPipeAndPassReceiver()); + + auto media_log_remote = media_log_receiver_.InitWithNewPipeAndPassRemote(); + auto renderer_extension_receiver = + renderer_extension_.BindNewPipeAndPassReceiver(); + UrlPlayerRendererTraits traits( + task_environment_.GetMainThreadTaskRunner(), + std::move(media_log_remote), &video_geometry_setter_service_, + overlay_plane_id_, gfx::Size(), std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + renderer_wrapper_ = + std::make_unique(std::move(traits)); + renderer_wrapper_->GetRenderer()->SetSbPlayerInterfaceForTesting( + &mock_sbplayer_interface_); + + ON_CALL(mock_sbplayer_interface_, GetUrlPlayerOutputModeSupported(_)) + .WillByDefault(Invoke([](SbPlayerOutputMode mode) { + return mode == kSbPlayerOutputModePunchOut; + })); + ON_CALL(mock_sbplayer_interface_, SetPlaybackRate(_, _)) + .WillByDefault(Return(true)); + } + + base::test::TaskEnvironment task_environment_; + cobalt::media::VideoGeometrySetterService video_geometry_setter_service_; + const base::UnguessableToken overlay_plane_id_; + NiceMock mock_sbplayer_interface_; + StrictMock client_extension_; + mojo::Receiver + client_extension_receiver_; + mojo::Remote renderer_extension_; + mojo::PendingReceiver media_log_receiver_; + std::unique_ptr renderer_wrapper_; + NiceMock media_resource_; + NiceMock renderer_client_; +}; + +TEST_F(UrlPlayerRendererWrapperIntegrationTest, + ForwardsUrlWindowGeometryAndClientCallbacks) { + constexpr uint64_t kWindowHandle = 0x1234; + SbWindow window = reinterpret_cast(kWindowHandle); + SbPlayer player = new SbPlayerPrivate(); + SbPlayerStatusFunc player_status_cb = nullptr; + void* player_context = nullptr; + + base::MockOnceCallback init_cb; + EXPECT_CALL(client_extension_, GetSbWindowHandle()); + renderer_wrapper_->InitializeWithUrl(&renderer_client_, GURL(kSourceUrl), + init_cb.Get()); + task_environment_.RunUntilIdle(); + + renderer_wrapper_->OnVideoGeometryChange( + gfx::RectF(10.0f, 20.0f, 640.0f, 360.0f), gfx::OVERLAY_TRANSFORM_NONE); + EXPECT_CALL(mock_sbplayer_interface_, + CreateUrlPlayer(StrEq(kSourceUrl), window, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(&player_status_cb), + SaveArg<5>(&player_context), Return(player))); + EXPECT_CALL(mock_sbplayer_interface_, SetBounds(player, _, 10, 20, 640, 360)); + EXPECT_CALL(client_extension_, + UpdateStarboardRenderingMode(StarboardRenderingMode::kPunchOut)); + renderer_extension_->OnSbWindowHandleReady(kWindowHandle); + task_environment_.RunUntilIdle(); + + ASSERT_TRUE(player_status_cb); + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))); + player_status_cb(player, player_context, kSbPlayerStateInitialized, + SB_PLAYER_INITIAL_TICKET); + task_environment_.RunUntilIdle(); + + EXPECT_CALL(mock_sbplayer_interface_, GetInfo(player, _)) + .WillOnce(Invoke([](SbPlayer, SbPlayerInfo* info) { + *info = {}; + info->frame_width = 1920; + info->frame_height = 1080; + })); + EXPECT_CALL(renderer_client_, + OnVideoNaturalSizeChange(gfx::Size(1920, 1080))); + EXPECT_CALL(renderer_client_, + OnBufferingStateChange(BUFFERING_HAVE_ENOUGH, + BUFFERING_CHANGE_REASON_UNKNOWN)); + EXPECT_CALL(client_extension_, PaintVideoHoleFrame(gfx::Size(1920, 1080))); + player_status_cb(player, player_context, kSbPlayerStatePresenting, + SB_PLAYER_INITIAL_TICKET); + task_environment_.RunUntilIdle(); +} + +} // namespace +} // namespace media diff --git a/media/starboard/BUILD.gn b/media/starboard/BUILD.gn index bb7b8fb00656..22b166a2e940 100644 --- a/media/starboard/BUILD.gn +++ b/media/starboard/BUILD.gn @@ -59,6 +59,12 @@ source_set("starboard") { deps += [ ":buildflags" ] } + if (use_starboard_url_player) { + sources += [ + "url_player_renderer.cc", + "url_player_renderer.h", + ] + } deps += [ "//base", @@ -98,6 +104,9 @@ source_set("unit_tests") { "starboard_utils_test.cc", ] } + if (use_starboard_url_player) { + sources += [ "url_player_renderer_unittest.cc" ] + } configs += [ "//media:media_config" ] deps = [ "//base", diff --git a/media/starboard/url_player_renderer.cc b/media/starboard/url_player_renderer.cc new file mode 100644 index 000000000000..6585f9aac018 --- /dev/null +++ b/media/starboard/url_player_renderer.cc @@ -0,0 +1,503 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/starboard/url_player_renderer.h" + +#include "base/logging.h" +#include "base/notreached.h" +#include "base/strings/stringprintf.h" +#include "base/task/sequenced_task_runner.h" +#include "base/trace_event/trace_event.h" +#include "media/base/pipeline_status.h" +#include "media/starboard/buildflags.h" +#include "starboard/common/player.h" + +namespace media { + +using base::Time; +using base::TimeDelta; + +UrlPlayerRenderer::UrlPlayerRenderer( + const scoped_refptr& task_runner, + std::unique_ptr media_log, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size) + : task_runner_(task_runner), + media_log_(std::move(media_log)), + viewport_size_(viewport_size) { + LOG(INFO) << "UrlPlayerRenderer created."; +} + +UrlPlayerRenderer::~UrlPlayerRenderer() { + LOG(INFO) << "UrlPlayerRenderer destroyed."; +} + +void UrlPlayerRenderer::Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_EQ(state_, STATE_UNINITIALIZED); + + LOG(INFO) << "Initializing UrlPlayerRenderer."; + + client_ = client; + init_cb_ = std::move(init_cb); + + if (source_url_.empty()) { + LOG(ERROR) << "Initialize() called without source URL set. " + << "URL must be set via SetSourceUrl() before Initialize()."; + state_ = STATE_ERROR; + task_runner_->PostTask( + FROM_HERE, base::BindOnce(std::move(init_cb_), + PIPELINE_ERROR_INITIALIZATION_FAILED)); + return; + } + + state_ = STATE_INITIALIZING; + + if (get_sb_window_handle_cb_) { + // Get SbWindow from CobaltRenderContentClient. + get_sb_window_handle_cb_.Run(); + return; + } + + CreatePlayerBridge(); +} + +void UrlPlayerRenderer::SetCdm(CdmContext* cdm_context, + CdmAttachedCB cdm_attached_cb) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(cdm_context); + + if (cdm_context_ || SbDrmSystemIsValid(drm_system_)) { + LOG(WARNING) << "Switching CDM not supported."; + std::move(cdm_attached_cb).Run(false); + return; + } + + cdm_context_ = cdm_context; + drm_system_ = cdm_context_->GetSbDrmSystem(); + + if (player_bridge_ && SbDrmSystemIsValid(drm_system_)) { + LOG(INFO) << "Attaching DRM system to existing player bridge."; + player_bridge_->SetDrmSystem(drm_system_); + } + + std::move(cdm_attached_cb).Run(true); + LOG(INFO) << "CDM set successfully."; +} + +void UrlPlayerRenderer::SetLatencyHint( + std::optional latency_hint) { + LOG(INFO) << "SetLatencyHint() not supported for URL player."; +} + +void UrlPlayerRenderer::Flush(base::OnceClosure flush_cb) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(flush_cb); + + if (!player_bridge_) { + LOG(WARNING) << "Flush() called with no player bridge."; + std::move(flush_cb).Run(); + return; + } + + player_bridge_->PrepareForSeek(); + playback_rate_ = 0.0; + + if (buffering_state_ != BUFFERING_HAVE_NOTHING) { + buffering_state_ = BUFFERING_HAVE_NOTHING; + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&UrlPlayerRenderer::OnBufferingStateChange, + weak_factory_.GetWeakPtr(), buffering_state_)); + } + + // URL player has no in-flight demuxer reads, complete immediately. + state_ = STATE_FLUSHED; + std::move(flush_cb).Run(); +} + +void UrlPlayerRenderer::StartPlayingFrom(TimeDelta time) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + LOG(INFO) << "UrlPlayerRenderer::StartPlayingFrom() called with " << time + << '.'; + StoreMediaTime(time); + seek_time_ = time; + + if (state_ != STATE_FLUSHED) { + DCHECK_EQ(state_, STATE_ERROR); + return; + } + + state_ = STATE_PLAYING; + player_bridge_->Seek(time); +} + +void UrlPlayerRenderer::SetPlaybackRate(double playback_rate) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + if (playback_rate < 0.0) { + LOG(INFO) << "UrlPlayerRenderer::SetPlaybackRate(): invalid playback rate " + << playback_rate << '.'; + return; + } + + LOG(INFO) << "UrlPlayerRenderer changes playback rate from " << playback_rate_ + << " to " << playback_rate << '.'; + + if (playback_rate_ == playback_rate) { + return; + } + + playback_rate_ = playback_rate; + + if (player_bridge_) { + player_bridge_->SetPlaybackRate(playback_rate_); + } +} + +void UrlPlayerRenderer::SetVolume(float volume) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (volume < 0.0f || volume > 1.0f) { + LOG(INFO) << "UrlPlayerRenderer::SetVolume(): invalid volume " << volume + << '.'; + return; + } + + LOG(INFO) << "UrlPlayerRenderer changes volume from " << volume_ << " to " + << volume << '.'; + + if (volume_ == volume) { + return; + } + + volume_ = volume; + + if (player_bridge_) { + player_bridge_->SetVolume(volume_); + } +} + +TimeDelta UrlPlayerRenderer::GetMediaTime() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (!player_bridge_) { + StoreMediaTime(TimeDelta()); + return base::Microseconds(0); + } + + uint32_t video_frames_decoded, video_frames_dropped; + uint64_t audio_bytes_decoded, video_bytes_decoded; + TimeDelta media_time; + SbPlayerBridge::PlayerInfo info{&video_frames_decoded, &video_frames_dropped, + &audio_bytes_decoded, &video_bytes_decoded, + &media_time}; + + player_bridge_->GetInfo(&info); + + PipelineStatistics statistics; + statistics.audio_bytes_decoded = audio_bytes_decoded; + statistics.video_bytes_decoded = video_bytes_decoded; + + if (video_frames_decoded > last_video_frames_decoded_) { + statistics.video_frames_decoded = + video_frames_decoded - last_video_frames_decoded_; + last_video_frames_decoded_ = video_frames_decoded; + } + + if (video_frames_dropped > last_video_frames_dropped_) { + statistics.video_frames_dropped = + video_frames_dropped - last_video_frames_dropped_; + last_video_frames_dropped_ = video_frames_dropped; + } + + if (statistics.video_frames_decoded > 0 || + statistics.video_frames_dropped > 0) { + task_runner_->PostTask( + FROM_HERE, base::BindOnce(&UrlPlayerRenderer::OnStatisticsUpdate, + weak_factory_.GetWeakPtr(), statistics)); + } + + // Retry video dimensions if not yet reported (AVPlayer may expose them + // after the initial kSbPlayerStatePresenting event). + if (!has_reported_dimensions_ && state_ == STATE_PLAYING) { + TryReportVideoDimensions(); + } + + StoreMediaTime(media_time); + return media_time; +} + +void UrlPlayerRenderer::OnTracksChanged( + DemuxerStream::Type track_type, + std::vector enabled_tracks, + base::OnceClosure change_completed_cb) { + LOG(INFO) << "OnTracksChanged not supported for type: " << track_type; + std::move(change_completed_cb).Run(); +} + +void UrlPlayerRenderer::SetSourceUrl(const std::string& source_url) { + LOG(INFO) << "SetSourceUrl() called with url length=" << source_url.size(); + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_EQ(state_, STATE_UNINITIALIZED) + << "SetSourceUrl() must be called before Initialize()."; + source_url_ = source_url; +} + +void UrlPlayerRenderer::SetUrlPlayerRendererCallbacks( + PaintVideoHoleFrameCallback paint_video_hole_frame_cb, + UpdateStarboardRenderingModeCallback update_starboard_rendering_mode_cb, + GetSbWindowHandleCallback get_sb_window_handle_cb) { + paint_video_hole_frame_cb_ = std::move(paint_video_hole_frame_cb); + update_starboard_rendering_mode_cb_ = + std::move(update_starboard_rendering_mode_cb); + get_sb_window_handle_cb_ = std::move(get_sb_window_handle_cb); +} + +void UrlPlayerRenderer::OnVideoGeometryChange(const gfx::Rect& output_rect) { + CHECK(task_runner_->RunsTasksInCurrentSequence()); + output_rect_ = output_rect; + ApplyPendingBounds(); +} + +void UrlPlayerRenderer::OnSbWindowHandleReady(uint64_t sb_window_handle) { + CHECK(task_runner_->RunsTasksInCurrentSequence()); + if (sb_window_handle != 0) { + sb_window_ = reinterpret_cast(sb_window_handle); + if (!SbWindowIsValid(sb_window_)) { + LOG(WARNING) << "SbWindow is not valid."; + } + } + CreatePlayerBridge(); +} + +SbPlayerInterface* UrlPlayerRenderer::GetSbPlayerInterface() { + return test_sbplayer_interface_ ? test_sbplayer_interface_ + : &sbplayer_interface_; +} + +// Private methods + +void UrlPlayerRenderer::CreatePlayerBridge() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(init_cb_); + DCHECK_EQ(state_, STATE_INITIALIZING); + DCHECK(!source_url_.empty()); + + TRACE_EVENT0("media", "UrlPlayerRenderer::CreatePlayerBridge"); + + DCHECK(!player_bridge_); + player_bridge_.reset(); + + LOG(INFO) << "Creating SbPlayerBridge."; + + player_bridge_.reset(new SbPlayerBridge( + GetSbPlayerInterface(), task_runner_, source_url_, sb_window_, this, + /*allow_resume_after_suspend=*/false, kSbPlayerOutputModePunchOut, + base::BindRepeating( + &UrlPlayerRenderer::OnEncryptedMediaInitDataEncountered, + base::Unretained(this)) +#if BUILDFLAG(COBALT_MEDIA_ENABLE_CVAL) + , + /*pipeline_identifier=*/"" +#endif // BUILDFLAG(COBALT_MEDIA_ENABLE_CVAL) + )); + + if (player_bridge_ && player_bridge_->IsValid()) { + ApplyPendingBounds(); + + const auto output_mode = player_bridge_->GetSbPlayerOutputMode(); + if (output_mode != kSbPlayerOutputModePunchOut) { + LOG(ERROR) << "URL player requires punch-out mode but got output mode " + << output_mode; + player_bridge_.reset(); + state_ = STATE_ERROR; + std::move(init_cb_).Run( + PipelineStatus(DECODER_ERROR_NOT_SUPPORTED, + "URL player requires punch-out output mode")); + return; + } + + update_starboard_rendering_mode_cb_.Run(StarboardRenderingMode::kPunchOut); + + if (SbDrmSystemIsValid(drm_system_)) { + LOG(INFO) << "Attaching DRM system to player bridge."; + player_bridge_->SetDrmSystem(drm_system_); + } + + player_bridge_->SetVolume(volume_); + + state_ = STATE_FLUSHED; + + // Defer init_cb_ until SbPlayer reports kSbPlayerStateInitialized + // via OnPlayerStatus(). + LOG(INFO) << "SbPlayerBridge created, waiting for initialized."; + return; + } + + std::string error_message = player_bridge_->GetPlayerCreationErrorMessage(); + player_bridge_.reset(); + LOG(ERROR) << "Failed to create SbPlayerBridge: \"" << error_message << "\""; + state_ = STATE_ERROR; + std::move(init_cb_).Run( + PipelineStatus(DECODER_ERROR_NOT_SUPPORTED, + "UrlPlayerRenderer::CreatePlayerBridge() failed - \"" + + error_message + "\"")); +} + +void UrlPlayerRenderer::ApplyPendingBounds() { + if (!player_bridge_ || !output_rect_) { + return; + } + + player_bridge_->SetBounds(*output_rect_); +} + +void UrlPlayerRenderer::OnStatisticsUpdate(const PipelineStatistics& stats) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + client_->OnStatisticsUpdate(stats); +} + +void UrlPlayerRenderer::OnNeedData(DemuxerStream::Type type, + int max_number_of_buffers_to_write) { + // URL player handles all buffering natively; ignore. +} + +void UrlPlayerRenderer::OnPlayerStatus(SbPlayerState state) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + LOG(INFO) << "UrlPlayerRenderer::OnPlayerStatus() called with state=" + << ::starboard::GetPlayerStateName(state); + + if (!player_bridge_) { + LOG(WARNING) + << "UrlPlayerRenderer::OnPlayerStatus() called without player bridge."; + return; + } + + switch (state) { + case kSbPlayerStateInitialized: + CHECK(init_cb_); + std::move(init_cb_).Run(PipelineStatus(PIPELINE_OK)); + break; + case kSbPlayerStatePrerolling: + break; + case kSbPlayerStatePresenting: { + buffering_state_ = BUFFERING_HAVE_ENOUGH; + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&UrlPlayerRenderer::OnBufferingStateChange, + weak_factory_.GetWeakPtr(), buffering_state_)); + + // Query video resolution and report for video hole punch-out. + TryReportVideoDimensions(); + if (!has_reported_dimensions_) { + LOG(WARNING) << "Dimensions not yet available at presenting; " + "will retry during GetMediaTime() polling."; + } + + // Re-apply playback rate. AVPlayer silently ignores rate changes + // before ReadyToPlay. + player_bridge_->SetPlaybackRate(playback_rate_); + break; + } + case kSbPlayerStateEndOfStream: + client_->OnEnded(); + break; + case kSbPlayerStateDestroyed: + break; + } +} + +void UrlPlayerRenderer::OnPlayerError(SbPlayerError error, + const std::string& message) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + LOG(ERROR) << "UrlPlayerRenderer::OnPlayerError() code=" << error + << " message=\"" << message << "\""; + + if (state_ == STATE_ERROR) { + return; + } + + state_ = STATE_ERROR; + + switch (error) { + case kSbPlayerErrorDecode: + MEDIA_LOG(ERROR, media_log_) << message; + NotifyError(PIPELINE_ERROR_DECODE); + break; + case kSbPlayerErrorCapabilityChanged: + MEDIA_LOG(ERROR, media_log_) + << (message.empty() + ? kSbPlayerCapabilityChangedErrorMessage + : base::StringPrintf("%s: %s", + kSbPlayerCapabilityChangedErrorMessage, + message.c_str())); + NotifyError(PIPELINE_ERROR_DECODE); + break; + case kSbPlayerErrorMax: + NOTREACHED(); + break; + } +} + +void UrlPlayerRenderer::NotifyError(PipelineStatus status) { + if (init_cb_) { + std::move(init_cb_).Run(status); + } else { + client_->OnError(status); + } +} +void UrlPlayerRenderer::StoreMediaTime(TimeDelta media_time) { + last_media_time_ = media_time; + last_time_media_time_retrieved_ = Time::Now(); +} + +void UrlPlayerRenderer::TryReportVideoDimensions() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(player_bridge_); + + int width = 0, height = 0; + player_bridge_->GetVideoResolution(&width, &height); + if (width > 0 && height > 0) { + gfx::Size size(width, height); + client_->OnVideoNaturalSizeChange(size); + paint_video_hole_frame_cb_.Run(size); + has_reported_dimensions_ = true; + LOG(INFO) << "Video size: " << width << "x" << height; + } +} + +void UrlPlayerRenderer::OnBufferingStateChange(BufferingState state) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + if (client_) { + client_->OnBufferingStateChange(state, BUFFERING_CHANGE_REASON_UNKNOWN); + } +} + +SbDecodeTarget UrlPlayerRenderer::GetSbDecodeTarget() { + LOG(INFO) << "GetSbDecodeTarget() not supported for URL player."; + return kSbDecodeTargetInvalid; +} + +void UrlPlayerRenderer::OnEncryptedMediaInitDataEncountered( + const char* init_data_type, + const unsigned char* init_data, + unsigned int init_data_length) { + LOG(INFO) << "OnEncryptedMediaInitDataEncountered() (no-op)."; + // TODO: Forward encrypted media init data to the EME/DRM layer. +} + +} // namespace media diff --git a/media/starboard/url_player_renderer.h b/media/starboard/url_player_renderer.h new file mode 100644 index 000000000000..70da0faf0928 --- /dev/null +++ b/media/starboard/url_player_renderer.h @@ -0,0 +1,163 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEDIA_STARBOARD_URL_PLAYER_RENDERER_H_ +#define MEDIA_STARBOARD_URL_PLAYER_RENDERER_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "base/task/sequenced_task_runner.h" +#include "base/time/time.h" +#include "media/base/cdm_context.h" +#include "media/base/media_export.h" +#include "media/base/media_log.h" +#include "media/base/pipeline_status.h" +#include "media/base/renderer.h" +#include "media/base/renderer_client.h" +#include "media/base/starboard/starboard_rendering_mode.h" +#include "media/starboard/sbplayer_bridge.h" +#include "ui/gfx/geometry/rect.h" + +namespace media { + +// Renderer for URL-based playback on tvOS (e.g., HLS via AVPlayer). +// Delegates all media loading, demuxing, buffering, and decoding to the +// platform URL player through SbPlayerBridge. Streams from MediaResource +// are ignored — the URL is supplied by the wrapper before initialization. +class MEDIA_EXPORT UrlPlayerRenderer : public Renderer, + private SbPlayerBridge::Host { + public: + UrlPlayerRenderer(const scoped_refptr& task_runner, + std::unique_ptr media_log, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size); + + UrlPlayerRenderer(const UrlPlayerRenderer&) = delete; + UrlPlayerRenderer& operator=(const UrlPlayerRenderer&) = delete; + + ~UrlPlayerRenderer() override; + + // Renderer implementation. + void Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) override; + void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; + void SetLatencyHint(std::optional latency_hint) override; + void Flush(base::OnceClosure flush_cb) override; + void StartPlayingFrom(base::TimeDelta time) override; + void SetPlaybackRate(double playback_rate) override; + void SetVolume(float volume) override; + base::TimeDelta GetMediaTime() override; + void OnTracksChanged(DemuxerStream::Type track_type, + std::vector enabled_tracks, + base::OnceClosure change_completed_cb) override; + RendererType GetRendererType() override { return RendererType::kUrlPlayer; } + + // URL setup — called by the wrapper before Initialize(). + void SetSourceUrl(const std::string& source_url); + + using PaintVideoHoleFrameCallback = + base::RepeatingCallback; + using UpdateStarboardRenderingModeCallback = + base::RepeatingCallback; + using GetSbWindowHandleCallback = base::RepeatingCallback; + + void SetUrlPlayerRendererCallbacks( + PaintVideoHoleFrameCallback paint_video_hole_frame_cb, + UpdateStarboardRenderingModeCallback update_starboard_rendering_mode_cb, + GetSbWindowHandleCallback get_sb_window_handle_cb); + + void OnVideoGeometryChange(const gfx::Rect& output_rect); + void OnSbWindowHandleReady(uint64_t sb_window_handle); + + SbDecodeTarget GetSbDecodeTarget(); + + SbPlayerInterface* GetSbPlayerInterface(); + void SetSbPlayerInterfaceForTesting(SbPlayerInterface* sbplayer_interface) { + test_sbplayer_interface_ = sbplayer_interface; + } + + private: + enum State { + STATE_UNINITIALIZED, + STATE_INITIALIZING, + STATE_FLUSHED, + STATE_PLAYING, + STATE_ERROR, + }; + + void CreatePlayerBridge(); + void ApplyPendingBounds(); + + // SbPlayerBridge::Host implementation. + void OnNeedData(DemuxerStream::Type type, + int max_number_of_buffers_to_write) override; + void OnPlayerStatus(SbPlayerState state) override; + void OnPlayerError(SbPlayerError error, const std::string& message) override; + + void StoreMediaTime(base::TimeDelta media_time); + void TryReportVideoDimensions(); + void OnStatisticsUpdate(const PipelineStatistics& stats); + void OnBufferingStateChange(BufferingState state); + void NotifyError(PipelineStatus status); + void OnEncryptedMediaInitDataEncountered(const char* init_data_type, + const unsigned char* init_data, + unsigned int init_data_length); + + State state_ = STATE_UNINITIALIZED; + const scoped_refptr task_runner_; + const std::unique_ptr media_log_; + const gfx::Size viewport_size_; + + std::string source_url_; + bool has_reported_dimensions_ = false; + raw_ptr cdm_context_ = nullptr; + SbDrmSystem drm_system_{kSbDrmSystemInvalid}; + raw_ptr client_ = nullptr; + PipelineStatusCallback init_cb_; + BufferingState buffering_state_ = BUFFERING_HAVE_NOTHING; + + PaintVideoHoleFrameCallback paint_video_hole_frame_cb_; + UpdateStarboardRenderingModeCallback update_starboard_rendering_mode_cb_; + GetSbWindowHandleCallback get_sb_window_handle_cb_; + + std::optional output_rect_; + base::TimeDelta seek_time_; + base::TimeDelta last_media_time_; + base::Time last_time_media_time_retrieved_; + double playback_rate_ = 0.0; + float volume_ = 1.0f; + + SbWindow sb_window_ = kSbWindowInvalid; + DefaultSbPlayerInterface sbplayer_interface_; + raw_ptr test_sbplayer_interface_ = nullptr; + std::unique_ptr player_bridge_; + + uint32_t last_video_frames_decoded_ = 0; + uint32_t last_video_frames_dropped_ = 0; + + static inline constexpr const char* kSbPlayerCapabilityChangedErrorMessage = + "MEDIA_ERR_CAPABILITY_CHANGED"; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace media + +#endif // MEDIA_STARBOARD_URL_PLAYER_RENDERER_H_ diff --git a/media/starboard/url_player_renderer_unittest.cc b/media/starboard/url_player_renderer_unittest.cc new file mode 100644 index 000000000000..1a9b2cab1e02 --- /dev/null +++ b/media/starboard/url_player_renderer_unittest.cc @@ -0,0 +1,277 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/starboard/url_player_renderer.h" + +#include +#include + +#include "base/functional/callback_helpers.h" +#include "base/test/mock_callback.h" +#include "base/test/task_environment.h" +#include "media/base/media_util.h" +#include "media/base/mock_filters.h" +#include "media/base/test_helpers.h" +#include "media/starboard/sbplayer_interface.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +struct SbPlayerPrivate {}; +struct SbDrmSystemPrivate {}; + +using ::testing::_; +using ::testing::DoAll; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::StrEq; + +namespace media { +namespace { + +constexpr char kSourceUrl[] = "https://example.test/video.m3u8"; + +class MockSbPlayerInterface : public SbPlayerInterface { + public: + MOCK_METHOD8(Create, + SbPlayer(SbWindow, + const SbPlayerCreationParam*, + SbPlayerDeallocateSampleFunc, + SbPlayerDecoderStatusFunc, + SbPlayerStatusFunc, + SbPlayerErrorFunc, + void*, + SbDecodeTargetGraphicsContextProvider*)); + MOCK_METHOD1(GetPreferredOutputMode, + SbPlayerOutputMode(const SbPlayerCreationParam*)); + void Destroy(SbPlayer player) override { delete player; } + MOCK_METHOD3(Seek, void(SbPlayer, base::TimeDelta, int)); + MOCK_METHOD4(WriteSamples, + void(SbPlayer, SbMediaType, const SbPlayerSampleInfo*, int)); + MOCK_METHOD2(GetMaximumNumberOfSamplesPerWrite, int(SbPlayer, SbMediaType)); + MOCK_METHOD2(WriteEndOfStream, void(SbPlayer, SbMediaType)); + MOCK_METHOD6(SetBounds, void(SbPlayer, int, int, int, int, int)); + MOCK_METHOD2(SetPlaybackRate, bool(SbPlayer, double)); + MOCK_METHOD2(SetVolume, void(SbPlayer, double)); + MOCK_METHOD2(GetInfo, void(SbPlayer, SbPlayerInfo*)); + MOCK_METHOD1(GetCurrentFrame, SbDecodeTarget(SbPlayer)); + + MOCK_METHOD6(CreateUrlPlayer, + SbPlayer(const char*, + SbWindow, + SbPlayerStatusFunc, + SbPlayerEncryptedMediaInitDataEncounteredCB, + SbPlayerErrorFunc, + void*)); + MOCK_METHOD2(SetUrlPlayerDrmSystem, void(SbPlayer, SbDrmSystem)); + MOCK_METHOD1(GetUrlPlayerOutputModeSupported, bool(SbPlayerOutputMode)); + MOCK_METHOD2(GetUrlPlayerExtraInfo, void(SbPlayer, SbUrlPlayerExtraInfo*)); + MOCK_METHOD3(GetAudioConfiguration, + bool(SbPlayer, int, SbMediaAudioConfiguration*)); +}; + +class FakeCdmContext : public CdmContext { + public: + explicit FakeCdmContext(SbDrmSystem drm_system) : drm_system_(drm_system) {} + SbDrmSystem GetSbDrmSystem() override { return drm_system_; } + + private: + SbDrmSystem drm_system_; +}; + +class UrlPlayerRendererTest : public testing::Test { + protected: + UrlPlayerRendererTest() { + ON_CALL(mock_sbplayer_interface_, GetUrlPlayerOutputModeSupported(_)) + .WillByDefault(Invoke([this](SbPlayerOutputMode output_mode) { + if (output_mode == kSbPlayerOutputModePunchOut) { + return punch_out_supported_; + } + return decode_to_texture_supported_; + })); + ON_CALL(mock_sbplayer_interface_, SetPlaybackRate(_, _)) + .WillByDefault(Return(true)); + + renderer_->SetSbPlayerInterfaceForTesting(&mock_sbplayer_interface_); + renderer_->SetUrlPlayerRendererCallbacks( + base::DoNothing(), base::DoNothing(), base::NullCallback()); + } + + SbPlayer ExpectPlayerCreation(SbWindow expected_window = kSbWindowInvalid) { + SbPlayer player = new SbPlayerPrivate(); + EXPECT_CALL(mock_sbplayer_interface_, + CreateUrlPlayer(StrEq(kSourceUrl), expected_window, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(&player_status_cb_), + SaveArg<4>(&player_error_cb_), SaveArg<5>(&context_), + Return(player))); + return player; + } + + void Initialize(SbPlayer player) { + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(init_cb_, Run(HasStatusCode(PIPELINE_OK))); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + ASSERT_TRUE(player_status_cb_); + player_status_cb_(player, context_, kSbPlayerStateInitialized, + SB_PLAYER_INITIAL_TICKET); + task_environment_.RunUntilIdle(); + } + + base::test::TaskEnvironment task_environment_; + NiceMock mock_sbplayer_interface_; + NiceMock media_resource_; + NiceMock renderer_client_; + base::MockOnceCallback init_cb_; + SbPlayerStatusFunc player_status_cb_ = nullptr; + SbPlayerErrorFunc player_error_cb_ = nullptr; + void* context_ = nullptr; + bool punch_out_supported_ = true; + bool decode_to_texture_supported_ = false; + const std::unique_ptr renderer_ = + std::make_unique( + task_environment_.GetMainThreadTaskRunner(), + std::make_unique(), + base::UnguessableToken::Create(), + gfx::Size()); +}; + +TEST_F(UrlPlayerRendererTest, InitializeWithoutUrlFailsAsync) { + // Initialize without URL set — should fail asynchronously. + EXPECT_CALL(mock_sbplayer_interface_, CreateUrlPlayer(_, _, _, _, _, _)) + .Times(0); + EXPECT_CALL(init_cb_, + Run(HasStatusCode(PIPELINE_ERROR_INITIALIZATION_FAILED))); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, InitializeCompletesOnPlayerInitialized) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + EXPECT_EQ(renderer_->GetRendererType(), RendererType::kUrlPlayer); +} + +TEST_F(UrlPlayerRendererTest, InitializeWaitsForWindowHandle) { + base::MockRepeatingCallback get_window_cb; + renderer_->SetUrlPlayerRendererCallbacks(base::DoNothing(), base::DoNothing(), + get_window_cb.Get()); + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(get_window_cb, Run()); + EXPECT_CALL(mock_sbplayer_interface_, CreateUrlPlayer(_, _, _, _, _, _)) + .Times(0); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + + testing::Mock::VerifyAndClearExpectations(&mock_sbplayer_interface_); + SbWindow window = reinterpret_cast(0x1234); + SbPlayer player = ExpectPlayerCreation(window); + EXPECT_CALL(init_cb_, Run(HasStatusCode(PIPELINE_OK))); + renderer_->OnSbWindowHandleReady(reinterpret_cast(window)); + player_status_cb_(player, context_, kSbPlayerStateInitialized, + SB_PLAYER_INITIAL_TICKET); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, RejectsDecodeToTextureFallback) { + punch_out_supported_ = false; + decode_to_texture_supported_ = true; + ExpectPlayerCreation(); + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(init_cb_, Run(HasStatusCode(DECODER_ERROR_NOT_SUPPORTED))); + + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); +} + +TEST_F(UrlPlayerRendererTest, DelegatesRateVolumeSeekAndFlush) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + + // Volume is not gated by seek_pending. + EXPECT_CALL(mock_sbplayer_interface_, SetVolume(player, 0.5)); + renderer_->SetVolume(0.5f); + + // StartPlayingFrom triggers Seek which clears seek_pending. + EXPECT_CALL(mock_sbplayer_interface_, Seek(player, base::Seconds(12), _)); + renderer_->StartPlayingFrom(base::Seconds(12)); + + // SetPlaybackRate now reaches the interface after seek_pending is cleared. + EXPECT_CALL(mock_sbplayer_interface_, SetPlaybackRate(player, 1.25)); + renderer_->SetPlaybackRate(1.25); + + // Flush calls PrepareForSeek which pauses via SetPlaybackRate(0). + base::MockOnceClosure flush_cb; + EXPECT_CALL(mock_sbplayer_interface_, SetPlaybackRate(player, 0.0)); + EXPECT_CALL(flush_cb, Run()); + renderer_->Flush(flush_cb.Get()); +} + +TEST_F(UrlPlayerRendererTest, ReportsPlayerErrorDuringInitialization) { + SbPlayer player = ExpectPlayerCreation(); + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(init_cb_, Run(HasStatusCode(PIPELINE_ERROR_DECODE))); + EXPECT_CALL(renderer_client_, OnError(_)).Times(0); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + + ASSERT_TRUE(player_error_cb_); + player_error_cb_(player, context_, kSbPlayerErrorDecode, "decode failed"); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, ReportsPlayerErrorAfterInitialization) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + EXPECT_CALL(renderer_client_, OnError(HasStatusCode(PIPELINE_ERROR_DECODE))); + + player_error_cb_(player, context_, kSbPlayerErrorDecode, "decode failed"); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, AttachesCdmBeforePlayerCreation) { + SbDrmSystem drm_system = new SbDrmSystemPrivate(); + FakeCdmContext cdm_context(drm_system); + base::MockOnceCallback cdm_cb; + EXPECT_CALL(cdm_cb, Run(true)); + renderer_->SetCdm(&cdm_context, cdm_cb.Get()); + + SbPlayer player = ExpectPlayerCreation(); + EXPECT_CALL(mock_sbplayer_interface_, + SetUrlPlayerDrmSystem(player, drm_system)); + Initialize(player); + delete drm_system; +} + +TEST_F(UrlPlayerRendererTest, AttachesCdmAfterPlayerCreationAndRejectsSwitch) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + + SbDrmSystem drm_system = new SbDrmSystemPrivate(); + FakeCdmContext cdm_context(drm_system); + base::MockOnceCallback cdm_cb; + { + InSequence sequence; + EXPECT_CALL(mock_sbplayer_interface_, + SetUrlPlayerDrmSystem(player, drm_system)); + EXPECT_CALL(cdm_cb, Run(true)); + } + renderer_->SetCdm(&cdm_context, cdm_cb.Get()); + + base::MockOnceCallback replacement_cb; + EXPECT_CALL(replacement_cb, Run(false)); + renderer_->SetCdm(&cdm_context, replacement_cb.Get()); + delete drm_system; +} + +} // namespace +} // namespace media From 495d84abf1f668929773df2d4549d369d304bbd1 Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Wed, 17 Jun 2026 20:04:23 +0200 Subject: [PATCH 3/6] media: Add URL-player renderer client and GetMediaUrl Add UrlPlayerRendererClient to the tvOS URL-player build, which drives URL-player playback over Mojo by calling InitializeWithUrl() directly on the renderer pipe. Add MediaResource::GetMediaUrl() behind USE_STARBOARD_URL_PLAYER so the client can retrieve the media URL from the demuxer. The base implementation returns an empty GURL; UrlPlayerDemuxer will override it in a later CL. Client unit tests are deferred to a later CL when all their dependencies (InitializeWithStreamPointers, SetSourceUrl mojom) are available. Build: ninja -C out/tvos-arm64-device_qa cobalt media_unittests (pass) --- media/base/media_resource.cc | 7 + media/base/media_resource.h | 10 + media/mojo/clients/BUILD.gn | 7 + .../starboard/url_player_renderer_client.cc | 205 ++++++++++++++++++ .../starboard/url_player_renderer_client.h | 118 ++++++++++ 5 files changed, 347 insertions(+) create mode 100644 media/mojo/clients/starboard/url_player_renderer_client.cc create mode 100644 media/mojo/clients/starboard/url_player_renderer_client.h diff --git a/media/base/media_resource.cc b/media/base/media_resource.cc index deeadbbf89b2..866a540f4529 100644 --- a/media/base/media_resource.cc +++ b/media/base/media_resource.cc @@ -5,6 +5,7 @@ #include "media/base/media_resource.h" #include "base/no_destructor.h" +#include "media/media_buildflags.h" #include "net/cookies/site_for_cookies.h" #include "net/storage_access_api/status.h" #include "url/gurl.h" @@ -25,4 +26,10 @@ DemuxerStream* MediaResource::GetFirstStream(DemuxerStream::Type type) { return nullptr; } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +GURL MediaResource::GetMediaUrl() const { + return GURL(); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + } // namespace media diff --git a/media/base/media_resource.h b/media/base/media_resource.h index 62e0b605df2f..71156cfdbc93 100644 --- a/media/base/media_resource.h +++ b/media/base/media_resource.h @@ -11,6 +11,11 @@ #include "base/time/time.h" #include "media/base/demuxer_stream.h" #include "media/base/media_export.h" +#include "build/build_config.h" +#include "media/media_buildflags.h" +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#include "url/gurl.h" +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) namespace media { @@ -41,6 +46,11 @@ class MEDIA_EXPORT MediaResource { // A helper function that return the first stream of the given `type` if one // exists or a null pointer if there is no streams of that type. DemuxerStream* GetFirstStream(DemuxerStream::Type type); + +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + // Returns the media URL for URL based players (e.g., HLS via AVPlayer). + virtual GURL GetMediaUrl() const; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) }; } // namespace media diff --git a/media/mojo/clients/BUILD.gn b/media/mojo/clients/BUILD.gn index 40e2f7ab676a..f62c15fcbf9d 100644 --- a/media/mojo/clients/BUILD.gn +++ b/media/mojo/clients/BUILD.gn @@ -148,6 +148,13 @@ source_set("clients") { "//cobalt/renderer", ] } + + if (is_cobalt && use_starboard_url_player) { + sources += [ + "starboard/url_player_renderer_client.cc", + "starboard/url_player_renderer_client.h", + ] + } } static_library("test_support") { diff --git a/media/mojo/clients/starboard/url_player_renderer_client.cc b/media/mojo/clients/starboard/url_player_renderer_client.cc new file mode 100644 index 000000000000..d9c896bb00bb --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client.cc @@ -0,0 +1,205 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/mojo/clients/starboard/url_player_renderer_client.h" + +#include "base/functional/bind.h" +#include "base/logging.h" +#include "media/base/media_log.h" +#include "media/base/media_resource.h" +#include "media/base/video_frame.h" +#include "media/mojo/clients/mojo_renderer.h" +#include "media/renderers/video_overlay_factory.h" +#include "url/gurl.h" + +namespace media { + +UrlPlayerRendererClient::UrlPlayerRendererClient( + const scoped_refptr& media_task_runner, + std::unique_ptr media_log, + std::unique_ptr mojo_renderer, + std::unique_ptr video_overlay_factory, + VideoRendererSink* video_renderer_sink, + mojo::PendingRemote pending_renderer_extension, + mojo::PendingReceiver client_extension_receiver, + GetSbWindowHandleCallback get_sb_window_handle_callback) + : MojoRendererWrapper(std::move(mojo_renderer)), + media_task_runner_(media_task_runner), + media_log_(std::move(media_log)), + video_overlay_factory_(std::move(video_overlay_factory)), + video_renderer_sink_(video_renderer_sink), + pending_renderer_extension_(std::move(pending_renderer_extension)), + pending_client_extension_receiver_(std::move(client_extension_receiver)), + client_extension_receiver_(this), + get_sb_window_handle_callback_(get_sb_window_handle_callback) { + DCHECK(media_task_runner_); + DCHECK(video_renderer_sink_); + DCHECK(video_overlay_factory_); + LOG(INFO) << "UrlPlayerRendererClient constructed."; +} + +UrlPlayerRendererClient::~UrlPlayerRendererClient() = default; + +void UrlPlayerRendererClient::Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!init_cb_); + + client_ = client; + init_cb_ = std::move(init_cb); + + DCHECK(!AreMojoPipesConnected()); + InitAndBindMojoRenderer(base::BindOnce( + &UrlPlayerRendererClient::InitializeMojoRenderer, + weak_factory_.GetWeakPtr(), media_resource, client, + base::BindOnce(&UrlPlayerRendererClient::OnMojoRendererInitialized, + weak_factory_.GetWeakPtr()))); +} + +RendererType UrlPlayerRendererClient::GetRendererType() { + return RendererType::kUrlPlayer; +} + +void UrlPlayerRendererClient::PaintVideoHoleFrame(const gfx::Size& size) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + video_renderer_sink_->PaintSingleFrame( + video_overlay_factory_->CreateFrame(size)); +} + +void UrlPlayerRendererClient::UpdateStarboardRenderingMode( + const StarboardRenderingMode mode) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + rendering_mode_ = mode; + + switch (rendering_mode_) { + case StarboardRenderingMode::kPunchOut: + // URL player always uses punch-out. Nothing to do. + break; + case StarboardRenderingMode::kDecodeToTexture: + // URL player does not support DTT. Fail initialization. + LOG(ERROR) << "DTT mode not supported for URL player."; + SetMojoRendererInitialized( + PipelineStatus(DECODER_ERROR_NOT_SUPPORTED, + "URL player does not support decode-to-texture")); + if (!init_cb_.is_null()) { + std::move(init_cb_).Run(pipeline_status()); + } + return; + case StarboardRenderingMode::kInvalid: + LOG(ERROR) << "Invalid rendering mode."; + SetMojoRendererInitialized( + PipelineStatus(PIPELINE_ERROR_INITIALIZATION_FAILED, + "Invalid rendering mode for URL player")); + if (!init_cb_.is_null()) { + std::move(init_cb_).Run(pipeline_status()); + } + return; + } + + // Two-event protocol: if Mojo init already completed, run init_cb_ now. + if (IsMojoRendererInitialized() && !init_cb_.is_null()) { + std::move(init_cb_).Run(pipeline_status()); + } +} + +void UrlPlayerRendererClient::GetSbWindowHandle() { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + uint64_t sb_window_handle = 0; + if (get_sb_window_handle_callback_) { + sb_window_handle = get_sb_window_handle_callback_.Run(); + } + renderer_extension_->OnSbWindowHandleReady(sb_window_handle); +} + +void UrlPlayerRendererClient::OnConnectionError() { + LOG(ERROR) << "Mojo connection error."; + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + MEDIA_LOG(ERROR, media_log_) << "UrlPlayerRendererClient disconnected"; + if (!init_cb_.is_null()) { + std::move(init_cb_).Run(PIPELINE_ERROR_DISCONNECTED); + return; + } + client_->OnError(PIPELINE_ERROR_DISCONNECTED); +} + +void UrlPlayerRendererClient::InitAndBindMojoRenderer( + base::OnceClosure complete_cb) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(!AreMojoPipesConnected()); + + renderer_extension_.Bind(std::move(pending_renderer_extension_), + media_task_runner_); + client_extension_receiver_.Bind(std::move(pending_client_extension_receiver_), + media_task_runner_); + + renderer_extension_.set_disconnect_handler(base::BindOnce( + &UrlPlayerRendererClient::OnConnectionError, base::Unretained(this))); + + // URL player does not need GPU channel. Skip OnGpuChannelTokenReady. + DCHECK(complete_cb); + std::move(complete_cb).Run(); +} + +void UrlPlayerRendererClient::InitializeMojoRenderer( + MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(AreMojoPipesConnected()); + + GURL url = media_resource->GetMediaUrl(); + if (!url.is_valid()) { + LOG(ERROR) << "Invalid media URL."; + media_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(std::move(init_cb), + PipelineStatus(PIPELINE_ERROR_INITIALIZATION_FAILED, + "Invalid media URL for URL player"))); + return; + } + + InitializeWithUrl(client, url, std::move(init_cb)); +} + +void UrlPlayerRendererClient::OnMojoRendererInitialized(PipelineStatus status) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + + // Two-event protocol: if rendering mode already arrived, complete now. + // Otherwise, UpdateStarboardRenderingMode() will complete when it arrives. + if (rendering_mode_ != StarboardRenderingMode::kInvalid) { + DCHECK(!init_cb_.is_null()); + std::move(init_cb_).Run(status); + } + SetMojoRendererInitialized(status); +} + +void UrlPlayerRendererClient::SetMojoRendererInitialized( + PipelineStatus status) { + base::AutoLock auto_lock(lock_); + is_mojo_renderer_initialized_ = true; + pipeline_status_ = status; +} + +bool UrlPlayerRendererClient::IsMojoRendererInitialized() const { + base::AutoLock auto_lock(lock_); + return is_mojo_renderer_initialized_; +} + +PipelineStatus UrlPlayerRendererClient::pipeline_status() const { + base::AutoLock auto_lock(lock_); + return pipeline_status_; +} + +} // namespace media diff --git a/media/mojo/clients/starboard/url_player_renderer_client.h b/media/mojo/clients/starboard/url_player_renderer_client.h new file mode 100644 index 000000000000..a9360987e284 --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client.h @@ -0,0 +1,118 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEDIA_MOJO_CLIENTS_STARBOARD_URL_PLAYER_RENDERER_CLIENT_H_ +#define MEDIA_MOJO_CLIENTS_STARBOARD_URL_PLAYER_RENDERER_CLIENT_H_ + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/synchronization/lock.h" +#include "base/task/sequenced_task_runner.h" +#include "media/base/pipeline_status.h" +#include "media/base/renderer_client.h" +#include "media/base/starboard/starboard_rendering_mode.h" +#include "media/base/video_renderer_sink.h" +#include "media/mojo/clients/mojo_renderer_wrapper.h" +#include "media/mojo/mojom/renderer_extensions.mojom.h" +#include "media/starboard/starboard_callbacks.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" + +namespace media { +class MediaLog; +class MediaResource; +class MojoRenderer; +class VideoOverlayFactory; + +// UrlPlayerRendererClient lives in the renderer process (media thread) and +// talks to UrlPlayerRendererWrapper in the GPU process via Mojo. +// Simplified from StarboardRendererClient: no DTT, no RenderCallback, +// no GPU factories, no Android overlay. +class MEDIA_EXPORT UrlPlayerRendererClient + : public MojoRendererWrapper, + public mojom::StarboardRendererClientExtension { + public: + using RendererExtension = mojom::StarboardRendererExtension; + using ClientExtension = mojom::StarboardRendererClientExtension; + + UrlPlayerRendererClient( + const scoped_refptr& media_task_runner, + std::unique_ptr media_log, + std::unique_ptr mojo_renderer, + std::unique_ptr video_overlay_factory, + VideoRendererSink* video_renderer_sink, + mojo::PendingRemote pending_renderer_extension, + mojo::PendingReceiver client_extension_receiver, + GetSbWindowHandleCallback get_sb_window_handle_callback); + + UrlPlayerRendererClient(const UrlPlayerRendererClient&) = delete; + UrlPlayerRendererClient& operator=(const UrlPlayerRendererClient&) = delete; + + ~UrlPlayerRendererClient() override; + + // MojoRendererWrapper overrides. + void Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) override; + RendererType GetRendererType() override; + + // mojom::StarboardRendererClientExtension implementation. + void PaintVideoHoleFrame(const gfx::Size& size) override; + void UpdateStarboardRenderingMode(StarboardRenderingMode mode) override; + void GetSbWindowHandle() override; + + private: + void OnConnectionError(); + void InitAndBindMojoRenderer(base::OnceClosure complete_cb); + void InitializeMojoRenderer(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb); + bool AreMojoPipesConnected() const { + return client_extension_receiver_.is_bound() && + renderer_extension_.is_bound(); + } + void OnMojoRendererInitialized(PipelineStatus status); + void SetMojoRendererInitialized(PipelineStatus status); + bool IsMojoRendererInitialized() const; + PipelineStatus pipeline_status() const; + + scoped_refptr media_task_runner_; + std::unique_ptr media_log_; + std::unique_ptr video_overlay_factory_; + raw_ptr video_renderer_sink_ = nullptr; + mojo::PendingRemote pending_renderer_extension_; + mojo::PendingReceiver pending_client_extension_receiver_; + mojo::Receiver client_extension_receiver_; + const GetSbWindowHandleCallback get_sb_window_handle_callback_; + + mojo::Remote renderer_extension_; + + raw_ptr client_ = nullptr; + PipelineStatusCallback init_cb_; + + StarboardRenderingMode rendering_mode_ = StarboardRenderingMode::kInvalid; + + mutable base::Lock lock_; + bool is_mojo_renderer_initialized_ GUARDED_BY(lock_) = false; + PipelineStatus pipeline_status_ GUARDED_BY(lock_) = + PipelineStatus(PIPELINE_ERROR_INVALID_STATE); + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace media + +#endif // MEDIA_MOJO_CLIENTS_STARBOARD_URL_PLAYER_RENDERER_CLIENT_H_ From 81f108a8efd0cf1a297059603233581c3ee5dc63 Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Wed, 17 Jun 2026 20:42:05 +0200 Subject: [PATCH 4/6] media: Wire URL-player renderer factory plumbing Add CreateUrlPlayerRenderer to mojom::InterfaceFactory, guarded by use_starboard_url_player, and route it through the renderer process, browser media proxy, and GPU media service. Add UrlPlayerRendererClientFactory, which creates a URL-player MojoRenderer via MojoRendererFactory::CreateUrlPlayerRenderer and wraps it in UrlPlayerRendererClient. Add URL-player factory coverage and update fake InterfaceFactory implementations in tests. Build: ninja -C out/tvos-arm64-device_qa cobalt nplb media_unittests Bug: 512045535 --- .../media/frameless_media_interface_proxy.cc | 11 ++ .../media/frameless_media_interface_proxy.h | 10 ++ .../browser/media/media_interface_proxy.cc | 22 ++++ content/browser/media/media_interface_proxy.h | 10 ++ .../renderer/media/media_interface_factory.cc | 27 +++++ .../renderer/media/media_interface_factory.h | 10 ++ media/mojo/clients/BUILD.gn | 3 + media/mojo/clients/mojo_renderer_factory.cc | 26 ++++ media/mojo/clients/mojo_renderer_factory.h | 11 ++ .../url_player_renderer_client_factory.cc | 112 ++++++++++++++++++ .../url_player_renderer_client_factory.h | 66 +++++++++++ media/mojo/mojom/interface_factory.mojom | 10 ++ media/mojo/services/gpu_mojo_media_client.cc | 23 ++++ media/mojo/services/gpu_mojo_media_client.h | 11 ++ media/mojo/services/interface_factory_impl.cc | 25 ++++ media/mojo/services/interface_factory_impl.h | 10 ++ media/mojo/services/mojo_media_client.cc | 14 +++ media/mojo/services/mojo_media_client.h | 11 ++ media/mojo/services/test_mojo_media_client.cc | 14 +++ media/mojo/services/test_mojo_media_client.h | 11 ++ .../audio_track_mojo_encoder_unittest.cc | 12 ++ .../webcodecs/audio_decoder_broker_test.cc | 10 ++ .../webcodecs/video_decoder_broker_test.cc | 10 ++ 23 files changed, 469 insertions(+) create mode 100644 media/mojo/clients/starboard/url_player_renderer_client_factory.cc create mode 100644 media/mojo/clients/starboard/url_player_renderer_client_factory.h diff --git a/content/browser/media/frameless_media_interface_proxy.cc b/content/browser/media/frameless_media_interface_proxy.cc index 4a3925ae74c4..a20c500e43ad 100644 --- a/content/browser/media/frameless_media_interface_proxy.cc +++ b/content/browser/media/frameless_media_interface_proxy.cc @@ -152,6 +152,17 @@ void FramelessMediaInterfaceProxy::CreateStarboardRenderer( client_extension_remote) {} #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void FramelessMediaInterfaceProxy::CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) {} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void FramelessMediaInterfaceProxy::CreateCdm(const media::CdmConfig& cdm_config, CreateCdmCallback callback) { std::move(callback).Run(mojo::NullRemote(), nullptr, diff --git a/content/browser/media/frameless_media_interface_proxy.h b/content/browser/media/frameless_media_interface_proxy.h index 0725e852cf1f..2ce31934486b 100644 --- a/content/browser/media/frameless_media_interface_proxy.h +++ b/content/browser/media/frameless_media_interface_proxy.h @@ -94,6 +94,16 @@ class FramelessMediaInterfaceProxy final mojo::PendingRemote client_extension_remote) final; #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void CreateCdm(const media::CdmConfig& cdm_config, CreateCdmCallback callback) final; diff --git a/content/browser/media/media_interface_proxy.cc b/content/browser/media/media_interface_proxy.cc index db48d47d177a..caeef168185d 100644 --- a/content/browser/media/media_interface_proxy.cc +++ b/content/browser/media/media_interface_proxy.cc @@ -402,6 +402,28 @@ void MediaInterfaceProxy::CreateStarboardRenderer( } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MediaInterfaceProxy::CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) { + DCHECK(thread_checker_.CalledOnValidThread()); + DVLOG(1) << __func__ << ": this=" << this; + + InterfaceFactory* factory = media_interface_factory_ptr_->Get(); + if (factory) { + factory->CreateUrlPlayerRenderer( + std::move(media_log_remote), config, + std::move(receiver), std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + } +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void MediaInterfaceProxy::CreateCdm(const media::CdmConfig& cdm_config, CreateCdmCallback create_cdm_cb) { DCHECK(thread_checker_.CalledOnValidThread()); diff --git a/content/browser/media/media_interface_proxy.h b/content/browser/media/media_interface_proxy.h index 4ef8f90d46c7..aa27f83fc5c4 100644 --- a/content/browser/media/media_interface_proxy.h +++ b/content/browser/media/media_interface_proxy.h @@ -97,6 +97,16 @@ class MediaInterfaceProxy final : public DocumentUserData, mojo::PendingRemote client_extension_remote) final; #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void CreateCdm(const media::CdmConfig& cdm_config, CreateCdmCallback create_cdm_cb) final; diff --git a/content/renderer/media/media_interface_factory.cc b/content/renderer/media/media_interface_factory.cc index 580ac2cb8588..9ecea775e678 100644 --- a/content/renderer/media/media_interface_factory.cc +++ b/content/renderer/media/media_interface_factory.cc @@ -202,6 +202,33 @@ void MediaInterfaceFactory::CreateStarboardRenderer( } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MediaInterfaceFactory::CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) { + if (!task_runner_->BelongsToCurrentThread()) { + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&MediaInterfaceFactory::CreateUrlPlayerRenderer, + weak_this_, std::move(media_log_remote), + config, std::move(receiver), + std::move(renderer_extension_receiver), + std::move(client_extension_remote))); + return; + } + + GetMediaInterfaceFactory()->CreateUrlPlayerRenderer( + std::move(media_log_remote), config, + std::move(receiver), std::move(renderer_extension_receiver), + std::move(client_extension_remote)); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void MediaInterfaceFactory::CreateCdm(const media::CdmConfig& cdm_config, CreateCdmCallback callback) { if (!task_runner_->BelongsToCurrentThread()) { diff --git a/content/renderer/media/media_interface_factory.h b/content/renderer/media/media_interface_factory.h index 17ae767af946..6a90318024ef 100644 --- a/content/renderer/media/media_interface_factory.h +++ b/content/renderer/media/media_interface_factory.h @@ -92,6 +92,16 @@ class MediaInterfaceFactory final : public media::mojom::InterfaceFactory { mojo::PendingRemote client_extension_remote) final; #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void CreateCdm(const media::CdmConfig& cdm_config, CreateCdmCallback callback) final; diff --git a/media/mojo/clients/BUILD.gn b/media/mojo/clients/BUILD.gn index f62c15fcbf9d..e36691ee765c 100644 --- a/media/mojo/clients/BUILD.gn +++ b/media/mojo/clients/BUILD.gn @@ -153,6 +153,8 @@ source_set("clients") { sources += [ "starboard/url_player_renderer_client.cc", "starboard/url_player_renderer_client.h", + "starboard/url_player_renderer_client_factory.cc", + "starboard/url_player_renderer_client_factory.h", ] } } @@ -219,4 +221,5 @@ source_set("unit_tests") { if (is_cobalt && use_starboard_media) { sources += [ "starboard/starboard_renderer_client_unittest.cc" ] } + } diff --git a/media/mojo/clients/mojo_renderer_factory.cc b/media/mojo/clients/mojo_renderer_factory.cc index 2e6a336a82f7..3af0fb158317 100644 --- a/media/mojo/clients/mojo_renderer_factory.cc +++ b/media/mojo/clients/mojo_renderer_factory.cc @@ -134,4 +134,30 @@ std::unique_ptr MojoRendererFactory::CreateStarboardRenderer( } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +std::unique_ptr MojoRendererFactory::CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote, + const scoped_refptr& media_task_runner, + VideoRendererSink* video_renderer_sink) { + DCHECK(interface_factory_); + + mojo::PendingRemote renderer_remote; + interface_factory_->CreateUrlPlayerRenderer( + std::move(media_log_remote), config, + renderer_remote.InitWithNewPipeAndPassReceiver(), + std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + + return std::make_unique( + media_task_runner, nullptr, video_renderer_sink, + std::move(renderer_remote), + config.experimental_features.bypass_mojo_for_media); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + } // namespace media diff --git a/media/mojo/clients/mojo_renderer_factory.h b/media/mojo/clients/mojo_renderer_factory.h index 5bba7c45ba00..da43d044727a 100644 --- a/media/mojo/clients/mojo_renderer_factory.h +++ b/media/mojo/clients/mojo_renderer_factory.h @@ -86,6 +86,17 @@ class MojoRendererFactory final : public RendererFactory { const scoped_refptr& media_task_runner, VideoRendererSink* video_renderer_sink); #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + std::unique_ptr CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote, + const scoped_refptr& media_task_runner, + VideoRendererSink* video_renderer_sink); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) private: // InterfaceFactory or InterfaceProvider used to create or connect to remote diff --git a/media/mojo/clients/starboard/url_player_renderer_client_factory.cc b/media/mojo/clients/starboard/url_player_renderer_client_factory.cc new file mode 100644 index 000000000000..303c9ea39e83 --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client_factory.cc @@ -0,0 +1,112 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/mojo/clients/starboard/url_player_renderer_client_factory.h" + +#include "base/check.h" +#include "base/logging.h" +#include "base/task/sequenced_task_runner.h" +#include "base/time/time.h" +#include "media/base/starboard/starboard_renderer_config.h" +#include "media/mojo/clients/mojo_media_log_service.h" +#include "media/mojo/clients/mojo_renderer.h" +#include "media/mojo/clients/mojo_renderer_factory.h" +#include "media/mojo/clients/starboard/url_player_renderer_client.h" +#include "media/mojo/mojom/renderer_extensions.mojom.h" +#include "media/renderers/video_overlay_factory.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/self_owned_receiver.h" + +namespace media { + +namespace { + +StarboardRendererConfig::ExperimentalFeatures GetUrlPlayerFeatures( + const StarboardRendererConfig::ExperimentalFeatures& features) { + auto url_player_features = features; + url_player_features.bypass_mojo_for_media = false; + return url_player_features; +} +} // namespace + +UrlPlayerRendererClientFactory::UrlPlayerRendererClientFactory( + MediaLog* media_log, + std::unique_ptr mojo_renderer_factory, + const media::RendererFactoryTraits* traits) + : media_log_(media_log), + mojo_renderer_factory_(std::move(mojo_renderer_factory)), + experimental_features_( + GetUrlPlayerFeatures(traits->experimental_features)), + viewport_size_(traits->viewport_size), + get_sb_window_handle_callback_(traits->get_sb_window_handle_callback) {} + +UrlPlayerRendererClientFactory::~UrlPlayerRendererClientFactory() = default; + +std::unique_ptr UrlPlayerRendererClientFactory::CreateRenderer( + const scoped_refptr& media_task_runner, + const scoped_refptr& /*worker_task_runner*/, + AudioRendererSink* /*audio_renderer_sink*/, + VideoRendererSink* video_renderer_sink, + RequestOverlayInfoCB /*request_overlay_info_cb*/, + const gfx::ColorSpace& /*target_color_space*/) { + DCHECK(video_renderer_sink); + DCHECK(media_log_); + DCHECK(mojo_renderer_factory_); + + // Clone media log via Mojo pipe. + mojo::PendingReceiver media_log_pending_receiver; + auto media_log_pending_remote = + media_log_pending_receiver.InitWithNewPipeAndPassRemote(); + mojo::MakeSelfOwnedReceiver( + std::make_unique(media_log_->Clone()), + std::move(media_log_pending_receiver)); + + // Create renderer extension pipe pair. + mojo::PendingRemote + renderer_extension_remote; + auto renderer_extension_receiver = + renderer_extension_remote.InitWithNewPipeAndPassReceiver(); + + // Create client extension pipe pair. + mojo::PendingRemote + client_extension_remote; + auto client_extension_receiver = + client_extension_remote.InitWithNewPipeAndPassReceiver(); + + // Create overlay factory for video hole punch-out. + auto overlay_factory = std::make_unique(); + + // Build config. URL player ignores audio write durations and max video + // capabilities, but StarboardRendererConfig requires all fields. + StarboardRendererConfig config( + overlay_factory->overlay_plane_id(), base::TimeDelta(), base::TimeDelta(), + std::string(), experimental_features_, viewport_size_); + + // Create MojoRenderer via the URL player factory path. + std::unique_ptr mojo_renderer = + mojo_renderer_factory_->CreateUrlPlayerRenderer( + std::move(media_log_pending_remote), config, + std::move(renderer_extension_receiver), + std::move(client_extension_remote), media_task_runner, + video_renderer_sink); + + return std::make_unique( + media_task_runner, media_log_->Clone(), std::move(mojo_renderer), + std::move(overlay_factory), video_renderer_sink, + std::move(renderer_extension_remote), + std::move(client_extension_receiver), get_sb_window_handle_callback_); +} + +} // namespace media diff --git a/media/mojo/clients/starboard/url_player_renderer_client_factory.h b/media/mojo/clients/starboard/url_player_renderer_client_factory.h new file mode 100644 index 000000000000..8da20f3ef5b6 --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client_factory.h @@ -0,0 +1,66 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEDIA_MOJO_CLIENTS_STARBOARD_URL_PLAYER_RENDERER_CLIENT_FACTORY_H_ +#define MEDIA_MOJO_CLIENTS_STARBOARD_URL_PLAYER_RENDERER_CLIENT_FACTORY_H_ + +#include "base/memory/raw_ptr.h" +#include "base/task/sequenced_task_runner.h" +#include "media/base/renderer_factory.h" +#include "media/base/starboard/renderer_factory_traits.h" +#include "media/base/starboard/starboard_renderer_config.h" +#include "media/starboard/starboard_callbacks.h" + +namespace media { +class MediaLog; +class MojoRendererFactory; + +// Creates UrlPlayerRendererClient for tvOS URL-based playback. +// Mirrors StarboardRendererClientFactory but omits GPU factories, +// audio write durations, and max video capabilities. +class MEDIA_EXPORT UrlPlayerRendererClientFactory final + : public RendererFactory { + public: + UrlPlayerRendererClientFactory( + MediaLog* media_log, + std::unique_ptr mojo_renderer_factory, + const media::RendererFactoryTraits* traits); + + UrlPlayerRendererClientFactory(const UrlPlayerRendererClientFactory&) = + delete; + UrlPlayerRendererClientFactory& operator=( + const UrlPlayerRendererClientFactory&) = delete; + + ~UrlPlayerRendererClientFactory() final; + + // RendererFactory implementation. + std::unique_ptr CreateRenderer( + const scoped_refptr& media_task_runner, + const scoped_refptr& worker_task_runner, + AudioRendererSink* audio_renderer_sink, + VideoRendererSink* video_renderer_sink, + RequestOverlayInfoCB request_overlay_info_cb, + const gfx::ColorSpace& target_color_space) final; + + private: + raw_ptr media_log_; + std::unique_ptr mojo_renderer_factory_; + const StarboardRendererConfig::ExperimentalFeatures experimental_features_; + const gfx::Size viewport_size_; + const GetSbWindowHandleCallback get_sb_window_handle_callback_; +}; + +} // namespace media + +#endif // MEDIA_MOJO_CLIENTS_STARBOARD_URL_PLAYER_RENDERER_CLIENT_FACTORY_H_ diff --git a/media/mojo/mojom/interface_factory.mojom b/media/mojo/mojom/interface_factory.mojom index 734abf1b2e4e..71c4261546db 100644 --- a/media/mojo/mojom/interface_factory.mojom +++ b/media/mojo/mojom/interface_factory.mojom @@ -113,6 +113,16 @@ interface InterfaceFactory { pending_receiver renderer_extension, pending_remote client_extension); + [EnableIf=use_starboard_url_player] + // Creates a UrlPlayerRenderer for tvOS URL-based playback. + // Uses the same extension endpoints as StarboardRenderer. + CreateUrlPlayerRenderer( + pending_remote media_log, + StarboardRendererConfig config, + pending_receiver renderer, + pending_receiver renderer_extension, + pending_remote client_extension); + // Creates a CDM based on the `cdm_config` provided. The `key_system` in the // `cdm_config` is a generic term for a decryption mechanism and/or content // protection provider. It should be a reverse domain name, diff --git a/media/mojo/services/gpu_mojo_media_client.cc b/media/mojo/services/gpu_mojo_media_client.cc index e144fa5fa3e4..7052f4743ba1 100644 --- a/media/mojo/services/gpu_mojo_media_client.cc +++ b/media/mojo/services/gpu_mojo_media_client.cc @@ -28,6 +28,10 @@ #include "media/mojo/mojom/video_decoder.mojom.h" #include "media/video/video_decode_accelerator.h" +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#include "media/mojo/services/starboard/url_player_renderer_wrapper.h" +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + namespace media { namespace { @@ -312,6 +316,25 @@ std::unique_ptr GpuMojoMediaClient::CreateStarboardRenderer( } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +std::unique_ptr GpuMojoMediaClient::CreateUrlPlayerRenderer( + mojom::FrameInterfaceFactory* /* frame_interfaces */, + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) { + UrlPlayerRendererTraits traits( + task_runner, std::move(media_log_remote), + video_geometry_setter_service_, config.overlay_plane_id, + config.viewport_size, std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + return std::make_unique(std::move(traits)); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + std::unique_ptr GpuMojoMediaClient::CreateCdmFactory( mojom::FrameInterfaceFactory* frame_interfaces) { return CreatePlatformCdmFactory(frame_interfaces); diff --git a/media/mojo/services/gpu_mojo_media_client.h b/media/mojo/services/gpu_mojo_media_client.h index 66d7113ede2e..9101c8f7752b 100644 --- a/media/mojo/services/gpu_mojo_media_client.h +++ b/media/mojo/services/gpu_mojo_media_client.h @@ -200,6 +200,17 @@ class MEDIA_MOJO_EXPORT GpuMojoMediaClient : public MojoMediaClient { mojo::PendingRemote client_extension_remote) final; #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + std::unique_ptr CreateUrlPlayerRenderer( + mojom::FrameInterfaceFactory* frame_interfaces, + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) std::unique_ptr CreateCdmFactory( mojom::FrameInterfaceFactory* interface_provider) final; diff --git a/media/mojo/services/interface_factory_impl.cc b/media/mojo/services/interface_factory_impl.cc index 755e536eee6e..6d86a80069dc 100644 --- a/media/mojo/services/interface_factory_impl.cc +++ b/media/mojo/services/interface_factory_impl.cc @@ -312,6 +312,31 @@ void InterfaceFactoryImpl::CreateStarboardRenderer( } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void InterfaceFactoryImpl::CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) { + DVLOG(2) << __func__; + auto renderer = mojo_media_client_->CreateUrlPlayerRenderer( + frame_interfaces_.get(), + base::SingleThreadTaskRunner::GetCurrentDefault(), + std::move(media_log_remote), config, + std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + if (!renderer) { + DLOG(ERROR) << "UrlPlayerRenderer creation failed."; + return; + } + + AddRenderer(std::move(renderer), std::move(receiver)); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InterfaceFactoryImpl::CreateCdm(const CdmConfig& cdm_config, CreateCdmCallback callback) { DVLOG(2) << __func__; diff --git a/media/mojo/services/interface_factory_impl.h b/media/mojo/services/interface_factory_impl.h index b6f46a402d94..dd8353629060 100644 --- a/media/mojo/services/interface_factory_impl.h +++ b/media/mojo/services/interface_factory_impl.h @@ -98,6 +98,16 @@ class InterfaceFactoryImpl final mojo::PendingRemote client_extension_remote) final; #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void CreateCdm(const CdmConfig& cdm_config, CreateCdmCallback callback) final; diff --git a/media/mojo/services/mojo_media_client.cc b/media/mojo/services/mojo_media_client.cc index 28909de4dc68..41daad8f7cda 100644 --- a/media/mojo/services/mojo_media_client.cc +++ b/media/mojo/services/mojo_media_client.cc @@ -110,6 +110,20 @@ std::unique_ptr MojoMediaClient::CreateStarboardRenderer( } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +std::unique_ptr MojoMediaClient::CreateUrlPlayerRenderer( + mojom::FrameInterfaceFactory* frame_interfaces, + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) { + return nullptr; +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + std::unique_ptr MojoMediaClient::CreateCdmFactory( mojom::FrameInterfaceFactory* frame_interfaces) { return nullptr; diff --git a/media/mojo/services/mojo_media_client.h b/media/mojo/services/mojo_media_client.h index 9cd97687865a..1b5e0f10a6bf 100644 --- a/media/mojo/services/mojo_media_client.h +++ b/media/mojo/services/mojo_media_client.h @@ -137,6 +137,17 @@ class MEDIA_MOJO_EXPORT MojoMediaClient { mojo::PendingRemote client_extension_remote); #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + virtual std::unique_ptr CreateUrlPlayerRenderer( + mojom::FrameInterfaceFactory* frame_interfaces, + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) // Returns the CdmFactory to be used by MojoCdmService. |frame_interfaces| // can be used to request interfaces provided remotely by the host. It may diff --git a/media/mojo/services/test_mojo_media_client.cc b/media/mojo/services/test_mojo_media_client.cc index 31a731411f47..9ef232f0cee0 100644 --- a/media/mojo/services/test_mojo_media_client.cc +++ b/media/mojo/services/test_mojo_media_client.cc @@ -124,6 +124,20 @@ std::unique_ptr TestMojoMediaClient::CreateStarboardRenderer( } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +std::unique_ptr TestMojoMediaClient::CreateUrlPlayerRenderer( + mojom::FrameInterfaceFactory* frame_interfaces, + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) { + return nullptr; +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + std::unique_ptr TestMojoMediaClient::CreateCdmFactory( mojom::FrameInterfaceFactory* /* frame_interfaces */) { DVLOG(1) << __func__; diff --git a/media/mojo/services/test_mojo_media_client.h b/media/mojo/services/test_mojo_media_client.h index 75d805ca5cb4..d60a16f49c29 100644 --- a/media/mojo/services/test_mojo_media_client.h +++ b/media/mojo/services/test_mojo_media_client.h @@ -56,6 +56,17 @@ class TestMojoMediaClient final : public MojoMediaClient { mojo::PendingRemote client_extension_remote) final; #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + std::unique_ptr CreateUrlPlayerRenderer( + mojom::FrameInterfaceFactory* frame_interfaces, + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + const StarboardRendererConfig& config, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) std::unique_ptr CreateCdmFactory( mojom::FrameInterfaceFactory* /* frame_interfaces */) final; diff --git a/third_party/blink/renderer/modules/mediarecorder/audio_track_mojo_encoder_unittest.cc b/third_party/blink/renderer/modules/mediarecorder/audio_track_mojo_encoder_unittest.cc index e14703213525..abf46f9efe06 100644 --- a/third_party/blink/renderer/modules/mediarecorder/audio_track_mojo_encoder_unittest.cc +++ b/third_party/blink/renderer/modules/mediarecorder/audio_track_mojo_encoder_unittest.cc @@ -175,6 +175,18 @@ class TestInterfaceFactory final : public media::mojom::InterfaceFactory { NOTREACHED(); } #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) { + NOTREACHED(); + } +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) private: TestAudioEncoder audio_encoder_; diff --git a/third_party/blink/renderer/modules/webcodecs/audio_decoder_broker_test.cc b/third_party/blink/renderer/modules/webcodecs/audio_decoder_broker_test.cc index ea58cf49684f..9d5577989565 100644 --- a/third_party/blink/renderer/modules/webcodecs/audio_decoder_broker_test.cc +++ b/third_party/blink/renderer/modules/webcodecs/audio_decoder_broker_test.cc @@ -184,6 +184,16 @@ class FakeInterfaceFactory : public media::mojom::InterfaceFactory { mojo::PendingRemote client_extension_remote) override {} #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) override {} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) private: FakeMojoMediaClient mojo_media_client_; diff --git a/third_party/blink/renderer/modules/webcodecs/video_decoder_broker_test.cc b/third_party/blink/renderer/modules/webcodecs/video_decoder_broker_test.cc index 602df9af07dd..e08db594f0b4 100644 --- a/third_party/blink/renderer/modules/webcodecs/video_decoder_broker_test.cc +++ b/third_party/blink/renderer/modules/webcodecs/video_decoder_broker_test.cc @@ -178,6 +178,16 @@ class FakeInterfaceFactory : public media::mojom::InterfaceFactory { mojo::PendingRemote client_extension_remote) override {} #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log_remote, + const media::StarboardRendererConfig& config, + mojo::PendingReceiver receiver, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) override {} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) private: media::MojoCdmServiceContext cdm_service_context_; FakeMojoMediaClient mojo_media_client_; From 3c3523251d046ba07f2ccd17b73de8e88d4c89ef Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Thu, 18 Jun 2026 08:14:51 +0200 Subject: [PATCH 5/6] media: Add URL-player demuxer and HLS URL predicate Add UrlPlayerDemuxer for tvOS URL-based playback. The demuxer provides placeholder audio/video DemuxerStreams required by PipelineImpl while the platform handles actual demuxing, and exposes the source URL through GetMediaUrl(). Add IsHlsUrl() for detecting HLS URLs, plus a stub OverrideDemuxerForUrl() in CobaltContentRendererClient. The override is left inactive until the renderer routing CL lands. Add kUrlPlayerDemuxer to DemuxerType, including the Mojom enum, traits mapping, and histogram entry. Build: ninja -C out/tvos-arm64-device_qa cobalt nplb media_unittests Bug: 512045535 --- .../cobalt_content_renderer_client.cc | 12 +- .../renderer/cobalt_content_renderer_client.h | 6 + media/base/demuxer.h | 1 + media/mojo/mojom/media_types.mojom | 1 + .../mojom/media_types_enum_mojom_traits.h | 9 +- media/starboard/BUILD.gn | 2 + media/starboard/url_player_demuxer.cc | 158 ++++++++++++++++++ media/starboard/url_player_demuxer.h | 102 +++++++++++ tools/metrics/histograms/enums.xml | 1 + 9 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 media/starboard/url_player_demuxer.cc create mode 100644 media/starboard/url_player_demuxer.h diff --git a/cobalt/renderer/cobalt_content_renderer_client.cc b/cobalt/renderer/cobalt_content_renderer_client.cc index f7ff3113e180..aeba2aa1deab 100644 --- a/cobalt/renderer/cobalt_content_renderer_client.cc +++ b/cobalt/renderer/cobalt_content_renderer_client.cc @@ -19,6 +19,7 @@ #include "base/task/bind_post_task.h" #include "base/time/time.h" +#include "build/build_config.h" #include "cobalt/media/service/mojom/platform_window_provider.mojom.h" #include "cobalt/renderer/cobalt_render_frame_observer.h" #include "cobalt/shell/common/url_constants.h" @@ -30,6 +31,7 @@ #include "media/base/key_systems_support_registration.h" #include "media/base/media_log.h" #include "media/base/renderer_factory.h" +#include "media/media_buildflags.h" #include "media/mojo/clients/starboard/starboard_renderer_client_factory.h" #include "mojo/public/cpp/bindings/generic_pending_receiver.h" #include "starboard/media.h" @@ -41,7 +43,6 @@ #include "third_party/blink/public/web/web_security_policy.h" #include "third_party/blink/public/web/web_view.h" #include "ui/gfx/geometry/size_conversions.h" - namespace cobalt { namespace { @@ -509,4 +510,13 @@ void CobaltContentRendererClient::PostSandboxInitialized() { } } +std::unique_ptr<::media::Demuxer> +CobaltContentRendererClient::OverrideDemuxerForUrl( + content::RenderFrame* render_frame, + const GURL& url, + scoped_refptr task_runner) { + // TODO(512045535): Override with UrlPlayerDemuxer for HLS URLs. + return nullptr; +} + } // namespace cobalt diff --git a/cobalt/renderer/cobalt_content_renderer_client.h b/cobalt/renderer/cobalt_content_renderer_client.h index 42c8cf5bc100..61f8e7c23ce7 100644 --- a/cobalt/renderer/cobalt_content_renderer_client.h +++ b/cobalt/renderer/cobalt_content_renderer_client.h @@ -16,6 +16,7 @@ #define COBALT_RENDERER_COBALT_CONTENT_RENDERER_CLIENT_H_ #include +#include #include "base/functional/callback.h" #include "base/memory/weak_ptr.h" @@ -26,6 +27,7 @@ #include "cobalt/media/audio/cobalt_audio_device_factory.h" #include "cobalt/media/service/mojom/platform_window_provider.mojom.h" #include "content/public/renderer/content_renderer_client.h" +#include "media/base/demuxer.h" #include "media/base/key_systems_support_registration.h" #include "media/base/starboard/renderer_factory_traits.h" #include "mojo/public/cpp/bindings/remote.h" @@ -66,6 +68,10 @@ class CobaltContentRendererClient : public content::ContentRendererClient { void GetStarboardRendererFactoryTraits( ::media::RendererFactoryTraits* traits) override; void PostSandboxInitialized() override; + std::unique_ptr<::media::Demuxer> OverrideDemuxerForUrl( + content::RenderFrame* render_frame, + const GURL& url, + scoped_refptr task_runner) override; uint64_t GetSbWindowHandle() const { return sb_window_handle_; } diff --git a/media/base/demuxer.h b/media/base/demuxer.h index 6d4512496f23..7581f21fe7f4 100644 --- a/media/base/demuxer.h +++ b/media/base/demuxer.h @@ -38,6 +38,7 @@ enum class DemuxerType { kFrameInjectingDemuxer = 5, kStreamProviderDemuxer = 6, kManifestDemuxer = 7, + kUrlPlayerDemuxer = 8, // URL-based native player (e.g., HLS on tvOS). }; class MEDIA_EXPORT DemuxerHost { diff --git a/media/mojo/mojom/media_types.mojom b/media/mojo/mojom/media_types.mojom index 0621d33fbef5..067b09a903aa 100644 --- a/media/mojo/mojom/media_types.mojom +++ b/media/mojo/mojom/media_types.mojom @@ -664,6 +664,7 @@ enum DemuxerType { kFrameInjectingDemuxer = 5, kStreamProviderDemuxer = 6, kManifestDemuxer = 7, + kUrlPlayerDemuxer = 8, // URL-based native player (e.g., HLS on tvOS). }; // See media::CreateCdmStatus diff --git a/media/mojo/mojom/media_types_enum_mojom_traits.h b/media/mojo/mojom/media_types_enum_mojom_traits.h index 60d99fa84231..03b24b66af48 100644 --- a/media/mojo/mojom/media_types_enum_mojom_traits.h +++ b/media/mojo/mojom/media_types_enum_mojom_traits.h @@ -391,11 +391,11 @@ struct EnumTraits { #if BUILDFLAG(USE_STARBOARD_MEDIA) case ::media::RendererType::kStarboard: return media::mojom::RendererType::kStarboard; +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) #if BUILDFLAG(USE_STARBOARD_URL_PLAYER) case ::media::RendererType::kUrlPlayer: return media::mojom::RendererType::kUrlPlayer; #endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) } NOTREACHED(); @@ -440,12 +440,12 @@ struct EnumTraits { case media::mojom::RendererType::kStarboard: *output = ::media::RendererType::kStarboard; return true; +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) #if BUILDFLAG(USE_STARBOARD_URL_PLAYER) case media::mojom::RendererType::kUrlPlayer: *output = ::media::RendererType::kUrlPlayer; return true; #endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) } NOTREACHED(); @@ -470,6 +470,8 @@ struct EnumTraits { return media::mojom::DemuxerType::kStreamProviderDemuxer; case ::media::DemuxerType::kManifestDemuxer: return media::mojom::DemuxerType::kManifestDemuxer; + case ::media::DemuxerType::kUrlPlayerDemuxer: + return media::mojom::DemuxerType::kUrlPlayerDemuxer; } NOTREACHED(); @@ -501,6 +503,9 @@ struct EnumTraits { case media::mojom::DemuxerType::kManifestDemuxer: *output = ::media::DemuxerType::kManifestDemuxer; return true; + case media::mojom::DemuxerType::kUrlPlayerDemuxer: + *output = ::media::DemuxerType::kUrlPlayerDemuxer; + return true; } NOTREACHED(); diff --git a/media/starboard/BUILD.gn b/media/starboard/BUILD.gn index 22b166a2e940..77e6d25b680e 100644 --- a/media/starboard/BUILD.gn +++ b/media/starboard/BUILD.gn @@ -61,6 +61,8 @@ source_set("starboard") { } if (use_starboard_url_player) { sources += [ + "url_player_demuxer.cc", + "url_player_demuxer.h", "url_player_renderer.cc", "url_player_renderer.h", ] diff --git a/media/starboard/url_player_demuxer.cc b/media/starboard/url_player_demuxer.cc new file mode 100644 index 000000000000..6b09996448f6 --- /dev/null +++ b/media/starboard/url_player_demuxer.cc @@ -0,0 +1,158 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/starboard/url_player_demuxer.h" + +#include +#include + +#include "base/functional/bind.h" +#include "base/logging.h" +#include "base/task/sequenced_task_runner.h" +#include "media/base/audio_decoder_config.h" +#include "media/base/channel_layout.h" +#include "media/base/decoder_buffer.h" +#include "media/base/media_track.h" +#include "media/base/sample_format.h" +#include "media/base/video_decoder_config.h" + +namespace media { + +bool IsHlsUrl(const GURL& url) { + // See + // https://github.com/youtube/cobalt/blob/25.lts.stable/cobalt/dom/html_media_element.cc#L103 + auto path = url.path_piece(); + return path.ends_with(".m3u8") || + path.find("hls_variant") != std::string_view::npos; +} + +// --- UrlPlayerDemuxerStream --- + +UrlPlayerDemuxerStream::UrlPlayerDemuxerStream(Type type) : type_(type) {} + +UrlPlayerDemuxerStream::~UrlPlayerDemuxerStream() = default; + +void UrlPlayerDemuxerStream::Read(uint32_t count, ReadCB read_cb) { + // The platform URL player handles all data loading and Read() should never + // be called on placeholder streams. + NOTREACHED(); +} + +AudioDecoderConfig UrlPlayerDemuxerStream::audio_decoder_config() { + return AudioDecoderConfig(AudioCodec::kAAC, kSampleFormatS16, + CHANNEL_LAYOUT_STEREO, + /*samples_per_second=*/44100, /*extra_data=*/{}, + EncryptionScheme::kUnencrypted); +} + +VideoDecoderConfig UrlPlayerDemuxerStream::video_decoder_config() { + static const gfx::Size kPlaceholderSize(1, 1); + return VideoDecoderConfig( + VideoCodec::kH264, VideoCodecProfile::H264PROFILE_BASELINE, + VideoDecoderConfig::AlphaMode::kIsOpaque, VideoColorSpace(), + kNoTransformation, kPlaceholderSize, gfx::Rect(kPlaceholderSize), + kPlaceholderSize, /*extra_data=*/{}, EncryptionScheme::kUnencrypted); +} + +DemuxerStream::Type UrlPlayerDemuxerStream::type() const { + return type_; +} + +bool UrlPlayerDemuxerStream::SupportsConfigChanges() { + return false; +} + +// --- UrlPlayerDemuxer --- + +UrlPlayerDemuxer::UrlPlayerDemuxer( + scoped_refptr media_task_runner, + GURL url) + : media_task_runner_(std::move(media_task_runner)), url_(std::move(url)) { + DCHECK(media_task_runner_); +} + +UrlPlayerDemuxer::~UrlPlayerDemuxer() = default; + +std::vector UrlPlayerDemuxer::GetAllStreams() { + return {&audio_stream_, &video_stream_}; +} + +GURL UrlPlayerDemuxer::GetMediaUrl() const { + return url_; +} + +std::string UrlPlayerDemuxer::GetDisplayName() const { + return "UrlPlayerDemuxer"; +} + +DemuxerType UrlPlayerDemuxer::GetDemuxerType() const { + return DemuxerType::kUrlPlayerDemuxer; +} + +void UrlPlayerDemuxer::Initialize(DemuxerHost* host, + PipelineStatusCallback status_cb) { + DVLOG(1) << __func__; + host_ = host; + media_task_runner_->PostTask( + FROM_HERE, base::BindOnce(std::move(status_cb), PIPELINE_OK)); +} + +void UrlPlayerDemuxer::AbortPendingReads() {} +void UrlPlayerDemuxer::StartWaitingForSeek(base::TimeDelta seek_time) {} +void UrlPlayerDemuxer::CancelPendingSeek(base::TimeDelta seek_time) {} + +void UrlPlayerDemuxer::Seek(base::TimeDelta time, + PipelineStatusCallback status_cb) { + DVLOG(1) << __func__ << "(" << time << ")"; + media_task_runner_->PostTask( + FROM_HERE, base::BindOnce(std::move(status_cb), PIPELINE_OK)); +} + +// While the demuxer itself is not seekable, the underlying URL player is. +bool UrlPlayerDemuxer::IsSeekable() const { + return true; +} + +void UrlPlayerDemuxer::Stop() {} + +base::TimeDelta UrlPlayerDemuxer::GetStartTime() const { + return base::TimeDelta(); +} + +base::Time UrlPlayerDemuxer::GetTimelineOffset() const { + return base::Time(); +} + +int64_t UrlPlayerDemuxer::GetMemoryUsage() const { + return 0; +} + +std::optional +UrlPlayerDemuxer::GetContainerForMetrics() const { + return std::nullopt; +} + +void UrlPlayerDemuxer::OnTracksChanged( + DemuxerStream::Type track_type, + const std::vector& track_ids, + base::TimeDelta curr_time, + TrackChangeCB change_completed_cb) { + std::vector streams; + std::move(change_completed_cb).Run(streams); + DLOG(WARNING) << "Track changes are not supported by UrlPlayerDemuxer."; +} + +void UrlPlayerDemuxer::SetPlaybackRate(double rate) {} + +} // namespace media diff --git a/media/starboard/url_player_demuxer.h b/media/starboard/url_player_demuxer.h new file mode 100644 index 000000000000..871a324cda76 --- /dev/null +++ b/media/starboard/url_player_demuxer.h @@ -0,0 +1,102 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEDIA_STARBOARD_URL_PLAYER_DEMUXER_H_ +#define MEDIA_STARBOARD_URL_PLAYER_DEMUXER_H_ + +#include +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/task/sequenced_task_runner.h" +#include "media/base/demuxer.h" +#include "media/base/demuxer_stream.h" +#include "media/base/media_export.h" +#include "url/gurl.h" + +namespace media { + +// Returns true if the URL looks like an HLS stream that the platform URL +// player (e.g., AVPlayer on tvOS) should handle. This centralizes the +// detection so that WebMediaPlayerImpl and DemuxerManager use the same logic. +MEDIA_EXPORT bool IsHlsUrl(const GURL& url); + +// Placeholder stream used only to satisfy Chromium's stream-based pipeline +// initialization. The URL player path ignores these streams and delegates +// loading, demuxing, buffering, and decoding to the platform URL player. +class MEDIA_EXPORT UrlPlayerDemuxerStream : public DemuxerStream { + public: + explicit UrlPlayerDemuxerStream(Type type); + ~UrlPlayerDemuxerStream() override; + + // DemuxerStream implementation. + void Read(uint32_t count, ReadCB read_cb) override; + AudioDecoderConfig audio_decoder_config() override; + VideoDecoderConfig video_decoder_config() override; + Type type() const override; + bool SupportsConfigChanges() override; + + private: + const Type type_; +}; + +// Demuxer placeholder for URL-based playback. It carries the media URL and +// exposes placeholder streams because PipelineImpl requires at least one +// DemuxerStream. The URL is delivered to the GPU-side renderer via +// mojom::Renderer::InitializeWithUrl() on the renderer pipe. +class MEDIA_EXPORT UrlPlayerDemuxer : public Demuxer { + public: + UrlPlayerDemuxer(scoped_refptr media_task_runner, + GURL url); + ~UrlPlayerDemuxer() override; + + // MediaResource implementation. + std::vector GetAllStreams() override; + GURL GetMediaUrl() const override; + + // Demuxer implementation. + std::string GetDisplayName() const override; + DemuxerType GetDemuxerType() const override; + void Initialize(DemuxerHost* host, PipelineStatusCallback status_cb) override; + void AbortPendingReads() override; + void StartWaitingForSeek(base::TimeDelta seek_time) override; + void CancelPendingSeek(base::TimeDelta seek_time) override; + void Seek(base::TimeDelta time, PipelineStatusCallback status_cb) override; + bool IsSeekable() const override; + void Stop() override; + base::TimeDelta GetStartTime() const override; + base::Time GetTimelineOffset() const override; + int64_t GetMemoryUsage() const override; + std::optional GetContainerForMetrics() + const override; + void OnTracksChanged(DemuxerStream::Type track_type, + const std::vector& track_ids, + base::TimeDelta curr_time, + TrackChangeCB change_completed_cb) override; + void SetPlaybackRate(double rate) override; + + private: + scoped_refptr media_task_runner_; + raw_ptr host_ = nullptr; + const GURL url_; + UrlPlayerDemuxerStream audio_stream_{DemuxerStream::AUDIO}; + UrlPlayerDemuxerStream video_stream_{DemuxerStream::VIDEO}; +}; + +} // namespace media + +#endif // MEDIA_STARBOARD_URL_PLAYER_DEMUXER_H_ diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index 31be098edba6..3b0c962aaac8 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -19911,6 +19911,7 @@ from previous Chrome versions. + From afc83bd1c0277874502bec66b10cc1f50f7595d1 Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Thu, 18 Jun 2026 08:19:10 +0200 Subject: [PATCH 6/6] media: Register conditional factory and HLS routing Register kUrlPlayer as a conditional factory with IsHlsUrl() predicate in MediaFactory so HLS URLs select the URL-player pipeline while ordinary URLs continue using the Starboard renderer. Add routing tests for .m3u8, hls_variant, and non-HLS URLs to RendererFactorySelectorTest. Build: ninja -C out/tvos-arm64-device_qa cobalt nplb media_unittests (pass) --- .../cobalt_content_renderer_client.cc | 11 +++- content/renderer/media/media_factory.cc | 18 ++++++- .../renderer_factory_selector_unittest.cc | 54 ++++++++++++++++++- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/cobalt/renderer/cobalt_content_renderer_client.cc b/cobalt/renderer/cobalt_content_renderer_client.cc index aeba2aa1deab..0cd704eaf001 100644 --- a/cobalt/renderer/cobalt_content_renderer_client.cc +++ b/cobalt/renderer/cobalt_content_renderer_client.cc @@ -43,6 +43,10 @@ #include "third_party/blink/public/web/web_security_policy.h" #include "third_party/blink/public/web/web_view.h" #include "ui/gfx/geometry/size_conversions.h" +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#include "media/starboard/url_player_demuxer.h" +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + namespace cobalt { namespace { @@ -515,7 +519,12 @@ CobaltContentRendererClient::OverrideDemuxerForUrl( content::RenderFrame* render_frame, const GURL& url, scoped_refptr task_runner) { - // TODO(512045535): Override with UrlPlayerDemuxer for HLS URLs. +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + if (::media::IsHlsUrl(url)) { + return std::make_unique<::media::UrlPlayerDemuxer>(std::move(task_runner), + url); + } +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) return nullptr; } diff --git a/content/renderer/media/media_factory.cc b/content/renderer/media/media_factory.cc index 6eb15892bc66..f44259fe9fbc 100644 --- a/content/renderer/media/media_factory.cc +++ b/content/renderer/media/media_factory.cc @@ -141,7 +141,11 @@ #if BUILDFLAG(USE_STARBOARD_MEDIA) #include "media/base/starboard/renderer_factory_traits.h" #include "media/mojo/clients/starboard/starboard_renderer_client_factory.h" -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#include "media/mojo/clients/starboard/url_player_renderer_client_factory.h" +#include "media/starboard/url_player_demuxer.h" +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) namespace { @@ -616,7 +620,17 @@ MediaFactory::CreateRendererFactorySelector( base::BindRepeating(&RenderThreadImpl::GetGpuFactories, base::Unretained(render_thread)), &renderer_factory_traits)); -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + GURL media_url(url); + factory_selector->AddConditionalFactory( + RendererType::kUrlPlayer, + std::make_unique( + media_log, CreateMojoRendererFactory(), &renderer_factory_traits), + base::BindRepeating( + [](const GURL& u) { return media::IsHlsUrl(u); }, + media_url)); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) if (!is_base_renderer_factory_set && renderer_media_playback_options.is_mojo_renderer_enabled()) { is_base_renderer_factory_set = true; diff --git a/media/base/renderer_factory_selector_unittest.cc b/media/base/renderer_factory_selector_unittest.cc index 69a4a8b806b4..2b70f61d2e29 100644 --- a/media/base/renderer_factory_selector_unittest.cc +++ b/media/base/renderer_factory_selector_unittest.cc @@ -2,14 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "media/base/renderer_factory_selector.h" + #include #include "base/functional/bind.h" #include "base/task/sequenced_task_runner.h" #include "media/base/overlay_info.h" -#include "media/base/renderer_factory_selector.h" +#include "media/media_buildflags.h" #include "testing/gtest/include/gtest/gtest.h" +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#include "media/starboard/url_player_demuxer.h" +#include "url/gurl.h" +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + namespace media { class RendererFactorySelectorTest : public testing::Test { @@ -56,6 +63,17 @@ class RendererFactorySelectorTest : public testing::Test { base::Unretained(this), type)); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void AddUrlPlayerConditionalFactory() { + selector_.AddConditionalFactory( + RendererType::kUrlPlayer, + std::make_unique(RendererType::kUrlPlayer), + base::BindRepeating( + [](const GURL* url) { return media::IsHlsUrl(*url); }, + ¤t_url_)); + } +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + RendererType GetCurrentlySelectedRendererType() { return reinterpret_cast(selector_.GetCurrentFactory()) ->factory_type(); @@ -69,6 +87,9 @@ class RendererFactorySelectorTest : public testing::Test { protected: RendererFactorySelector selector_; std::map condition_met_map_; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + GURL current_url_; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) }; TEST_F(RendererFactorySelectorTest, SingleFactory) { @@ -125,4 +146,35 @@ TEST_F(RendererFactorySelectorTest, MultipleConditionalFactories) { GetCurrentlySelectedRendererType() == RendererType::kCourier); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +TEST_F(RendererFactorySelectorTest, UrlPlayerConditionalFactorySelectsM3u8) { + AddBaseFactory(RendererType::kStarboard); + AddUrlPlayerConditionalFactory(); + + current_url_ = GURL("https://example.test/master.m3u8"); + + EXPECT_EQ(RendererType::kUrlPlayer, GetCurrentlySelectedRendererType()); +} + +TEST_F(RendererFactorySelectorTest, + UrlPlayerConditionalFactorySelectsHlsVariant) { + AddBaseFactory(RendererType::kStarboard); + AddUrlPlayerConditionalFactory(); + + current_url_ = GURL("https://example.test/hls_variant/video"); + + EXPECT_EQ(RendererType::kUrlPlayer, GetCurrentlySelectedRendererType()); +} + +TEST_F(RendererFactorySelectorTest, + UrlPlayerConditionalFactorySkipsOrdinaryUrl) { + AddBaseFactory(RendererType::kStarboard); + AddUrlPlayerConditionalFactory(); + + current_url_ = GURL("https://example.test/video.mp4"); + + EXPECT_EQ(RendererType::kStarboard, GetCurrentlySelectedRendererType()); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + } // namespace media