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/cobalt/renderer/cobalt_content_renderer_client.cc b/cobalt/renderer/cobalt_content_renderer_client.cc index f7ff3113e180..0cd704eaf001 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,6 +43,9 @@ #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 { @@ -509,4 +514,18 @@ void CobaltContentRendererClient::PostSandboxInitialized() { } } +std::unique_ptr<::media::Demuxer> +CobaltContentRendererClient::OverrideDemuxerForUrl( + content::RenderFrame* render_frame, + const GURL& url, + scoped_refptr task_runner) { +#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; +} + } // 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/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_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/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/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/base/media_resource.cc b/media/base/media_resource.cc index deeadbbf89b2..1966566e7d39 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,17 @@ DemuxerStream* MediaResource::GetFirstStream(DemuxerStream::Type type) { return nullptr; } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +GURL MediaResource::GetMediaUrl() const { + return GURL(); +} + +void MediaResource::ForwardDurationChangeToDemuxerHost( + base::TimeDelta duration) {} + +void MediaResource::ForwardBufferedTimeRangesToDemuxerHost( + base::TimeDelta start, + base::TimeDelta length) {} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + } // namespace media diff --git a/media/base/media_resource.h b/media/base/media_resource.h index 62e0b605df2f..4cc1d470ab9a 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,21 @@ 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; + + // Propagates duration changes from the native URL player to the + // DemuxerHost, which forwards to the pipeline and JS video.duration. + virtual void ForwardDurationChangeToDemuxerHost(base::TimeDelta duration); + + // Propagates buffered time ranges from the native URL player to the + // DemuxerHost. Unlike duration (reported once at Presenting), buffered + // ranges are polled continuously as AVPlayer downloads HLS segments. + virtual void ForwardBufferedTimeRangesToDemuxerHost(base::TimeDelta start, + base::TimeDelta length); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) }; } // namespace media 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/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 diff --git a/media/mojo/clients/BUILD.gn b/media/mojo/clients/BUILD.gn index 40e2f7ab676a..fb72fb120052 100644 --- a/media/mojo/clients/BUILD.gn +++ b/media/mojo/clients/BUILD.gn @@ -148,6 +148,15 @@ 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", + "starboard/url_player_renderer_client_factory.cc", + "starboard/url_player_renderer_client_factory.h", + ] + } } static_library("test_support") { @@ -212,4 +221,12 @@ source_set("unit_tests") { if (is_cobalt && use_starboard_media) { sources += [ "starboard/starboard_renderer_client_unittest.cc" ] } + + if (is_cobalt && use_starboard_url_player) { + sources += [ + "starboard/url_player_renderer_client_factory_unittest.cc", + "starboard/url_player_renderer_client_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_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/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.cc b/media/mojo/clients/starboard/starboard_renderer_client.cc index a3b5d6f44b93..bcc5e11f7ae5 100644 --- a/media/mojo/clients/starboard/starboard_renderer_client.cc +++ b/media/mojo/clients/starboard/starboard_renderer_client.cc @@ -264,6 +264,20 @@ void StarboardRendererClient::GetSbWindowHandle() { renderer_extension_->OnSbWindowHandleReady(sb_window_handle); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void StarboardRendererClient::OnDurationChange(base::TimeDelta duration) { + // Duration reporting is handled by UrlPlayerRendererClient. + // StarboardRendererClient does not use URL player paths. +} + +void StarboardRendererClient::OnBufferedTimeRangesChange( + base::TimeDelta start, + base::TimeDelta length) { + // Buffered ranges reporting is handled by UrlPlayerRendererClient. + // StarboardRendererClient does not use URL player paths. +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + #if BUILDFLAG(IS_ANDROID) void StarboardRendererClient::RequestOverlayInfo(bool restart_for_transitions) { DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); diff --git a/media/mojo/clients/starboard/starboard_renderer_client.h b/media/mojo/clients/starboard/starboard_renderer_client.h index a71f3e323de1..c927c4aee3d5 100644 --- a/media/mojo/clients/starboard/starboard_renderer_client.h +++ b/media/mojo/clients/starboard/starboard_renderer_client.h @@ -21,6 +21,7 @@ #include "base/memory/weak_ptr.h" #include "base/synchronization/lock.h" #include "base/task/sequenced_task_runner.h" +#include "build/build_config.h" #include "media/base/pipeline_status.h" #include "media/base/renderer_client.h" #include "media/base/starboard/starboard_rendering_mode.h" @@ -108,6 +109,11 @@ class MEDIA_EXPORT StarboardRendererClient void PaintVideoHoleFrame(const gfx::Size& size) override; void UpdateStarboardRenderingMode(const StarboardRenderingMode mode) override; void GetSbWindowHandle() override; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void OnDurationChange(base::TimeDelta duration) override; + void OnBufferedTimeRangesChange(base::TimeDelta start, + base::TimeDelta length) override; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) #if BUILDFLAG(IS_ANDROID) void RequestOverlayInfo(bool restart_for_transitions) override; #endif // BUILDFLAG(IS_ANDROID) 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/clients/starboard/url_player_renderer_client.cc b/media/mojo/clients/starboard/url_player_renderer_client.cc new file mode 100644 index 000000000000..b962166c4f43 --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client.cc @@ -0,0 +1,236 @@ +// 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; + media_resource_ = media_resource; + 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)); +} + +// Two-event initialization protocol: +// +// UrlPlayerRendererClient initialization requires two independent events +// before firing init_cb_: +// Event A: Mojo init response (OnMojoRendererInitialized) - response to +// InitializeWithUrl sent over mojom::Renderer pipe. +// Event B: Rendering mode update (UpdateStarboardRenderingMode) - sent by +// UrlPlayerRendererWrapper over +// mojom::StarboardRendererClientExtension pipe after +// UrlPlayerRenderer creates the SbPlayerBridge. +// +// These arrive on different Mojo pipes with no ordering guarantee. +// Whichever arrives second sees the first's state and fires init_cb_. +// +// URL player only supports punch-out mode. The rendering mode is determined +// at the Starboard layer by SbUrlPlayerOutputModeSupported() +// (starboard/tvos/shared/media/url_player_output_mode_supported.mm), which +// returns true only for kSbPlayerOutputModePunchOut. This is called via +// SbPlayerBridge::ComputeSbUrlPlayerOutputMode() (media/starboard/ +// sbplayer_bridge.cc) before player creation. After creation, +// UrlPlayerRenderer::CreatePlayerBridge() (media/starboard/ +// url_player_renderer.cc) validates the mode and always sends kPunchOut +// through the callback chain: +// UrlPlayerRenderer -> UrlPlayerRendererWrapper:: +// OnUpdateStarboardRenderingMode -> Mojo -> here. +// +// Because the rendering mode is always kPunchOut for URL player, +// DTT and invalid modes are not expected. LOG(WARNING) is used to flag +// unexpected values without crashing, since the platform guarantees +// punch-out. +void UrlPlayerRendererClient::UpdateStarboardRenderingMode( + const StarboardRenderingMode mode) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + rendering_mode_ = mode; + + if (rendering_mode_ != StarboardRenderingMode::kPunchOut) { + LOG(WARNING) << "URL player received unexpected rendering mode: " + << static_cast(rendering_mode_) + << ". Only punch-out is supported."; + 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::OnDurationChange(base::TimeDelta duration) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + if (media_resource_) { + media_resource_->ForwardDurationChangeToDemuxerHost(duration); + } +} + +void UrlPlayerRendererClient::OnBufferedTimeRangesChange( + base::TimeDelta start, + base::TimeDelta length) { + DCHECK(media_task_runner_->RunsTasksInCurrentSequence()); + if (media_resource_) { + media_resource_->ForwardBufferedTimeRangesToDemuxerHost(start, length); + } +} + +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()); + + SetMojoRendererInitialized(status); + + // Two-event protocol: if punch-out rendering mode already arrived, complete + // now. Otherwise, UpdateStarboardRenderingMode() will fire init_cb_ when it + // arrives. See the protocol comment above UpdateStarboardRenderingMode(). + if (rendering_mode_ == StarboardRenderingMode::kPunchOut && + !init_cb_.is_null()) { + std::move(init_cb_).Run(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..a92846a5004b --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client.h @@ -0,0 +1,122 @@ +// 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; + void OnDurationChange(base::TimeDelta duration) override; + void OnBufferedTimeRangesChange(base::TimeDelta start, + base::TimeDelta length) 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; + raw_ptr media_resource_ = 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_ 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/clients/starboard/url_player_renderer_client_factory_unittest.cc b/media/mojo/clients/starboard/url_player_renderer_client_factory_unittest.cc new file mode 100644 index 000000000000..3f7b56633644 --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client_factory_unittest.cc @@ -0,0 +1,185 @@ +// 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 +#include +#include + +#include "base/test/task_environment.h" +#include "media/base/media_util.h" +#include "media/base/mock_video_renderer_sink.h" +#include "media/mojo/clients/mojo_renderer_factory.h" +#include "media/mojo/mojom/interface_factory.mojom.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace media { +namespace { + +class CapturingInterfaceFactory : public mojom::InterfaceFactory { + public: + void CreateAudioDecoder( + mojo::PendingReceiver receiver) override {} + void CreateVideoDecoder( + mojo::PendingReceiver receiver, + mojo::PendingRemote dst_video_decoder) override {} + void CreateAudioEncoder( + mojo::PendingReceiver receiver) override {} + void CreateDefaultRenderer( + const std::string& audio_device_id, + mojo::PendingReceiver receiver) override {} + void CreateStarboardRenderer( + mojo::PendingRemote media_log, + const StarboardRendererConfig& config, + mojo::PendingReceiver renderer, + mojo::PendingReceiver + renderer_extension, + mojo::PendingRemote + client_extension) override { + ++starboard_renderer_create_count_; + } + void CreateUrlPlayerRenderer( + mojo::PendingRemote media_log, + const StarboardRendererConfig& config, + mojo::PendingReceiver renderer, + mojo::PendingReceiver + renderer_extension, + mojo::PendingRemote + client_extension) override { + ++url_player_renderer_create_count_; + url_player_config_ = config; + media_log_pipe_was_valid_ = media_log.is_valid(); + renderer_receiver_was_valid_ = renderer.is_valid(); + renderer_extension_receiver_was_valid_ = renderer_extension.is_valid(); + client_extension_remote_was_valid_ = client_extension.is_valid(); + } + void CreateCdm(const CdmConfig& cdm_config, + CreateCdmCallback callback) override {} + + const std::optional& url_player_config() const { + return url_player_config_; + } + int starboard_renderer_create_count() const { + return starboard_renderer_create_count_; + } + int url_player_renderer_create_count() const { + return url_player_renderer_create_count_; + } + bool media_log_pipe_was_valid() const { return media_log_pipe_was_valid_; } + bool renderer_receiver_was_valid() const { + return renderer_receiver_was_valid_; + } + bool renderer_extension_receiver_was_valid() const { + return renderer_extension_receiver_was_valid_; + } + bool client_extension_remote_was_valid() const { + return client_extension_remote_was_valid_; + } + + private: + std::optional url_player_config_; + int starboard_renderer_create_count_ = 0; + int url_player_renderer_create_count_ = 0; + bool media_log_pipe_was_valid_ = false; + bool renderer_receiver_was_valid_ = false; + bool renderer_extension_receiver_was_valid_ = false; + bool client_extension_remote_was_valid_ = false; +}; + +class UrlPlayerRendererClientFactoryTest : public testing::Test { + protected: + std::unique_ptr CreateRenderer( + const RendererFactoryTraits& traits = RendererFactoryTraits()) { + factory_ = std::make_unique( + &media_log_, std::make_unique(&interface_factory_), + &traits); + return factory_->CreateRenderer(task_environment_.GetMainThreadTaskRunner(), + task_environment_.GetMainThreadTaskRunner(), + nullptr, &video_renderer_sink_, + base::NullCallback(), gfx::ColorSpace()); + } + + base::test::TaskEnvironment task_environment_; + CapturingInterfaceFactory interface_factory_; + NullMediaLog media_log_; + MockVideoRendererSink video_renderer_sink_; + std::unique_ptr factory_; +}; + +TEST_F(UrlPlayerRendererClientFactoryTest, AlwaysDisablesBypassMojoForMedia) { + RendererFactoryTraits traits; + traits.experimental_features.bypass_mojo_for_media = true; + traits.experimental_features.force_decode_to_texture = true; + + std::unique_ptr renderer = CreateRenderer(traits); + + ASSERT_TRUE(renderer); + ASSERT_TRUE(interface_factory_.url_player_config()); + EXPECT_FALSE(interface_factory_.url_player_config() + ->experimental_features.bypass_mojo_for_media); + EXPECT_TRUE(interface_factory_.url_player_config() + ->experimental_features.force_decode_to_texture); +} + +TEST_F(UrlPlayerRendererClientFactoryTest, UsesUrlPlayerFactoryPathOnly) { + std::unique_ptr renderer = CreateRenderer(); + + ASSERT_TRUE(renderer); + EXPECT_EQ(renderer->GetRendererType(), RendererType::kUrlPlayer); + EXPECT_EQ(interface_factory_.url_player_renderer_create_count(), 1); + EXPECT_EQ(interface_factory_.starboard_renderer_create_count(), 0); +} + +TEST_F(UrlPlayerRendererClientFactoryTest, WiresMojoEndpoints) { + std::unique_ptr renderer = CreateRenderer(); + + ASSERT_TRUE(renderer); + EXPECT_TRUE(interface_factory_.media_log_pipe_was_valid()); + EXPECT_TRUE(interface_factory_.renderer_receiver_was_valid()); + EXPECT_TRUE(interface_factory_.renderer_extension_receiver_was_valid()); + EXPECT_TRUE(interface_factory_.client_extension_remote_was_valid()); +} + +TEST_F(UrlPlayerRendererClientFactoryTest, PropagatesConfigForUrlPlayer) { + RendererFactoryTraits traits; + traits.experimental_features.bypass_mojo_for_media = true; + traits.experimental_features.force_decode_to_texture = true; + traits.experimental_features.enable_low_latency = true; + traits.viewport_size = gfx::Size(1920, 1080); + + std::unique_ptr renderer = CreateRenderer(traits); + + ASSERT_TRUE(renderer); + ASSERT_TRUE(interface_factory_.url_player_config()); + EXPECT_FALSE( + interface_factory_.url_player_config()->overlay_plane_id.is_empty()); + EXPECT_TRUE(interface_factory_.url_player_config() + ->audio_write_duration_local.is_zero()); + EXPECT_TRUE(interface_factory_.url_player_config() + ->audio_write_duration_remote.is_zero()); + EXPECT_TRUE( + interface_factory_.url_player_config()->max_video_capabilities.empty()); + EXPECT_EQ(interface_factory_.url_player_config()->viewport_size, + traits.viewport_size); + EXPECT_FALSE(interface_factory_.url_player_config() + ->experimental_features.bypass_mojo_for_media); + EXPECT_TRUE(interface_factory_.url_player_config() + ->experimental_features.force_decode_to_texture); + EXPECT_TRUE(interface_factory_.url_player_config() + ->experimental_features.enable_low_latency); +} + +} // namespace +} // namespace media diff --git a/media/mojo/clients/starboard/url_player_renderer_client_unittest.cc b/media/mojo/clients/starboard/url_player_renderer_client_unittest.cc new file mode 100644 index 000000000000..d332eae885f3 --- /dev/null +++ b/media/mojo/clients/starboard/url_player_renderer_client_unittest.cc @@ -0,0 +1,275 @@ +// 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 +#include +#include + +#include "base/functional/callback_helpers.h" +#include "base/test/mock_callback.h" +#include "base/test/task_environment.h" +#include "media/base/media_resource.h" +#include "media/base/media_util.h" +#include "media/base/mock_filters.h" +#include "media/base/mock_video_renderer_sink.h" +#include "media/base/test_helpers.h" +#include "media/base/video_frame.h" +#include "media/mojo/clients/mojo_renderer.h" +#include "media/mojo/mojom/renderer.mojom.h" +#include "media/mojo/mojom/renderer_extensions.mojom.h" +#include "media/renderers/video_overlay_factory.h" +#include "mojo/public/cpp/bindings/associated_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace media { +namespace { + +using ::testing::_; +using ::testing::StrictMock; + +constexpr char kSourceUrl[] = "https://example.test/video.m3u8"; + +class UrlMediaResource final : public MediaResource { + public: + explicit UrlMediaResource(GURL url) : url_(std::move(url)) {} + + std::vector GetAllStreams() override { return {}; } + + GURL GetMediaUrl() const override { return url_; } + + private: + GURL url_; +}; + +class FakeMojomRenderer final : public mojom::Renderer { + public: + void Initialize( + mojo::PendingAssociatedRemote client, + std::optional>> + streams, + InitializeCallback cb) override { + ++initialize_count_; + initialize_cb_ = std::move(cb); + } + + void InitializeWithUrl( + mojo::PendingAssociatedRemote client, + const GURL& source_url, + InitializeWithUrlCallback cb) override { + ++initialize_with_url_count_; + source_url_ = source_url; + initialize_with_url_cb_ = std::move(cb); + } + + void Flush(FlushCallback cb) override { std::move(cb).Run(); } + void StartPlayingFrom(base::TimeDelta time) override {} + void SetPlaybackRate(double playback_rate) override {} + void SetVolume(float volume) override {} + void SetCdm(const std::optional& cdm_id, + SetCdmCallback cb) override { + std::move(cb).Run(true); + } + void SetLatencyHint(std::optional latency_hint) override {} + + void CompleteInitialize(bool success) { + if (initialize_cb_) { + std::move(initialize_cb_).Run(success); + return; + } + + ASSERT_TRUE(initialize_with_url_cb_); + std::move(initialize_with_url_cb_).Run(success); + } + + int initialize_count() const { return initialize_count_; } + int initialize_with_url_count() const { return initialize_with_url_count_; } + int initialize_with_bypass_bridge_count() const { + return initialize_with_bypass_bridge_count_; + } + const GURL& source_url() const { return source_url_; } + + private: + int initialize_count_ = 0; + int initialize_with_url_count_ = 0; + int initialize_with_bypass_bridge_count_ = 0; + GURL source_url_; + InitializeCallback initialize_cb_; + InitializeWithUrlCallback initialize_with_url_cb_; + + void InitializeWithBypassBridge( + mojo::PendingAssociatedRemote client, + uint32_t bypass_bridge_id, + InitializeWithBypassBridgeCallback cb) override { + ++initialize_with_bypass_bridge_count_; + std::move(cb).Run(false); + } +}; + +class FakeRendererExtension final : public mojom::StarboardRendererExtension { + public: + void GetCurrentVideoFrame(GetCurrentVideoFrameCallback cb) override { + std::move(cb).Run(nullptr); + } + void OnSbWindowHandleReady(uint64_t sb_window_handle) override {} + void OnGpuChannelTokenReady( + mojom::CommandBufferIdPtr command_buffer_id) override {} +}; + +class UrlPlayerRendererClientTest : public testing::Test { + protected: + UrlPlayerRendererClientTest() { + mojo::PendingRemote renderer_remote; + renderer_receiver_.Bind(renderer_remote.InitWithNewPipeAndPassReceiver()); + + mojo::PendingRemote renderer_extension; + renderer_extension_receiver_.Bind( + renderer_extension.InitWithNewPipeAndPassReceiver()); + + client_ = std::make_unique( + task_environment_.GetMainThreadTaskRunner(), + std::make_unique(), + std::make_unique( + task_environment_.GetMainThreadTaskRunner(), + /*video_overlay_factory=*/nullptr, + /*video_renderer_sink=*/nullptr, std::move(renderer_remote)), + std::make_unique(), &video_renderer_sink_, + std::move(renderer_extension), + client_extension_remote_.BindNewPipeAndPassReceiver(), + base::NullCallback()); + } + + void InitializeClient(PipelineStatusCallback init_cb) { + client_->Initialize(&media_resource_, &renderer_client_, + std::move(init_cb)); + task_environment_.RunUntilIdle(); + } + + base::test::TaskEnvironment task_environment_; + FakeMojomRenderer fake_renderer_; + mojo::Receiver renderer_receiver_{&fake_renderer_}; + FakeRendererExtension fake_renderer_extension_; + mojo::Receiver + renderer_extension_receiver_{&fake_renderer_extension_}; + mojo::Remote + client_extension_remote_; + StrictMock video_renderer_sink_; + StrictMock renderer_client_; + UrlMediaResource media_resource_{GURL(kSourceUrl)}; + std::unique_ptr client_; +}; + +TEST_F(UrlPlayerRendererClientTest, CompletesAfterMojoInitAndPunchOutMode) { + base::MockOnceCallback init_cb; + EXPECT_CALL(init_cb, Run(_)).Times(0); + + InitializeClient(init_cb.Get()); + EXPECT_EQ(fake_renderer_.initialize_count(), 0); + EXPECT_EQ(fake_renderer_.initialize_with_url_count(), 1); + EXPECT_EQ(fake_renderer_.source_url(), GURL(kSourceUrl)); + + fake_renderer_.CompleteInitialize(true); + task_environment_.RunUntilIdle(); + testing::Mock::VerifyAndClearExpectations(&init_cb); + + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))); + client_->UpdateStarboardRenderingMode(StarboardRenderingMode::kPunchOut); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererClientTest, CompletesWhenPunchOutModeArrivesFirst) { + base::MockOnceCallback init_cb; + EXPECT_CALL(init_cb, Run(_)).Times(0); + + InitializeClient(init_cb.Get()); + client_->UpdateStarboardRenderingMode(StarboardRenderingMode::kPunchOut); + task_environment_.RunUntilIdle(); + testing::Mock::VerifyAndClearExpectations(&init_cb); + + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))); + fake_renderer_.CompleteInitialize(true); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererClientTest, InvalidUrlFailsAfterRenderingModeArrives) { + UrlMediaResource invalid_media_resource{GURL()}; + base::MockOnceCallback init_cb; + + // Invalid URL causes mojo init to fail internally, but the two-event + // protocol waits for a rendering mode update before firing init_cb_. + EXPECT_CALL(init_cb, Run(_)).Times(0); + client_->Initialize(&invalid_media_resource, &renderer_client_, + init_cb.Get()); + task_environment_.RunUntilIdle(); + testing::Mock::VerifyAndClearExpectations(&init_cb); + + EXPECT_EQ(fake_renderer_.initialize_count(), 0); + EXPECT_EQ(fake_renderer_.initialize_with_bypass_bridge_count(), 0); + + // Rendering mode arrival completes init with the stored error. + EXPECT_CALL(init_cb, + Run(HasStatusCode(PIPELINE_ERROR_INITIALIZATION_FAILED))); + client_->UpdateStarboardRenderingMode(StarboardRenderingMode::kPunchOut); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererClientTest, DisconnectDuringInitFailsInit) { + base::MockOnceCallback init_cb; + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_ERROR_DISCONNECTED))); + + InitializeClient(init_cb.Get()); + renderer_extension_receiver_.reset(); + task_environment_.RunUntilIdle(); +} + +// URL player only supports punch-out mode. The rendering mode is determined +// at the Starboard layer by SbUrlPlayerOutputModeSupported() which returns +// true only for kSbPlayerOutputModePunchOut. DTT and invalid modes cannot +// arrive in production, so UpdateStarboardRenderingMode() ignores them. +// See the protocol comment in url_player_renderer_client.cc. +TEST_F(UrlPlayerRendererClientTest, IgnoresDecodeToTextureDuringInit) { + base::MockOnceCallback init_cb; + EXPECT_CALL(init_cb, Run(_)).Times(0); + + InitializeClient(init_cb.Get()); + // DTT is ignored by URL player. init_cb_ should not fire. + client_->UpdateStarboardRenderingMode( + StarboardRenderingMode::kDecodeToTexture); + task_environment_.RunUntilIdle(); + testing::Mock::VerifyAndClearExpectations(&init_cb); + + // Mojo init completing afterwards should also not fire init_cb_ + // because rendering_mode_ is not kPunchOut. + EXPECT_CALL(init_cb, Run(_)).Times(0); + fake_renderer_.CompleteInitialize(true); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererClientTest, IgnoresInvalidRenderingModeDuringInit) { + base::MockOnceCallback init_cb; + EXPECT_CALL(init_cb, Run(_)).Times(0); + + InitializeClient(init_cb.Get()); + // Invalid mode is ignored. init_cb_ should not fire. + client_->UpdateStarboardRenderingMode(StarboardRenderingMode::kInvalid); + task_environment_.RunUntilIdle(); +} + +} // namespace +} // namespace media 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/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/mojom/media_types.mojom b/media/mojo/mojom/media_types.mojom index d649d9e890a2..067b09a903aa 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. @@ -662,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 20563e88d10e..03b24b66af48 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(); @@ -461,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(); @@ -492,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/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/mojom/renderer_extensions.mojom b/media/mojo/mojom/renderer_extensions.mojom index 42c0f3c7c380..280a0e3cb3da 100644 --- a/media/mojo/mojom/renderer_extensions.mojom +++ b/media/mojo/mojom/renderer_extensions.mojom @@ -80,6 +80,15 @@ interface StarboardRendererClientExtension { // Get SbWindow from StarboardRendererClient. GetSbWindowHandle(); + [EnableIf=use_starboard_url_player] + // Report duration from the native URL player to the renderer process. + OnDurationChange(mojo_base.mojom.TimeDelta duration); + + [EnableIf=use_starboard_url_player] + // Report buffered time ranges from the native URL player. + OnBufferedTimeRangesChange(mojo_base.mojom.TimeDelta start, + mojo_base.mojom.TimeDelta length); + [EnableIf=is_android] // Request OverlayInfo from StarboardRendererClient. RequestOverlayInfo(bool restart_for_transitions); 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/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/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..5cc2788b7b3e --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper.cc @@ -0,0 +1,226 @@ +// 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())); + + // Wire duration and buffered ranges callbacks. + GetRenderer()->SetDurationChangeCB(base::BindRepeating( + &UrlPlayerRendererWrapper::OnDurationChange, weak_factory_.GetWeakPtr())); + GetRenderer()->SetBufferedRangesCB( + base::BindRepeating(&UrlPlayerRendererWrapper::OnBufferedTimeRangesChange, + 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::OnDurationChange(base::TimeDelta duration) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->OnDurationChange(duration); +} + +void UrlPlayerRendererWrapper::OnBufferedTimeRangesChange( + base::TimeDelta start, + base::TimeDelta length) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->OnBufferedTimeRangesChange(start, length); +} + +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..87c7f5152fa7 --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper.h @@ -0,0 +1,139 @@ +// 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 OnDurationChange(base::TimeDelta duration); + void OnBufferedTimeRangesChange(base::TimeDelta start, + base::TimeDelta length); + 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..ec981d57bc67 --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc @@ -0,0 +1,360 @@ +// 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)); + MOCK_METHOD(void, OnDurationChange, (base::TimeDelta), (override)); + MOCK_METHOD(void, + OnBufferedTimeRangesChange, + (base::TimeDelta, base::TimeDelta), + (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, _)) + .WillRepeatedly(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))); + // Zero duration from GetInfo maps to kInfiniteDuration (live HLS stream). + EXPECT_CALL(client_extension_, OnDurationChange(kInfiniteDuration)); + player_status_cb(player, player_context, kSbPlayerStatePresenting, + SB_PLAYER_INITIAL_TICKET); + task_environment_.RunUntilIdle(); +} + +} // namespace +} // namespace media 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/media/starboard/BUILD.gn b/media/starboard/BUILD.gn index bb7b8fb00656..c05dd41355a1 100644 --- a/media/starboard/BUILD.gn +++ b/media/starboard/BUILD.gn @@ -59,6 +59,14 @@ source_set("starboard") { deps += [ ":buildflags" ] } + if (use_starboard_url_player) { + sources += [ + "url_player_demuxer.cc", + "url_player_demuxer.h", + "url_player_renderer.cc", + "url_player_renderer.h", + ] + } deps += [ "//base", @@ -98,6 +106,12 @@ source_set("unit_tests") { "starboard_utils_test.cc", ] } + if (use_starboard_url_player) { + sources += [ + "url_player_demuxer_unittest.cc", + "url_player_renderer_unittest.cc", + ] + } configs += [ "//media:media_config" ] deps = [ "//base", diff --git a/media/starboard/url_player_demuxer.cc b/media/starboard/url_player_demuxer.cc new file mode 100644 index 000000000000..f9b7c9b9a00e --- /dev/null +++ b/media/starboard/url_player_demuxer.cc @@ -0,0 +1,176 @@ +// 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/ranges.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) {} + +void UrlPlayerDemuxer::ForwardDurationChangeToDemuxerHost( + base::TimeDelta duration) { + if (host_) { + host_->SetDuration(duration); + } +} + +void UrlPlayerDemuxer::ForwardBufferedTimeRangesToDemuxerHost( + base::TimeDelta start, + base::TimeDelta length) { + if (host_) { + Ranges ranges; + ranges.Add(start, start + length); + host_->OnBufferedTimeRangesChanged(ranges); + } +} + +} // namespace media diff --git a/media/starboard/url_player_demuxer.h b/media/starboard/url_player_demuxer.h new file mode 100644 index 000000000000..64e682018b50 --- /dev/null +++ b/media/starboard/url_player_demuxer.h @@ -0,0 +1,107 @@ +// 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; + + // MediaResource overrides. + void ForwardDurationChangeToDemuxerHost(base::TimeDelta duration) override; + void ForwardBufferedTimeRangesToDemuxerHost(base::TimeDelta start, + base::TimeDelta length) 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/media/starboard/url_player_demuxer_unittest.cc b/media/starboard/url_player_demuxer_unittest.cc new file mode 100644 index 000000000000..4a2f6b71f9f2 --- /dev/null +++ b/media/starboard/url_player_demuxer_unittest.cc @@ -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. + +#include "media/starboard/url_player_demuxer.h" + +#include "base/test/task_environment.h" +#include "media/base/demuxer_stream.h" +#include "media/base/mock_demuxer_host.h" +#include "media/base/pipeline_status.h" +#include "media/base/test_helpers.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace media { +namespace { + +// --- IsHlsUrl tests --- + +TEST(IsHlsUrlTest, DetectsM3u8Extension) { + EXPECT_TRUE(IsHlsUrl(GURL("https://example.com/video.m3u8"))); + EXPECT_TRUE(IsHlsUrl(GURL("https://cdn.test/path/to/stream.m3u8"))); + EXPECT_TRUE(IsHlsUrl(GURL("https://example.com/video.m3u8?token=abc&t=123"))); +} + +TEST(IsHlsUrlTest, DetectsHlsVariantInPath) { + EXPECT_TRUE(IsHlsUrl(GURL("https://example.com/hls_variant/stream"))); + EXPECT_TRUE( + IsHlsUrl(GURL("https://cdn.test/path/hls_variant_720p/index.mp4"))); +} + +TEST(IsHlsUrlTest, RejectsOrdinaryUrls) { + EXPECT_FALSE(IsHlsUrl(GURL("https://example.com/video.mp4"))); + EXPECT_FALSE(IsHlsUrl(GURL("https://example.com/video.webm"))); + EXPECT_FALSE(IsHlsUrl(GURL("https://example.com/stream"))); + EXPECT_FALSE(IsHlsUrl(GURL("https://example.com/"))); +} + +TEST(IsHlsUrlTest, RejectsEmptyAndInvalidUrls) { + EXPECT_FALSE(IsHlsUrl(GURL())); + EXPECT_FALSE(IsHlsUrl(GURL(""))); + EXPECT_FALSE(IsHlsUrl(GURL("not-a-url"))); +} + +// --- UrlPlayerDemuxer tests --- + +class UrlPlayerDemuxerTest : public testing::Test { + protected: + UrlPlayerDemuxerTest() + : demuxer_(task_environment_.GetMainThreadTaskRunner(), + GURL("https://example.com/video.m3u8")) {} + + base::test::TaskEnvironment task_environment_; + UrlPlayerDemuxer demuxer_; + testing::NiceMock host_; +}; + +TEST_F(UrlPlayerDemuxerTest, GetMediaUrlReturnsOriginalUrl) { + EXPECT_EQ(demuxer_.GetMediaUrl(), GURL("https://example.com/video.m3u8")); +} + +TEST_F(UrlPlayerDemuxerTest, GetAllStreamsReturnsAudioAndVideo) { + auto streams = demuxer_.GetAllStreams(); + ASSERT_EQ(streams.size(), 2u); + + bool has_audio = false; + bool has_video = false; + for (auto* stream : streams) { + if (stream->type() == DemuxerStream::AUDIO) { + has_audio = true; + } + if (stream->type() == DemuxerStream::VIDEO) { + has_video = true; + } + } + EXPECT_TRUE(has_audio); + EXPECT_TRUE(has_video); +} + +TEST_F(UrlPlayerDemuxerTest, ReportsDemuxerType) { + EXPECT_EQ(demuxer_.GetDemuxerType(), DemuxerType::kUrlPlayerDemuxer); +} + +TEST_F(UrlPlayerDemuxerTest, ReportsDisplayName) { + EXPECT_EQ(demuxer_.GetDisplayName(), "UrlPlayerDemuxer"); +} + +TEST_F(UrlPlayerDemuxerTest, InitializeSucceeds) { + PipelineStatus result = PIPELINE_ERROR_ABORT; + demuxer_.Initialize(&host_, base::BindOnce([](PipelineStatus* out, + PipelineStatus s) { *out = s; }, + &result)); + task_environment_.RunUntilIdle(); + EXPECT_EQ(result, PIPELINE_OK); +} + +TEST_F(UrlPlayerDemuxerTest, SeekSucceeds) { + demuxer_.Initialize(&host_, base::DoNothing()); + task_environment_.RunUntilIdle(); + + PipelineStatus result = PIPELINE_ERROR_ABORT; + demuxer_.Seek( + base::Seconds(10), + base::BindOnce([](PipelineStatus* out, PipelineStatus s) { *out = s; }, + &result)); + task_environment_.RunUntilIdle(); + EXPECT_EQ(result, PIPELINE_OK); +} + +TEST_F(UrlPlayerDemuxerTest, IsSeekable) { + EXPECT_TRUE(demuxer_.IsSeekable()); +} + +TEST_F(UrlPlayerDemuxerTest, PlaceholderStreamConfigs) { + auto streams = demuxer_.GetAllStreams(); + for (auto* stream : streams) { + if (stream->type() == DemuxerStream::AUDIO) { + auto config = stream->audio_decoder_config(); + EXPECT_TRUE(config.IsValidConfig()); + EXPECT_EQ(config.codec(), AudioCodec::kAAC); + } + if (stream->type() == DemuxerStream::VIDEO) { + auto config = stream->video_decoder_config(); + EXPECT_TRUE(config.IsValidConfig()); + EXPECT_EQ(config.codec(), VideoCodec::kH264); + } + } +} + +TEST_F(UrlPlayerDemuxerTest, ZeroMemoryUsage) { + EXPECT_EQ(demuxer_.GetMemoryUsage(), 0); +} + +} // namespace +} // namespace media diff --git a/media/starboard/url_player_renderer.cc b/media/starboard/url_player_renderer.cc new file mode 100644 index 000000000000..2c040cbbc9aa --- /dev/null +++ b/media/starboard/url_player_renderer.cc @@ -0,0 +1,523 @@ +// 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(); + } + + // Poll buffered ranges from the native URL player. + if (buffered_ranges_cb_) { + TimeDelta buffer_start, buffer_length; + player_bridge_->GetUrlPlayerBufferedTimeRanges(&buffer_start, + &buffer_length); + if (buffer_length > TimeDelta()) { + buffered_ranges_cb_.Run(buffer_start, buffer_length); + } + } + + 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."; + } + + // Report duration from native player to the pipeline. + // For HLS, zero duration indicates a live/infinite stream. + if (duration_change_cb_) { + TimeDelta duration = player_bridge_->GetDuration(); + if (duration.is_zero()) { + duration = kInfiniteDuration; + } + duration_change_cb_.Run(duration); + } + + // 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..45d28d59e554 --- /dev/null +++ b/media/starboard/url_player_renderer.h @@ -0,0 +1,177 @@ +// 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; + using DurationChangeCB = + base::RepeatingCallback; + using BufferedRangesCB = + base::RepeatingCallback; + + void SetDurationChangeCB(DurationChangeCB cb) { + duration_change_cb_ = std::move(cb); + } + void SetBufferedRangesCB(BufferedRangesCB cb) { + buffered_ranges_cb_ = std::move(cb); + } + + 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_; + DurationChangeCB duration_change_cb_; + BufferedRangesCB buffered_ranges_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 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/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_; diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index 0c8b02df7e97..3b0c962aaac8 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -19911,6 +19911,7 @@ from previous Chrome versions. + @@ -19946,6 +19947,8 @@ from previous Chrome versions. + +