From 3be6e42851d97b512b2c37aa212daf7bea1a2802 Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Wed, 17 Jun 2026 18:12:58 +0200 Subject: [PATCH 1/2] 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 | 13 +++++++++++-- starboard/build/buildflags.gni | 7 +++++++ tools/metrics/histograms/enums.xml | 2 ++ 9 files changed, 55 insertions(+), 5 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..3a15ffa10210 100644 --- a/media/mojo/mojom/media_types_enum_mojom_traits.h +++ b/media/mojo/mojom/media_types_enum_mojom_traits.h @@ -391,7 +391,11 @@ struct EnumTraits { #if BUILDFLAG(USE_STARBOARD_MEDIA) case ::media::RendererType::kStarboard: return media::mojom::RendererType::kStarboard; -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#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) } NOTREACHED(); @@ -436,7 +440,12 @@ struct EnumTraits { case media::mojom::RendererType::kStarboard: *output = ::media::RendererType::kStarboard; return true; -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#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) } NOTREACHED(); 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 f43638b0f56e0325df44fc3a11d4df630f24538e Mon Sep 17 00:00:00 2001 From: Abhijeet Kandalkar Date: Wed, 17 Jun 2026 18:34:10 +0200 Subject: [PATCH 2/2] media: Add InitializeWithUrl transport and URL player renderer Introduce InitializeWithUrl(client, source_url) on the Mojo renderer pipe to carry the media URL from the renderer process to the GPU process, keeping URL delivery and initialization ordered on the same pipe. On the GPU side, add UrlPlayerRenderer for tvOS URL-based playback via SbPlayerBridge in punch-out mode, and UrlPlayerRendererWrapper as the Mojo service bridge. The wrapper sets the URL on the renderer then calls Initialize(). On the renderer-process side, MojoRenderer and MojoRendererWrapper forward InitializeWithUrl over the Mojo pipe. MojoRendererService validates the renderer type and URL before forwarding, with runtime guards for unexpected state and invalid URLs. Bug: 512045535 --- media/mojo/clients/mojo_renderer.cc | 25 + 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 | 37 ++ media/mojo/services/mojo_renderer_service.h | 8 + .../starboard/url_player_renderer_wrapper.cc | 207 +++++++ .../starboard/url_player_renderer_wrapper.h | 146 +++++ .../url_player_renderer_wrapper_unittest.cc | 371 +++++++++++++ media/starboard/BUILD.gn | 9 + media/starboard/url_player_renderer.cc | 510 ++++++++++++++++++ media/starboard/url_player_renderer.h | 165 ++++++ .../starboard/url_player_renderer_unittest.cc | 277 ++++++++++ 16 files changed, 1806 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..1573295aeb72 100644 --- a/media/mojo/clients/mojo_renderer.cc +++ b/media/mojo/clients/mojo_renderer.cc @@ -151,6 +151,31 @@ 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()); + + if (!source_url.is_valid() || 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..e4578b455632 100644 --- a/media/mojo/clients/mojo_renderer.h +++ b/media/mojo/clients/mojo_renderer.h @@ -15,6 +15,7 @@ #include "base/task/sequenced_task_runner.h" #include "base/time/default_tick_clock.h" #include "base/unguessable_token.h" +#include "build/build_config.h" #include "media/base/demuxer_stream.h" #include "media/base/renderer.h" #include "media/base/time_delta_interpolator.h" @@ -22,6 +23,7 @@ #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..a78e5b278e12 100644 --- a/media/mojo/clients/mojo_renderer_wrapper.h +++ b/media/mojo/clients/mojo_renderer_wrapper.h @@ -7,8 +7,10 @@ #include +#include "build/build_config.h" #include "media/base/renderer.h" #include "media/mojo/clients/mojo_renderer.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..995815c63f19 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,39 @@ 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; + + if (state_ != STATE_UNINITIALIZED) { + std::move(callback).Run(false); + return; + } + + 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..06d9c2583744 100644 --- a/media/mojo/services/mojo_renderer_service.h +++ b/media/mojo/services/mojo_renderer_service.h @@ -17,6 +17,7 @@ #include "base/time/time.h" #include "base/timer/timer.h" #include "base/unguessable_token.h" +#include "build/build_config.h" #include "media/base/buffering_state.h" #include "media/base/media_resource.h" #include "media/base/pipeline_status.h" @@ -28,6 +29,7 @@ #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..44d36ded2dca --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper.h @@ -0,0 +1,146 @@ +// 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-to-texture +// (SbDecodeTarget) frame extraction, and Android overlay support because +// URL player delegates rendering to the platform (AVPlayer) via punch-out. +// This is consistent with Cobalt 25, where SbUrlPlayerOutputModeSupported() +// returned true only for kSbPlayerOutputModePunchOut on tvOS. +// OnGpuChannelTokenReady() and GetCurrentVideoFrame() are stubbed as no-ops; +// decode-to-texture support can be added when tvOS requires it. +// +// Lifetime and Ownership: Owned by MojoRendererService, lifetime is +// tied to the Mojo connection. +// +// Threading Model: Thread-affine, must be used on the media task +// runner sequence. +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..0386da6b4daf --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc @@ -0,0 +1,371 @@ +// 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/run_loop.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 ::base::test::RunOnceClosure; +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::RunLoop run_loop; + 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))) + .WillOnce(RunOnceClosure(run_loop.QuitClosure())); + + renderer_wrapper_->Initialize(&media_resource_, &renderer_client_, + init_cb.Get()); + run_loop.Run(); +} + +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::RunLoop run_loop; + 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))) + .WillOnce(RunOnceClosure(run_loop.QuitClosure())); + + renderer_wrapper_->InitializeWithUrl(&renderer_client_, GURL(kSourceUrl), + init_cb.Get()); + run_loop.Run(); +} + +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; + + // Phase 1: InitializeWithUrl triggers GetSbWindowHandle via Mojo. + base::RunLoop window_handle_loop; + base::MockOnceCallback init_cb; + EXPECT_CALL(client_extension_, GetSbWindowHandle()) + .WillOnce(RunOnceClosure(window_handle_loop.QuitClosure())); + renderer_wrapper_->InitializeWithUrl(&renderer_client_, GURL(kSourceUrl), + init_cb.Get()); + window_handle_loop.Run(); + + // Phase 2: Window handle arrives, player is created. + base::RunLoop player_created_loop; + 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)) + .WillOnce(RunOnceClosure(player_created_loop.QuitClosure())); + renderer_extension_->OnSbWindowHandleReady(kWindowHandle); + player_created_loop.Run(); + + // Phase 3: Player reports initialized. + base::RunLoop init_loop; + ASSERT_TRUE(player_status_cb); + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))) + .WillOnce(RunOnceClosure(init_loop.QuitClosure())); + player_status_cb(player, player_context, kSbPlayerStateInitialized, + SB_PLAYER_INITIAL_TICKET); + init_loop.Run(); + + // Phase 4: Player reports presenting with video dimensions. + base::RunLoop presenting_loop; + 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))) + .WillOnce(RunOnceClosure(presenting_loop.QuitClosure())); + player_status_cb(player, player_context, kSbPlayerStatePresenting, + SB_PLAYER_INITIAL_TICKET); + presenting_loop.Run(); +} + +} // 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..96ceb1dcdafc --- /dev/null +++ b/media/starboard/url_player_renderer.cc @@ -0,0 +1,510 @@ +// 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 + << '.'; + 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_) { + return base::Microseconds(0); + } + + uint32_t video_frames_decoded = 0; + uint32_t video_frames_dropped = 0; + uint64_t audio_bytes_decoded = 0; + uint64_t video_bytes_decoded = 0; + 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(); + } + + 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 (state_ != STATE_INITIALIZING || player_bridge_) { + LOG(WARNING) << "OnSbWindowHandleReady called in unexpected state: " + << state_; + return; + } + + 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_ ? player_bridge_->GetPlayerCreationErrorMessage() + : "Player bridge creation failed"; + player_bridge_.reset(); + state_ = STATE_ERROR; + PipelineStatus status(DECODER_ERROR_NOT_SUPPORTED, + "CreatePlayerBridge failed: " + error_message); + LOG(INFO) << status; + std::move(init_cb_).Run(status); +} + +void UrlPlayerRenderer::ApplyPendingBounds() { + if (!player_bridge_ || !output_rect_) { + return; + } + + player_bridge_->SetBounds(*output_rect_); +} + +void UrlPlayerRenderer::OnStatisticsUpdate(const PipelineStatistics& stats) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + if (client_) { + 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: + if (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: + if (client_) { + 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::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); + if (client_) { + 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..175d7e2ac421 --- /dev/null +++ b/media/starboard/url_player_renderer.h @@ -0,0 +1,165 @@ +// 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. +// +// Lifetime and Ownership: Owned by UrlPlayerRendererWrapper in the +// GPU process. +// +// Threading Model: Thread-affine, must be used on the media task +// runner sequence. +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 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_; + 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