Commit 63455546 authored by John Rummell's avatar John Rummell Committed by Commit Bot

EME: Check that output protection checks catch getDisplayMedia() recording

QueryOutputProtectionStatus() should report that a MediaRecorder is capturing
audio/video streams. This adds a test that sets up a MediaRecorder using
getDisplayMedia() to provide the stream, and verifies that the query does
detect it. ClearKey CDM updated to periodically query for changes, and if a
network link is detected, generate a keystatuseschange event with a dummy
key ID and status 'output-restricted'.

Bug: 856276
Test: new browser_test passes
Change-Id: I3f9f200dded6647ec9526d929b962b3ccfae1d7a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1894356
Commit-Queue: John Rummell <jrummell@chromium.org>
Reviewed-by: default avatarXiaohan Wang <xhwang@chromium.org>
Cr-Commit-Position: refs/heads/master@{#713670}
parent ae302f03
......@@ -61,8 +61,6 @@ const char kExternalClearKeyFileIOTestKeySystem[] =
"org.chromium.externalclearkey.fileiotest";
const char kExternalClearKeyInitializeFailKeySystem[] =
"org.chromium.externalclearkey.initializefail";
const char kExternalClearKeyOutputProtectionTestKeySystem[] =
"org.chromium.externalclearkey.outputprotectiontest";
const char kExternalClearKeyPlatformVerificationTestKeySystem[] =
"org.chromium.externalclearkey.platformverificationtest";
const char kExternalClearKeyCrashKeySystem[] =
......@@ -350,6 +348,17 @@ class ECKEncryptedMediaTest : public EncryptedMediaTestBase,
PlayCount::ONCE, expected_title);
}
void TestOutputProtection(bool create_recorder_before_media_keys) {
// Make sure the Clear Key CDM is properly registered in CdmRegistry.
EXPECT_TRUE(IsLibraryCdmRegistered(media::kClearKeyCdmGuid));
base::StringPairs query_params;
if (create_recorder_before_media_keys)
query_params.emplace_back("createMediaRecorderBeforeMediaKeys", "1");
RunMediaTestPage("eme_and_get_display_media.html", query_params,
kUnitTestSuccess, true);
}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
EncryptedMediaTestBase::SetUpCommandLine(command_line);
......@@ -358,6 +367,10 @@ class ECKEncryptedMediaTest : public EncryptedMediaTestBase,
command_line->AppendSwitchASCII(
switches::kOverrideEnabledCdmInterfaceVersion,
base::NumberToString(GetCdmInterfaceVersion()));
// The output protection tests create a MediaRecorder on a MediaStream,
// so this allows for a fake stream to be created.
command_line->AppendSwitch(switches::kUseFakeUIForMediaStream);
command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream);
}
};
......@@ -384,7 +397,6 @@ class ECKIncognitoEncryptedMediaTest : public EncryptedMediaTestBase {
command_line->AppendSwitch(switches::kIncognito);
}
};
#endif // BUILDFLAG(ENABLE_LIBRARY_CDMS)
// Tests encrypted media playback with a combination of parameters:
......@@ -795,11 +807,13 @@ IN_PROC_BROWSER_TEST_P(ECKEncryptedMediaTest, FileIOTest) {
TestNonPlaybackCases(kExternalClearKeyFileIOTestKeySystem, kUnitTestSuccess);
}
// TODO(xhwang): Investigate how to fake capturing activities to test the
// network link detection logic in OutputProtectionProxy.
IN_PROC_BROWSER_TEST_P(ECKEncryptedMediaTest, OutputProtectionTest) {
TestNonPlaybackCases(kExternalClearKeyOutputProtectionTestKeySystem,
kUnitTestSuccess);
// Output protection tests.
IN_PROC_BROWSER_TEST_P(ECKEncryptedMediaTest, OutputProtectionBeforeMediaKeys) {
TestOutputProtection(/*create_recorder_before_media_keys=*/true);
}
IN_PROC_BROWSER_TEST_P(ECKEncryptedMediaTest, OutputProtectionAfterMediaKeys) {
TestOutputProtection(/*create_recorder_before_media_keys=*/false);
}
IN_PROC_BROWSER_TEST_P(ECKEncryptedMediaTest, PlatformVerificationTest) {
......@@ -963,5 +977,4 @@ IN_PROC_BROWSER_TEST_F(ECKIncognitoEncryptedMediaTest, LoadSessionAfterClose) {
kExternalClearKeyKeySystem, query_params,
media::kEnded);
}
#endif // BUILDFLAG(ENABLE_LIBRARY_CDMS)
......@@ -14,6 +14,7 @@
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
......@@ -501,7 +502,7 @@ void ClearKeyCdm::OnUpdateSuccess(uint32_t promise_id,
// 100 years after 01 January 1970 UTC.
expiration = 3153600000.0; // 100 * 365 * 24 * 60 * 60;
if (!has_set_renewal_timer_) {
if (!has_set_timer_) {
// Make sure the CDM can get time and sleep if necessary.
constexpr auto kSleepDuration = base::TimeDelta::FromSeconds(1);
auto start_time = base::Time::Now();
......@@ -509,8 +510,7 @@ void ClearKeyCdm::OnUpdateSuccess(uint32_t promise_id,
auto time_elapsed = base::Time::Now() - start_time;
CHECK_GE(time_elapsed, kSleepDuration);
ScheduleNextRenewal();
has_set_renewal_timer_ = true;
ScheduleNextTimer();
}
// Also send an individualization request if never sent before. Only
......@@ -581,19 +581,27 @@ void ClearKeyCdm::SetServerCertificate(uint32_t promise_id,
void ClearKeyCdm::TimerExpired(void* context) {
DVLOG(1) << __func__;
DCHECK(has_set_renewal_timer_);
DCHECK(has_set_timer_);
std::string renewal_message;
if (!next_renewal_message_.empty() && context == &next_renewal_message_[0]) {
renewal_message = next_renewal_message_;
} else {
renewal_message = "ERROR: Invalid timer context found!";
}
cdm_host_proxy_->OnSessionMessage(
last_session_id_.data(), last_session_id_.length(), cdm::kLicenseRenewal,
renewal_message.data(), renewal_message.length());
if (key_system_ == kExternalClearKeyMessageTypeTestKeySystem) {
if (!next_renewal_message_.empty() &&
context == &next_renewal_message_[0]) {
renewal_message = next_renewal_message_;
} else {
renewal_message = "ERROR: Invalid timer context found!";
}
cdm_host_proxy_->OnSessionMessage(
last_session_id_.data(), last_session_id_.length(),
cdm::kLicenseRenewal, renewal_message.data(), renewal_message.length());
} else if (key_system_ == kExternalClearKeyOutputProtectionTestKeySystem) {
// Check output protection again.
cdm_host_proxy_->QueryOutputProtectionStatus();
}
ScheduleNextRenewal();
// Start the timer to schedule another timeout.
ScheduleNextTimer();
}
static void CopyDecryptResults(media::Decryptor::Status* status_copy,
......@@ -770,8 +778,9 @@ void ClearKeyCdm::Destroy() {
delete this;
}
void ClearKeyCdm::ScheduleNextRenewal() {
// Prepare the next renewal message and set timer.
void ClearKeyCdm::ScheduleNextTimer() {
// Prepare the next renewal message and set timer. Renewal message is only
// needed for the renewal test, and is ignored for other uses of the timer.
std::ostringstream msg_stream;
msg_stream << "Renewal from ClearKey CDM set at time "
<< base::Time::FromDoubleT(cdm_host_proxy_->GetCurrentWallTime())
......@@ -779,6 +788,7 @@ void ClearKeyCdm::ScheduleNextRenewal() {
next_renewal_message_ = msg_stream.str();
cdm_host_proxy_->SetTimer(timer_delay_ms_, &next_renewal_message_[0]);
has_set_timer_ = true;
// Use a smaller timer delay at start-up to facilitate testing. Increase the
// timer delay up to a limit to avoid message spam.
......@@ -838,27 +848,38 @@ void ClearKeyCdm::OnQueryOutputProtectionStatus(
cdm::QueryResult result,
uint32_t link_mask,
uint32_t output_protection_mask) {
DVLOG(1) << __func__;
DVLOG(1) << __func__ << " result:" << result << ", link_mask:" << link_mask
<< ", output_protection_mask:" << output_protection_mask;
if (!is_running_output_protection_test_) {
NOTREACHED() << "OnQueryOutputProtectionStatus() called unexpectedly.";
return;
}
is_running_output_protection_test_ = false;
// On Chrome OS, status query will fail on Linux Chrome OS build. So we ignore
// the query result. On all other platforms, status query should succeed.
// TODO(xhwang): Improve the check on Chrome OS builds. For example, use
// base::SysInfo::IsRunningOnChromeOS() to differentiate between real Chrome OS
// build and Linux Chrome OS build.
#if !defined(OS_CHROMEOS)
if (result != cdm::kQuerySucceeded || link_mask != 0) {
OnUnitTestComplete(false);
return;
// If the query fails or it succeeds and link mask contains kLinkTypeNetwork,
// send a 'keystatuschange' event with a key marked as output-restricted.
// As the JavaScript test doesn't check key IDs, use a dummy key ID.
//
// Note that QueryOutputProtectionStatus() is known to fail on Linux Chrome
// OS builds.
//
// Note that this does not modify any keys, so if the caller does not check
// the 'keystatuschange' event, nothing will happen as decoding will continue
// to work.
if (result != cdm::kQuerySucceeded || (link_mask & cdm::kLinkTypeNetwork)) {
// A session ID is needed, so use |last_session_id_|. However, if this is
// called before a session has been created, we have no session to send
// this to. Note that this only works with a single session, the same as
// renewal messages.
if (!last_session_id_.empty()) {
const uint8_t kDummyKeyId[] = {'d', 'u', 'm', 'm', 'y'};
std::vector<cdm::KeyInformation> keys_vector = {
{kDummyKeyId, base::size(kDummyKeyId), cdm::kOutputRestricted, 0}};
cdm_host_proxy_->OnSessionKeysChange(
last_session_id_.data(), last_session_id_.length(), false,
keys_vector.data(), keys_vector.size());
}
}
#endif
OnUnitTestComplete(true);
}
void ClearKeyCdm::OnStorageId(uint32_t version,
......@@ -972,6 +993,9 @@ void ClearKeyCdm::StartOutputProtectionTest() {
DVLOG(1) << __func__;
is_running_output_protection_test_ = true;
cdm_host_proxy_->QueryOutputProtectionStatus();
// Also start the timer to run this periodically.
ScheduleNextTimer();
}
void ClearKeyCdm::StartPlatformVerificationTest() {
......
......@@ -138,7 +138,7 @@ class ClearKeyCdm : public cdm::ContentDecryptionModule_10,
void OnUpdateSuccess(uint32_t promise_id, const std::string& session_id);
// Prepares next renewal message and sets a timer for it.
void ScheduleNextRenewal();
void ScheduleNextTimer();
// Decrypts the |encrypted_buffer| and puts the result in |decrypted_buffer|.
// Returns cdm::kSuccess if decryption succeeded. The decrypted result is
......@@ -158,6 +158,7 @@ class ClearKeyCdm : public cdm::ContentDecryptionModule_10,
void OnFileIOTestComplete(bool success);
void StartOutputProtectionTest();
void StartPlatformVerificationTest();
void ReportVerifyCdmHostTestResult();
void StartStorageIdTest();
......@@ -182,9 +183,9 @@ class ClearKeyCdm : public cdm::ContentDecryptionModule_10,
// Timer delay in milliseconds for the next cdm_host_proxy_->SetTimer() call.
int64_t timer_delay_ms_ = kInitialTimerDelayMs;
// Indicates whether a renewal timer has been set to prevent multiple timers
// from running.
bool has_set_renewal_timer_ = false;
// Indicates whether a timer has been set to prevent multiple timers from
// running.
bool has_set_timer_ = false;
bool has_sent_individualization_request_ = false;
......
<!DOCTYPE html>
<title>Test EME and getDisplayMedia()</title>
<div id="logs"></div>
<script src='eme_player_js/app_loader.js' type='text/javascript'></script>
<script type='text/javascript'>
// This test only checks for 'createMediaRecorderBeforeMediaKeys' in the URL
// parameters. If it is there, then the MediaRecorder is setup and started
// before MediaKeys is created. If not there, then the MediaRecorder is only
// created after MediaKeys is created.
var createMediaRecorderBeforeMediaKeys =
(window.location.href.indexOf('createMediaRecorderBeforeMediaKeys') > -1);
// Use the default KEY_ID and KEY as specified in eme_player_js/globals.js.
const keyId = KEY_ID;
const key = KEY;
// Returns a MediaKeys object that is already setup with a single session
// containing the key needed to play a typical test file.
async function setUpEME() {
// This test doesn't play any media, so use a simple
// MediaKeySystemConfiguration that should be supported by
// all platforms where External ClearKey CDM is supported.
const config = [{
initDataTypes : [ 'keyids' ],
videoCapabilities: [{contentType: 'video/webm; codecs="vp8"'}],
}];
var access = await navigator.requestMediaKeySystemAccess(
OUTPUT_PROTECTION_TEST_KEYSYSTEM, config);
var mediaKeys = await access.createMediaKeys();
Utils.timeLog('Creating session');
var mediaKeySession = mediaKeys.createSession();
// Handle 'keystatuseschange' events. There will be one after update() is
// called, as well as a later one when output protection detects the media
// recording. As this is testing output protection, if it reports
// 'output-restricted', then the test is a success. If not, simply continue
// on.
mediaKeySession.addEventListener('keystatuseschange', function(event) {
var result = [];
for (let item of event.target.keyStatuses) {
result.push(`{kid:${
Utils.getHexString(
Utils.convertToUint8Array(item[0]))},status:${item[1]}}`);
}
Utils.timeLog('Event: keystatuseschange ' + result.join(','));
for (let item of event.target.keyStatuses) {
if (item[1] == 'output-restricted') {
Utils.setResultInTitle(UNIT_TEST_SUCCESS);
}
}
});
// Register for the 'message' event before it happens. Although the event
// shouldn't be generated until after the generateRequest() promise is
// resolved, the handlers may be queued before the JavaScript code runs
// (and thus be lost if an event handler is not registered).
const waitForMessagePromise = Utils.waitForEvent(
mediaKeySession, 'message', function(event, resolve, reject) {
// When the 'message' event happens, we know the key to be
// used, so simply call update() and then call |resolve| or
// |reject| as appropriate.
Utils.timeLog('Calling update()');
const mediaKeySession = event.target;
const jwkSet = Utils.createJWKData(keyId, key);
mediaKeySession.update(jwkSet).then(resolve, reject);
});
// As this is using 'webm' initDataType, the data to generateRequest()
// is simply the key ID.
Utils.timeLog('Calling generateRequest()');
const generateRequestPromise = mediaKeySession.generateRequest(
'webm', Utils.convertToUint8Array(keyId));
await Promise.all([generateRequestPromise, waitForMessagePromise]);
return mediaKeys;
}
// Return a MediaRecorder object setup to record something (browsertests set
// a flag to by default capture the screen, if run manually the user will
// have to select something).
async function setUpRecorder() {
Utils.timeLog(
'Creating MediaRecorder on navigator.mediaDevices.getDisplayMedia()');
captureStream = await navigator.mediaDevices.getDisplayMedia({});
var recorder = new MediaRecorder(captureStream, {});
recorder.addEventListener('start', function() {
Utils.timeLog('Event: MediaRecorder::start');
});
recorder.start();
return recorder;
}
async function sleep(timeout) {
return new Promise(function(resolve) {
Utils.timeLog('Sleeping for ' + timeout + 'ms');
window.setTimeout(function() {
resolve();
}, timeout);
});
}
async function runTest() {
Utils.resetTitleChange();
if (createMediaRecorderBeforeMediaKeys) {
// Create the MediaRecorder before setting up EME.
await setUpRecorder();
}
var mediaKeys = await setUpEME();
if (!createMediaRecorderBeforeMediaKeys) {
// Create the MediaRecorder after a delay of 1/2 second.
await sleep(500);
await setUpRecorder();
}
}
try {
runTest();
} catch (error) {
Utils.timeLog(error);
Utils.failTest('Failed test.');
}
</script>
</html>
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