blob: 2dd051e59626ee9d47fcfd33c11c63a74c23dd3c [file] [log] [blame]
// Copyright 2014 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 "webencryptedmediaclient_impl.h"
#include "base/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "media/base/key_systems.h"
#include "media/base/media_permission.h"
#include "media/blink/webcontentdecryptionmodule_impl.h"
#include "media/blink/webcontentdecryptionmoduleaccess_impl.h"
#include "net/base/mime_util.h"
#include "third_party/WebKit/public/platform/WebEncryptedMediaRequest.h"
#include "third_party/WebKit/public/platform/WebMediaKeySystemConfiguration.h"
#include "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/platform/WebVector.h"
namespace media {
// These names are used by UMA.
const char kKeySystemSupportUMAPrefix[] =
"Media.EME.RequestMediaKeySystemAccess.";
// TODO(jrummell): Convert to an enum. http://crbug.com/418239
const char kTemporarySessionType[] = "temporary";
const char kPersistentLicenseSessionType[] = "persistent-license";
const char kPersistentReleaseMessageSessionType[] =
"persistent-release-message";
enum ConfigurationSupport {
CONFIGURATION_NOT_SUPPORTED,
CONFIGURATION_REQUIRES_PERMISSION,
CONFIGURATION_SUPPORTED,
};
static bool IsSupportedContentType(const std::string& key_system,
const std::string& mime_type,
const std::string& codecs) {
// TODO(sandersd): Move contentType parsing from Blink to here so that invalid
// parameters can be rejected. http://crbug.com/417561
// TODO(sandersd): Pass in the media type (audio or video) and check that the
// container type matches. http://crbug.com/457384
std::string container = base::StringToLowerASCII(mime_type);
// Check that |codecs| are supported by the CDM. This check does not handle
// extended codecs, so extended codec information is stripped.
// TODO(sandersd): Reject codecs that do not match the media type.
// http://crbug.com/457386
std::vector<std::string> codec_vector;
net::ParseCodecString(codecs, &codec_vector, true);
if (!IsSupportedKeySystemWithMediaMimeType(container, codec_vector,
key_system)) {
return false;
}
// Check that |codecs| are supported by Chrome. This is done primarily to
// validate extended codecs, but it also ensures that the CDM cannot support
// codecs that Chrome does not (which would be bad because it would require
// considering the accumulated configuration, and could affect whether secure
// decode is required).
// TODO(sandersd): Reject ambiguous codecs. http://crbug.com/374751
codec_vector.clear();
net::ParseCodecString(codecs, &codec_vector, false);
return net::AreSupportedMediaCodecs(codec_vector);
}
static bool GetSupportedCapabilities(
const std::string& key_system,
const blink::WebVector<blink::WebMediaKeySystemMediaCapability>&
capabilities,
std::vector<blink::WebMediaKeySystemMediaCapability>*
media_type_capabilities) {
// From
// https://w3c.github.io/encrypted-media/#get-supported-capabilities-for-media-type
// 1. Let accumulated capabilities be partial configuration.
// (Skipped as there are no configuration-based codec restrictions.)
// 2. Let media type capabilities be empty.
DCHECK_EQ(media_type_capabilities->size(), 0ul);
// 3. For each value in capabilities:
for (size_t i = 0; i < capabilities.size(); i++) {
// 3.1. Let contentType be the value's contentType member.
// 3.2. Let robustness be the value's robustness member.
const blink::WebMediaKeySystemMediaCapability& capability = capabilities[i];
// 3.3. If contentType is the empty string, return null.
if (capability.mimeType.isEmpty())
return false;
// 3.4-3.11. (Implemented by IsSupportedContentType().)
if (!base::IsStringASCII(capability.mimeType) ||
!base::IsStringASCII(capability.codecs) ||
!IsSupportedContentType(key_system,
base::UTF16ToASCII(capability.mimeType),
base::UTF16ToASCII(capability.codecs))) {
continue;
}
// 3.12. If robustness is not the empty string, run the following steps:
// (Robustness is not supported.)
// TODO(sandersd): Implement robustness. http://crbug.com/442586
if (!capability.robustness.isEmpty()) {
LOG(WARNING) << "Configuration rejected because rubustness strings are "
<< "not yet supported.";
continue;
}
// 3.13. If the user agent and implementation do not support playback of
// encrypted media data as specified by configuration, including all
// media types, in combination with accumulated capabilities,
// continue to the next iteration.
// (Skipped as there are no configuration-based codec restrictions.)
// 3.14. Add configuration to media type capabilities.
media_type_capabilities->push_back(capability);
// 3.15. Add configuration to accumulated capabilities.
// (Skipped as there are no configuration-based codec restrictions.)
}
// 4. If media type capabilities is empty, return null.
// 5. Return media type capabilities.
return !media_type_capabilities->empty();
}
static EmeFeatureRequirement ConvertRequirement(
blink::WebMediaKeySystemConfiguration::Requirement requirement) {
switch (requirement) {
case blink::WebMediaKeySystemConfiguration::Requirement::Required:
return EME_FEATURE_REQUIRED;
case blink::WebMediaKeySystemConfiguration::Requirement::Optional:
return EME_FEATURE_OPTIONAL;
case blink::WebMediaKeySystemConfiguration::Requirement::NotAllowed:
return EME_FEATURE_NOT_ALLOWED;
}
NOTREACHED();
return EME_FEATURE_NOT_ALLOWED;
}
static ConfigurationSupport GetSupportedConfiguration(
const std::string& key_system,
const blink::WebMediaKeySystemConfiguration& candidate,
blink::WebMediaKeySystemConfiguration* accumulated_configuration,
bool was_permission_requested,
bool is_permission_granted) {
DCHECK(was_permission_requested || !is_permission_granted);
// It is possible to obtain user permission unless permission was already
// requested and denied.
bool is_permission_possible =
!was_permission_requested || is_permission_granted;
// From https://w3c.github.io/encrypted-media/#get-supported-configuration
// 1. Let accumulated configuration be empty. (Done by caller.)
// 2. If candidate configuration's initDataTypes attribute is not empty, run
// the following steps:
if (!candidate.initDataTypes.isEmpty()) {
// 2.1. Let supported types be empty.
std::vector<blink::WebString> supported_types;
// 2.2. For each value in candidate configuration's initDataTypes attribute:
for (size_t i = 0; i < candidate.initDataTypes.size(); i++) {
// 2.2.1. Let initDataType be the value.
const blink::WebString& init_data_type = candidate.initDataTypes[i];
// 2.2.2. If initDataType is the empty string, return null.
if (init_data_type.isEmpty())
return CONFIGURATION_NOT_SUPPORTED;
// 2.2.3. If the implementation supports generating requests based on
// initDataType, add initDataType to supported types. String
// comparison is case-sensitive.
if (base::IsStringASCII(init_data_type) &&
IsSupportedKeySystemWithInitDataType(
key_system, base::UTF16ToASCII(init_data_type))) {
supported_types.push_back(init_data_type);
}
}
// 2.3. If supported types is empty, return null.
if (supported_types.empty())
return CONFIGURATION_NOT_SUPPORTED;
// 2.4. Add supported types to accumulated configuration.
accumulated_configuration->initDataTypes = supported_types;
}
// 3. Follow the steps for the value of candidate configuration's
// distinctiveIdentifier attribute from the following list:
// - "required": If the implementation does not support a persistent
// Distinctive Identifier in combination with accumulated configuration,
// return null.
// - "optional": Continue.
// - "not-allowed": If the implementation requires a Distinctive
// Identifier in combination with accumulated configuration, return
// null.
EmeFeatureRequirement di_requirement =
ConvertRequirement(candidate.distinctiveIdentifier);
if (!IsDistinctiveIdentifierRequirementSupported(key_system, di_requirement,
is_permission_possible)) {
return CONFIGURATION_NOT_SUPPORTED;
}
// 4. Add the value of the candidate configuration's distinctiveIdentifier
// attribute to accumulated configuration.
accumulated_configuration->distinctiveIdentifier =
candidate.distinctiveIdentifier;
// 5. Follow the steps for the value of candidate configuration's
// persistentState attribute from the following list:
// - "required": If the implementation does not support persisting state
// in combination with accumulated configuration, return null.
// - "optional": Continue.
// - "not-allowed": If the implementation requires persisting state in
// combination with accumulated configuration, return null.
EmeFeatureRequirement ps_requirement =
ConvertRequirement(candidate.persistentState);
if (!IsPersistentStateRequirementSupported(key_system, ps_requirement,
is_permission_possible)) {
return CONFIGURATION_NOT_SUPPORTED;
}
// 6. Add the value of the candidate configuration's persistentState
// attribute to accumulated configuration.
accumulated_configuration->persistentState = candidate.persistentState;
// 7. If candidate configuration's videoCapabilities attribute is not empty,
// run the following steps:
if (!candidate.videoCapabilities.isEmpty()) {
// 7.1. Let video capabilities be the result of executing the Get Supported
// Capabilities for Media Type algorithm on Video, candidate
// configuration's videoCapabilities attribute, and accumulated
// configuration.
// 7.2. If video capabilities is null, return null.
std::vector<blink::WebMediaKeySystemMediaCapability> video_capabilities;
if (!GetSupportedCapabilities(key_system, candidate.videoCapabilities,
&video_capabilities)) {
return CONFIGURATION_NOT_SUPPORTED;
}
// 7.3. Add video capabilities to accumulated configuration.
accumulated_configuration->videoCapabilities = video_capabilities;
}
// 8. If candidate configuration's audioCapabilities attribute is not empty,
// run the following steps:
if (!candidate.audioCapabilities.isEmpty()) {
// 8.1. Let audio capabilities be the result of executing the Get Supported
// Capabilities for Media Type algorithm on Audio, candidate
// configuration's audioCapabilities attribute, and accumulated
// configuration.
// 8.2. If audio capabilities is null, return null.
std::vector<blink::WebMediaKeySystemMediaCapability> audio_capabilities;
if (!GetSupportedCapabilities(key_system, candidate.audioCapabilities,
&audio_capabilities)) {
return CONFIGURATION_NOT_SUPPORTED;
}
// 8.3. Add audio capabilities to accumulated configuration.
accumulated_configuration->audioCapabilities = audio_capabilities;
}
// 9. If accumulated configuration's distinctiveIdentifier value is
// "optional", follow the steps for the first matching condition from the
// following list:
// - If the implementation requires a Distinctive Identifier for any of
// the combinations in accumulated configuration, change accumulated
// configuration's distinctiveIdentifier value to "required".
// - Otherwise, change accumulated configuration's distinctiveIdentifier
// value to "not-allowed".
// (Without robustness support, capabilities do not affect this.)
// TODO(sandersd): Implement robustness. http://crbug.com/442586
if (accumulated_configuration->distinctiveIdentifier ==
blink::WebMediaKeySystemConfiguration::Requirement::Optional) {
if (IsDistinctiveIdentifierRequirementSupported(
key_system, EME_FEATURE_NOT_ALLOWED, is_permission_possible)) {
accumulated_configuration->distinctiveIdentifier =
blink::WebMediaKeySystemConfiguration::Requirement::NotAllowed;
} else {
accumulated_configuration->distinctiveIdentifier =
blink::WebMediaKeySystemConfiguration::Requirement::Required;
}
}
// 10. If accumulated configuration's persistentState value is "optional",
// follow the steps for the first matching condition from the following
// list:
// - If the implementation requires persisting state for any of the
// combinations in accumulated configuration, change accumulated
// configuration's persistentState value to "required".
// - Otherwise, change accumulated configuration's persistentState value
// to "not-allowed".
if (accumulated_configuration->persistentState ==
blink::WebMediaKeySystemConfiguration::Requirement::Optional) {
if (IsPersistentStateRequirementSupported(
key_system, EME_FEATURE_NOT_ALLOWED, is_permission_possible)) {
accumulated_configuration->persistentState =
blink::WebMediaKeySystemConfiguration::Requirement::NotAllowed;
} else {
accumulated_configuration->persistentState =
blink::WebMediaKeySystemConfiguration::Requirement::Required;
}
}
// 11. If implementation in the configuration specified by the combination of
// the values in accumulated configuration is not supported or not allowed
// in the origin, return null.
di_requirement =
ConvertRequirement(accumulated_configuration->distinctiveIdentifier);
if (!IsDistinctiveIdentifierRequirementSupported(key_system, di_requirement,
is_permission_granted)) {
if (was_permission_requested) {
// The optional permission was requested and denied.
// TODO(sandersd): Avoid the need for this logic - crbug.com/460616.
DCHECK(candidate.distinctiveIdentifier ==
blink::WebMediaKeySystemConfiguration::Requirement::Optional);
DCHECK(di_requirement == EME_FEATURE_REQUIRED);
DCHECK(!is_permission_granted);
accumulated_configuration->distinctiveIdentifier =
blink::WebMediaKeySystemConfiguration::Requirement::NotAllowed;
} else {
return CONFIGURATION_REQUIRES_PERMISSION;
}
}
ps_requirement =
ConvertRequirement(accumulated_configuration->persistentState);
if (!IsPersistentStateRequirementSupported(key_system, ps_requirement,
is_permission_granted)) {
DCHECK(!was_permission_requested); // Should have failed at step 5.
return CONFIGURATION_REQUIRES_PERMISSION;
}
// 12. Return accumulated configuration.
// (As an extra step, we record the available session types so that
// createSession() can be synchronous.)
std::vector<blink::WebString> session_types;
session_types.push_back(kTemporarySessionType);
if (accumulated_configuration->persistentState ==
blink::WebMediaKeySystemConfiguration::Requirement::Required) {
if (IsPersistentLicenseSessionSupported(key_system,
is_permission_granted)) {
session_types.push_back(kPersistentLicenseSessionType);
}
if (IsPersistentReleaseMessageSessionSupported(key_system,
is_permission_granted)) {
session_types.push_back(kPersistentReleaseMessageSessionType);
}
}
accumulated_configuration->sessionTypes = session_types;
return CONFIGURATION_SUPPORTED;
}
// Report usage of key system to UMA. There are 2 different counts logged:
// 1. The key system is requested.
// 2. The requested key system and options are supported.
// Each stat is only reported once per renderer frame per key system.
// Note that WebEncryptedMediaClientImpl is only created once by each
// renderer frame.
class WebEncryptedMediaClientImpl::Reporter {
public:
enum KeySystemSupportStatus {
KEY_SYSTEM_REQUESTED = 0,
KEY_SYSTEM_SUPPORTED = 1,
KEY_SYSTEM_SUPPORT_STATUS_COUNT
};
explicit Reporter(const std::string& key_system_for_uma)
: uma_name_(kKeySystemSupportUMAPrefix + key_system_for_uma),
is_request_reported_(false),
is_support_reported_(false) {}
~Reporter() {}
void ReportRequested() {
if (is_request_reported_)
return;
Report(KEY_SYSTEM_REQUESTED);
is_request_reported_ = true;
}
void ReportSupported() {
DCHECK(is_request_reported_);
if (is_support_reported_)
return;
Report(KEY_SYSTEM_SUPPORTED);
is_support_reported_ = true;
}
private:
void Report(KeySystemSupportStatus status) {
// Not using UMA_HISTOGRAM_ENUMERATION directly because UMA_* macros
// require the names to be constant throughout the process' lifetime.
base::LinearHistogram::FactoryGet(
uma_name_, 1, KEY_SYSTEM_SUPPORT_STATUS_COUNT,
KEY_SYSTEM_SUPPORT_STATUS_COUNT + 1,
base::Histogram::kUmaTargetedHistogramFlag)->Add(status);
}
const std::string uma_name_;
bool is_request_reported_;
bool is_support_reported_;
};
WebEncryptedMediaClientImpl::WebEncryptedMediaClientImpl(
scoped_ptr<CdmFactory> cdm_factory,
MediaPermission* media_permission)
: cdm_factory_(cdm_factory.Pass()), media_permission_(media_permission),
weak_factory_(this) {
DCHECK(media_permission);
}
WebEncryptedMediaClientImpl::~WebEncryptedMediaClientImpl() {
}
void WebEncryptedMediaClientImpl::requestMediaKeySystemAccess(
blink::WebEncryptedMediaRequest request) {
// TODO(jrummell): This should be asynchronous, ideally not on the main
// thread.
// Continued from requestMediaKeySystemAccess(), step 7, from
// https://w3c.github.io/encrypted-media/#requestmediakeysystemaccess
//
// 7.1. If keySystem is not one of the Key Systems supported by the user
// agent, reject promise with with a new DOMException whose name is
// NotSupportedError. String comparison is case-sensitive.
if (!base::IsStringASCII(request.keySystem())) {
request.requestNotSupported("Only ASCII keySystems are supported");
return;
}
// Report this request to the UMA.
std::string key_system = base::UTF16ToASCII(request.keySystem());
GetReporter(key_system)->ReportRequested();
if (!IsSupportedKeySystem(key_system)) {
request.requestNotSupported("Unsupported keySystem");
return;
}
// 7.2-7.4. Implemented by SelectSupportedConfiguration().
SelectSupportedConfiguration(request, false, false);
}
void WebEncryptedMediaClientImpl::SelectSupportedConfiguration(
blink::WebEncryptedMediaRequest request,
bool was_permission_requested,
bool is_permission_granted) {
// Continued from requestMediaKeySystemAccess(), step 7.1, from
// https://w3c.github.io/encrypted-media/#requestmediakeysystemaccess
//
// 7.2. Let implementation be the implementation of keySystem.
std::string key_system = base::UTF16ToASCII(request.keySystem());
// 7.3. For each value in supportedConfigurations:
const blink::WebVector<blink::WebMediaKeySystemConfiguration>&
configurations = request.supportedConfigurations();
for (size_t i = 0; i < configurations.size(); i++) {
// 7.3.1. Let candidate configuration be the value.
const blink::WebMediaKeySystemConfiguration& candidate_configuration =
configurations[i];
// 7.3.2. Let supported configuration be the result of executing the Get
// Supported Configuration algorithm on implementation, candidate
// configuration, and origin.
// 7.3.3. If supported configuration is not null, [initialize and return a
// new MediaKeySystemAccess object.]
blink::WebMediaKeySystemConfiguration accumulated_configuration;
ConfigurationSupport supported = GetSupportedConfiguration(
key_system, candidate_configuration, &accumulated_configuration,
was_permission_requested, is_permission_granted);
switch (supported) {
case CONFIGURATION_NOT_SUPPORTED:
continue;
case CONFIGURATION_REQUIRES_PERMISSION:
DCHECK(!was_permission_requested);
media_permission_->RequestPermission(
MediaPermission::PROTECTED_MEDIA_IDENTIFIER,
GURL(request.securityOrigin().toString()),
// Try again with |was_permission_requested| true and
// |is_permission_granted| the value of the permission.
base::Bind(
&WebEncryptedMediaClientImpl::SelectSupportedConfiguration,
weak_factory_.GetWeakPtr(), request, true));
return;
case CONFIGURATION_SUPPORTED:
// Report that this request succeeded to the UMA.
GetReporter(key_system)->ReportSupported();
request.requestSucceeded(WebContentDecryptionModuleAccessImpl::Create(
request.keySystem(), accumulated_configuration,
request.securityOrigin(), weak_factory_.GetWeakPtr()));
return;
}
}
// 7.4. Reject promise with a new DOMException whose name is
// NotSupportedError.
request.requestNotSupported(
"None of the requested configurations were supported.");
}
void WebEncryptedMediaClientImpl::CreateCdm(
const blink::WebString& key_system,
const blink::WebSecurityOrigin& security_origin,
blink::WebContentDecryptionModuleResult result) {
WebContentDecryptionModuleImpl::Create(cdm_factory_.get(), security_origin,
key_system, result);
}
// Lazily create Reporters.
WebEncryptedMediaClientImpl::Reporter* WebEncryptedMediaClientImpl::GetReporter(
const std::string& key_system) {
std::string uma_name = GetKeySystemNameForUMA(key_system);
Reporter* reporter = reporters_.get(uma_name);
if (reporter != nullptr)
return reporter;
// Reporter not found, so create one.
auto result =
reporters_.add(uma_name, make_scoped_ptr(new Reporter(uma_name)));
DCHECK(result.second);
return result.first->second;
}
} // namespace media