Commit 4e31d46a authored by Andy Paicu's avatar Andy Paicu Committed by Commit Bot

Immediately stop media streams if users revokes consent

Since camera and microphone are particularly sensitive permissions we
should be viligent and stop streams immediately when the user revokes
the permission.

This CL uses the permission controller subscription functionality to
listen to permission changes on the relevant origins. Each DeviceRequest
will have up to 2 permission subscriptions (1 audio, 1 video). When
a permission is changed to non-Granted the specific DeviceRequest is
canceled. Subscriptions are removed when the DeviceRequest is deleted.

Because MediaStreamManager is an IO thread class but
PermissionController is a UI thread class, this CL has to do a bit of
thread hoping:

FinalizeStream(IO) > SubscribeToPermissionControllerOnUIThread(UI) >
SetPermissionSubscriptionIDs(IO) >(maybe)>
UnsubscribeFromPermissionControllerOnUIThread(UI)

DeleteRequest(IO) > UnsubscribeFromPermissionControllerOnUIThread(UI)

Bug: 1116435

Change-Id: I6649125e028d607eeb7ba5701b710a715addf6f2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2352790
Commit-Queue: Andy Paicu <andypaicu@chromium.org>
Reviewed-by: default avatarGuido Urdaneta <guidou@chromium.org>
Cr-Commit-Position: refs/heads/master@{#798564}
parent dfb55128
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#include "base/command_line.h" #include "base/command_line.h"
#include "base/files/file_util.h" #include "base/files/file_util.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/run_loop.h"
#include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" #include "chrome/browser/media/webrtc/webrtc_browsertest_base.h"
...@@ -65,7 +66,9 @@ class MediaStreamPermissionTest : public WebRtcTestBase { ...@@ -65,7 +66,9 @@ class MediaStreamPermissionTest : public WebRtcTestBase {
private: private:
content::WebContents* LoadTestPageInBrowser(Browser* browser) { content::WebContents* LoadTestPageInBrowser(Browser* browser) {
EXPECT_TRUE(embedded_test_server()->Start()); if (!embedded_test_server()->Started()) {
EXPECT_TRUE(embedded_test_server()->Start());
}
// Uses the default server. // Uses the default server.
GURL url = test_page_url(); GURL url = test_page_url();
...@@ -76,12 +79,6 @@ class MediaStreamPermissionTest : public WebRtcTestBase { ...@@ -76,12 +79,6 @@ class MediaStreamPermissionTest : public WebRtcTestBase {
return browser->tab_strip_model()->GetActiveWebContents(); return browser->tab_strip_model()->GetActiveWebContents();
} }
// Dummy callback for when we deny the current request directly.
static void OnMediaStreamResponse(
const blink::MediaStreamDevices& devices,
blink::mojom::MediaStreamRequestResult result,
std::unique_ptr<content::MediaStreamUI> ui) {}
DISALLOW_COPY_AND_ASSIGN(MediaStreamPermissionTest); DISALLOW_COPY_AND_ASSIGN(MediaStreamPermissionTest);
}; };
...@@ -169,3 +166,50 @@ IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest, ...@@ -169,3 +166,50 @@ IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest,
EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAccept( EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAccept(
tab_contents, kAudioOnlyCallConstraints)); tab_contents, kAudioOnlyCallConstraints));
} }
IN_PROC_BROWSER_TEST_F(MediaStreamPermissionTest,
DenyingPermissionStopsStreamWhenRelevant) {
struct {
std::string constraints;
ContentSettingsType setting_to_clear;
bool should_video_stop;
} kTests[] = {
{kAudioVideoCallConstraints, ContentSettingsType::MEDIASTREAM_CAMERA,
true},
{kAudioVideoCallConstraints, ContentSettingsType::MEDIASTREAM_MIC, true},
{kVideoOnlyCallConstraints, ContentSettingsType::MEDIASTREAM_CAMERA,
true},
{kVideoOnlyCallConstraints, ContentSettingsType::MEDIASTREAM_MIC, false},
};
HostContentSettingsMap* settings_map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
for (const auto& kTest : kTests) {
content::WebContents* tab_contents = LoadTestPageInTab();
EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAcceptIfPrompted(
tab_contents, kTest.constraints));
StartDetectingVideo(tab_contents, "local-view");
EXPECT_TRUE(WaitForVideoToPlay(tab_contents));
settings_map->ClearSettingsForOneType(kTest.setting_to_clear);
// Let all the cross-thread tasks do their work.
base::RunLoop().RunUntilIdle();
StartDetectingVideo(tab_contents, "local-view");
if (kTest.should_video_stop) {
EXPECT_TRUE(WaitForVideoToStop(tab_contents));
} else {
EXPECT_TRUE(WaitForVideoToPlay(tab_contents));
}
// Clean up settings for the following tests.
settings_map->ClearSettingsForOneType(ContentSettingsType::MEDIASTREAM_MIC);
settings_map->ClearSettingsForOneType(
ContentSettingsType::MEDIASTREAM_CAMERA);
}
}
...@@ -473,6 +473,14 @@ bool WebRtcTestBase::WaitForVideoToPlay( ...@@ -473,6 +473,14 @@ bool WebRtcTestBase::WaitForVideoToPlay(
return is_video_playing; return is_video_playing;
} }
bool WebRtcTestBase::WaitForVideoToStop(
content::WebContents* tab_contents) const {
bool is_video_stopped =
test::PollingWaitUntil("isVideoStopped()", "video-stopped", tab_contents);
EXPECT_TRUE(is_video_stopped);
return is_video_stopped;
}
std::string WebRtcTestBase::GetStreamSize( std::string WebRtcTestBase::GetStreamSize(
content::WebContents* tab_contents, content::WebContents* tab_contents,
const std::string& video_element) const { const std::string& video_element) const {
......
...@@ -182,7 +182,11 @@ class WebRtcTestBase : public InProcessBrowserTest { ...@@ -182,7 +182,11 @@ class WebRtcTestBase : public InProcessBrowserTest {
// make that work). Looks at a 320x240 area of the target video tag. // make that work). Looks at a 320x240 area of the target video tag.
void StartDetectingVideo(content::WebContents* tab_contents, void StartDetectingVideo(content::WebContents* tab_contents,
const std::string& video_element) const; const std::string& video_element) const;
// Wait for a video to start/stop playing. StartDetectingVideo must have
// been called already.
bool WaitForVideoToPlay(content::WebContents* tab_contents) const; bool WaitForVideoToPlay(content::WebContents* tab_contents) const;
bool WaitForVideoToStop(content::WebContents* tab_contents) const;
// Returns the stream size as a string on the format <width>x<height>. // Returns the stream size as a string on the format <width>x<height>.
std::string GetStreamSize(content::WebContents* tab_contents, std::string GetStreamSize(content::WebContents* tab_contents,
......
...@@ -970,7 +970,7 @@ if (!is_android) { ...@@ -970,7 +970,7 @@ if (!is_android) {
"../browser/media/test_license_server_config.h", "../browser/media/test_license_server_config.h",
"../browser/media/unified_autoplay_browsertest.cc", "../browser/media/unified_autoplay_browsertest.cc",
"../browser/media/webrtc/media_stream_devices_controller_browsertest.cc", "../browser/media/webrtc/media_stream_devices_controller_browsertest.cc",
"../browser/media/webrtc/media_stream_infobar_browsertest.cc", "../browser/media/webrtc/media_stream_permission_browsertest.cc",
"../browser/media/webrtc/test_stats_dictionary.cc", "../browser/media/webrtc/test_stats_dictionary.cc",
"../browser/media/webrtc/test_stats_dictionary.h", "../browser/media/webrtc/test_stats_dictionary.h",
"../browser/media/webrtc/test_stats_dictionary_unittest.cc", "../browser/media/webrtc/test_stats_dictionary_unittest.cc",
......
...@@ -78,6 +78,25 @@ function isVideoPlaying() { ...@@ -78,6 +78,25 @@ function isVideoPlaying() {
returnToTest('video-not-playing'); returnToTest('video-not-playing');
} }
/**
* Checks if the video has stopped
*
* @return {string} video-stopped or video-not-stopped.
*/
function isVideoStopped() {
// Video is considered to be stopped if the last 5 fingerprints are the same.
// We only check for rough equality though to account for rounding errors.
if (gFingerprints.length < 5)
returnToTest('video-not-stopped');
if (allElementsRoughlyEqualTo_(gFingerprints.slice(-5),
gFingerprints[gFingerprints.length - 1])) {
returnToTest('video-stopped');
}
returnToTest('video-not-stopped');
}
/** /**
* Queries for the stream size (not necessarily the size at which the video tag * Queries for the stream size (not necessarily the size at which the video tag
* is rendered). * is rendered).
......
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
#include "content/browser/gpu/gpu_process_host.h" #include "content/browser/gpu/gpu_process_host.h"
#include "content/browser/media/capture/desktop_capture_device_uma_types.h" #include "content/browser/media/capture/desktop_capture_device_uma_types.h"
#include "content/browser/media/media_devices_permission_checker.h" #include "content/browser/media/media_devices_permission_checker.h"
#include "content/browser/permissions/permission_controller_impl.h"
#include "content/browser/renderer_host/media/audio_input_device_manager.h" #include "content/browser/renderer_host/media/audio_input_device_manager.h"
#include "content/browser/renderer_host/media/audio_service_listener.h" #include "content/browser/renderer_host/media/audio_service_listener.h"
#include "content/browser/renderer_host/media/in_process_video_capture_provider.h" #include "content/browser/renderer_host/media/in_process_video_capture_provider.h"
...@@ -667,6 +668,10 @@ class MediaStreamManager::DeviceRequest { ...@@ -667,6 +668,10 @@ class MediaStreamManager::DeviceRequest {
std::string tab_capture_device_id; std::string tab_capture_device_id;
int audio_subscription_id = PermissionControllerImpl::kNoPendingOperation;
int video_subscription_id = PermissionControllerImpl::kNoPendingOperation;
private: private:
std::vector<MediaRequestState> state_; std::vector<MediaRequestState> state_;
std::unique_ptr<MediaStreamRequest> ui_request_; std::unique_ptr<MediaStreamRequest> ui_request_;
...@@ -1319,7 +1324,18 @@ void MediaStreamManager::DeleteRequest(const std::string& label) { ...@@ -1319,7 +1324,18 @@ void MediaStreamManager::DeleteRequest(const std::string& label) {
for (auto request_it = requests_.begin(); request_it != requests_.end(); for (auto request_it = requests_.begin(); request_it != requests_.end();
++request_it) { ++request_it) {
if (request_it->first == label) { if (request_it->first == label) {
// Clean up permission controller subscription.
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MediaStreamManager::
UnsubscribeFromPermissionControllerOnUIThread,
request_it->second->requesting_process_id,
request_it->second->requesting_frame_id,
request_it->second->audio_subscription_id,
request_it->second->video_subscription_id));
requests_.erase(request_it); requests_.erase(request_it);
return; return;
} }
} }
...@@ -1762,6 +1778,19 @@ void MediaStreamManager::FinalizeGenerateStream(const std::string& label, ...@@ -1762,6 +1778,19 @@ void MediaStreamManager::FinalizeGenerateStream(const std::string& label,
NOTREACHED(); NOTREACHED();
} }
// Subscribe to follow permission changes in order to close streams when the
// user denies mic/camera.
// It is safe to bind base::Unretained(this) because MediaStreamManager is
// owned by BrowserMainLoop.
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&MediaStreamManager::SubscribeToPermissionControllerOnUIThread,
base::Unretained(this), label, request->requesting_process_id,
request->requesting_frame_id, request->requester_id,
request->page_request_id, audio_devices.size() > 0,
video_devices.size() > 0, request->salt_and_origin.origin.GetURL()));
// It is safe to bind base::Unretained(this) because MediaStreamManager is // It is safe to bind base::Unretained(this) because MediaStreamManager is
// owned by BrowserMainLoop and so outlives the IO thread. // owned by BrowserMainLoop and so outlives the IO thread.
GetUIThreadTaskRunner({})->PostTaskAndReplyWithResult( GetUIThreadTaskRunner({})->PostTaskAndReplyWithResult(
...@@ -2614,4 +2643,144 @@ void MediaStreamManager::OnStreamStarted(const std::string& label) { ...@@ -2614,4 +2643,144 @@ void MediaStreamManager::OnStreamStarted(const std::string& label) {
} }
} }
// static
PermissionControllerImpl* MediaStreamManager::GetPermissionController(
int requesting_process_id,
int requesting_frame_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
RenderFrameHost* rfh =
RenderFrameHost::FromID(requesting_process_id, requesting_frame_id);
if (!rfh)
return nullptr;
return PermissionControllerImpl::FromBrowserContext(rfh->GetBrowserContext());
}
void MediaStreamManager::SubscribeToPermissionControllerOnUIThread(
const std::string& label,
int requesting_process_id,
int requesting_frame_id,
int requester_id,
int page_request_id,
bool is_audio_request,
bool is_video_request,
const GURL& origin) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
PermissionControllerImpl* controller =
GetPermissionController(requesting_process_id, requesting_frame_id);
if (!controller)
return;
int audio_subscription_id = PermissionControllerImpl::kNoPendingOperation;
int video_subscription_id = PermissionControllerImpl::kNoPendingOperation;
if (is_audio_request) {
// It is safe to bind base::Unretained(this) because MediaStreamManager is
// owned by BrowserMainLoop.
audio_subscription_id = controller->SubscribePermissionStatusChange(
PermissionType::AUDIO_CAPTURE,
RenderFrameHost::FromID(requesting_process_id, requesting_frame_id),
origin,
base::BindRepeating(&MediaStreamManager::PermissionChangedCallback,
base::Unretained(this), requesting_process_id,
requesting_frame_id, requester_id,
page_request_id));
}
if (is_video_request) {
// It is safe to bind base::Unretained(this) because MediaStreamManager is
// owned by BrowserMainLoop.
video_subscription_id = controller->SubscribePermissionStatusChange(
PermissionType::VIDEO_CAPTURE,
RenderFrameHost::FromID(requesting_process_id, requesting_frame_id),
origin,
base::BindRepeating(&MediaStreamManager::PermissionChangedCallback,
base::Unretained(this), requesting_process_id,
requesting_frame_id, requester_id,
page_request_id));
}
// It is safe to bind base::Unretained(this) because MediaStreamManager is
// owned by BrowserMainLoop.
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MediaStreamManager::SetPermissionSubscriptionIDs,
base::Unretained(this), label, requesting_process_id,
requesting_frame_id, audio_subscription_id,
video_subscription_id));
}
void MediaStreamManager::SetPermissionSubscriptionIDs(
const std::string& label,
int requesting_process_id,
int requesting_frame_id,
int audio_subscription_id,
int video_subscription_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DeviceRequest* const request = FindRequest(label);
if (!request) {
// Something happened with the request while the permission subscription was
// created, unsubscribe to clean up.
// It is safe to bind base::Unretained(this) because MediaStreamManager is
// owned by BrowserMainLoop.
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&MediaStreamManager::UnsubscribeFromPermissionControllerOnUIThread,
requesting_process_id, requesting_frame_id, audio_subscription_id,
video_subscription_id));
return;
}
request->audio_subscription_id = audio_subscription_id;
request->video_subscription_id = video_subscription_id;
}
// static
void MediaStreamManager::UnsubscribeFromPermissionControllerOnUIThread(
int requesting_process_id,
int requesting_frame_id,
int audio_subscription_id,
int video_subscription_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
PermissionControllerImpl* controller =
GetPermissionController(requesting_process_id, requesting_frame_id);
if (!controller)
return;
controller->UnsubscribePermissionStatusChange(audio_subscription_id);
controller->UnsubscribePermissionStatusChange(video_subscription_id);
}
void MediaStreamManager::PermissionChangedCallback(
int requesting_process_id,
int requesting_frame_id,
int requester_id,
int page_request_id,
blink::mojom::PermissionStatus status) {
if (status == blink::mojom::PermissionStatus::GRANTED)
return;
if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
// It is safe to bind base::Unretained(this) because MediaStreamManager is
// owned by BrowserMainLoop.
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MediaStreamManager::PermissionChangedCallback,
base::Unretained(this), requesting_process_id,
requesting_frame_id, requester_id, page_request_id,
status));
return;
}
CancelRequest(requesting_process_id, requesting_frame_id, requester_id,
page_request_id);
}
} // namespace content } // namespace content
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
#include "third_party/blink/public/common/mediastream/media_stream_controls.h" #include "third_party/blink/public/common/mediastream/media_stream_controls.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h" #include "third_party/blink/public/common/mediastream/media_stream_request.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h" #include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
namespace media { namespace media {
class AudioSystem; class AudioSystem;
...@@ -72,6 +73,7 @@ class FakeMediaStreamUIProxy; ...@@ -72,6 +73,7 @@ class FakeMediaStreamUIProxy;
class MediaStreamUIProxy; class MediaStreamUIProxy;
class VideoCaptureManager; class VideoCaptureManager;
class VideoCaptureProvider; class VideoCaptureProvider;
class PermissionControllerImpl;
// MediaStreamManager is used to generate and close new media devices, not to // MediaStreamManager is used to generate and close new media devices, not to
// start the media flow. The classes requesting new media streams are answered // start the media flow. The classes requesting new media streams are answered
...@@ -534,6 +536,48 @@ class CONTENT_EXPORT MediaStreamManager ...@@ -534,6 +536,48 @@ class CONTENT_EXPORT MediaStreamManager
// Activate the specified tab and bring it to the front. // Activate the specified tab and bring it to the front.
void ActivateTabOnUIThread(const DesktopMediaID source); void ActivateTabOnUIThread(const DesktopMediaID source);
// Get the permission controller for a particular RFH. Must be called on the
// UI thread.
static PermissionControllerImpl* GetPermissionController(
int requesting_process_id,
int requesting_frame_id);
// Subscribe to the permission controller in order to monitor camera/mic
// permission updates for a particular DeviceRequest. All the additional
// information is needed because `FindRequest` can't be called on the UI
// thread.
void SubscribeToPermissionControllerOnUIThread(const std::string& label,
int requesting_process_id,
int requesting_frame_id,
int requester_id,
int page_request_id,
bool is_audio_request,
bool is_video_request,
const GURL& origin);
// Store the subscription ids on a DeviceRequest in order to allow
// unsubscribing when the request is deleted.
void SetPermissionSubscriptionIDs(const std::string& label,
int requesting_process_id,
int requesting_frame_id,
int audio_subscription_id,
int video_subscription_id);
// Unsubscribe from following permission updates for the two specified
// subscription IDs. Called when a request is deleted.
static void UnsubscribeFromPermissionControllerOnUIThread(
int requesting_process_id,
int requesting_frame_id,
int audio_subscription_id,
int video_subscription_id);
// Callback that the PermissionController calls when a permission is updated.
void PermissionChangedCallback(int requesting_process_id,
int requesting_frame_id,
int requester_id,
int page_request_id,
blink::mojom::PermissionStatus status);
media::AudioSystem* const audio_system_; // not owned media::AudioSystem* const audio_system_; // not owned
scoped_refptr<AudioInputDeviceManager> audio_input_device_manager_; scoped_refptr<AudioInputDeviceManager> audio_input_device_manager_;
scoped_refptr<VideoCaptureManager> video_capture_manager_; scoped_refptr<VideoCaptureManager> video_capture_manager_;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment