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

#include "components/webrtc/media_stream_devices_controller.h"

#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "components/permissions/permissions_client.h"
#include "components/webrtc/media_stream_devices_util.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/mock_permission_controller.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "content/public/browser/render_widget_host_view.h"
#include "ui/android/window_android.h"
#endif

class HostContentSettingsMap;
namespace content_settings {
class CookieSettings;
}

using testing::_;

namespace {

blink::MediaStreamDevices CreateFakeDevices(
    blink::mojom::MediaStreamType type) {
  blink::MediaStreamDevices devices;
  devices.reserve(3);
  for (size_t i = 0; i < devices.capacity(); ++i) {
    devices.emplace_back(type, "id_" + base::NumberToString(i),
                         "name " + base::NumberToString(i));
  }
  return devices;
}

class FakeEnumerator : public webrtc::MediaStreamDeviceEnumeratorImpl {
 public:
  FakeEnumerator()
      : audio_capture_devices_(CreateFakeDevices(
            blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)),
        video_capture_devices_(CreateFakeDevices(
            blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)) {}

  const blink::MediaStreamDevices& GetAudioCaptureDevices() const override {
    return audio_capture_devices_;
  }

  const blink::MediaStreamDevices& GetVideoCaptureDevices() const override {
    return video_capture_devices_;
  }

  const std::optional<blink::MediaStreamDevice>
  GetPreferredAudioDeviceForBrowserContext(
      content::BrowserContext* context,
      const std::vector<std::string>& eligible_device_ids) const override {
    auto devices = GetAudioCaptureDevices();
    if (!eligible_device_ids.empty()) {
      devices = webrtc::FilterMediaDevices(devices, eligible_device_ids);
    }
    if (devices.empty()) {
      return std::nullopt;
    }
    if (!eligible_device_ids.empty()) {
      return devices.back();
    } else {
      return devices.front();
    }
  }

  const std::optional<blink::MediaStreamDevice>
  GetPreferredVideoDeviceForBrowserContext(
      content::BrowserContext* context,
      const std::vector<std::string>& eligible_device_ids) const override {
    auto devices = GetVideoCaptureDevices();
    if (!eligible_device_ids.empty()) {
      devices = webrtc::FilterMediaDevices(devices, eligible_device_ids);
    }

    if (devices.empty()) {
      return std::nullopt;
    }
    if (!eligible_device_ids.empty()) {
      return devices.back();
    } else {
      return devices.front();
    }
  }

 private:
  const blink::MediaStreamDevices audio_capture_devices_;
  const blink::MediaStreamDevices video_capture_devices_;
};

class TestPermissionsClient : public permissions::PermissionsClient {
 public:
  TestPermissionsClient() = default;
  ~TestPermissionsClient() override = default;

  HostContentSettingsMap* GetSettingsMap(
      content::BrowserContext* browser_context) override {
    return nullptr;
  }

  bool IsSubresourceFilterActivated(content::BrowserContext* browser_context,
                                    const GURL& url) override {
    return false;
  }

  scoped_refptr<content_settings::CookieSettings> GetCookieSettings(
      content::BrowserContext* browser_context) override {
    return nullptr;
  }

  permissions::OriginKeyedPermissionActionService*
  GetOriginKeyedPermissionActionService(
      content::BrowserContext* browser_context) override {
    return nullptr;
  }

  permissions::PermissionActionsHistory* GetPermissionActionsHistory(
      content::BrowserContext* browser_context) override {
    return nullptr;
  }

  permissions::PermissionDecisionAutoBlocker* GetPermissionDecisionAutoBlocker(
      content::BrowserContext* browser_context) override {
    return nullptr;
  }

  permissions::ObjectPermissionContextBase* GetChooserContext(
      content::BrowserContext* browser_context,
      ContentSettingsType type) override {
    return nullptr;
  }

#if BUILDFLAG(IS_ANDROID)
  const std::u16string GetClientApplicationName() const override {
    return u"TestApp";
  }
#endif
};

class MockPermissionController : public content::MockPermissionController {
 public:
  MOCK_METHOD(
      void,
      RequestPermissionsFromCurrentDocument,
      (content::RenderFrameHost * render_frame_host,
       content::PermissionRequestDescription request_description,
       base::OnceCallback<void(const std::vector<content::PermissionResult>&)>
           callback));
};

std::vector<std::string> GetIds(const blink::MediaStreamDevices& devices,
                                size_t start_index) {
  CHECK_LT(start_index, devices.size());
  std::vector<std::string> device_ids;
  std::transform(devices.begin() + start_index, devices.end(),
                 std::back_inserter(device_ids),
                 [](const auto& device) { return device.id; });
  return device_ids;
}

}  // namespace

class MediaStreamDevicesControllerTest : public testing::Test {
 public:
  void SetUp() override {
    InitializeWebContents();
    permissions_client_ = std::make_unique<TestPermissionsClient>();
  }

  void TearDown() override { permissions_client_.reset(); }

  void InitializeWebContents() {
    browser_context_.SetPermissionControllerForTesting(
        std::make_unique<MockPermissionController>());
    web_contents_ =
        test_web_contents_factory_.CreateWebContents(&browser_context_);
    render_frame_host_ = web_contents_->GetPrimaryMainFrame();
    render_frame_host_id_ = render_frame_host_->GetGlobalId();
    content::OverrideLastCommittedOrigin(render_frame_host_, origin_);

#if BUILDFLAG(IS_ANDROID)
    // Create a scoped window so that
    // WebContents::GetNativeView()->GetWindowAndroid() does not return
    // null.
    window_ = ui::WindowAndroid::CreateForTesting();
    window_.get()->get()->AddChild(web_contents_->GetNativeView());
    web_contents_->WasShown();
#endif
  }

 protected:
  std::unique_ptr<TestPermissionsClient> permissions_client_;
  std::tuple<blink::mojom::MediaStreamRequestResult,
             blink::mojom::StreamDevicesPtr>
  RequestPermissionsRaw(
      blink::MediaStreamRequestType request_type,
      const std::vector<std::string>& requested_audio_device_ids,
      const std::vector<std::string>& requested_video_device_ids,
      blink::mojom::MediaStreamType audio_type,
      blink::mojom::MediaStreamType video_type) {
    base::test::TestFuture<blink::mojom::MediaStreamRequestResult,
                           blink::mojom::StreamDevicesPtr>
        result_future;
    // TODO(crbug.com/379869738) Remove GetUnsafeValue.
    webrtc::MediaStreamDevicesController::RequestPermissions(
        content::MediaStreamRequest{
            /*render_process_id=*/render_frame_host_id_.child_id
                .GetUnsafeValue(),
            /*render_frame_id=*/render_frame_host_id_.frame_routing_id,
            /*page_request_id=*/0,
            /*url_origin=*/origin_,
            /*user_gesture=*/false,
            request_type,
            requested_audio_device_ids,
            requested_video_device_ids,
            audio_type,
            video_type,
            /*disable_local_echo=*/false,
            /*request_pan_tilt_zoom_permission=*/false,
            /*captured_surface_control_active=*/false,
        },
        &enumerator_,
        base::BindLambdaForTesting(
            [&](const blink::mojom::StreamDevicesSet& stream_devices_set,
                blink::mojom::MediaStreamRequestResult result,
                bool blocked_by_permissions_policy,
                ContentSetting audio_setting, ContentSetting video_setting) {
              if (result == blink::mojom::MediaStreamRequestResult::OK) {
                CHECK_EQ(stream_devices_set.stream_devices.size(), 1u);
              } else {
                CHECK(stream_devices_set.stream_devices.empty());
              }
              result_future.SetValue(
                  result, stream_devices_set.stream_devices.empty()
                              ? blink::mojom::StreamDevicesPtr()
                              : stream_devices_set.stream_devices[0]->Clone());
            }));
    return result_future.Take();
  }

  std::tuple<blink::mojom::MediaStreamRequestResult,
             blink::mojom::StreamDevicesPtr>
  MakeRequest(blink::MediaStreamRequestType request_type,
              const std::vector<std::string>& requested_audio_device_ids,
              const std::vector<std::string>& requested_video_device_ids,
              bool request_audio,
              bool request_video) {
    auto* mock_permission_controller = static_cast<MockPermissionController*>(
        browser_context_.GetPermissionController());
    ON_CALL(*mock_permission_controller, GetPermissionResultForCurrentDocument)
        .WillByDefault([](const blink::mojom::PermissionDescriptorPtr&
                              permission_descriptor,
                          content::RenderFrameHost* render_frame_host) {
          return content::PermissionResult{
              content::PermissionStatus::GRANTED,
              content::PermissionStatusSource::UNSPECIFIED,
          };
        });

    std::vector<blink::mojom::PermissionDescriptorPtr> expected_permissions;
    if (request_audio) {
      expected_permissions.push_back(
          content::PermissionDescriptorUtil::
              CreatePermissionDescriptorForPermissionType(
                  blink::PermissionType::AUDIO_CAPTURE));
    }
    if (request_video) {
      expected_permissions.push_back(
          content::PermissionDescriptorUtil::
              CreatePermissionDescriptorForPermissionType(
                  blink::PermissionType::VIDEO_CAPTURE));
    }

    content::PermissionRequestDescription expected_description{
        std::move(expected_permissions), false, origin_.GetURL()};
    expected_description.requested_audio_capture_device_ids =
        requested_audio_device_ids;
    expected_description.requested_video_capture_device_ids =
        requested_video_device_ids;

    EXPECT_CALL(*mock_permission_controller,
                RequestPermissionsFromCurrentDocument(render_frame_host_.get(),
                                                      expected_description, _))
        .WillOnce(
            [](auto*, auto,
               base::OnceCallback<void(
                   const std::vector<content::PermissionResult>&)> callback) {
              std::move(callback).Run(
                  {content::PermissionResult(
                       content::PermissionStatus::GRANTED,
                       content::PermissionStatusSource::UNSPECIFIED),
                   content::PermissionResult(
                       content::PermissionStatus::GRANTED,
                       content::PermissionStatusSource::UNSPECIFIED)});
            });

    return RequestPermissionsRaw(
        request_type, requested_audio_device_ids, requested_video_device_ids,
        request_audio ? blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE
                      : blink::mojom::MediaStreamType::NO_SERVICE,
        request_video ? blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE
                      : blink::mojom::MediaStreamType::NO_SERVICE);
  }

  content::BrowserTaskEnvironment task_environment_;
  FakeEnumerator enumerator_;
  content::TestBrowserContext browser_context_;
  content::TestWebContentsFactory test_web_contents_factory_;
  raw_ptr<content::WebContents> web_contents_;
  raw_ptr<content::RenderFrameHost> render_frame_host_;
  content::GlobalRenderFrameHostId render_frame_host_id_;
  const url::Origin origin_ = url::Origin::Create(GURL{"https://stuff.com"});

#if BUILDFLAG(IS_ANDROID)
  std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window_;
#endif
};

TEST_F(MediaStreamDevicesControllerTest,
       RequestPermissions_MEDIA_GENERATE_STREAM) {
  // Request all but the first device to exercise filtering.
  const auto kRequestedAudioCaptureDeviceIds =
      GetIds(enumerator_.GetAudioCaptureDevices(), /*start_index=*/1);
  const auto kRequestedVideoCaptureDeviceIds =
      GetIds(enumerator_.GetVideoCaptureDevices(), /*start_index=*/1);

  auto [result, stream_devices] = MakeRequest(
      blink::MediaStreamRequestType::MEDIA_GENERATE_STREAM,
      kRequestedAudioCaptureDeviceIds, kRequestedVideoCaptureDeviceIds,
      /*request_audio=*/true, /*request_video=*/true);
  ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
  ASSERT_TRUE(stream_devices->audio_device.has_value());
  EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
      enumerator_.GetAudioCaptureDevices().back()));
  ASSERT_TRUE(stream_devices->video_device.has_value());
  EXPECT_TRUE(stream_devices->video_device->IsSameDevice(
      enumerator_.GetVideoCaptureDevices().back()));
}

TEST_F(MediaStreamDevicesControllerTest,
       RequestPermissions_MEDIA_DEVICE_ACCESS) {
  auto [result, stream_devices] = MakeRequest(
      blink::MediaStreamRequestType::MEDIA_DEVICE_ACCESS,
      /*requested_audio_device_ids=*/{}, /*requested_video_device_ids=*/{},
      /*request_audio=*/true, /*request_video=*/false);
  ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
  ASSERT_TRUE(stream_devices->audio_device.has_value());
  EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
      enumerator_.GetAudioCaptureDevices().front()));
  ASSERT_FALSE(stream_devices->video_device.has_value());
}

TEST_F(
    MediaStreamDevicesControllerTest,
    RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Audio_UsesDefualtWhenNoRequestedDevices) {
  auto [result, stream_devices] = MakeRequest(
      blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
      /*requested_audio_device_ids=*/{}, /*requested_video_device_ids=*/{},
      /*request_audio=*/true, /*request_video=*/false);
  ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
  ASSERT_TRUE(stream_devices->audio_device.has_value());
  EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
      enumerator_.GetAudioCaptureDevices().front()));
  ASSERT_FALSE(stream_devices->video_device.has_value());
}

TEST_F(MediaStreamDevicesControllerTest,
       RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Audio_UsesRequestedDevices) {
  // Request all but the first device to exercise filtering.
  const auto kRequestedAudioCaptureDeviceIds =
      GetIds(enumerator_.GetAudioCaptureDevices(), /*start_index=*/1);

  auto [result, stream_devices] = MakeRequest(
      blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
      kRequestedAudioCaptureDeviceIds, /*requested_video_device_ids=*/{},
      /*request_audio=*/true, /*request_video=*/false);
  ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
  ASSERT_TRUE(stream_devices->audio_device.has_value());
  EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
      enumerator_.GetAudioCaptureDevices().back()));
  ASSERT_FALSE(stream_devices->video_device.has_value());
}

TEST_F(
    MediaStreamDevicesControllerTest,
    RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Video_UsesDefualtWhenNoRequestedDevices) {
  auto [result, stream_devices] = MakeRequest(
      blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
      /*requested_audio_device_ids=*/{}, /*requested_video_device_ids=*/{},
      /*request_audio=*/false, /*request_video=*/true);
  ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
  ASSERT_TRUE(stream_devices->video_device.has_value());
  EXPECT_TRUE(stream_devices->video_device->IsSameDevice(
      enumerator_.GetVideoCaptureDevices().front()));
  ASSERT_FALSE(stream_devices->audio_device.has_value());
}

TEST_F(MediaStreamDevicesControllerTest,
       RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Video_UsesRequestedDevices) {
  // Request all but the first device to exercise filtering.
  const auto kRequestedVideoCaptureDeviceIds =
      GetIds(enumerator_.GetVideoCaptureDevices(), /*start_index=*/1);

  auto [result, stream_devices] = MakeRequest(
      blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
      /*requested_audio_device_ids=*/{},
      /*requested_video_device_ids=*/kRequestedVideoCaptureDeviceIds,
      /*request_audio=*/false, /*request_video=*/true);
  ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
  ASSERT_TRUE(stream_devices->video_device.has_value());
  EXPECT_TRUE(stream_devices->video_device->IsSameDevice(
      enumerator_.GetVideoCaptureDevices().back()));
  ASSERT_FALSE(stream_devices->audio_device.has_value());
}

#if BUILDFLAG(IS_ANDROID)
TEST_F(MediaStreamDevicesControllerTest, RequestBlockedWhenTabIsHidden) {
  // Hide the RenderWidgetHostView.
  web_contents_->WasHidden();

  // We expect that RequestPermissionsFromCurrentDocument will be called with
  // empty permissions (since they are blocked early).
  auto* mock_permission_controller = static_cast<MockPermissionController*>(
      browser_context_.GetPermissionController());
  EXPECT_CALL(
      *mock_permission_controller,
      RequestPermissionsFromCurrentDocument(
          _,
          testing::Field(&content::PermissionRequestDescription::permissions,
                         testing::IsEmpty()),
          _))
      .WillOnce(
          [](auto*, auto,
             base::OnceCallback<void(
                 const std::vector<content::PermissionResult>&)> callback) {
            std::move(callback).Run({});
          });

  // Try to request video capture.
  auto [result, stream_devices] = RequestPermissionsRaw(
      blink::MediaStreamRequestType::MEDIA_GENERATE_STREAM,
      /*requested_audio_device_ids=*/{},
      /*requested_video_device_ids=*/
      GetIds(enumerator_.GetVideoCaptureDevices(), 0),
      blink::mojom::MediaStreamType::NO_SERVICE,
      blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);

  EXPECT_EQ(
      result,
      blink::mojom::MediaStreamRequestResult::ANDROID_CANT_REQUEST_PERMISSION);
}

TEST_F(MediaStreamDevicesControllerTest,
       RequestAllowedWhenTabIsHiddenButHasPictureInPictureDocument) {
  // Hide the RenderWidgetHostView.
  web_contents_->WasHidden();

  // Mark that WebContents has a Picture-in-Picture document.
  content::WebContentsTester::For(web_contents_)
      ->SetHasPictureInPictureDocument(true);

  // Try to request video capture.
  auto [result, stream_devices] =
      MakeRequest(blink::MediaStreamRequestType::MEDIA_GENERATE_STREAM,
                  /*requested_audio_device_ids=*/{},
                  /*requested_video_device_ids=*/
                  GetIds(enumerator_.GetVideoCaptureDevices(), 0),
                  /*request_audio=*/false, /*request_video=*/true);

  // It should succeed (or get OK).
  EXPECT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
}

TEST_F(MediaStreamDevicesControllerTest,
       FallbackToPrimaryMainFrameWhenRenderFrameHostChangesOnAndroid) {
  content::WebContentsTester::For(web_contents_)
      ->NavigateAndCommit(origin_.GetURL());
  render_frame_host_ = web_contents_->GetPrimaryMainFrame();
  render_frame_host_id_ = render_frame_host_->GetGlobalId();

  auto* mock_permission_controller = static_cast<MockPermissionController*>(
      browser_context_.GetPermissionController());
  ON_CALL(*mock_permission_controller, GetPermissionResultForCurrentDocument)
      .WillByDefault([](const blink::mojom::PermissionDescriptorPtr&,
                        content::RenderFrameHost*) {
        return content::PermissionResult{
            content::PermissionStatus::GRANTED,
            content::PermissionStatusSource::UNSPECIFIED,
        };
      });

  base::OnceCallback<void(const std::vector<content::PermissionResult>&)>
      saved_callback;
  EXPECT_CALL(
      *mock_permission_controller,
      RequestPermissionsFromCurrentDocument(render_frame_host_.get(), _, _))
      .WillOnce(
          [&](auto*, auto,
              base::OnceCallback<void(
                  const std::vector<content::PermissionResult>&)> callback) {
            saved_callback = std::move(callback);
          });

  base::test::TestFuture<blink::mojom::MediaStreamRequestResult,
                         blink::mojom::StreamDevicesPtr>
      result_future;
  webrtc::MediaStreamDevicesController::RequestPermissions(
      content::MediaStreamRequest{
          /*render_process_id=*/render_frame_host_id_.child_id.GetUnsafeValue(),
          /*render_frame_id=*/render_frame_host_id_.frame_routing_id,
          /*page_request_id=*/0,
          /*url_origin=*/origin_,
          /*user_gesture=*/false,
          blink::MediaStreamRequestType::MEDIA_GENERATE_STREAM,
          /*requested_audio_device_ids=*/{},
          /*requested_video_device_ids=*/
          GetIds(enumerator_.GetVideoCaptureDevices(), 0),
          blink::mojom::MediaStreamType::NO_SERVICE,
          blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE,
          /*disable_local_echo=*/false,
          /*request_pan_tilt_zoom_permission=*/false,
          /*captured_surface_control_active=*/false,
      },
      &enumerator_,
      base::BindLambdaForTesting(
          [&](const blink::mojom::StreamDevicesSet& stream_devices_set,
              blink::mojom::MediaStreamRequestResult result,
              bool blocked_by_permissions_policy, ContentSetting audio_setting,
              ContentSetting video_setting) {
            result_future.SetValue(
                result, stream_devices_set.stream_devices.empty()
                            ? blink::mojom::StreamDevicesPtr()
                            : stream_devices_set.stream_devices[0]->Clone());
          }));

  // Re-navigate to the same URL to simulate a new RenderFrameHost replacing the
  // old one.
  content::WebContentsTester::For(web_contents_)
      ->NavigateAndCommit(origin_.GetURL());

  std::move(saved_callback)
      .Run({content::PermissionResult(
          content::PermissionStatus::GRANTED,
          content::PermissionStatusSource::UNSPECIFIED)});

  auto [result, stream_devices] = result_future.Take();
  EXPECT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
  EXPECT_TRUE(stream_devices->video_device.has_value());
}

TEST_F(MediaStreamDevicesControllerTest,
       FallbackRejectedWhenNavigatedAwayOnAndroid) {
  content::WebContentsTester::For(web_contents_)
      ->NavigateAndCommit(origin_.GetURL());
  render_frame_host_ = web_contents_->GetPrimaryMainFrame();
  render_frame_host_id_ = render_frame_host_->GetGlobalId();

  auto* mock_permission_controller = static_cast<MockPermissionController*>(
      browser_context_.GetPermissionController());
  ON_CALL(*mock_permission_controller, GetPermissionResultForCurrentDocument)
      .WillByDefault([](const blink::mojom::PermissionDescriptorPtr&,
                        content::RenderFrameHost*) {
        return content::PermissionResult{
            content::PermissionStatus::GRANTED,
            content::PermissionStatusSource::UNSPECIFIED,
        };
      });

  base::OnceCallback<void(const std::vector<content::PermissionResult>&)>
      saved_callback;
  EXPECT_CALL(
      *mock_permission_controller,
      RequestPermissionsFromCurrentDocument(render_frame_host_.get(), _, _))
      .WillOnce(
          [&](auto*, auto,
              base::OnceCallback<void(
                  const std::vector<content::PermissionResult>&)> callback) {
            saved_callback = std::move(callback);
          });

  base::test::TestFuture<blink::mojom::MediaStreamRequestResult,
                         blink::mojom::StreamDevicesPtr>
      result_future;
  webrtc::MediaStreamDevicesController::RequestPermissions(
      content::MediaStreamRequest{
          /*render_process_id=*/render_frame_host_id_.child_id.GetUnsafeValue(),
          /*render_frame_id=*/render_frame_host_id_.frame_routing_id,
          /*page_request_id=*/0,
          /*url_origin=*/origin_,
          /*user_gesture=*/false,
          blink::MediaStreamRequestType::MEDIA_GENERATE_STREAM,
          /*requested_audio_device_ids=*/{},
          /*requested_video_device_ids=*/
          GetIds(enumerator_.GetVideoCaptureDevices(), 0),
          blink::mojom::MediaStreamType::NO_SERVICE,
          blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE,
          /*disable_local_echo=*/false,
          /*request_pan_tilt_zoom_permission=*/false,
          /*captured_surface_control_active=*/false,
      },
      &enumerator_,
      base::BindLambdaForTesting(
          [&](const blink::mojom::StreamDevicesSet& stream_devices_set,
              blink::mojom::MediaStreamRequestResult result,
              bool blocked_by_permissions_policy, ContentSetting audio_setting,
              ContentSetting video_setting) {
            result_future.SetValue(
                result, stream_devices_set.stream_devices.empty()
                            ? blink::mojom::StreamDevicesPtr()
                            : stream_devices_set.stream_devices[0]->Clone());
          }));

  // Navigate to a different page within the same origin.
  content::WebContentsTester::For(web_contents_)
      ->NavigateAndCommit(GURL("https://stuff.com/other"));

  std::move(saved_callback)
      .Run({content::PermissionResult(
          content::PermissionStatus::GRANTED,
          content::PermissionStatusSource::UNSPECIFIED)});

  auto [result, stream_devices] = result_future.Take();
  EXPECT_EQ(result, blink::mojom::MediaStreamRequestResult::
                        FAILED_DUE_TO_SHUTDOWN_CONTROLLER_DESTRUCTOR);
  EXPECT_FALSE(stream_devices);
}
#endif
