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

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "cc/base/math_util.h"
#include "components/viz/common/gpu/raster_context_provider.h"
#include "content/browser/webrtc/mock_camera_device.h"
#include "content/browser/webrtc/mock_capture_device_controller.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/video_capture_service.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "gpu/GLES2/gl2extchromium.h"
#include "gpu/command_buffer/client/client_shared_image.h"
#include "gpu/command_buffer/client/raster_interface.h"
#include "gpu/command_buffer/client/shared_image_interface.h"
#include "gpu/command_buffer/common/shared_image_usage.h"
#include "media/base/media_switches.h"
#include "media/base/video_frame.h"
#include "media/base/video_frame_metadata.h"
#include "media/capture/mojom/video_capture_buffer.mojom.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.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 "net/test/embedded_test_server/embedded_test_server.h"
#include "services/video_capture/public/cpp/mock_video_frame_handler.h"
#include "services/video_capture/public/mojom/constants.mojom.h"
#include "services/video_capture/public/mojom/producer.mojom.h"
#include "services/video_capture/public/mojom/video_capture_service.mojom.h"
#include "services/video_capture/public/mojom/video_source_provider.mojom.h"
#include "services/video_capture/public/mojom/virtual_device.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/compositor/compositor.h"

// ImageTransportFactory::GetInstance is not available on all build configs.
#if defined(USE_AURA) || BUILDFLAG(IS_MAC)
#define CAN_USE_IMAGE_TRANSPORT_FACTORY 1
#endif

#if defined(CAN_USE_IMAGE_TRANSPORT_FACTORY)
#include "content/browser/compositor/image_transport_factory.h"

namespace content {

namespace {

static const char kVideoCaptureHtmlFile[] = "/media/video_capture_test.html";
static const char kStartVideoCaptureAndVerify[] =
    "startVideoCaptureFromVirtualDeviceAndVerifyUniformColorVideoWithSize(%d, "
    "%d)";

static const char kVirtualDeviceId[] = "virtual-chromium-device";
static const char kVirtualDeviceName[] = "Virtual Chromium Device";

static const gfx::Size kDummyFrameCodedSize(320, 200);
static const gfx::Rect kDummyFrameVisibleRect(94, 36, 178, 150);
static const int kDummyFrameRate = 5;

}  // namespace

// Abstraction for logic that is different between exercising
// DeviceFactory.AddTextureVirtualDevice() and
// DeviceFactory.AddSharedMemoryVirtualDevice().
class VirtualDeviceExerciser {
 public:
  virtual ~VirtualDeviceExerciser() {}
  virtual void Initialize() = 0;
  virtual void RegisterVirtualDeviceAtVideoSourceProvider(
      mojo::Remote<video_capture::mojom::VideoSourceProvider>* video_source,
      const media::VideoCaptureDeviceInfo& info) = 0;
  virtual gfx::Size GetVideoSize() = 0;
  virtual void PushNextFrame(base::TimeDelta timestamp) = 0;
  virtual void ShutDown() = 0;
};

// A VirtualDeviceExerciser for exercising
// DeviceFactory.AddTextureVirtualDevice(). It alternates between two texture
// RGB dummy frames, one dark one and one light one.
class TextureDeviceExerciser : public VirtualDeviceExerciser {
 public:
  TextureDeviceExerciser() { DETACH_FROM_SEQUENCE(sequence_checker_); }

  void Initialize() override {
    ImageTransportFactory* factory = ImageTransportFactory::GetInstance();
    CHECK(factory);
    context_provider_ =
        factory->GetContextFactory()->SharedMainThreadRasterContextProvider();
    CHECK(context_provider_);
    gpu::raster::RasterInterface* ri = context_provider_->RasterInterface();
    CHECK(ri);

    gpu::SharedImageInterface* sii = context_provider_->SharedImageInterface();
    CHECK(sii);

    const SkColor4f kDarkFrameColor = SkColors::kBlack;
    const SkColor4f kLightFrameColor = SkColors::kGray;
    dummy_frame_0_shared_image_ = CreateDummyRgbFrame(
        ri, sii, kDarkFrameColor, dummy_frame_0_sync_token_);
    dummy_frame_1_shared_image_ = CreateDummyRgbFrame(
        ri, sii, kLightFrameColor, dummy_frame_1_sync_token_);
  }

  void RegisterVirtualDeviceAtVideoSourceProvider(
      mojo::Remote<video_capture::mojom::VideoSourceProvider>* video_source,
      const media::VideoCaptureDeviceInfo& info) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    (*video_source)
        ->AddTextureVirtualDevice(info,
                                  virtual_device_.BindNewPipeAndPassReceiver());

    gpu::ExportedSharedImage dummy_frame_0_exported_shared_image =
        dummy_frame_0_shared_image_->Export();
    gpu::ExportedSharedImage dummy_frame_1_exported_shared_image =
        dummy_frame_1_shared_image_->Export();

    virtual_device_->OnNewSharedImageBufferHandle(
        0, media::mojom::SharedImageBufferHandleSet::New(
               std::move(dummy_frame_0_exported_shared_image),
               dummy_frame_0_sync_token_));
    virtual_device_->OnNewSharedImageBufferHandle(
        1, media::mojom::SharedImageBufferHandleSet::New(
               std::move(dummy_frame_1_exported_shared_image),
               dummy_frame_1_sync_token_));
    frame_being_consumed_[0] = false;
    frame_being_consumed_[1] = false;
  }

  gfx::Size GetVideoSize() override { return kDummyFrameCodedSize; }

  void PushNextFrame(base::TimeDelta timestamp) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (frame_being_consumed_[dummy_frame_index_]) {
      LOG(INFO) << "Frame " << dummy_frame_index_ << " is still being consumed";
      return;
    }

    if (!virtual_device_has_frame_access_handler_) {
      mojo::PendingRemote<video_capture::mojom::VideoFrameAccessHandler>
          pending_frame_access_handler;
      mojo::MakeSelfOwnedReceiver<
          video_capture::mojom::VideoFrameAccessHandler>(
          std::make_unique<video_capture::FakeVideoFrameAccessHandler>(
              base::BindRepeating(
                  &TextureDeviceExerciser::OnFrameConsumptionFinished,
                  weak_factory_.GetWeakPtr())),
          pending_frame_access_handler.InitWithNewPipeAndPassReceiver());
      virtual_device_->OnFrameAccessHandlerReady(
          std::move(pending_frame_access_handler));
      virtual_device_has_frame_access_handler_ = true;
    }

    media::VideoFrameMetadata metadata;
    metadata.frame_rate = kDummyFrameRate;
    metadata.reference_time = base::TimeTicks::Now();

    media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New();
    info->timestamp = timestamp;
    info->pixel_format = media::PIXEL_FORMAT_ABGR;
    info->coded_size = kDummyFrameCodedSize;
    info->visible_rect = gfx::Rect(kDummyFrameCodedSize);
    info->natural_size = kDummyFrameCodedSize;
    info->metadata = metadata;

    frame_being_consumed_[dummy_frame_index_] = true;
    virtual_device_->OnFrameReadyInBuffer(dummy_frame_index_, std::move(info));

    dummy_frame_index_ = (dummy_frame_index_ + 1) % 2;
  }

  void ShutDown() override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    virtual_device_.reset();
    weak_factory_.InvalidateWeakPtrs();
  }

 private:
  scoped_refptr<gpu::ClientSharedImage> CreateDummyRgbFrame(
      gpu::raster::RasterInterface* ri,
      gpu::SharedImageInterface* sii,
      SkColor4f frame_color,
      gpu::SyncToken& ri_token) {
    SkBitmap frame_bitmap;
    frame_bitmap.allocPixels(SkImageInfo::Make(
        kDummyFrameCodedSize.width(), kDummyFrameCodedSize.height(),
        kRGBA_8888_SkColorType, kOpaque_SkAlphaType));
    frame_bitmap.eraseColor(frame_color);

    // This SharedImage is populated via the raster interface below and may
    // be read via the raster interface in normal VideoFrame usage exercised
    // by the tests.
    auto shared_image = sii->CreateSharedImage(
        {viz::SinglePlaneFormat::kRGBA_8888, kDummyFrameCodedSize,
         gfx::ColorSpace::CreateSRGB(), kTopLeft_GrSurfaceOrigin,
         kOpaque_SkAlphaType,
         gpu::SHARED_IMAGE_USAGE_RASTER_READ |
             gpu::SHARED_IMAGE_USAGE_RASTER_WRITE,
         "TestLabel"},
        gpu::kNullSurfaceHandle);

    auto ri_access = shared_image->BeginRasterAccess(
        ri, shared_image->creation_sync_token(), /*readonly=*/false);
    ri->WritePixels(shared_image->mailbox(), 0, 0, GL_TEXTURE_2D,
                    frame_bitmap.pixmap());

    ri_token = gpu::RasterScopedAccess::EndAccess(std::move(ri_access));
    int8_t* ri_token_data = ri_token.GetData();
    ri->VerifySyncTokensCHROMIUM(&ri_token_data, 1);
    ri->ShallowFlushCHROMIUM();
    CHECK_EQ(ri->GetError(), static_cast<GLenum>(GL_NO_ERROR));

    return shared_image;
  }

  void OnFrameConsumptionFinished(int32_t frame_index) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    frame_being_consumed_[frame_index] = false;
  }

  SEQUENCE_CHECKER(sequence_checker_);
  scoped_refptr<viz::RasterContextProvider> context_provider_;
  mojo::Remote<video_capture::mojom::TextureVirtualDevice> virtual_device_;
  bool virtual_device_has_frame_access_handler_ = false;
  int dummy_frame_index_ = 0;
  scoped_refptr<gpu::ClientSharedImage> dummy_frame_0_shared_image_;
  scoped_refptr<gpu::ClientSharedImage> dummy_frame_1_shared_image_;
  gpu::SyncToken dummy_frame_0_sync_token_;
  gpu::SyncToken dummy_frame_1_sync_token_;
  std::array<bool, 2> frame_being_consumed_;
  base::WeakPtrFactory<TextureDeviceExerciser> weak_factory_{this};
};

// A VirtualDeviceExerciser for exercising
// DeviceFactory.AddSharedMemoryVirtualDevice().
// It generates (dummy) I420 frame data by setting all bytes equal to the
// current frame count. Padding bytes are set to 0.
class SharedMemoryDeviceExerciser : public VirtualDeviceExerciser,
                                    public video_capture::mojom::Producer {
 public:
  explicit SharedMemoryDeviceExerciser(
      media::mojom::PlaneStridesPtr strides = nullptr)
      : strides_(std::move(strides)) {}

  // VirtualDeviceExerciser implementation.
  void Initialize() override {}
  void RegisterVirtualDeviceAtVideoSourceProvider(
      mojo::Remote<video_capture::mojom::VideoSourceProvider>* video_source,
      const media::VideoCaptureDeviceInfo& info) override {
    mojo::PendingRemote<video_capture::mojom::Producer> producer;
    producer_receiver_.Bind(producer.InitWithNewPipeAndPassReceiver());
    (*video_source)
        ->AddSharedMemoryVirtualDevice(
            info, std::move(producer),
            virtual_device_.BindNewPipeAndPassReceiver());
  }
  gfx::Size GetVideoSize() override {
    return gfx::Size(kDummyFrameVisibleRect.width(),
                     kDummyFrameVisibleRect.height());
  }
  void PushNextFrame(base::TimeDelta timestamp) override {
    virtual_device_->RequestFrameBuffer(
        kDummyFrameCodedSize, media::VideoPixelFormat::PIXEL_FORMAT_I420,
        strides_.Clone(),
        base::BindOnce(&SharedMemoryDeviceExerciser::OnFrameBufferReceived,
                       weak_factory_.GetWeakPtr(), timestamp));
  }
  void ShutDown() override {
    virtual_device_.reset();
    producer_receiver_.reset();
    weak_factory_.InvalidateWeakPtrs();
  }

  // video_capture::mojom::Producer implementation.
  void OnNewBuffer(int32_t buffer_id,
                   media::mojom::VideoBufferHandlePtr buffer_handle,
                   OnNewBufferCallback callback) override {
    CHECK(buffer_handle->is_unsafe_shmem_region());
    base::UnsafeSharedMemoryRegion region =
        std::move(buffer_handle->get_unsafe_shmem_region());
    CHECK(region.IsValid());
    base::WritableSharedMemoryMapping mapping = region.Map();
    CHECK(mapping.IsValid());
    outgoing_buffer_id_to_buffer_map_.insert(
        std::make_pair(buffer_id, std::move(mapping)));
    std::move(callback).Run();
  }
  void OnBufferRetired(int32_t buffer_id) override {
    outgoing_buffer_id_to_buffer_map_.erase(buffer_id);
  }

 private:
  void OnFrameBufferReceived(base::TimeDelta timestamp, int32_t buffer_id) {
    if (buffer_id == video_capture::mojom::kInvalidBufferId)
      return;

    media::VideoFrameMetadata metadata;
    metadata.frame_rate = kDummyFrameRate;
    metadata.reference_time = base::TimeTicks::Now();

    media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New();
    info->timestamp = timestamp;
    info->pixel_format = media::PIXEL_FORMAT_I420;
    info->coded_size = kDummyFrameCodedSize;
    info->visible_rect = kDummyFrameVisibleRect;
    info->natural_size = kDummyFrameVisibleRect.size();
    info->metadata = metadata;
    info->strides = strides_.Clone();

    base::WritableSharedMemoryMapping& outgoing_buffer =
        outgoing_buffer_id_to_buffer_map_.at(buffer_id);

    static int frame_count = 0;
    frame_count++;
    const uint8_t dummy_value = frame_count % 256;

    // Reset the whole buffer to 0
    UNSAFE_TODO(memset(outgoing_buffer.memory(), 0, outgoing_buffer.size()));

    // Set all bytes affecting |info->visible_rect| to |dummy_value|.
    const int kYStride = info->strides ? info->strides->stride_by_plane[0]
                                       : info->coded_size.width();
    const int kYColsToSkipAtStart = info->visible_rect.x();
    const int kYVisibleColCount = info->visible_rect.width();
    const int kYCodedRowCount = info->coded_size.height();
    const int kYRowsToSkipAtStart = info->visible_rect.y();
    const int kYVisibleRowCount = info->visible_rect.height();

    const int kUStride = info->strides ? info->strides->stride_by_plane[1]
                                       : info->coded_size.width() / 2;
    const int kUColsToSkipAtStart =
        cc::MathUtil::UncheckedRoundDown(info->visible_rect.x(), 2) / 2;
    const int kUVisibleColCount =
        (cc::MathUtil::UncheckedRoundUp(info->visible_rect.right(), 2) / 2) -
        kUColsToSkipAtStart;
    const int kUCodedRowCount = info->coded_size.height() / 2;
    const int kURowsToSkipAtStart =
        cc::MathUtil::UncheckedRoundDown(info->visible_rect.y(), 2) / 2;
    const int kUVisibleRowCount =
        (cc::MathUtil::UncheckedRoundUp(info->visible_rect.bottom(), 2) / 2) -
        kURowsToSkipAtStart;

    const int kVStride = info->strides ? info->strides->stride_by_plane[2]
                                       : info->coded_size.width() / 2;

    uint8_t* write_ptr = outgoing_buffer.GetMemoryAsSpan<uint8_t>().data();
    FillVisiblePortionOfPlane(&write_ptr, dummy_value, kYCodedRowCount,
                              kYRowsToSkipAtStart, kYVisibleRowCount, kYStride,
                              kYColsToSkipAtStart, kYVisibleColCount);
    FillVisiblePortionOfPlane(&write_ptr, dummy_value, kUCodedRowCount,
                              kURowsToSkipAtStart, kUVisibleRowCount, kUStride,
                              kUColsToSkipAtStart, kUVisibleColCount);
    FillVisiblePortionOfPlane(&write_ptr, dummy_value, kUCodedRowCount,
                              kURowsToSkipAtStart, kUVisibleRowCount, kVStride,
                              kUColsToSkipAtStart, kUVisibleColCount);

    virtual_device_->OnFrameReadyInBuffer(buffer_id, std::move(info));
  }

  void FillVisiblePortionOfPlane(uint8_t** write_ptr,
                                 uint8_t fill_value,
                                 int row_count,
                                 int rows_to_skip_at_start,
                                 int visible_row_count,
                                 int col_count,
                                 int cols_to_skip_at_start,
                                 int visible_col_count) {
    const int kColsToSkipAtEnd =
        col_count - visible_col_count - cols_to_skip_at_start;
    const int kRowsToSkipAtEnd =
        row_count - visible_row_count - rows_to_skip_at_start;

    // Skip rows at start
    UNSAFE_TODO((*write_ptr) += col_count * rows_to_skip_at_start);
    // Fill rows
    for (int i = 0; i < visible_row_count; i++) {
      // Skip cols at start
      UNSAFE_TODO((*write_ptr) += cols_to_skip_at_start);
      // Fill visible bytes
      UNSAFE_TODO(memset(*write_ptr, fill_value, visible_col_count));
      UNSAFE_TODO((*write_ptr) += visible_col_count);
      // Skip cols at end
      UNSAFE_TODO((*write_ptr) += kColsToSkipAtEnd);
    }
    // Skip rows at end
    UNSAFE_TODO((*write_ptr) += col_count * kRowsToSkipAtEnd);
  }

  media::mojom::PlaneStridesPtr strides_;
  mojo::Receiver<video_capture::mojom::Producer> producer_receiver_{this};
  mojo::Remote<video_capture::mojom::SharedMemoryVirtualDevice> virtual_device_;
  std::map<int32_t /*buffer_id*/, base::WritableSharedMemoryMapping>
      outgoing_buffer_id_to_buffer_map_;
  base::WeakPtrFactory<SharedMemoryDeviceExerciser> weak_factory_{this};
};

// Integration test that obtains a connection to the video capture service. It
// It then registers a virtual device at the service and feeds frames to it. It
// opens the virtual device in a <video> element on a test page and verifies
// that the element plays in the expected dimensions and the pixel content on
// the element changes.
class WebRtcVideoCaptureServiceBrowserTest : public ContentBrowserTest {
 public:
  WebRtcVideoCaptureServiceBrowserTest()
      : virtual_device_thread_("Virtual Device Thread") {
    virtual_device_thread_.Start();
  }

  WebRtcVideoCaptureServiceBrowserTest(
      const WebRtcVideoCaptureServiceBrowserTest&) = delete;
  WebRtcVideoCaptureServiceBrowserTest& operator=(
      const WebRtcVideoCaptureServiceBrowserTest&) = delete;

  ~WebRtcVideoCaptureServiceBrowserTest() override {}

  void AddVirtualDeviceAndStartCapture(VirtualDeviceExerciser* device_exerciser,
                                       base::OnceClosure finish_test_cb) {
    DCHECK(virtual_device_thread_.task_runner()->RunsTasksInCurrentSequence());

    main_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(
            [](mojo::PendingReceiver<video_capture::mojom::VideoSourceProvider>
                   receiver) {
              GetVideoCaptureService().ConnectToVideoSourceProvider(
                  std::move(receiver));
            },
            video_source_provider_.BindNewPipeAndPassReceiver()));

    media::VideoCaptureDeviceInfo info;
    info.descriptor.device_id = kVirtualDeviceId;
    info.descriptor.set_display_name(kVirtualDeviceName);
    info.descriptor.capture_api = media::VideoCaptureApi::VIRTUAL_DEVICE;

    video_size_ = device_exerciser->GetVideoSize();
    device_exerciser->RegisterVirtualDeviceAtVideoSourceProvider(
        &video_source_provider_, info);

    main_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
                           OpenVirtualDeviceInRendererAndWaitForPlaying,
                       base::Unretained(this),
                       base::BindPostTaskToCurrentDefault(base::BindOnce(
                           &WebRtcVideoCaptureServiceBrowserTest::
                               ShutDownVirtualDeviceAndContinue,
                           base::Unretained(this), device_exerciser,
                           std::move(finish_test_cb)))));

    PushDummyFrameAndScheduleNextPush(device_exerciser);
  }

  void RunOnVirtualDeviceThreadAndWait(base::OnceClosure task) {
    base::RunLoop run_loop;

    CHECK(virtual_device_thread_.task_runner()->PostTaskAndReply(
        FROM_HERE, std::move(task), run_loop.QuitClosure()));

    run_loop.Run();
  }

  bool SetUpMockCaptureDeviceTest() {
    Initialize();
    embedded_test_server()->StartAcceptingConnections();

    if (!NavigateToURL(shell(),
                       embedded_test_server()->GetURL(kVideoCaptureHtmlFile))) {
      return false;
    }

    CreateMockCaptureDeviceController();
    return true;
  }

  void CreateMockCaptureDeviceController() {
    RunOnVirtualDeviceThreadAndWait(base::BindOnce(
        [](WebRtcVideoCaptureServiceBrowserTest* test) {
          DCHECK(test->virtual_device_thread_.task_runner()
                     ->RunsTasksInCurrentSequence());

          test->mock_capture_device_controller_ =
              std::make_unique<MockCaptureDeviceController>(
                  test->main_task_runner_,
                  base::BindRepeating(
                      [](mojo::PendingReceiver<
                          video_capture::mojom::VideoSourceProvider> receiver) {
                        GetVideoCaptureService().ConnectToVideoSourceProvider(
                            std::move(receiver));
                      }));
        },
        base::Unretained(this)));
  }

  void DestroyMockCaptureDeviceController() {
    if (!mock_capture_device_controller_) {
      return;
    }

    RunOnVirtualDeviceThreadAndWait(base::BindOnce(
        [](WebRtcVideoCaptureServiceBrowserTest* test) {
          DCHECK(test->virtual_device_thread_.task_runner()
                     ->RunsTasksInCurrentSequence());
          test->mock_capture_device_controller_.reset();
        },
        base::Unretained(this)));
  }

  void AddDefaultMockCamera() {
    RunOnVirtualDeviceThreadAndWait(base::BindOnce(
        [](MockCaptureDeviceController* controller) {
          controller->AddMockCamera(MockCameraConfig{
              .device_id = kVirtualDeviceId,
              .label = kVirtualDeviceName,
              .size = gfx::Size(640, 480),
              .frame_rate = 5.0,
          });
        },
        base::Unretained(mock_capture_device_controller_.get())));
  }

  void RemoveDefaultMockCamera() {
    RunOnVirtualDeviceThreadAndWait(base::BindOnce(
        [](MockCaptureDeviceController* controller) {
          controller->RemoveMockCamera(kVirtualDeviceId);
        },
        base::Unretained(mock_capture_device_controller_.get())));
  }

  void ResetMockCameras() {
    RunOnVirtualDeviceThreadAndWait(base::BindOnce(
        [](MockCaptureDeviceController* controller) { controller->Reset(); },
        base::Unretained(mock_capture_device_controller_.get())));
  }

  void PrepareDeviceChangeWatcher() {
    EXPECT_EQ("OK", EvalJs(shell(), R"JS(
    (async () => {
      const stream =
          await navigator.mediaDevices.getUserMedia({video: true});
      stream.getTracks().forEach(track => track.stop());

      window.deviceChangeEventCount = 0;
      navigator.mediaDevices.addEventListener("devicechange", () => {
        window.deviceChangeEventCount++;
      });

      await navigator.mediaDevices.enumerateDevices();
      await new Promise(resolve => setTimeout(resolve, 0));

      return "OK";
    })()
  )JS")
                        .ExtractString());
  }

  void ArmNextDeviceChange() {
    EXPECT_EQ("OK", EvalJs(shell(), R"JS(
    (() => {
      window.nextDeviceChange = new Promise(resolve => {
        const handler = () => {
          navigator.mediaDevices.removeEventListener("devicechange", handler);
          resolve("OK");
        };
        navigator.mediaDevices.addEventListener("devicechange", handler);
      });
      return "OK";
    })()
  )JS")
                        .ExtractString());
  }

  std::string WaitForNextDeviceChange() {
    return EvalJs(shell(), R"JS(
    (async () => {
      return await Promise.race([
        window.nextDeviceChange,
        new Promise(resolve => {
          setTimeout(() => {
            resolve("FAIL: timeout waiting for devicechange; count=" +
                    String(window.deviceChangeEventCount || 0));
          }, 5000);
        }),
      ]);
    })()
  )JS")
        .ExtractString();
  }

  int GetVideoInputCount() {
    return EvalJs(shell(), R"JS(
    (async () => {
      const devices = await navigator.mediaDevices.enumerateDevices();
      return devices.filter(d => d.kind === 'videoinput').length;
    })()
  )JS")
        .ExtractInt();
  }

  void SaveCurrentVideoInputIds() {
    EXPECT_EQ("OK", EvalJs(shell(), R"JS(
    (async () => {
      // Warm-up getUserMedia to make device metadata available in this test
      // environment. Stop immediately because we only need permission/metadata.
      let warmupStream;
      try {
        warmupStream = await navigator.mediaDevices.getUserMedia({
          video: true,
        });
        warmupStream.getTracks().forEach(track => track.stop());
      } catch (e) {
        return "FAIL: warmup getUserMedia rejected: " +
            e.name + ": " + e.message;
      }

      const devices = await navigator.mediaDevices.enumerateDevices();
      window.beforeVideoInputIds = devices
        .filter(d => d.kind === "videoinput")
        .map(d => d.deviceId);

      return "OK";
    })()
  )JS")
                        .ExtractString());
  }

  std::string WaitForAddedVideoInputAndOpenIt() {
    return EvalJs(shell(), R"JS(
    (async () => {
      const beforeIds = new Set(window.beforeVideoInputIds || []);
      let addedDevice = null;

      for (let i = 0; i < 50; ++i) {
        const devices = await navigator.mediaDevices.enumerateDevices();
        addedDevice = devices
          .filter(d => d.kind === "videoinput")
          .find(d => !beforeIds.has(d.deviceId));

        if (addedDevice && addedDevice.deviceId)
          break;

        await new Promise(resolve => setTimeout(resolve, 100));
      }

      if (!addedDevice)
        return "FAIL: no added videoinput";

      if (!addedDevice.deviceId)
        return "FAIL: added videoinput has empty deviceId";

      const withTimeout = (promise, message) => {
        let timeoutId;
        const timeoutPromise = new Promise((_, reject) => {
          timeoutId = setTimeout(() => reject(new Error(message)), 5000);
        });

        return Promise.race([promise, timeoutPromise]).finally(() => {
          clearTimeout(timeoutId);
        });
      };

      let stream;
      try {
        stream = await withTimeout(
            navigator.mediaDevices.getUserMedia({
              video: {
                deviceId: { exact: addedDevice.deviceId },
              },
            }),
            "timed out waiting for exact-device getUserMedia");
      } catch (e) {
        return "FAIL: exact-device getUserMedia failed: " +
            e.name + ": " + e.message;
      }

      const tracks = stream.getVideoTracks();
      if (tracks.length !== 1 || tracks[0].readyState !== "live") {
        const result =
            "FAIL: expected one live video track, got " +
            JSON.stringify(tracks.map(t => ({
              label: t.label,
              readyState: t.readyState,
              settings: t.getSettings(),
            })));
        stream.getTracks().forEach(track => track.stop());
        return result;
      }

      const video = document.createElement("video");
      video.autoplay = true;
      video.muted = true;
      video.playsInline = true;
      video.srcObject = stream;
      document.body.appendChild(video);

      const framePromise = new Promise(resolve => {
        const timeout = setTimeout(() => {
          resolve("FAIL: timed out waiting for a video frame; " +
                  JSON.stringify({
                    readyState: video.readyState,
                    videoWidth: video.videoWidth,
                    videoHeight: video.videoHeight,
                    paused: video.paused,
                    trackStates: stream.getTracks().map(track => ({
                      kind: track.kind,
                      readyState: track.readyState,
                      muted: track.muted,
                    })),
                  }));
        }, 5000);

        video.requestVideoFrameCallback(() => {
          clearTimeout(timeout);

          if (video.videoWidth === 640 && video.videoHeight === 480) {
            resolve("OK");
            return;
          }

          resolve("FAIL: unexpected video size " +
                  video.videoWidth + "x" + video.videoHeight);
        });
      });

      try {
        await withTimeout(video.play(), "timed out waiting for video.play()");
        return await framePromise;
      } catch (e) {
        return "FAIL: video playback failed: " + e.name + ": " + e.message;
      } finally {
        video.srcObject = null;
        video.remove();
        stream.getTracks().forEach(track => track.stop());
      }
    })()
  )JS")
        .ExtractString();
  }

  std::string WaitForVideoInputCount(int expected_count) {
    return EvalJs(shell(), base::StringPrintf(R"JS(
    (async () => {
      for (let i = 0; i < 50; ++i) {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const videoInputs = devices.filter(d => d.kind === 'videoinput');

        if (videoInputs.length === %d)
          return "OK";

        await new Promise(resolve => setTimeout(resolve, 100));
      }

      const devices = await navigator.mediaDevices.enumerateDevices();
      return "FAIL: " + JSON.stringify(devices.map(d => ({
        kind: d.kind,
        label: d.label,
        deviceId: d.deviceId,
        groupId: d.groupId,
      })));
    })()
  )JS",
                                              expected_count))
        .ExtractString();
  }

  std::string WaitForVideoInputCountAtLeast(int expected_count) {
    return EvalJs(shell(), base::StringPrintf(R"JS(
    (async () => {
      for (let i = 0; i < 50; ++i) {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const videoInputs = devices.filter(d => d.kind === 'videoinput');

        if (videoInputs.length >= %d)
          return "OK";

        await new Promise(resolve => setTimeout(resolve, 100));
      }

      const devices = await navigator.mediaDevices.enumerateDevices();
      return "FAIL: " + JSON.stringify(devices.map(d => ({
        kind: d.kind,
        label: d.label,
        deviceId: d.deviceId,
        groupId: d.groupId,
      })));
    })()
  )JS",
                                              expected_count))
        .ExtractString();
  }

  void PushDummyFrameAndScheduleNextPush(
      VirtualDeviceExerciser* device_exerciser) {
    DCHECK(virtual_device_thread_.task_runner()->RunsTasksInCurrentSequence());
    device_exerciser->PushNextFrame(CalculateTimeSinceFirstInvocation());
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
                           PushDummyFrameAndScheduleNextPush,
                       weak_factory_.GetWeakPtr(), device_exerciser),
        base::Milliseconds(1000 / kDummyFrameRate));
  }

  void ShutDownVirtualDeviceAndContinue(
      VirtualDeviceExerciser* device_exerciser,
      base::OnceClosure continuation) {
    DCHECK(virtual_device_thread_.task_runner()->RunsTasksInCurrentSequence());
    LOG(INFO) << "Shutting down virtual device";
    device_exerciser->ShutDown();
    video_source_provider_.reset();
    weak_factory_.InvalidateWeakPtrs();
    std::move(continuation).Run();
  }

  void OpenVirtualDeviceInRendererAndWaitForPlaying(
      base::OnceClosure finish_test_cb) {
    DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
    embedded_test_server()->StartAcceptingConnections();
    GURL url(embedded_test_server()->GetURL(kVideoCaptureHtmlFile));
    EXPECT_TRUE(NavigateToURL(shell(), url));

    std::string javascript_to_execute = base::StringPrintf(
        kStartVideoCaptureAndVerify, video_size_.width(), video_size_.height());
    // Start video capture and wait until it started rendering
    ASSERT_TRUE(ExecJs(shell(), javascript_to_execute));

    std::move(finish_test_cb).Run();
  }

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Note: We are not planning to actually use the fake device, but we want
    // to avoid enumerating or otherwise calling into real capture devices.
    command_line->AppendSwitch(switches::kUseFakeUIForMediaStream);
  }

  void SetUp() override {
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
    EnablePixelOutput();
    ContentBrowserTest::SetUp();
  }

  void TearDownOnMainThread() override {
    DestroyMockCaptureDeviceController();
    ContentBrowserTest::TearDownOnMainThread();
  }

  void Initialize() {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    main_task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault();
  }

  base::Thread virtual_device_thread_;
  scoped_refptr<base::SequencedTaskRunner> main_task_runner_;

 private:
  base::TimeDelta CalculateTimeSinceFirstInvocation() {
    if (first_frame_time_.is_null())
      first_frame_time_ = base::TimeTicks::Now();
    return base::TimeTicks::Now() - first_frame_time_;
  }

  // The browsertest selects |virtual_device_thread_| as the controller's
  // owning sequence. Production callers may select a different sequence.
  std::unique_ptr<MockCaptureDeviceController> mock_capture_device_controller_;
  mojo::Remote<video_capture::mojom::VideoSourceProvider>
      video_source_provider_;
  gfx::Size video_size_;
  base::TimeTicks first_frame_time_;
  base::WeakPtrFactory<WebRtcVideoCaptureServiceBrowserTest> weak_factory_{
      this};
};

IN_PROC_BROWSER_TEST_F(
    WebRtcVideoCaptureServiceBrowserTest,
    MockCaptureDeviceControllerCanAddAndRemoveCameraFromEnumerateDevices) {
  ASSERT_TRUE(SetUpMockCaptureDeviceTest());
  const int before_count = GetVideoInputCount();

  AddDefaultMockCamera();
  EXPECT_EQ("OK", WaitForVideoInputCountAtLeast(before_count + 1));

  RemoveDefaultMockCamera();
  EXPECT_EQ("OK", WaitForVideoInputCount(before_count));
}

IN_PROC_BROWSER_TEST_F(WebRtcVideoCaptureServiceBrowserTest,
                       MockCaptureDeviceControllerFiresDeviceChange) {
  ASSERT_TRUE(SetUpMockCaptureDeviceTest());
  PrepareDeviceChangeWatcher();

  ArmNextDeviceChange();
  AddDefaultMockCamera();
  EXPECT_EQ("OK", WaitForNextDeviceChange());

  ArmNextDeviceChange();

  RemoveDefaultMockCamera();
  EXPECT_EQ("OK", WaitForNextDeviceChange());
}

#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/40781953): This test is flakey on macOS.
#define MAYBE_MockCaptureDeviceControllerCameraCanBeTargetedByGetUserMedia \
  DISABLED_MockCaptureDeviceControllerCameraCanBeTargetedByGetUserMedia
#else
#define MAYBE_MockCaptureDeviceControllerCameraCanBeTargetedByGetUserMedia \
  MockCaptureDeviceControllerCameraCanBeTargetedByGetUserMedia
#endif
IN_PROC_BROWSER_TEST_F(
    WebRtcVideoCaptureServiceBrowserTest,
    MAYBE_MockCaptureDeviceControllerCameraCanBeTargetedByGetUserMedia) {
  ASSERT_TRUE(SetUpMockCaptureDeviceTest());
  SaveCurrentVideoInputIds();

  AddDefaultMockCamera();
  EXPECT_EQ("OK", WaitForAddedVideoInputAndOpenIt());
  RemoveDefaultMockCamera();
}

IN_PROC_BROWSER_TEST_F(WebRtcVideoCaptureServiceBrowserTest,
                       MockCaptureDeviceControllerResetRemovesCamera) {
  ASSERT_TRUE(SetUpMockCaptureDeviceTest());
  const int before_count = GetVideoInputCount();

  AddDefaultMockCamera();
  EXPECT_EQ("OK", WaitForVideoInputCountAtLeast(before_count + 1));

  ResetMockCameras();
  EXPECT_EQ("OK", WaitForVideoInputCount(before_count));
}

// TODO(crbug.com/40835247): Fix and enable on Fuchsia.
// TODO(crbug.com/40781953): This test is flakey on macOS.
// TODO(crbug.com/41484083): This test is flakey on ChromeOS.
#if BUILDFLAG(IS_FUCHSIA) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_FramesSentThroughTextureVirtualDeviceGetDisplayedOnPage \
  DISABLED_FramesSentThroughTextureVirtualDeviceGetDisplayedOnPage
#else
#define MAYBE_FramesSentThroughTextureVirtualDeviceGetDisplayedOnPage \
  FramesSentThroughTextureVirtualDeviceGetDisplayedOnPage
#endif
IN_PROC_BROWSER_TEST_F(
    WebRtcVideoCaptureServiceBrowserTest,
    MAYBE_FramesSentThroughTextureVirtualDeviceGetDisplayedOnPage) {
  Initialize();
  auto device_exerciser = std::make_unique<TextureDeviceExerciser>();
  device_exerciser->Initialize();

  base::RunLoop run_loop;
  virtual_device_thread_.task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
                                    AddVirtualDeviceAndStartCapture,
                                base::Unretained(this), device_exerciser.get(),
                                base::BindPostTaskToCurrentDefault(
                                    run_loop.QuitClosure())));
  run_loop.Run();
}

#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/40781953): This test is flakey on macOS.
#define MAYBE_FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage \
  DISABLED_FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage
#else
#define MAYBE_FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage \
  FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage
#endif
IN_PROC_BROWSER_TEST_F(
    WebRtcVideoCaptureServiceBrowserTest,
    MAYBE_FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage) {
  Initialize();
  auto device_exerciser = std::make_unique<SharedMemoryDeviceExerciser>();
  device_exerciser->Initialize();

  base::RunLoop run_loop;
  virtual_device_thread_.task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
                                    AddVirtualDeviceAndStartCapture,
                                base::Unretained(this), device_exerciser.get(),
                                base::BindPostTaskToCurrentDefault(
                                    run_loop.QuitClosure())));
  run_loop.Run();
}

#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/40781953): This test is flakey on macOS.
#define MAYBE_PaddedI420FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage \
  DISABLED_PaddedI420FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage
#else
#define MAYBE_PaddedI420FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage \
  PaddedI420FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage
#endif
IN_PROC_BROWSER_TEST_F(
    WebRtcVideoCaptureServiceBrowserTest,
    MAYBE_PaddedI420FramesSentThroughSharedMemoryVirtualDeviceGetDisplayedOnPage) {
  Initialize();
  auto device_exerciser = std::make_unique<SharedMemoryDeviceExerciser>(
      media::mojom::PlaneStrides::New(
          std::vector<uint32_t>({1024u, 512u, 1024u, 0u})));
  device_exerciser->Initialize();

  base::RunLoop run_loop;
  virtual_device_thread_.task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&WebRtcVideoCaptureServiceBrowserTest::
                                    AddVirtualDeviceAndStartCapture,
                                base::Unretained(this), device_exerciser.get(),
                                base::BindPostTaskToCurrentDefault(
                                    run_loop.QuitClosure())));
  run_loop.Run();
}

}  // namespace content

#endif  // defined(CAN_USE_IMAGE_TRANSPORT_FACTORY)
