chromium/media/mojo/clients/win/media_foundation_renderer_client_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/mojo/clients/win/media_foundation_renderer_client.h"

#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "content/renderer/media/win/overlay_state_service_provider.h"
#include "media/base/fake_demuxer_stream.h"
#include "media/base/media_switches.h"
#include "media/base/mock_media_log.h"
#include "media/base/video_renderer_sink.h"
#include "media/base/win/dcomp_texture_wrapper.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/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/rect.h"

namespace media {

class FakeMojomRenderer : public mojom::Renderer {
 public:
  FakeMojomRenderer() = default;
  ~FakeMojomRenderer() override = default;
  // mojom::Renderer implementations.
  void Initialize(
      mojo::PendingAssociatedRemote<mojom::RendererClient>,
      std::optional<std::vector<mojo::PendingRemote<mojom::DemuxerStream>>>,
      mojom::MediaUrlParamsPtr,
      InitializeCallback cb) override {
    std::move(cb).Run(true);
  }
  // mojom::Renderer mocks.
  MOCK_METHOD(void, Flush, (FlushCallback), (override));
  MOCK_METHOD(void, StartPlayingFrom, (base::TimeDelta), (override));
  MOCK_METHOD(void, SetPlaybackRate, (double), (override));
  MOCK_METHOD(void, SetVolume, (float), (override));
  MOCK_METHOD(void,
              SetCdm,
              (const std::optional<base::UnguessableToken>&, SetCdmCallback),
              (override));
  MOCK_METHOD(void,
              SetLatencyHint,
              (std::optional<base::TimeDelta>),
              (override));
};

class FakeMediaFoundationRendererExtension
    : public mojom::MediaFoundationRendererExtension {
 public:
  FakeMediaFoundationRendererExtension() = default;
  ~FakeMediaFoundationRendererExtension() override = default;

  // MediaFoundationRendererExtension implementations.
  void GetDCOMPSurface(GetDCOMPSurfaceCallback cb) override {
    // Generate random token
    base::UnguessableToken token = base::UnguessableToken::Create();
    std::move(cb).Run(token, "");
  }

  void SetOutputRect(const gfx::Rect&, SetOutputRectCallback cb) override {
    std::move(cb).Run(true);
  }

  // MediaFoundationRendererExtension mocks.
  MOCK_METHOD(void, SetVideoStreamEnabled, (bool), (override));
  MOCK_METHOD(void,
              NotifyFrameReleased,
              (const base::UnguessableToken&),
              (override));
  MOCK_METHOD(void, RequestNextFrame, (), (override));
  MOCK_METHOD(void,
              SetMediaFoundationRenderingMode,
              (MediaFoundationRenderingMode),
              (override));
};

class FakeDCOMPTextureWrapper : public DCOMPTextureWrapper {
 public:
  FakeDCOMPTextureWrapper(gpu::Mailbox frame_server_mailbox,
                          gpu::Mailbox dcomp_mailbox)
      : frame_server_mailbox_(frame_server_mailbox),
        dcomp_mailbox_(dcomp_mailbox) {}
  ~FakeDCOMPTextureWrapper() override = default;

  // DCompTextureWrapper implementations.
  bool Initialize(const gfx::Size&, OutputRectChangeCB cb) override {
    gfx::Rect rect(1920, 1080);
    std::move(cb).Run(rect);
    return true;
  }

  void SetDCOMPSurfaceHandle(const base::UnguessableToken&,
                             SetDCOMPSurfaceHandleCB cb) override {
    std::move(cb).Run(true);
  }

  void CreateVideoFrame(const gfx::Size& size, CreateVideoFrameCB cb) override {
    // DComp CreateVideoFrame
    scoped_refptr<VideoFrame> video_frame = VideoFrame::CreateBlackFrame(size);
    std::move(cb).Run(std::move(video_frame), std::move(frame_server_mailbox_));
  }

  void CreateVideoFrame(const gfx::Size& size,
                        gfx::GpuMemoryBufferHandle,
                        CreateDXVideoFrameCB cb) override {
    // Frame Server CreateVideoFrame
    scoped_refptr<VideoFrame> video_frame = VideoFrame::CreateBlackFrame(size);
    std::move(cb).Run(std::move(video_frame), std::move(dcomp_mailbox_));
  }

  // DCompTextureWrapper mocks.
  MOCK_METHOD(void, UpdateTextureSize, (const gfx::Size&), (override));

 private:
  gpu::Mailbox frame_server_mailbox_;
  gpu::Mailbox dcomp_mailbox_;
};

class MockVideoRendererSink : public VideoRendererSink {
 public:
  MockVideoRendererSink() = default;
  ~MockVideoRendererSink() override = default;

  // VideoRendererSink mocks.
  MOCK_METHOD(void, Start, (RenderCallback*));
  MOCK_METHOD(void, Stop, ());
  MOCK_METHOD(void, PaintSingleFrame, (scoped_refptr<VideoFrame>, bool));
};

class MockOverlayStateObserverSubscription
    : public OverlayStateObserverSubscription {
 public:
  MockOverlayStateObserverSubscription() = default;
  ~MockOverlayStateObserverSubscription() override = default;
};

class MockRendererClientMF : public RendererClient {
 public:
  MockRendererClientMF() = default;
  ~MockRendererClientMF() = default;

  // RendererClient mocks.
  MOCK_METHOD(void, OnError, (PipelineStatus), (override));
  MOCK_METHOD(void, OnFallback, (PipelineStatus), (override));
  MOCK_METHOD(void, OnEnded, (), (override));
  MOCK_METHOD(void,
              OnStatisticsUpdate,
              (const PipelineStatistics&),
              (override));
  MOCK_METHOD(void,
              OnBufferingStateChange,
              (BufferingState, BufferingStateChangeReason),
              (override));
  MOCK_METHOD(void, OnWaiting, (WaitingReason), (override));
  MOCK_METHOD(void,
              OnAudioConfigChange,
              (const AudioDecoderConfig&),
              (override));
  MOCK_METHOD(void,
              OnVideoConfigChange,
              (const VideoDecoderConfig&),
              (override));
  MOCK_METHOD(void, OnVideoNaturalSizeChange, (const gfx::Size&), (override));
  MOCK_METHOD(void, OnVideoOpacityChange, (bool), (override));
  MOCK_METHOD(void, OnVideoFrameRateChange, (std::optional<int>), (override));
};

class MediaFoundationRendererClientTest
    : public ::testing::Test,
      public media::mojom::MediaFoundationRendererObserver {
 public:
  MediaFoundationRendererClientTest() = default;
  ~MediaFoundationRendererClientTest() override = default;

  void SetUp() override {
    // Create the necessary mocks & fakes to create a
    // MediaFoundationRendererClient.
    mojo::PendingRemote<mojom::Renderer> renderer_remote;
    mojo::MakeSelfOwnedReceiver(
        std::make_unique<FakeMojomRenderer>(),
        renderer_remote.InitWithNewPipeAndPassReceiver());

    mojo::PendingRemote<mojom::MediaFoundationRendererExtension>
        mf_renderer_extensions_remote;
    mojo::MakeSelfOwnedReceiver(
        std::make_unique<FakeMediaFoundationRendererExtension>(),
        mf_renderer_extensions_remote.InitWithNewPipeAndPassReceiver());

    mojo::PendingRemote<media::mojom::MediaFoundationRendererClientExtension>
        client_extension_remote;
    auto client_extension_receiver =
        client_extension_remote.InitWithNewPipeAndPassReceiver();

    auto mojo_renderer = std::make_unique<MojoRenderer>(
        task_environment_.GetMainThreadTaskRunner(),
        /*video_overlay_factory*/ nullptr,
        /*video_renderer_sink*/ nullptr, std::move(renderer_remote));

    frame_server_mailbox_ = gpu::Mailbox::Generate();
    dcomp_mailbox_ = gpu::Mailbox::Generate();
    dcomp_texture_wrapper_ = std::make_unique<FakeDCOMPTextureWrapper>(
        frame_server_mailbox_, dcomp_mailbox_);

    ObserveOverlayStateCB observe_overlay_state_cb = base::BindRepeating(
        &MediaFoundationRendererClientTest::ObserveOverlaySupscription,
        base::Unretained(this));

    mojo::PendingRemote<media::mojom::MediaFoundationRendererObserver>
        media_foundation_renderer_observer_remote;

    media_foundation_renderer_client_ =
        std::make_unique<MediaFoundationRendererClient>(
            task_environment_.GetMainThreadTaskRunner(), media_log_.Clone(),
            std::move(mojo_renderer), std::move(mf_renderer_extensions_remote),
            std::move(client_extension_receiver),
            std::move(dcomp_texture_wrapper_), observe_overlay_state_cb,
            &mock_video_renderer_sink_,
            std::move(media_foundation_renderer_observer_remote));
  }

  void OnPipelineStatus(PipelineStatus status) { last_status_ = status; }

  std::unique_ptr<media::OverlayStateObserverSubscription>
  ObserveOverlaySupscription(
      const gpu::Mailbox& mailbox,
      OverlayStateObserverSubscription::StateChangedCB cb) {
    if (mailbox == frame_server_mailbox_) {
      frame_server_on_state_change_cb_ = cb;
    } else if (mailbox == dcomp_mailbox_) {
      dcomp_on_state_change_cb_ = cb;
    } else {
      // Unexpected
      NOTREACHED_IN_MIGRATION();
    }

    return std::make_unique<MockOverlayStateObserverSubscription>();
  }

  void InitializeMediaFoundationRendererClient() {
    fake_media_resource_ = std::make_unique<FakeMediaResource>(3, 9, false);
    renderer_client_ = new MockRendererClientMF();
    media_foundation_renderer_client_->Initialize(
        fake_media_resource_.get(), renderer_client_,
        base::BindOnce(&MediaFoundationRendererClientTest::OnPipelineStatus,
                       base::Unretained(this)));
    // Force a call to OnVideoNaturalSizeChange to force creation of a
    // DComp video frame.
    media_foundation_renderer_client_->OnVideoNaturalSizeChange(
        gfx::Size(1920, 1080));
  }

  void InitializeFramePool() {
    gfx::GpuMemoryBufferHandle gpu_handle;
    HANDLE shared_texture_handle = INVALID_HANDLE_VALUE;
    base::win::ScopedHandle scoped_shared_texture_handle;
    scoped_shared_texture_handle.Set(shared_texture_handle);

    gpu_handle.dxgi_handle = std::move(scoped_shared_texture_handle);
    gpu_handle.dxgi_token = gfx::DXGIHandleToken();
    gpu_handle.type = gfx::GpuMemoryBufferType::DXGI_SHARED_HANDLE;

    auto frame_info = media::mojom::FrameTextureInfo::New();
    frame_info->token = base::UnguessableToken::Create();
    frame_info->texture_handle = std::move(gpu_handle);

    auto pool_params = media::mojom::FramePoolInitializationParameters::New();
    pool_params->frame_textures.emplace_back(std::move(frame_info));
    pool_params->texture_size = gfx::Size(1920, 1080);

    media_foundation_renderer_client_->InitializeFramePool(
        std::move(pool_params));
  }

 protected:
  base::test::SingleThreadTaskEnvironment task_environment_;
  base::test::ScopedFeatureList feature_list_;

  MockMediaLog media_log_;
  MockVideoRendererSink mock_video_renderer_sink_;
  raw_ptr<RendererClient> renderer_client_;
  std::unique_ptr<FakeMediaResource> fake_media_resource_;
  std::unique_ptr<MediaFoundationRendererClient>
      media_foundation_renderer_client_;

  gpu::Mailbox dcomp_mailbox_;
  gpu::Mailbox frame_server_mailbox_;
  OverlayStateObserverSubscription::StateChangedCB
      frame_server_on_state_change_cb_;
  OverlayStateObserverSubscription::StateChangedCB dcomp_on_state_change_cb_;

  PipelineStatus last_status_ = PIPELINE_STATUS_MAX;
  std::unique_ptr<DCOMPTextureWrapper> dcomp_texture_wrapper_;
};

TEST_F(MediaFoundationRendererClientTest, CreateAndDestroy) {}

TEST_F(MediaFoundationRendererClientTest, CreateInitializeAndDestroy) {
  feature_list_.InitWithFeatures({kMediaFoundationClearPlayback}, {});
  InitializeMediaFoundationRendererClient();
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(last_status_, PIPELINE_OK);
}

TEST_F(MediaFoundationRendererClientTest, InitializeFrameServer) {
  base::FieldTrialParams params;
  params["strategy"] = "frame-server";
  feature_list_.InitWithFeaturesAndParameters(
      {{kMediaFoundationClearRendering, params},
       {kMediaFoundationClearPlayback, {}}},
      {});
  InitializeMediaFoundationRendererClient();
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(last_status_, PIPELINE_OK);
  EXPECT_TRUE(media_foundation_renderer_client_->IsFrameServerMode());
}

TEST_F(MediaFoundationRendererClientTest, InitializeDComp) {
  base::FieldTrialParams params;
  params["strategy"] = "direct-composition";
  feature_list_.InitWithFeaturesAndParameters(
      {{kMediaFoundationClearRendering, params},
       {kMediaFoundationClearPlayback, {}}},
      {});
  InitializeMediaFoundationRendererClient();
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(last_status_, PIPELINE_OK);
  EXPECT_FALSE(media_foundation_renderer_client_->IsFrameServerMode());
}

TEST_F(MediaFoundationRendererClientTest, InitializeDynamic) {
  base::FieldTrialParams params;
  params["strategy"] = "dynamic";
  feature_list_.InitWithFeaturesAndParameters(
      {{kMediaFoundationClearRendering, params},
       {kMediaFoundationClearPlayback, {}}},
      {});
  InitializeMediaFoundationRendererClient();
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(last_status_, PIPELINE_OK);
  // Dynamic mode should start in Frame Server until a successful overlay
  // promotion hint.
  EXPECT_TRUE(media_foundation_renderer_client_->IsFrameServerMode());
}

TEST_F(MediaFoundationRendererClientTest, VerifyDynamicPromotionAndDemotion) {
  base::FieldTrialParams params;
  params["strategy"] = "dynamic";
  feature_list_.InitWithFeaturesAndParameters(
      {{kMediaFoundationClearRendering, params},
       {kMediaFoundationClearPlayback, {}}},
      {});
  InitializeMediaFoundationRendererClient();
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(last_status_, PIPELINE_OK);
  // Dynamic mode should start in Frame Server until a successful overlay
  // promotion hint.
  EXPECT_TRUE(media_foundation_renderer_client_->IsFrameServerMode());

  // Initialize Frame Pool
  InitializeFramePool();
  base::RunLoop().RunUntilIdle();
  // We should still be in Frame Server mode.
  EXPECT_TRUE(media_foundation_renderer_client_->IsFrameServerMode());

  DCHECK(frame_server_on_state_change_cb_);
  // Promote
  std::move(frame_server_on_state_change_cb_).Run(true);
  base::RunLoop().RunUntilIdle();

  // Verify we're now in Direct Composition mode
  EXPECT_FALSE(media_foundation_renderer_client_->IsFrameServerMode());

  DCHECK(dcomp_on_state_change_cb_);
  // Demote
  std::move(dcomp_on_state_change_cb_).Run(false);
  base::RunLoop().RunUntilIdle();

  // Verify we're back in Frame Server mode
  EXPECT_TRUE(media_foundation_renderer_client_->IsFrameServerMode());
}

}  // namespace media