diff --git a/build/build_config.h b/build/build_config.h index d098eba7d4bd..8d420be31d5a 100644 --- a/build/build_config.h +++ b/build/build_config.h @@ -310,6 +310,12 @@ #define BUILDFLAG_INTERNAL_USE_STARBOARD_MEDIA() (0) #endif +#if defined(ENABLE_BUILDFLAG_USE_STARBOARD_URL_PLAYER) +#define BUILDFLAG_INTERNAL_USE_STARBOARD_URL_PLAYER() (1) +#else +#define BUILDFLAG_INTERNAL_USE_STARBOARD_URL_PLAYER() (0) +#endif + #if defined(ENABLE_BUILDFLAG_USE_EVERGREEN) #define BUILDFLAG_INTERNAL_USE_EVERGREEN() (1) #else diff --git a/cobalt/build/configs/BUILD.gn b/cobalt/build/configs/BUILD.gn index b1d06843dd4e..a359e742d09e 100644 --- a/cobalt/build/configs/BUILD.gn +++ b/cobalt/build/configs/BUILD.gn @@ -111,6 +111,14 @@ config("buildflag_defines") { defines += [ "ENABLE_BUILDFLAG_USE_STARBOARD_MEDIA" ] } + # use_starboard_url_player and BUILDFLAG(USE_STARBOARD_URL_PLAYER) should be + # used to wrap URL-based player functionality (e.g., HLS via AVPlayer on + # tvOS). This is a derived capability that is true only for tvOS builds with + # Starboard media enabled. + if (use_starboard_url_player) { + defines += [ "ENABLE_BUILDFLAG_USE_STARBOARD_URL_PLAYER" ] + } + # use_evergreen and BUILDFLAG(USE_EVERGREEN) should be used for # Evergreen-specific functionality, i.e. the Evergreen loader_app. Do not use # this flag for hermetic build functionality, instead use diff --git a/media/base/renderer.cc b/media/base/renderer.cc index bfa21ffc7cc8..51c40fb39b32 100644 --- a/media/base/renderer.cc +++ b/media/base/renderer.cc @@ -3,7 +3,9 @@ // found in the LICENSE file. #include "media/base/renderer.h" + #include "base/logging.h" +#include "build/build_config.h" namespace media { @@ -34,7 +36,11 @@ std::string GetRendererName(RendererType renderer_type) { #if BUILDFLAG(USE_STARBOARD_MEDIA) case RendererType::kStarboard: return "StarboardRenderer"; -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + case RendererType::kUrlPlayer: + return "UrlPlayerRenderer"; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) } } diff --git a/media/base/renderer.h b/media/base/renderer.h index cdb63a54ac74..6dbe77c31fda 100644 --- a/media/base/renderer.h +++ b/media/base/renderer.h @@ -9,6 +9,7 @@ #include "base/functional/callback.h" #include "base/time/time.h" +#include "build/build_config.h" #include "media/base/buffering_state.h" #include "media/base/demuxer_stream.h" #include "media/base/media_export.h" @@ -39,10 +40,15 @@ enum class RendererType { kTest = 11, // Renderer implementations used in tests #if BUILDFLAG(USE_STARBOARD_MEDIA) kStarboard = 12, // StarboardRendererFactory +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + kUrlPlayer = 13, // UrlPlayerRendererClientFactory + kMaxValue = kUrlPlayer, +#elif BUILDFLAG(USE_STARBOARD_MEDIA) kMaxValue = kStarboard, -#else // BUILDFLAG(USE_STARBOARD_MEDIA) +#else kMaxValue = kTest, -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) }; // Get the name of the Renderer for `renderer_type`. The returned name could be diff --git a/media/mojo/clients/mojo_renderer.cc b/media/mojo/clients/mojo_renderer.cc index 3dee3d43280e..1573295aeb72 100644 --- a/media/mojo/clients/mojo_renderer.cc +++ b/media/mojo/clients/mojo_renderer.cc @@ -151,6 +151,31 @@ void MojoRenderer::Initialize(MediaResource* media_resource, base::Unretained(this), client)); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MojoRenderer::InitializeWithUrl(media::RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb) { + DVLOG(1) << __func__; + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (!source_url.is_valid() || encountered_error_) { + task_runner_->PostTask( + FROM_HERE, base::BindOnce(std::move(init_cb), + PIPELINE_ERROR_INITIALIZATION_FAILED)); + return; + } + + init_cb_ = std::move(init_cb); + + BindRemoteRendererIfNeeded(); + + remote_renderer_->InitializeWithUrl( + client_receiver_.BindNewEndpointAndPassRemote(), source_url, + base::BindOnce(&MojoRenderer::OnInitialized, base::Unretained(this), + client)); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void MojoRenderer::SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) { DVLOG(1) << __func__; diff --git a/media/mojo/clients/mojo_renderer.h b/media/mojo/clients/mojo_renderer.h index 00a32abedc09..e4578b455632 100644 --- a/media/mojo/clients/mojo_renderer.h +++ b/media/mojo/clients/mojo_renderer.h @@ -15,6 +15,7 @@ #include "base/task/sequenced_task_runner.h" #include "base/time/default_tick_clock.h" #include "base/unguessable_token.h" +#include "build/build_config.h" #include "media/base/demuxer_stream.h" #include "media/base/renderer.h" #include "media/base/time_delta_interpolator.h" @@ -22,6 +23,7 @@ #include "mojo/public/cpp/bindings/associated_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" +#include "url/gurl.h" #if BUILDFLAG(USE_STARBOARD_MEDIA) #include "media/mojo/common/starboard/mojo_renderer_bypass_bridge.h" @@ -65,6 +67,11 @@ class MojoRenderer : public Renderer, public mojom::RendererClient { void Initialize(MediaResource* media_resource, media::RendererClient* client, PipelineStatusCallback init_cb) override; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl(media::RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; void SetLatencyHint(std::optional latency_hint) override; void Flush(base::OnceClosure flush_cb) override; diff --git a/media/mojo/clients/mojo_renderer_wrapper.cc b/media/mojo/clients/mojo_renderer_wrapper.cc index 6bdf72389ee9..ce0a1397360e 100644 --- a/media/mojo/clients/mojo_renderer_wrapper.cc +++ b/media/mojo/clients/mojo_renderer_wrapper.cc @@ -6,6 +6,8 @@ #include +#include "base/logging.h" + namespace media { MojoRendererWrapper::MojoRendererWrapper( @@ -20,6 +22,15 @@ void MojoRendererWrapper::Initialize(MediaResource* media_resource, mojo_renderer_->Initialize(media_resource, client, std::move(init_cb)); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MojoRendererWrapper::InitializeWithUrl(RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb) { + DVLOG(1) << __func__; + mojo_renderer_->InitializeWithUrl(client, source_url, std::move(init_cb)); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void MojoRendererWrapper::Flush(base::OnceClosure flush_cb) { mojo_renderer_->Flush(std::move(flush_cb)); } diff --git a/media/mojo/clients/mojo_renderer_wrapper.h b/media/mojo/clients/mojo_renderer_wrapper.h index 0b42900b84de..a78e5b278e12 100644 --- a/media/mojo/clients/mojo_renderer_wrapper.h +++ b/media/mojo/clients/mojo_renderer_wrapper.h @@ -7,8 +7,10 @@ #include +#include "build/build_config.h" #include "media/base/renderer.h" #include "media/mojo/clients/mojo_renderer.h" +#include "url/gurl.h" namespace media { @@ -30,6 +32,11 @@ class MojoRendererWrapper : public Renderer { void Initialize(MediaResource* media_resource, RendererClient* client, PipelineStatusCallback init_cb) override; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl(RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb); +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; void SetLatencyHint(std::optional latency_hint) override; void Flush(base::OnceClosure flush_cb) override; diff --git a/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc b/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc index 7304375fa7de..d80bf29056f9 100644 --- a/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc +++ b/media/mojo/clients/starboard/starboard_renderer_client_unittest.cc @@ -24,6 +24,7 @@ #include "media/base/mock_media_log.h" #include "media/base/test_helpers.h" #include "media/base/video_renderer_sink.h" +#include "media/media_buildflags.h" #include "media/mojo/mojom/renderer.mojom.h" #include "media/mojo/mojom/renderer_extensions.mojom.h" #include "media/renderers/video_overlay_factory.h" @@ -61,6 +62,13 @@ class FakeMojomRenderer : public mojom::Renderer { InitializeWithBypassBridgeCallback cb) override { std::move(cb).Run(true); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl(mojo::PendingAssociatedRemote, + const GURL& source_url, + InitializeWithUrlCallback cb) override { + std::move(cb).Run(true); + } +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) MOCK_METHOD1(Flush, void(FlushCallback)); void StartPlayingFrom(base::TimeDelta time) override {} MOCK_METHOD1(SetPlaybackRate, void(double)); diff --git a/media/mojo/mojom/BUILD.gn b/media/mojo/mojom/BUILD.gn index c15765653f54..0a97c6e23e0f 100644 --- a/media/mojo/mojom/BUILD.gn +++ b/media/mojo/mojom/BUILD.gn @@ -150,6 +150,10 @@ mojom("mojom") { enabled_features += [ "use_starboard_media" ] } + if (is_cobalt && use_starboard_url_player) { + enabled_features += [ "use_starboard_url_player" ] + } + shared_typemaps = [ { types = [ diff --git a/media/mojo/mojom/media_types.mojom b/media/mojo/mojom/media_types.mojom index d649d9e890a2..0621d33fbef5 100644 --- a/media/mojo/mojom/media_types.mojom +++ b/media/mojo/mojom/media_types.mojom @@ -650,6 +650,8 @@ enum RendererType { kTest= 11, // Renderer implementations used in tests [EnableIf=use_starboard_media] kStarboard = 12, // StarboardRendererFactory + [EnableIf=use_starboard_url_player] + kUrlPlayer = 13, // UrlPlayerRendererClientFactory }; // See media/base/demuxer.h for description. diff --git a/media/mojo/mojom/media_types_enum_mojom_traits.h b/media/mojo/mojom/media_types_enum_mojom_traits.h index 20563e88d10e..3a15ffa10210 100644 --- a/media/mojo/mojom/media_types_enum_mojom_traits.h +++ b/media/mojo/mojom/media_types_enum_mojom_traits.h @@ -391,7 +391,11 @@ struct EnumTraits { #if BUILDFLAG(USE_STARBOARD_MEDIA) case ::media::RendererType::kStarboard: return media::mojom::RendererType::kStarboard; -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + case ::media::RendererType::kUrlPlayer: + return media::mojom::RendererType::kUrlPlayer; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) } NOTREACHED(); @@ -436,7 +440,12 @@ struct EnumTraits { case media::mojom::RendererType::kStarboard: *output = ::media::RendererType::kStarboard; return true; -#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + case media::mojom::RendererType::kUrlPlayer: + *output = ::media::RendererType::kUrlPlayer; + return true; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) } NOTREACHED(); diff --git a/media/mojo/mojom/renderer.mojom b/media/mojo/mojom/renderer.mojom index 87f9ac58c64e..8d8412d105f3 100644 --- a/media/mojo/mojom/renderer.mojom +++ b/media/mojo/mojom/renderer.mojom @@ -25,6 +25,14 @@ interface Renderer { Initialize(pending_associated_remote client, array>? streams) => (bool success); + // Initializes the tvOS URL player with a `source_url` delivered on the + // renderer pipe. No demuxer streams are needed - the platform handles + // demuxing. Sending the URL on the renderer pipe keeps URL delivery + // and initialization ordered on the same Mojo pipe. + [EnableIf=use_starboard_url_player] + InitializeWithUrl(pending_associated_remote client, + url.mojom.Url source_url) => (bool success); + // Supported only in single-process mode with the feature enabled. [EnableIf=use_starboard_media] InitializeWithBypassBridge(pending_associated_remote client, diff --git a/media/mojo/services/BUILD.gn b/media/mojo/services/BUILD.gn index 251f8c1bde67..a1129380798e 100644 --- a/media/mojo/services/BUILD.gn +++ b/media/mojo/services/BUILD.gn @@ -143,6 +143,13 @@ component("services") { sources += [ "gpu_mojo_media_client_stubs.cc" ] } + if (is_cobalt && use_starboard_url_player) { + sources += [ + "starboard/url_player_renderer_wrapper.cc", + "starboard/url_player_renderer_wrapper.h", + ] + } + if (is_chromeos) { deps += [ "//chromeos/components/cdm_factory_daemon:cdm_factory_daemon_gpu" ] @@ -366,6 +373,9 @@ source_set("unit_tests") { "//starboard/common", ] } + if (is_cobalt && use_starboard_url_player) { + sources += [ "starboard/url_player_renderer_wrapper_unittest.cc" ] + } } mojolpm_fuzzer_test("webrtc_video_perf_mojolpm_fuzzer") { diff --git a/media/mojo/services/mojo_renderer_service.cc b/media/mojo/services/mojo_renderer_service.cc index 40359f29aa83..995815c63f19 100644 --- a/media/mojo/services/mojo_renderer_service.cc +++ b/media/mojo/services/mojo_renderer_service.cc @@ -21,6 +21,10 @@ #include "media/mojo/common/starboard/mojo_renderer_bypass_bridge.h" #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +#include "media/mojo/services/starboard/url_player_renderer_wrapper.h" +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + namespace media { // Time interval to update media time. @@ -135,6 +139,39 @@ void MojoRendererService::Initialize( std::move(callback))); } +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) +void MojoRendererService::InitializeWithUrl( + mojo::PendingAssociatedRemote client, + const GURL& source_url, + InitializeWithUrlCallback callback) { + DVLOG(1) << __func__ << ": " << source_url; + + if (state_ != STATE_UNINITIALIZED) { + std::move(callback).Run(false); + return; + } + + if (renderer_->GetRendererType() != RendererType::kUrlPlayer) { + mojo::ReportBadMessage("InitializeWithUrl requires URL player renderer"); + return; + } + + if (!source_url.is_valid()) { + mojo::ReportBadMessage("InitializeWithUrl requires a valid source URL"); + return; + } + + client_.Bind(std::move(client)); + state_ = STATE_INITIALIZING; + + static_cast(renderer_.get()) + ->InitializeWithUrl( + this, source_url, + base::BindOnce(&MojoRendererService::OnRendererInitializeDone, + weak_this_, std::move(callback))); +} +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) + #if BUILDFLAG(USE_STARBOARD_MEDIA) void MojoRendererService::InitializeWithBypassBridge( mojo::PendingAssociatedRemote client, diff --git a/media/mojo/services/mojo_renderer_service.h b/media/mojo/services/mojo_renderer_service.h index 6e056373b104..06d9c2583744 100644 --- a/media/mojo/services/mojo_renderer_service.h +++ b/media/mojo/services/mojo_renderer_service.h @@ -17,6 +17,7 @@ #include "base/time/time.h" #include "base/timer/timer.h" #include "base/unguessable_token.h" +#include "build/build_config.h" #include "media/base/buffering_state.h" #include "media/base/media_resource.h" #include "media/base/pipeline_status.h" @@ -28,6 +29,7 @@ #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/self_owned_receiver.h" +#include "url/gurl.h" namespace media { @@ -67,6 +69,12 @@ class MEDIA_MOJO_EXPORT MojoRendererService final : public mojom::Renderer, std::optional>> streams, InitializeCallback callback) final; +#if BUILDFLAG(USE_STARBOARD_URL_PLAYER) + void InitializeWithUrl( + mojo::PendingAssociatedRemote client, + const GURL& source_url, + InitializeWithUrlCallback callback) final; +#endif // BUILDFLAG(USE_STARBOARD_URL_PLAYER) #if BUILDFLAG(USE_STARBOARD_MEDIA) void InitializeWithBypassBridge( mojo::PendingAssociatedRemote client, diff --git a/media/mojo/services/starboard/url_player_renderer_wrapper.cc b/media/mojo/services/starboard/url_player_renderer_wrapper.cc new file mode 100644 index 000000000000..7502e05d7f85 --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper.cc @@ -0,0 +1,207 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/mojo/services/starboard/url_player_renderer_wrapper.h" + +#include + +#include "base/logging.h" +#include "media/mojo/services/mojo_media_log.h" +#include "ui/gfx/geometry/rect_conversions.h" + +namespace media { + +UrlPlayerRendererTraits::UrlPlayerRendererTraits( + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote) + : task_runner(std::move(task_runner)), + media_log_remote(std::move(media_log_remote)), + video_geometry_setter_service(video_geometry_setter_service), + overlay_plane_id(overlay_plane_id), + viewport_size(viewport_size), + renderer_extension_receiver(std::move(renderer_extension_receiver)), + client_extension_remote(std::move(client_extension_remote)) {} + +UrlPlayerRendererTraits::~UrlPlayerRendererTraits() = default; + +UrlPlayerRendererWrapper::UrlPlayerRendererWrapper( + UrlPlayerRendererTraits traits) + : renderer_extension_receiver_( + this, + std::move(traits.renderer_extension_receiver)), + client_extension_remote_(std::move(traits.client_extension_remote), + traits.task_runner), + video_geometry_setter_service_(traits.video_geometry_setter_service), + overlay_plane_id_(traits.overlay_plane_id), + renderer_( + traits.task_runner, + std::make_unique(std::move(traits.media_log_remote), + traits.task_runner), + traits.overlay_plane_id, + traits.viewport_size) { + DETACH_FROM_THREAD(thread_checker_); +} + +UrlPlayerRendererWrapper::~UrlPlayerRendererWrapper() = default; + +void UrlPlayerRendererWrapper::Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + DCHECK(init_cb); + + // Subscribe to video geometry changes. + DCHECK(video_geometry_setter_service_); + video_geometry_setter_service_->GetVideoGeometryChangeSubscriber( + video_geometry_change_subscriber_remote_.BindNewPipeAndPassReceiver()); + DCHECK(video_geometry_change_subscriber_remote_); + video_geometry_change_subscriber_remote_->SubscribeToVideoGeometryChange( + overlay_plane_id_, + video_geometry_change_client_receiver_.BindNewPipeAndPassRemote(), + base::BindOnce( + &UrlPlayerRendererWrapper::OnSubscribeToVideoGeometryChange, + weak_factory_.GetWeakPtr(), media_resource, client, + std::move(init_cb))); + + // Install renderer callbacks for paint, rendering mode, and window handle. + GetRenderer()->SetUrlPlayerRendererCallbacks( + base::BindRepeating(&UrlPlayerRendererWrapper::OnPaintVideoHoleFrame, + weak_factory_.GetWeakPtr()), + base::BindRepeating( + &UrlPlayerRendererWrapper::OnUpdateStarboardRenderingMode, + weak_factory_.GetWeakPtr()), + base::BindRepeating(&UrlPlayerRendererWrapper::OnGetSbWindowHandle, + weak_factory_.GetWeakPtr())); +} + +void UrlPlayerRendererWrapper::InitializeWithUrl( + RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + DCHECK(source_url.is_valid()); + + GetRenderer()->SetSourceUrl(source_url.spec()); + Initialize(/*media_resource=*/nullptr, client, std::move(init_cb)); +} + +void UrlPlayerRendererWrapper::Flush(base::OnceClosure flush_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->Flush(std::move(flush_cb)); +} + +void UrlPlayerRendererWrapper::StartPlayingFrom(base::TimeDelta time) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->StartPlayingFrom(time); +} + +void UrlPlayerRendererWrapper::SetPlaybackRate(double playback_rate) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetPlaybackRate(playback_rate); +} + +void UrlPlayerRendererWrapper::SetVolume(float volume) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetVolume(volume); +} + +void UrlPlayerRendererWrapper::SetCdm(CdmContext* cdm_context, + CdmAttachedCB cdm_attached_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetCdm(cdm_context, std::move(cdm_attached_cb)); +} + +void UrlPlayerRendererWrapper::SetLatencyHint( + std::optional latency_hint) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->SetLatencyHint(latency_hint); +} + +base::TimeDelta UrlPlayerRendererWrapper::GetMediaTime() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + return GetRenderer()->GetMediaTime(); +} + +RendererType UrlPlayerRendererWrapper::GetRendererType() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + return RendererType::kUrlPlayer; +} + +void UrlPlayerRendererWrapper::OnGpuChannelTokenReady( + mojom::CommandBufferIdPtr command_buffer_id) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + // URL player uses punch-out only; GPU channel is not needed. +} + +void UrlPlayerRendererWrapper::GetCurrentVideoFrame( + GetCurrentVideoFrameCallback callback) { + // URL player uses punch-out only; decode-to-texture is unsupported. + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + std::move(callback).Run(nullptr); +} + +void UrlPlayerRendererWrapper::OnSbWindowHandleReady( + uint64_t sb_window_handle) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + GetRenderer()->OnSbWindowHandleReady(sb_window_handle); +} + +UrlPlayerRenderer* UrlPlayerRendererWrapper::GetRenderer() { + if (test_renderer_) { + return test_renderer_; + } + return &renderer_; +} + +void UrlPlayerRendererWrapper::OnVideoGeometryChange( + const gfx::RectF& rect_f, + gfx::OverlayTransform /* transform */) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + gfx::Rect new_bounds = gfx::ToEnclosedRect(rect_f); + GetRenderer()->OnVideoGeometryChange(new_bounds); +} + +void UrlPlayerRendererWrapper::OnPaintVideoHoleFrame(const gfx::Size& size) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->PaintVideoHoleFrame(size); +} + +void UrlPlayerRendererWrapper::OnUpdateStarboardRenderingMode( + StarboardRenderingMode mode) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->UpdateStarboardRenderingMode(mode); +} + +void UrlPlayerRendererWrapper::OnGetSbWindowHandle() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + client_extension_remote_->GetSbWindowHandle(); +} + +void UrlPlayerRendererWrapper::OnSubscribeToVideoGeometryChange( + MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + DCHECK(init_cb); + GetRenderer()->Initialize(media_resource, client, std::move(init_cb)); +} + +} // namespace media diff --git a/media/mojo/services/starboard/url_player_renderer_wrapper.h b/media/mojo/services/starboard/url_player_renderer_wrapper.h new file mode 100644 index 000000000000..44d36ded2dca --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper.h @@ -0,0 +1,146 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEDIA_MOJO_SERVICES_STARBOARD_URL_PLAYER_RENDERER_WRAPPER_H_ +#define MEDIA_MOJO_SERVICES_STARBOARD_URL_PLAYER_RENDERER_WRAPPER_H_ + +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "cobalt/media/service/mojom/video_geometry_setter.mojom.h" +#include "cobalt/media/service/video_geometry_setter_service.h" +#include "media/base/renderer.h" +#include "media/mojo/mojom/renderer_extensions.mojom.h" +#include "media/starboard/url_player_renderer.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "url/gurl.h" + +namespace media { + +struct UrlPlayerRendererTraits { + scoped_refptr task_runner; + mojo::PendingRemote media_log_remote; + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service; + const base::UnguessableToken& overlay_plane_id; + const gfx::Size& viewport_size; + mojo::PendingReceiver + renderer_extension_receiver; + mojo::PendingRemote + client_extension_remote; + + UrlPlayerRendererTraits( + scoped_refptr task_runner, + mojo::PendingRemote media_log_remote, + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size, + mojo::PendingReceiver + renderer_extension_receiver, + mojo::PendingRemote + client_extension_remote); + UrlPlayerRendererTraits(UrlPlayerRendererTraits&& that) = default; + ~UrlPlayerRendererTraits(); +}; + +// Wrapper around UrlPlayerRenderer that bridges to Mojo extension endpoints. +// Mirrors StarboardRendererWrapper but omits GPU factory, decode-to-texture +// (SbDecodeTarget) frame extraction, and Android overlay support because +// URL player delegates rendering to the platform (AVPlayer) via punch-out. +// This is consistent with Cobalt 25, where SbUrlPlayerOutputModeSupported() +// returned true only for kSbPlayerOutputModePunchOut on tvOS. +// OnGpuChannelTokenReady() and GetCurrentVideoFrame() are stubbed as no-ops; +// decode-to-texture support can be added when tvOS requires it. +// +// Lifetime and Ownership: Owned by MojoRendererService, lifetime is +// tied to the Mojo connection. +// +// Threading Model: Thread-affine, must be used on the media task +// runner sequence. +class UrlPlayerRendererWrapper + : public Renderer, + public mojom::StarboardRendererExtension, + public cobalt::media::mojom::VideoGeometryChangeClient { + public: + using RendererExtension = mojom::StarboardRendererExtension; + using ClientExtension = mojom::StarboardRendererClientExtension; + + explicit UrlPlayerRendererWrapper(UrlPlayerRendererTraits traits); + + UrlPlayerRendererWrapper(const UrlPlayerRendererWrapper&) = delete; + UrlPlayerRendererWrapper& operator=(const UrlPlayerRendererWrapper&) = delete; + + ~UrlPlayerRendererWrapper() override; + + // Renderer implementation. + void Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) override; + void InitializeWithUrl(RendererClient* client, + const GURL& source_url, + PipelineStatusCallback init_cb); + void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; + void SetLatencyHint(std::optional latency_hint) override; + void Flush(base::OnceClosure flush_cb) override; + void StartPlayingFrom(base::TimeDelta time) override; + void SetPlaybackRate(double playback_rate) override; + void SetVolume(float volume) override; + base::TimeDelta GetMediaTime() override; + RendererType GetRendererType() override; + + // mojom::StarboardRendererExtension implementation. + void OnGpuChannelTokenReady( + mojom::CommandBufferIdPtr command_buffer_id) override; + void GetCurrentVideoFrame(GetCurrentVideoFrameCallback callback) override; + void OnSbWindowHandleReady(uint64_t sb_window_handle) override; + // cobalt::media::mojom::VideoGeometryChangeClient implementation. + void OnVideoGeometryChange(const gfx::RectF& rect_f, + gfx::OverlayTransform transform) override; + + UrlPlayerRenderer* GetRenderer(); + + void SetRendererForTesting(UrlPlayerRenderer* renderer) { + test_renderer_ = renderer; + } + + private: + void OnPaintVideoHoleFrame(const gfx::Size& size); + void OnUpdateStarboardRenderingMode(StarboardRenderingMode mode); + void OnGetSbWindowHandle(); + void OnSubscribeToVideoGeometryChange(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb); + + mojo::Receiver renderer_extension_receiver_; + mojo::Remote client_extension_remote_; + cobalt::media::VideoGeometrySetterService* video_geometry_setter_service_; + const base::UnguessableToken overlay_plane_id_; + UrlPlayerRenderer renderer_; + + raw_ptr test_renderer_ = nullptr; + + mojo::Remote + video_geometry_change_subscriber_remote_; + mojo::Receiver + video_geometry_change_client_receiver_{this}; + + THREAD_CHECKER(thread_checker_); + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace media + +#endif // MEDIA_MOJO_SERVICES_STARBOARD_URL_PLAYER_RENDERER_WRAPPER_H_ diff --git a/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc b/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc new file mode 100644 index 000000000000..0386da6b4daf --- /dev/null +++ b/media/mojo/services/starboard/url_player_renderer_wrapper_unittest.cc @@ -0,0 +1,371 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/mojo/services/starboard/url_player_renderer_wrapper.h" + +#include +#include + +#include "base/functional/callback_helpers.h" +#include "base/run_loop.h" +#include "base/test/gmock_callback_support.h" +#include "base/test/mock_callback.h" +#include "base/test/task_environment.h" +#include "cobalt/media/service/video_geometry_setter_service.h" +#include "media/base/media_util.h" +#include "media/base/mock_filters.h" +#include "media/base/test_helpers.h" +#include "media/starboard/sbplayer_interface.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +struct SbPlayerPrivate {}; + +namespace media { +namespace { + +using ::base::test::RunOnceCallback; +using ::base::test::RunOnceClosure; +using ::testing::_; +using ::testing::DoAll; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::StrEq; +using ::testing::StrictMock; + +constexpr char kSourceUrl[] = "https://example.test/video.m3u8"; + +class MockUrlPlayerRenderer : public UrlPlayerRenderer { + public: + explicit MockUrlPlayerRenderer( + const scoped_refptr& task_runner) + : UrlPlayerRenderer(task_runner, + std::make_unique(), + base::UnguessableToken::Create(), + gfx::Size()) {} + + void Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) override { + OnInitialize(media_resource, client, init_cb); + } + MOCK_METHOD(void, + OnInitialize, + (MediaResource*, RendererClient*, PipelineStatusCallback&)); + MOCK_METHOD(void, SetCdm, (CdmContext*, CdmAttachedCB), (override)); + MOCK_METHOD(void, + SetLatencyHint, + (std::optional), + (override)); + MOCK_METHOD(void, Flush, (base::OnceClosure), (override)); + MOCK_METHOD(void, StartPlayingFrom, (base::TimeDelta), (override)); + MOCK_METHOD(void, SetPlaybackRate, (double), (override)); + MOCK_METHOD(void, SetVolume, (float), (override)); + MOCK_METHOD(base::TimeDelta, GetMediaTime, (), (override)); +}; + +class MockStarboardRendererClientExtension + : public mojom::StarboardRendererClientExtension { + public: + MOCK_METHOD(void, PaintVideoHoleFrame, (const gfx::Size&), (override)); + MOCK_METHOD(void, + UpdateStarboardRenderingMode, + (StarboardRenderingMode), + (override)); + MOCK_METHOD(void, GetSbWindowHandle, (), (override)); +}; + +class MockSbPlayerInterface : public SbPlayerInterface { + public: + MOCK_METHOD(SbPlayer, + Create, + (SbWindow, + const SbPlayerCreationParam*, + SbPlayerDeallocateSampleFunc, + SbPlayerDecoderStatusFunc, + SbPlayerStatusFunc, + SbPlayerErrorFunc, + void*, + SbDecodeTargetGraphicsContextProvider*), + (override)); + MOCK_METHOD(SbPlayerOutputMode, + GetPreferredOutputMode, + (const SbPlayerCreationParam*), + (override)); + void Destroy(SbPlayer player) override { delete player; } + MOCK_METHOD(void, Seek, (SbPlayer, base::TimeDelta, int), (override)); + MOCK_METHOD(void, + WriteSamples, + (SbPlayer, SbMediaType, const SbPlayerSampleInfo*, int), + (override)); + MOCK_METHOD(int, + GetMaximumNumberOfSamplesPerWrite, + (SbPlayer, SbMediaType), + (override)); + MOCK_METHOD(void, WriteEndOfStream, (SbPlayer, SbMediaType), (override)); + MOCK_METHOD(void, SetBounds, (SbPlayer, int, int, int, int, int), (override)); + MOCK_METHOD(bool, SetPlaybackRate, (SbPlayer, double), (override)); + MOCK_METHOD(void, SetVolume, (SbPlayer, double), (override)); + MOCK_METHOD(void, GetInfo, (SbPlayer, SbPlayerInfo*), (override)); + MOCK_METHOD(SbDecodeTarget, GetCurrentFrame, (SbPlayer), (override)); + MOCK_METHOD(SbPlayer, + CreateUrlPlayer, + (const char*, + SbWindow, + SbPlayerStatusFunc, + SbPlayerEncryptedMediaInitDataEncounteredCB, + SbPlayerErrorFunc, + void*), + (override)); + MOCK_METHOD(void, SetUrlPlayerDrmSystem, (SbPlayer, SbDrmSystem), (override)); + MOCK_METHOD(bool, + GetUrlPlayerOutputModeSupported, + (SbPlayerOutputMode), + (override)); + MOCK_METHOD(void, + GetUrlPlayerExtraInfo, + (SbPlayer, SbUrlPlayerExtraInfo*), + (override)); + MOCK_METHOD(bool, + GetAudioConfiguration, + (SbPlayer, int, SbMediaAudioConfiguration*), + (override)); +}; + +class UrlPlayerRendererWrapperTest : public testing::Test { + protected: + UrlPlayerRendererWrapperTest() + : mock_renderer_(std::make_unique>( + task_environment_.GetMainThreadTaskRunner())), + client_extension_receiver_(&client_extension_) { + mojo::PendingRemote + client_extension_remote; + client_extension_receiver_.Bind( + client_extension_remote.InitWithNewPipeAndPassReceiver()); + + auto media_log_remote = media_log_receiver_.InitWithNewPipeAndPassRemote(); + auto renderer_extension_receiver = + renderer_extension_.BindNewPipeAndPassReceiver(); + UrlPlayerRendererTraits traits( + task_environment_.GetMainThreadTaskRunner(), + std::move(media_log_remote), &video_geometry_setter_service_, + base::UnguessableToken::Create(), gfx::Size(), + std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + renderer_wrapper_ = + std::make_unique(std::move(traits)); + renderer_wrapper_->SetRendererForTesting(mock_renderer_.get()); + } + + ~UrlPlayerRendererWrapperTest() override { + renderer_wrapper_.reset(); + mock_renderer_.reset(); + } + + base::test::TaskEnvironment task_environment_; + cobalt::media::VideoGeometrySetterService video_geometry_setter_service_; + std::unique_ptr> mock_renderer_; + StrictMock client_extension_; + mojo::Receiver + client_extension_receiver_; + mojo::Remote renderer_extension_; + mojo::PendingReceiver media_log_receiver_; + std::unique_ptr renderer_wrapper_; + NiceMock media_resource_; + NiceMock renderer_client_; +}; + +TEST_F(UrlPlayerRendererWrapperTest, InitializesAfterGeometrySubscription) { + base::RunLoop run_loop; + base::MockOnceCallback init_cb; + EXPECT_CALL(*mock_renderer_, + OnInitialize(&media_resource_, &renderer_client_, _)) + .WillOnce(RunOnceCallback<2>(PIPELINE_OK)); + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))) + .WillOnce(RunOnceClosure(run_loop.QuitClosure())); + + renderer_wrapper_->Initialize(&media_resource_, &renderer_client_, + init_cb.Get()); + run_loop.Run(); +} + +TEST_F(UrlPlayerRendererWrapperTest, DelegatesRendererMethods) { + const base::TimeDelta start_time = base::Seconds(5); + const base::TimeDelta media_time = base::Seconds(42); + base::MockOnceClosure flush_cb; + base::MockOnceCallback cdm_cb; + + EXPECT_CALL(*mock_renderer_, + SetLatencyHint(std::optional(base::Milliseconds(250)))); + renderer_wrapper_->SetLatencyHint(base::Milliseconds(250)); + EXPECT_CALL(*mock_renderer_, StartPlayingFrom(start_time)); + renderer_wrapper_->StartPlayingFrom(start_time); + EXPECT_CALL(*mock_renderer_, SetPlaybackRate(1.25)); + renderer_wrapper_->SetPlaybackRate(1.25); + EXPECT_CALL(*mock_renderer_, SetVolume(0.5f)); + renderer_wrapper_->SetVolume(0.5f); + EXPECT_CALL(*mock_renderer_, GetMediaTime()).WillOnce(Return(media_time)); + EXPECT_EQ(renderer_wrapper_->GetMediaTime(), media_time); + EXPECT_CALL(*mock_renderer_, Flush(_)) + .WillOnce(Invoke([](base::OnceClosure cb) { std::move(cb).Run(); })); + EXPECT_CALL(flush_cb, Run()); + renderer_wrapper_->Flush(flush_cb.Get()); + EXPECT_CALL(*mock_renderer_, SetCdm(nullptr, _)) + .WillOnce(Invoke([](CdmContext*, Renderer::CdmAttachedCB cb) { + std::move(cb).Run(true); + })); + EXPECT_CALL(cdm_cb, Run(true)); + renderer_wrapper_->SetCdm(nullptr, cdm_cb.Get()); +} + +TEST_F(UrlPlayerRendererWrapperTest, ReportsUrlPlayerTypeAndNoGpuFrame) { + EXPECT_EQ(renderer_wrapper_->GetRendererType(), RendererType::kUrlPlayer); + + auto command_buffer_id = mojom::CommandBufferId::New(); + command_buffer_id->channel_token = base::UnguessableToken::Create(); + command_buffer_id->route_id = 7; + renderer_wrapper_->OnGpuChannelTokenReady(std::move(command_buffer_id)); + + base::MockCallback + frame_cb; + EXPECT_CALL(frame_cb, Run(testing::IsNull())); + renderer_wrapper_->GetCurrentVideoFrame(frame_cb.Get()); +} + +TEST_F(UrlPlayerRendererWrapperTest, InitializeWithUrlPassesNullMediaResource) { + base::RunLoop run_loop; + base::MockOnceCallback init_cb; + // InitializeWithUrl should call Initialize with nullptr media_resource. + EXPECT_CALL(*mock_renderer_, OnInitialize(nullptr, &renderer_client_, _)) + .WillOnce(RunOnceCallback<2>(PIPELINE_OK)); + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))) + .WillOnce(RunOnceClosure(run_loop.QuitClosure())); + + renderer_wrapper_->InitializeWithUrl(&renderer_client_, GURL(kSourceUrl), + init_cb.Get()); + run_loop.Run(); +} + +class UrlPlayerRendererWrapperIntegrationTest : public testing::Test { + protected: + UrlPlayerRendererWrapperIntegrationTest() + : overlay_plane_id_(base::UnguessableToken::Create()), + client_extension_receiver_(&client_extension_) { + mojo::PendingRemote + client_extension_remote; + client_extension_receiver_.Bind( + client_extension_remote.InitWithNewPipeAndPassReceiver()); + + auto media_log_remote = media_log_receiver_.InitWithNewPipeAndPassRemote(); + auto renderer_extension_receiver = + renderer_extension_.BindNewPipeAndPassReceiver(); + UrlPlayerRendererTraits traits( + task_environment_.GetMainThreadTaskRunner(), + std::move(media_log_remote), &video_geometry_setter_service_, + overlay_plane_id_, gfx::Size(), std::move(renderer_extension_receiver), + std::move(client_extension_remote)); + renderer_wrapper_ = + std::make_unique(std::move(traits)); + renderer_wrapper_->GetRenderer()->SetSbPlayerInterfaceForTesting( + &mock_sbplayer_interface_); + + ON_CALL(mock_sbplayer_interface_, GetUrlPlayerOutputModeSupported(_)) + .WillByDefault(Invoke([](SbPlayerOutputMode mode) { + return mode == kSbPlayerOutputModePunchOut; + })); + ON_CALL(mock_sbplayer_interface_, SetPlaybackRate(_, _)) + .WillByDefault(Return(true)); + } + + base::test::TaskEnvironment task_environment_; + cobalt::media::VideoGeometrySetterService video_geometry_setter_service_; + const base::UnguessableToken overlay_plane_id_; + NiceMock mock_sbplayer_interface_; + StrictMock client_extension_; + mojo::Receiver + client_extension_receiver_; + mojo::Remote renderer_extension_; + mojo::PendingReceiver media_log_receiver_; + std::unique_ptr renderer_wrapper_; + NiceMock media_resource_; + NiceMock renderer_client_; +}; + +TEST_F(UrlPlayerRendererWrapperIntegrationTest, + ForwardsUrlWindowGeometryAndClientCallbacks) { + constexpr uint64_t kWindowHandle = 0x1234; + SbWindow window = reinterpret_cast(kWindowHandle); + SbPlayer player = new SbPlayerPrivate(); + SbPlayerStatusFunc player_status_cb = nullptr; + void* player_context = nullptr; + + // Phase 1: InitializeWithUrl triggers GetSbWindowHandle via Mojo. + base::RunLoop window_handle_loop; + base::MockOnceCallback init_cb; + EXPECT_CALL(client_extension_, GetSbWindowHandle()) + .WillOnce(RunOnceClosure(window_handle_loop.QuitClosure())); + renderer_wrapper_->InitializeWithUrl(&renderer_client_, GURL(kSourceUrl), + init_cb.Get()); + window_handle_loop.Run(); + + // Phase 2: Window handle arrives, player is created. + base::RunLoop player_created_loop; + renderer_wrapper_->OnVideoGeometryChange( + gfx::RectF(10.0f, 20.0f, 640.0f, 360.0f), gfx::OVERLAY_TRANSFORM_NONE); + EXPECT_CALL(mock_sbplayer_interface_, + CreateUrlPlayer(StrEq(kSourceUrl), window, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(&player_status_cb), + SaveArg<5>(&player_context), Return(player))); + EXPECT_CALL(mock_sbplayer_interface_, SetBounds(player, _, 10, 20, 640, 360)); + EXPECT_CALL(client_extension_, + UpdateStarboardRenderingMode(StarboardRenderingMode::kPunchOut)) + .WillOnce(RunOnceClosure(player_created_loop.QuitClosure())); + renderer_extension_->OnSbWindowHandleReady(kWindowHandle); + player_created_loop.Run(); + + // Phase 3: Player reports initialized. + base::RunLoop init_loop; + ASSERT_TRUE(player_status_cb); + EXPECT_CALL(init_cb, Run(HasStatusCode(PIPELINE_OK))) + .WillOnce(RunOnceClosure(init_loop.QuitClosure())); + player_status_cb(player, player_context, kSbPlayerStateInitialized, + SB_PLAYER_INITIAL_TICKET); + init_loop.Run(); + + // Phase 4: Player reports presenting with video dimensions. + base::RunLoop presenting_loop; + EXPECT_CALL(mock_sbplayer_interface_, GetInfo(player, _)) + .WillOnce(Invoke([](SbPlayer, SbPlayerInfo* info) { + *info = {}; + info->frame_width = 1920; + info->frame_height = 1080; + })); + EXPECT_CALL(renderer_client_, + OnVideoNaturalSizeChange(gfx::Size(1920, 1080))); + EXPECT_CALL(renderer_client_, + OnBufferingStateChange(BUFFERING_HAVE_ENOUGH, + BUFFERING_CHANGE_REASON_UNKNOWN)); + EXPECT_CALL(client_extension_, PaintVideoHoleFrame(gfx::Size(1920, 1080))) + .WillOnce(RunOnceClosure(presenting_loop.QuitClosure())); + player_status_cb(player, player_context, kSbPlayerStatePresenting, + SB_PLAYER_INITIAL_TICKET); + presenting_loop.Run(); +} + +} // namespace +} // namespace media diff --git a/media/starboard/BUILD.gn b/media/starboard/BUILD.gn index bb7b8fb00656..22b166a2e940 100644 --- a/media/starboard/BUILD.gn +++ b/media/starboard/BUILD.gn @@ -59,6 +59,12 @@ source_set("starboard") { deps += [ ":buildflags" ] } + if (use_starboard_url_player) { + sources += [ + "url_player_renderer.cc", + "url_player_renderer.h", + ] + } deps += [ "//base", @@ -98,6 +104,9 @@ source_set("unit_tests") { "starboard_utils_test.cc", ] } + if (use_starboard_url_player) { + sources += [ "url_player_renderer_unittest.cc" ] + } configs += [ "//media:media_config" ] deps = [ "//base", diff --git a/media/starboard/url_player_renderer.cc b/media/starboard/url_player_renderer.cc new file mode 100644 index 000000000000..96ceb1dcdafc --- /dev/null +++ b/media/starboard/url_player_renderer.cc @@ -0,0 +1,510 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/starboard/url_player_renderer.h" + +#include "base/logging.h" +#include "base/notreached.h" +#include "base/strings/stringprintf.h" +#include "base/task/sequenced_task_runner.h" +#include "base/trace_event/trace_event.h" +#include "media/base/pipeline_status.h" +#include "media/starboard/buildflags.h" +#include "starboard/common/player.h" + +namespace media { + +using base::Time; +using base::TimeDelta; + +UrlPlayerRenderer::UrlPlayerRenderer( + const scoped_refptr& task_runner, + std::unique_ptr media_log, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size) + : task_runner_(task_runner), + media_log_(std::move(media_log)), + viewport_size_(viewport_size) { + LOG(INFO) << "UrlPlayerRenderer created."; +} + +UrlPlayerRenderer::~UrlPlayerRenderer() { + LOG(INFO) << "UrlPlayerRenderer destroyed."; +} + +void UrlPlayerRenderer::Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_EQ(state_, STATE_UNINITIALIZED); + + LOG(INFO) << "Initializing UrlPlayerRenderer."; + + client_ = client; + init_cb_ = std::move(init_cb); + + if (source_url_.empty()) { + LOG(ERROR) << "Initialize() called without source URL set. " + << "URL must be set via SetSourceUrl() before Initialize()."; + state_ = STATE_ERROR; + task_runner_->PostTask( + FROM_HERE, base::BindOnce(std::move(init_cb_), + PIPELINE_ERROR_INITIALIZATION_FAILED)); + return; + } + + state_ = STATE_INITIALIZING; + + if (get_sb_window_handle_cb_) { + // Get SbWindow from CobaltRenderContentClient. + get_sb_window_handle_cb_.Run(); + return; + } + + CreatePlayerBridge(); +} + +void UrlPlayerRenderer::SetCdm(CdmContext* cdm_context, + CdmAttachedCB cdm_attached_cb) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(cdm_context); + + if (cdm_context_ || SbDrmSystemIsValid(drm_system_)) { + LOG(WARNING) << "Switching CDM not supported."; + std::move(cdm_attached_cb).Run(false); + return; + } + + cdm_context_ = cdm_context; + drm_system_ = cdm_context_->GetSbDrmSystem(); + + if (player_bridge_ && SbDrmSystemIsValid(drm_system_)) { + LOG(INFO) << "Attaching DRM system to existing player bridge."; + player_bridge_->SetDrmSystem(drm_system_); + } + + std::move(cdm_attached_cb).Run(true); + LOG(INFO) << "CDM set successfully."; +} + +void UrlPlayerRenderer::SetLatencyHint( + std::optional latency_hint) { + LOG(INFO) << "SetLatencyHint() not supported for URL player."; +} + +void UrlPlayerRenderer::Flush(base::OnceClosure flush_cb) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(flush_cb); + + if (!player_bridge_) { + LOG(WARNING) << "Flush() called with no player bridge."; + std::move(flush_cb).Run(); + return; + } + + player_bridge_->PrepareForSeek(); + playback_rate_ = 0.0; + + if (buffering_state_ != BUFFERING_HAVE_NOTHING) { + buffering_state_ = BUFFERING_HAVE_NOTHING; + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&UrlPlayerRenderer::OnBufferingStateChange, + weak_factory_.GetWeakPtr(), buffering_state_)); + } + + // URL player has no in-flight demuxer reads, complete immediately. + state_ = STATE_FLUSHED; + std::move(flush_cb).Run(); +} + +void UrlPlayerRenderer::StartPlayingFrom(TimeDelta time) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + LOG(INFO) << "UrlPlayerRenderer::StartPlayingFrom() called with " << time + << '.'; + if (state_ != STATE_FLUSHED) { + DCHECK_EQ(state_, STATE_ERROR); + return; + } + + state_ = STATE_PLAYING; + player_bridge_->Seek(time); +} + +void UrlPlayerRenderer::SetPlaybackRate(double playback_rate) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + if (playback_rate < 0.0) { + LOG(INFO) << "UrlPlayerRenderer::SetPlaybackRate(): invalid playback rate " + << playback_rate << '.'; + return; + } + + LOG(INFO) << "UrlPlayerRenderer changes playback rate from " << playback_rate_ + << " to " << playback_rate << '.'; + + if (playback_rate_ == playback_rate) { + return; + } + + playback_rate_ = playback_rate; + + if (player_bridge_) { + player_bridge_->SetPlaybackRate(playback_rate_); + } +} + +void UrlPlayerRenderer::SetVolume(float volume) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (volume < 0.0f || volume > 1.0f) { + LOG(INFO) << "UrlPlayerRenderer::SetVolume(): invalid volume " << volume + << '.'; + return; + } + + LOG(INFO) << "UrlPlayerRenderer changes volume from " << volume_ << " to " + << volume << '.'; + + if (volume_ == volume) { + return; + } + + volume_ = volume; + + if (player_bridge_) { + player_bridge_->SetVolume(volume_); + } +} + +TimeDelta UrlPlayerRenderer::GetMediaTime() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (!player_bridge_) { + return base::Microseconds(0); + } + + uint32_t video_frames_decoded = 0; + uint32_t video_frames_dropped = 0; + uint64_t audio_bytes_decoded = 0; + uint64_t video_bytes_decoded = 0; + TimeDelta media_time; + SbPlayerBridge::PlayerInfo info{&video_frames_decoded, &video_frames_dropped, + &audio_bytes_decoded, &video_bytes_decoded, + &media_time}; + + player_bridge_->GetInfo(&info); + + PipelineStatistics statistics; + statistics.audio_bytes_decoded = audio_bytes_decoded; + statistics.video_bytes_decoded = video_bytes_decoded; + + if (video_frames_decoded > last_video_frames_decoded_) { + statistics.video_frames_decoded = + video_frames_decoded - last_video_frames_decoded_; + last_video_frames_decoded_ = video_frames_decoded; + } + + if (video_frames_dropped > last_video_frames_dropped_) { + statistics.video_frames_dropped = + video_frames_dropped - last_video_frames_dropped_; + last_video_frames_dropped_ = video_frames_dropped; + } + + if (statistics.video_frames_decoded > 0 || + statistics.video_frames_dropped > 0) { + task_runner_->PostTask( + FROM_HERE, base::BindOnce(&UrlPlayerRenderer::OnStatisticsUpdate, + weak_factory_.GetWeakPtr(), statistics)); + } + + // Retry video dimensions if not yet reported (AVPlayer may expose them + // after the initial kSbPlayerStatePresenting event). + if (!has_reported_dimensions_ && state_ == STATE_PLAYING) { + TryReportVideoDimensions(); + } + + return media_time; +} + +void UrlPlayerRenderer::OnTracksChanged( + DemuxerStream::Type track_type, + std::vector enabled_tracks, + base::OnceClosure change_completed_cb) { + LOG(INFO) << "OnTracksChanged not supported for type: " << track_type; + std::move(change_completed_cb).Run(); +} + +void UrlPlayerRenderer::SetSourceUrl(const std::string& source_url) { + LOG(INFO) << "SetSourceUrl() called with url length=" << source_url.size(); + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK_EQ(state_, STATE_UNINITIALIZED) + << "SetSourceUrl() must be called before Initialize()."; + source_url_ = source_url; +} + +void UrlPlayerRenderer::SetUrlPlayerRendererCallbacks( + PaintVideoHoleFrameCallback paint_video_hole_frame_cb, + UpdateStarboardRenderingModeCallback update_starboard_rendering_mode_cb, + GetSbWindowHandleCallback get_sb_window_handle_cb) { + paint_video_hole_frame_cb_ = std::move(paint_video_hole_frame_cb); + update_starboard_rendering_mode_cb_ = + std::move(update_starboard_rendering_mode_cb); + get_sb_window_handle_cb_ = std::move(get_sb_window_handle_cb); +} + +void UrlPlayerRenderer::OnVideoGeometryChange(const gfx::Rect& output_rect) { + CHECK(task_runner_->RunsTasksInCurrentSequence()); + output_rect_ = output_rect; + ApplyPendingBounds(); +} + +void UrlPlayerRenderer::OnSbWindowHandleReady(uint64_t sb_window_handle) { + CHECK(task_runner_->RunsTasksInCurrentSequence()); + + if (state_ != STATE_INITIALIZING || player_bridge_) { + LOG(WARNING) << "OnSbWindowHandleReady called in unexpected state: " + << state_; + return; + } + + if (sb_window_handle != 0) { + sb_window_ = reinterpret_cast(sb_window_handle); + if (!SbWindowIsValid(sb_window_)) { + LOG(WARNING) << "SbWindow is not valid."; + } + } + CreatePlayerBridge(); +} + +SbPlayerInterface* UrlPlayerRenderer::GetSbPlayerInterface() { + return test_sbplayer_interface_ ? test_sbplayer_interface_ + : &sbplayer_interface_; +} + +// Private methods + +void UrlPlayerRenderer::CreatePlayerBridge() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(init_cb_); + DCHECK_EQ(state_, STATE_INITIALIZING); + DCHECK(!source_url_.empty()); + + TRACE_EVENT0("media", "UrlPlayerRenderer::CreatePlayerBridge"); + + DCHECK(!player_bridge_); + player_bridge_.reset(); + + LOG(INFO) << "Creating SbPlayerBridge."; + + player_bridge_.reset(new SbPlayerBridge( + GetSbPlayerInterface(), task_runner_, source_url_, sb_window_, this, + /*allow_resume_after_suspend=*/false, kSbPlayerOutputModePunchOut, + base::BindRepeating( + &UrlPlayerRenderer::OnEncryptedMediaInitDataEncountered, + base::Unretained(this)) +#if BUILDFLAG(COBALT_MEDIA_ENABLE_CVAL) + , + /*pipeline_identifier=*/"" +#endif // BUILDFLAG(COBALT_MEDIA_ENABLE_CVAL) + )); + + if (player_bridge_ && player_bridge_->IsValid()) { + ApplyPendingBounds(); + + const auto output_mode = player_bridge_->GetSbPlayerOutputMode(); + if (output_mode != kSbPlayerOutputModePunchOut) { + LOG(ERROR) << "URL player requires punch-out mode but got output mode " + << output_mode; + player_bridge_.reset(); + state_ = STATE_ERROR; + std::move(init_cb_).Run( + PipelineStatus(DECODER_ERROR_NOT_SUPPORTED, + "URL player requires punch-out output mode")); + return; + } + + update_starboard_rendering_mode_cb_.Run(StarboardRenderingMode::kPunchOut); + + if (SbDrmSystemIsValid(drm_system_)) { + LOG(INFO) << "Attaching DRM system to player bridge."; + player_bridge_->SetDrmSystem(drm_system_); + } + + player_bridge_->SetVolume(volume_); + + state_ = STATE_FLUSHED; + + // Defer init_cb_ until SbPlayer reports kSbPlayerStateInitialized + // via OnPlayerStatus(). + LOG(INFO) << "SbPlayerBridge created, waiting for initialized."; + return; + } + + std::string error_message = + player_bridge_ ? player_bridge_->GetPlayerCreationErrorMessage() + : "Player bridge creation failed"; + player_bridge_.reset(); + state_ = STATE_ERROR; + PipelineStatus status(DECODER_ERROR_NOT_SUPPORTED, + "CreatePlayerBridge failed: " + error_message); + LOG(INFO) << status; + std::move(init_cb_).Run(status); +} + +void UrlPlayerRenderer::ApplyPendingBounds() { + if (!player_bridge_ || !output_rect_) { + return; + } + + player_bridge_->SetBounds(*output_rect_); +} + +void UrlPlayerRenderer::OnStatisticsUpdate(const PipelineStatistics& stats) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + if (client_) { + client_->OnStatisticsUpdate(stats); + } +} + +void UrlPlayerRenderer::OnNeedData(DemuxerStream::Type type, + int max_number_of_buffers_to_write) { + // URL player handles all buffering natively; ignore. +} + +void UrlPlayerRenderer::OnPlayerStatus(SbPlayerState state) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + + LOG(INFO) << "UrlPlayerRenderer::OnPlayerStatus() called with state=" + << ::starboard::GetPlayerStateName(state); + + if (!player_bridge_) { + LOG(WARNING) + << "UrlPlayerRenderer::OnPlayerStatus() called without player bridge."; + return; + } + + switch (state) { + case kSbPlayerStateInitialized: + if (init_cb_) { + std::move(init_cb_).Run(PipelineStatus(PIPELINE_OK)); + } + break; + case kSbPlayerStatePrerolling: + break; + case kSbPlayerStatePresenting: { + buffering_state_ = BUFFERING_HAVE_ENOUGH; + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&UrlPlayerRenderer::OnBufferingStateChange, + weak_factory_.GetWeakPtr(), buffering_state_)); + + // Query video resolution and report for video hole punch-out. + TryReportVideoDimensions(); + if (!has_reported_dimensions_) { + LOG(WARNING) << "Dimensions not yet available at presenting; " + "will retry during GetMediaTime() polling."; + } + + // Re-apply playback rate. AVPlayer silently ignores rate changes + // before ReadyToPlay. + player_bridge_->SetPlaybackRate(playback_rate_); + break; + } + case kSbPlayerStateEndOfStream: + if (client_) { + client_->OnEnded(); + } + break; + case kSbPlayerStateDestroyed: + break; + } +} + +void UrlPlayerRenderer::OnPlayerError(SbPlayerError error, + const std::string& message) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + LOG(ERROR) << "UrlPlayerRenderer::OnPlayerError() code=" << error + << " message=\"" << message << "\""; + + if (state_ == STATE_ERROR) { + return; + } + + state_ = STATE_ERROR; + + switch (error) { + case kSbPlayerErrorDecode: + MEDIA_LOG(ERROR, media_log_) << message; + NotifyError(PIPELINE_ERROR_DECODE); + break; + case kSbPlayerErrorCapabilityChanged: + MEDIA_LOG(ERROR, media_log_) + << (message.empty() + ? kSbPlayerCapabilityChangedErrorMessage + : base::StringPrintf("%s: %s", + kSbPlayerCapabilityChangedErrorMessage, + message.c_str())); + NotifyError(PIPELINE_ERROR_DECODE); + break; + case kSbPlayerErrorMax: + NOTREACHED(); + break; + } +} + +void UrlPlayerRenderer::NotifyError(PipelineStatus status) { + if (init_cb_) { + std::move(init_cb_).Run(status); + } else { + client_->OnError(status); + } +} +void UrlPlayerRenderer::TryReportVideoDimensions() { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + DCHECK(player_bridge_); + + int width = 0, height = 0; + player_bridge_->GetVideoResolution(&width, &height); + if (width > 0 && height > 0) { + gfx::Size size(width, height); + if (client_) { + client_->OnVideoNaturalSizeChange(size); + } + paint_video_hole_frame_cb_.Run(size); + has_reported_dimensions_ = true; + LOG(INFO) << "Video size: " << width << "x" << height; + } +} + +void UrlPlayerRenderer::OnBufferingStateChange(BufferingState state) { + DCHECK(task_runner_->RunsTasksInCurrentSequence()); + if (client_) { + client_->OnBufferingStateChange(state, BUFFERING_CHANGE_REASON_UNKNOWN); + } +} + +SbDecodeTarget UrlPlayerRenderer::GetSbDecodeTarget() { + LOG(INFO) << "GetSbDecodeTarget() not supported for URL player."; + return kSbDecodeTargetInvalid; +} + +void UrlPlayerRenderer::OnEncryptedMediaInitDataEncountered( + const char* init_data_type, + const unsigned char* init_data, + unsigned int init_data_length) { + LOG(INFO) << "OnEncryptedMediaInitDataEncountered() (no-op)."; + // TODO: Forward encrypted media init data to the EME/DRM layer. +} + +} // namespace media diff --git a/media/starboard/url_player_renderer.h b/media/starboard/url_player_renderer.h new file mode 100644 index 000000000000..175d7e2ac421 --- /dev/null +++ b/media/starboard/url_player_renderer.h @@ -0,0 +1,165 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MEDIA_STARBOARD_URL_PLAYER_RENDERER_H_ +#define MEDIA_STARBOARD_URL_PLAYER_RENDERER_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "base/task/sequenced_task_runner.h" +#include "base/time/time.h" +#include "media/base/cdm_context.h" +#include "media/base/media_export.h" +#include "media/base/media_log.h" +#include "media/base/pipeline_status.h" +#include "media/base/renderer.h" +#include "media/base/renderer_client.h" +#include "media/base/starboard/starboard_rendering_mode.h" +#include "media/starboard/sbplayer_bridge.h" +#include "ui/gfx/geometry/rect.h" + +namespace media { + +// Renderer for URL-based playback on tvOS (e.g., HLS via AVPlayer). +// Delegates all media loading, demuxing, buffering, and decoding to the +// platform URL player through SbPlayerBridge. Streams from MediaResource +// are ignored; the URL is supplied by the wrapper before initialization. +// +// Lifetime and Ownership: Owned by UrlPlayerRendererWrapper in the +// GPU process. +// +// Threading Model: Thread-affine, must be used on the media task +// runner sequence. +class MEDIA_EXPORT UrlPlayerRenderer : public Renderer, + private SbPlayerBridge::Host { + public: + UrlPlayerRenderer(const scoped_refptr& task_runner, + std::unique_ptr media_log, + const base::UnguessableToken& overlay_plane_id, + const gfx::Size& viewport_size); + + UrlPlayerRenderer(const UrlPlayerRenderer&) = delete; + UrlPlayerRenderer& operator=(const UrlPlayerRenderer&) = delete; + + ~UrlPlayerRenderer() override; + + // Renderer implementation. + void Initialize(MediaResource* media_resource, + RendererClient* client, + PipelineStatusCallback init_cb) override; + void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override; + void SetLatencyHint(std::optional latency_hint) override; + void Flush(base::OnceClosure flush_cb) override; + void StartPlayingFrom(base::TimeDelta time) override; + void SetPlaybackRate(double playback_rate) override; + void SetVolume(float volume) override; + base::TimeDelta GetMediaTime() override; + void OnTracksChanged(DemuxerStream::Type track_type, + std::vector enabled_tracks, + base::OnceClosure change_completed_cb) override; + RendererType GetRendererType() override { return RendererType::kUrlPlayer; } + + // URL setup — called by the wrapper before Initialize(). + void SetSourceUrl(const std::string& source_url); + + using PaintVideoHoleFrameCallback = + base::RepeatingCallback; + using UpdateStarboardRenderingModeCallback = + base::RepeatingCallback; + using GetSbWindowHandleCallback = base::RepeatingCallback; + + void SetUrlPlayerRendererCallbacks( + PaintVideoHoleFrameCallback paint_video_hole_frame_cb, + UpdateStarboardRenderingModeCallback update_starboard_rendering_mode_cb, + GetSbWindowHandleCallback get_sb_window_handle_cb); + + void OnVideoGeometryChange(const gfx::Rect& output_rect); + void OnSbWindowHandleReady(uint64_t sb_window_handle); + + SbDecodeTarget GetSbDecodeTarget(); + + SbPlayerInterface* GetSbPlayerInterface(); + void SetSbPlayerInterfaceForTesting(SbPlayerInterface* sbplayer_interface) { + test_sbplayer_interface_ = sbplayer_interface; + } + + private: + enum State { + STATE_UNINITIALIZED, + STATE_INITIALIZING, + STATE_FLUSHED, + STATE_PLAYING, + STATE_ERROR, + }; + + void CreatePlayerBridge(); + void ApplyPendingBounds(); + + // SbPlayerBridge::Host implementation. + void OnNeedData(DemuxerStream::Type type, + int max_number_of_buffers_to_write) override; + void OnPlayerStatus(SbPlayerState state) override; + void OnPlayerError(SbPlayerError error, const std::string& message) override; + + void TryReportVideoDimensions(); + void OnStatisticsUpdate(const PipelineStatistics& stats); + void OnBufferingStateChange(BufferingState state); + void NotifyError(PipelineStatus status); + void OnEncryptedMediaInitDataEncountered(const char* init_data_type, + const unsigned char* init_data, + unsigned int init_data_length); + + State state_ = STATE_UNINITIALIZED; + const scoped_refptr task_runner_; + const std::unique_ptr media_log_; + const gfx::Size viewport_size_; + + std::string source_url_; + bool has_reported_dimensions_ = false; + raw_ptr cdm_context_ = nullptr; + SbDrmSystem drm_system_{kSbDrmSystemInvalid}; + raw_ptr client_ = nullptr; + PipelineStatusCallback init_cb_; + BufferingState buffering_state_ = BUFFERING_HAVE_NOTHING; + + PaintVideoHoleFrameCallback paint_video_hole_frame_cb_; + UpdateStarboardRenderingModeCallback update_starboard_rendering_mode_cb_; + GetSbWindowHandleCallback get_sb_window_handle_cb_; + + std::optional output_rect_; + double playback_rate_ = 0.0; + float volume_ = 1.0f; + + SbWindow sb_window_ = kSbWindowInvalid; + DefaultSbPlayerInterface sbplayer_interface_; + raw_ptr test_sbplayer_interface_ = nullptr; + std::unique_ptr player_bridge_; + + uint32_t last_video_frames_decoded_ = 0; + uint32_t last_video_frames_dropped_ = 0; + + static inline constexpr const char* kSbPlayerCapabilityChangedErrorMessage = + "MEDIA_ERR_CAPABILITY_CHANGED"; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace media + +#endif // MEDIA_STARBOARD_URL_PLAYER_RENDERER_H_ diff --git a/media/starboard/url_player_renderer_unittest.cc b/media/starboard/url_player_renderer_unittest.cc new file mode 100644 index 000000000000..1a9b2cab1e02 --- /dev/null +++ b/media/starboard/url_player_renderer_unittest.cc @@ -0,0 +1,277 @@ +// Copyright 2026 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "media/starboard/url_player_renderer.h" + +#include +#include + +#include "base/functional/callback_helpers.h" +#include "base/test/mock_callback.h" +#include "base/test/task_environment.h" +#include "media/base/media_util.h" +#include "media/base/mock_filters.h" +#include "media/base/test_helpers.h" +#include "media/starboard/sbplayer_interface.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +struct SbPlayerPrivate {}; +struct SbDrmSystemPrivate {}; + +using ::testing::_; +using ::testing::DoAll; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::StrEq; + +namespace media { +namespace { + +constexpr char kSourceUrl[] = "https://example.test/video.m3u8"; + +class MockSbPlayerInterface : public SbPlayerInterface { + public: + MOCK_METHOD8(Create, + SbPlayer(SbWindow, + const SbPlayerCreationParam*, + SbPlayerDeallocateSampleFunc, + SbPlayerDecoderStatusFunc, + SbPlayerStatusFunc, + SbPlayerErrorFunc, + void*, + SbDecodeTargetGraphicsContextProvider*)); + MOCK_METHOD1(GetPreferredOutputMode, + SbPlayerOutputMode(const SbPlayerCreationParam*)); + void Destroy(SbPlayer player) override { delete player; } + MOCK_METHOD3(Seek, void(SbPlayer, base::TimeDelta, int)); + MOCK_METHOD4(WriteSamples, + void(SbPlayer, SbMediaType, const SbPlayerSampleInfo*, int)); + MOCK_METHOD2(GetMaximumNumberOfSamplesPerWrite, int(SbPlayer, SbMediaType)); + MOCK_METHOD2(WriteEndOfStream, void(SbPlayer, SbMediaType)); + MOCK_METHOD6(SetBounds, void(SbPlayer, int, int, int, int, int)); + MOCK_METHOD2(SetPlaybackRate, bool(SbPlayer, double)); + MOCK_METHOD2(SetVolume, void(SbPlayer, double)); + MOCK_METHOD2(GetInfo, void(SbPlayer, SbPlayerInfo*)); + MOCK_METHOD1(GetCurrentFrame, SbDecodeTarget(SbPlayer)); + + MOCK_METHOD6(CreateUrlPlayer, + SbPlayer(const char*, + SbWindow, + SbPlayerStatusFunc, + SbPlayerEncryptedMediaInitDataEncounteredCB, + SbPlayerErrorFunc, + void*)); + MOCK_METHOD2(SetUrlPlayerDrmSystem, void(SbPlayer, SbDrmSystem)); + MOCK_METHOD1(GetUrlPlayerOutputModeSupported, bool(SbPlayerOutputMode)); + MOCK_METHOD2(GetUrlPlayerExtraInfo, void(SbPlayer, SbUrlPlayerExtraInfo*)); + MOCK_METHOD3(GetAudioConfiguration, + bool(SbPlayer, int, SbMediaAudioConfiguration*)); +}; + +class FakeCdmContext : public CdmContext { + public: + explicit FakeCdmContext(SbDrmSystem drm_system) : drm_system_(drm_system) {} + SbDrmSystem GetSbDrmSystem() override { return drm_system_; } + + private: + SbDrmSystem drm_system_; +}; + +class UrlPlayerRendererTest : public testing::Test { + protected: + UrlPlayerRendererTest() { + ON_CALL(mock_sbplayer_interface_, GetUrlPlayerOutputModeSupported(_)) + .WillByDefault(Invoke([this](SbPlayerOutputMode output_mode) { + if (output_mode == kSbPlayerOutputModePunchOut) { + return punch_out_supported_; + } + return decode_to_texture_supported_; + })); + ON_CALL(mock_sbplayer_interface_, SetPlaybackRate(_, _)) + .WillByDefault(Return(true)); + + renderer_->SetSbPlayerInterfaceForTesting(&mock_sbplayer_interface_); + renderer_->SetUrlPlayerRendererCallbacks( + base::DoNothing(), base::DoNothing(), base::NullCallback()); + } + + SbPlayer ExpectPlayerCreation(SbWindow expected_window = kSbWindowInvalid) { + SbPlayer player = new SbPlayerPrivate(); + EXPECT_CALL(mock_sbplayer_interface_, + CreateUrlPlayer(StrEq(kSourceUrl), expected_window, _, _, _, _)) + .WillOnce(DoAll(SaveArg<2>(&player_status_cb_), + SaveArg<4>(&player_error_cb_), SaveArg<5>(&context_), + Return(player))); + return player; + } + + void Initialize(SbPlayer player) { + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(init_cb_, Run(HasStatusCode(PIPELINE_OK))); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + ASSERT_TRUE(player_status_cb_); + player_status_cb_(player, context_, kSbPlayerStateInitialized, + SB_PLAYER_INITIAL_TICKET); + task_environment_.RunUntilIdle(); + } + + base::test::TaskEnvironment task_environment_; + NiceMock mock_sbplayer_interface_; + NiceMock media_resource_; + NiceMock renderer_client_; + base::MockOnceCallback init_cb_; + SbPlayerStatusFunc player_status_cb_ = nullptr; + SbPlayerErrorFunc player_error_cb_ = nullptr; + void* context_ = nullptr; + bool punch_out_supported_ = true; + bool decode_to_texture_supported_ = false; + const std::unique_ptr renderer_ = + std::make_unique( + task_environment_.GetMainThreadTaskRunner(), + std::make_unique(), + base::UnguessableToken::Create(), + gfx::Size()); +}; + +TEST_F(UrlPlayerRendererTest, InitializeWithoutUrlFailsAsync) { + // Initialize without URL set — should fail asynchronously. + EXPECT_CALL(mock_sbplayer_interface_, CreateUrlPlayer(_, _, _, _, _, _)) + .Times(0); + EXPECT_CALL(init_cb_, + Run(HasStatusCode(PIPELINE_ERROR_INITIALIZATION_FAILED))); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, InitializeCompletesOnPlayerInitialized) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + EXPECT_EQ(renderer_->GetRendererType(), RendererType::kUrlPlayer); +} + +TEST_F(UrlPlayerRendererTest, InitializeWaitsForWindowHandle) { + base::MockRepeatingCallback get_window_cb; + renderer_->SetUrlPlayerRendererCallbacks(base::DoNothing(), base::DoNothing(), + get_window_cb.Get()); + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(get_window_cb, Run()); + EXPECT_CALL(mock_sbplayer_interface_, CreateUrlPlayer(_, _, _, _, _, _)) + .Times(0); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + + testing::Mock::VerifyAndClearExpectations(&mock_sbplayer_interface_); + SbWindow window = reinterpret_cast(0x1234); + SbPlayer player = ExpectPlayerCreation(window); + EXPECT_CALL(init_cb_, Run(HasStatusCode(PIPELINE_OK))); + renderer_->OnSbWindowHandleReady(reinterpret_cast(window)); + player_status_cb_(player, context_, kSbPlayerStateInitialized, + SB_PLAYER_INITIAL_TICKET); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, RejectsDecodeToTextureFallback) { + punch_out_supported_ = false; + decode_to_texture_supported_ = true; + ExpectPlayerCreation(); + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(init_cb_, Run(HasStatusCode(DECODER_ERROR_NOT_SUPPORTED))); + + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); +} + +TEST_F(UrlPlayerRendererTest, DelegatesRateVolumeSeekAndFlush) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + + // Volume is not gated by seek_pending. + EXPECT_CALL(mock_sbplayer_interface_, SetVolume(player, 0.5)); + renderer_->SetVolume(0.5f); + + // StartPlayingFrom triggers Seek which clears seek_pending. + EXPECT_CALL(mock_sbplayer_interface_, Seek(player, base::Seconds(12), _)); + renderer_->StartPlayingFrom(base::Seconds(12)); + + // SetPlaybackRate now reaches the interface after seek_pending is cleared. + EXPECT_CALL(mock_sbplayer_interface_, SetPlaybackRate(player, 1.25)); + renderer_->SetPlaybackRate(1.25); + + // Flush calls PrepareForSeek which pauses via SetPlaybackRate(0). + base::MockOnceClosure flush_cb; + EXPECT_CALL(mock_sbplayer_interface_, SetPlaybackRate(player, 0.0)); + EXPECT_CALL(flush_cb, Run()); + renderer_->Flush(flush_cb.Get()); +} + +TEST_F(UrlPlayerRendererTest, ReportsPlayerErrorDuringInitialization) { + SbPlayer player = ExpectPlayerCreation(); + renderer_->SetSourceUrl(kSourceUrl); + EXPECT_CALL(init_cb_, Run(HasStatusCode(PIPELINE_ERROR_DECODE))); + EXPECT_CALL(renderer_client_, OnError(_)).Times(0); + renderer_->Initialize(&media_resource_, &renderer_client_, init_cb_.Get()); + + ASSERT_TRUE(player_error_cb_); + player_error_cb_(player, context_, kSbPlayerErrorDecode, "decode failed"); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, ReportsPlayerErrorAfterInitialization) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + EXPECT_CALL(renderer_client_, OnError(HasStatusCode(PIPELINE_ERROR_DECODE))); + + player_error_cb_(player, context_, kSbPlayerErrorDecode, "decode failed"); + task_environment_.RunUntilIdle(); +} + +TEST_F(UrlPlayerRendererTest, AttachesCdmBeforePlayerCreation) { + SbDrmSystem drm_system = new SbDrmSystemPrivate(); + FakeCdmContext cdm_context(drm_system); + base::MockOnceCallback cdm_cb; + EXPECT_CALL(cdm_cb, Run(true)); + renderer_->SetCdm(&cdm_context, cdm_cb.Get()); + + SbPlayer player = ExpectPlayerCreation(); + EXPECT_CALL(mock_sbplayer_interface_, + SetUrlPlayerDrmSystem(player, drm_system)); + Initialize(player); + delete drm_system; +} + +TEST_F(UrlPlayerRendererTest, AttachesCdmAfterPlayerCreationAndRejectsSwitch) { + SbPlayer player = ExpectPlayerCreation(); + Initialize(player); + + SbDrmSystem drm_system = new SbDrmSystemPrivate(); + FakeCdmContext cdm_context(drm_system); + base::MockOnceCallback cdm_cb; + { + InSequence sequence; + EXPECT_CALL(mock_sbplayer_interface_, + SetUrlPlayerDrmSystem(player, drm_system)); + EXPECT_CALL(cdm_cb, Run(true)); + } + renderer_->SetCdm(&cdm_context, cdm_cb.Get()); + + base::MockOnceCallback replacement_cb; + EXPECT_CALL(replacement_cb, Run(false)); + renderer_->SetCdm(&cdm_context, replacement_cb.Get()); + delete drm_system; +} + +} // namespace +} // namespace media diff --git a/starboard/build/buildflags.gni b/starboard/build/buildflags.gni index 0ce582db6d3e..04c2d3ae5fe5 100644 --- a/starboard/build/buildflags.gni +++ b/starboard/build/buildflags.gni @@ -23,3 +23,10 @@ declare_args() { is_cobalt && current_toolchain != host_toolchain && (!is_starboard || current_toolchain != v8_snapshot_toolchain) } + +if (is_ios) { + import("//build/config/apple/mobile_config.gni") + use_starboard_url_player = use_starboard_media && target_platform == "tvos" +} else { + use_starboard_url_player = false +} diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index 0c8b02df7e97..31be098edba6 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -19946,6 +19946,8 @@ from previous Chrome versions. + +