blob: 2e92ce49363df691b7687ea102820834b49e8580 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/media/media_stream_ui_proxy.h"
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/run_loop.h"
#include "base/task/post_task.h"
#include "build/build_config.h"
#include "content/browser/frame_host/render_frame_host_delegate.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/media_stream_request.h"
#include "content/public/test/browser_task_environment.h"
#include "content/test/test_render_frame_host.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/rect.h"
#include "url/gurl.h"
#include "url/origin.h"
using testing::_;
using testing::Return;
using testing::SaveArg;
namespace content {
namespace {
class MockRenderFrameHostDelegate : public RenderFrameHostDelegate {
public:
void RequestMediaAccessPermission(const MediaStreamRequest& request,
MediaResponseCallback callback) {
return RequestMediaAccessPermission(request, &callback);
}
MOCK_METHOD2(RequestMediaAccessPermission,
void(const MediaStreamRequest& request,
MediaResponseCallback* callback));
MOCK_METHOD3(CheckMediaAccessPermission,
bool(RenderFrameHost* render_frame_host,
const url::Origin& security_origin,
blink::mojom::MediaStreamType type));
};
class MockResponseCallback {
public:
MOCK_METHOD2(OnAccessRequestResponse,
void(const blink::MediaStreamDevices& devices,
blink::mojom::MediaStreamRequestResult result));
MOCK_METHOD1(OnCheckResponse, void(bool have_access));
};
class MockMediaStreamUI : public MediaStreamUI {
public:
gfx::NativeViewId OnStarted(base::OnceClosure stop,
MediaStreamUI::SourceCallback source) override {
// gmock cannot handle move-only types:
return MockOnStarted(base::AdaptCallbackForRepeating(std::move(stop)),
source);
}
MOCK_METHOD2(MockOnStarted,
gfx::NativeViewId(base::RepeatingClosure stop,
MediaStreamUI::SourceCallback source));
};
class MockStopStreamHandler {
public:
MOCK_METHOD0(OnStop, void());
MOCK_METHOD1(OnWindowId, void(gfx::NativeViewId window_id));
};
class MockChangeSourceStreamHandler {
public:
MOCK_METHOD1(OnChangeSource, void(const DesktopMediaID& media_id));
};
} // namespace
class MediaStreamUIProxyTest : public testing::Test {
public:
MediaStreamUIProxyTest() {
proxy_ = MediaStreamUIProxy::CreateForTests(&delegate_);
}
~MediaStreamUIProxyTest() override {
proxy_.reset();
base::RunLoop().RunUntilIdle();
}
protected:
BrowserTaskEnvironment task_environment_;
MockRenderFrameHostDelegate delegate_;
MockResponseCallback response_callback_;
std::unique_ptr<MediaStreamUIProxy> proxy_;
};
MATCHER_P(SameRequest, expected, "") {
return
expected->render_process_id == arg.render_process_id &&
expected->render_frame_id == arg.render_frame_id &&
expected->security_origin == arg.security_origin &&
expected->request_type == arg.request_type &&
expected->requested_audio_device_id == arg.requested_audio_device_id &&
expected->requested_video_device_id == arg.requested_video_device_id &&
expected->audio_type == arg.audio_type &&
expected->video_type == arg.video_type;
}
TEST_F(MediaStreamUIProxyTest, Deny) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(),
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
blink::MediaStreamDevices devices;
std::move(callback).Run(devices, blink::mojom::MediaStreamRequestResult::OK,
std::unique_ptr<MediaStreamUI>());
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(response.empty());
}
TEST_F(MediaStreamUIProxyTest, AcceptAndStart) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(),
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
blink::MediaStreamDevices devices;
devices.push_back(blink::MediaStreamDevice(
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, "Mic", "Mic"));
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _)).WillOnce(Return(0));
std::move(callback).Run(devices, blink::mojom::MediaStreamRequestResult::OK,
std::move(ui));
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(response.empty());
proxy_->OnStarted(base::OnceClosure(), MediaStreamUI::SourceCallback(),
MediaStreamUIProxy::WindowIdCallback());
base::RunLoop().RunUntilIdle();
}
// Verify that the proxy can be deleted before the request is processed.
TEST_F(MediaStreamUIProxyTest, DeleteBeforeAccepted) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(),
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
proxy_.reset();
blink::MediaStreamDevices devices;
std::unique_ptr<MediaStreamUI> ui;
std::move(callback).Run(devices, blink::mojom::MediaStreamRequestResult::OK,
std::move(ui));
}
TEST_F(MediaStreamUIProxyTest, StopFromUI) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(),
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
base::Closure stop_callback;
blink::MediaStreamDevices devices;
devices.push_back(blink::MediaStreamDevice(
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, "Mic", "Mic"));
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _))
.WillOnce(testing::DoAll(SaveArg<0>(&stop_callback), Return(0)));
std::move(callback).Run(devices, blink::mojom::MediaStreamRequestResult::OK,
std::move(ui));
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(response.empty());
MockStopStreamHandler stop_handler;
proxy_->OnStarted(base::BindOnce(&MockStopStreamHandler::OnStop,
base::Unretained(&stop_handler)),
MediaStreamUI::SourceCallback(),
MediaStreamUIProxy::WindowIdCallback());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(stop_callback.is_null());
EXPECT_CALL(stop_handler, OnStop());
stop_callback.Run();
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaStreamUIProxyTest, WindowIdCallbackCalled) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), blink::mojom::MediaStreamType::NO_SERVICE,
blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
const int kWindowId = 1;
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _)).WillOnce(Return(kWindowId));
std::move(callback).Run(blink::MediaStreamDevices(),
blink::mojom::MediaStreamRequestResult::OK,
std::move(ui));
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _));
MockStopStreamHandler handler;
EXPECT_CALL(handler, OnWindowId(kWindowId));
proxy_->OnStarted(base::BindOnce(&MockStopStreamHandler::OnStop,
base::Unretained(&handler)),
MediaStreamUI::SourceCallback(),
base::BindOnce(&MockStopStreamHandler::OnWindowId,
base::Unretained(&handler)));
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaStreamUIProxyTest, ChangeSourceFromUI) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(),
blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
MediaStreamUI::SourceCallback source_callback;
blink::MediaStreamDevices devices;
devices.push_back(blink::MediaStreamDevice(
blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE,
"fake_desktop_video_device", "Fake Desktop Video Device"));
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _))
.WillOnce(testing::DoAll(SaveArg<1>(&source_callback), Return(0)));
std::move(callback).Run(devices, blink::mojom::MediaStreamRequestResult::OK,
std::move(ui));
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(response.empty());
MockStopStreamHandler stop_handler;
MockChangeSourceStreamHandler source_handler;
proxy_->OnStarted(
base::BindOnce(&MockStopStreamHandler::OnStop,
base::Unretained(&stop_handler)),
base::BindRepeating(&MockChangeSourceStreamHandler::OnChangeSource,
base::Unretained(&source_handler)),
MediaStreamUIProxy::WindowIdCallback());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(source_callback.is_null());
EXPECT_CALL(source_handler, OnChangeSource(DesktopMediaID()));
source_callback.Run(DesktopMediaID());
base::RunLoop().RunUntilIdle();
}
// Basic tests for feature policy checks through the MediaStreamUIProxy. These
// tests are not meant to cover every edge case as the FeaturePolicy class
// itself is tested thoroughly in feature_policy_unittest.cc and in
// render_frame_host_feature_policy_unittest.cc.
class MediaStreamUIProxyFeaturePolicyTest
: public RenderViewHostImplTestHarness {
public:
void SetUp() override {
RenderViewHostImplTestHarness::SetUp();
NavigateAndCommit(GURL("https://example.com"));
}
protected:
// The header policy should only be set once on page load, so we refresh the
// page to simulate that.
void RefreshPageAndSetHeaderPolicy(
blink::mojom::FeaturePolicyFeature feature) {
NavigateAndCommit(main_rfh()->GetLastCommittedURL());
std::vector<url::Origin> empty_allowlist;
RenderFrameHostTester::For(main_rfh())
->SimulateFeaturePolicyHeader(feature, empty_allowlist);
}
void GetResultForRequest(std::unique_ptr<MediaStreamRequest> request,
blink::MediaStreamDevices* devices_out,
blink::mojom::MediaStreamRequestResult* result_out) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
base::PostTask(
FROM_HERE, {BrowserThread::IO},
base::BindOnce(
&MediaStreamUIProxyFeaturePolicyTest::GetResultForRequestOnIOThread,
base::Unretained(this), std::move(request)));
run_loop.Run();
*devices_out = devices_;
*result_out = result_;
}
std::unique_ptr<MediaStreamRequest> CreateRequest(
RenderFrameHost* rfh,
blink::mojom::MediaStreamType mic_type,
blink::mojom::MediaStreamType cam_type) {
return std::make_unique<MediaStreamRequest>(
rfh->GetProcess()->GetID(), rfh->GetRoutingID(), 0,
rfh->GetLastCommittedURL(), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), mic_type, cam_type, false);
}
private:
class TestRFHDelegate : public RenderFrameHostDelegate {
void RequestMediaAccessPermission(const MediaStreamRequest& request,
MediaResponseCallback callback) override {
blink::MediaStreamDevices devices;
if (request.audio_type ==
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE) {
devices.push_back(blink::MediaStreamDevice(
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, "Mic", "Mic"));
}
if (request.video_type ==
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE) {
devices.push_back(blink::MediaStreamDevice(
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, "Camera",
"Camera"));
}
auto ui = std::make_unique<MockMediaStreamUI>();
std::move(callback).Run(
devices, blink::mojom::MediaStreamRequestResult::OK, std::move(ui));
}
};
void GetResultForRequestOnIOThread(
std::unique_ptr<MediaStreamRequest> request) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
proxy_ = MediaStreamUIProxy::CreateForTests(&delegate_);
proxy_->RequestAccess(
std::move(request),
base::BindOnce(
&MediaStreamUIProxyFeaturePolicyTest::FinishedGetResultOnIOThread,
base::Unretained(this)));
}
void FinishedGetResultOnIOThread(
const blink::MediaStreamDevices& devices,
blink::mojom::MediaStreamRequestResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
proxy_.reset();
base::PostTask(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&MediaStreamUIProxyFeaturePolicyTest::FinishedGetResult,
base::Unretained(this), devices, result));
}
void FinishedGetResult(const blink::MediaStreamDevices& devices,
blink::mojom::MediaStreamRequestResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
devices_ = devices;
result_ = result;
quit_closure_.Run();
}
// These should only be accessed on the UI thread.
blink::MediaStreamDevices devices_;
blink::mojom::MediaStreamRequestResult result_;
base::Closure quit_closure_;
// These should only be accessed on the IO thread.
TestRFHDelegate delegate_;
std::unique_ptr<MediaStreamUIProxy> proxy_;
};
TEST_F(MediaStreamUIProxyFeaturePolicyTest, FeaturePolicy) {
blink::MediaStreamDevices devices;
blink::mojom::MediaStreamRequestResult result;
// Default FP.
GetResultForRequest(
CreateRequest(main_rfh(),
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result);
ASSERT_EQ(2u, devices.size());
EXPECT_EQ(blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
devices[0].type);
EXPECT_EQ(blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE,
devices[1].type);
// Mic disabled.
RefreshPageAndSetHeaderPolicy(
blink::mojom::FeaturePolicyFeature::kMicrophone);
GetResultForRequest(
CreateRequest(main_rfh(),
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result);
ASSERT_EQ(1u, devices.size());
EXPECT_EQ(blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE,
devices[0].type);
// Camera disabled.
RefreshPageAndSetHeaderPolicy(blink::mojom::FeaturePolicyFeature::kCamera);
GetResultForRequest(
CreateRequest(main_rfh(),
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::mojom::MediaStreamRequestResult::OK, result);
ASSERT_EQ(1u, devices.size());
EXPECT_EQ(blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
devices[0].type);
// Camera disabled resulting in no devices being returned.
RefreshPageAndSetHeaderPolicy(blink::mojom::FeaturePolicyFeature::kCamera);
GetResultForRequest(
CreateRequest(main_rfh(), blink::mojom::MediaStreamType::NO_SERVICE,
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED, result);
ASSERT_EQ(0u, devices.size());
}
} // namespace content