blob: 411573296ea19ebcfa88e09514a5eb5dd70b9c26 [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 "modules/encryptedmedia/NavigatorRequestMediaKeySystemAccess.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "bindings/core/v8/ScriptState.h"
#include "core/dom/DOMException.h"
#include "core/dom/Document.h"
#include "core/dom/ExceptionCode.h"
#include "core/frame/Deprecation.h"
#include "core/frame/OriginsUsingFeatures.h"
#include "core/inspector/ConsoleMessage.h"
#include "modules/encryptedmedia/EncryptedMediaUtils.h"
#include "modules/encryptedmedia/MediaKeySession.h"
#include "modules/encryptedmedia/MediaKeySystemAccess.h"
#include "modules/encryptedmedia/MediaKeysController.h"
#include "platform/EncryptedMediaRequest.h"
#include "platform/Histogram.h"
#include "platform/Logging.h"
#include "platform/network/ParsedContentType.h"
#include "public/platform/WebEncryptedMediaClient.h"
#include "public/platform/WebEncryptedMediaRequest.h"
#include "public/platform/WebMediaKeySystemConfiguration.h"
#include "public/platform/WebMediaKeySystemMediaCapability.h"
#include "public/platform/WebVector.h"
#include "wtf/Vector.h"
#include "wtf/text/WTFString.h"
#include <algorithm>
namespace blink {
namespace {
static WebVector<WebEncryptedMediaInitDataType> convertInitDataTypes(const Vector<String>& initDataTypes)
{
WebVector<WebEncryptedMediaInitDataType> result(initDataTypes.size());
for (size_t i = 0; i < initDataTypes.size(); ++i)
result[i] = EncryptedMediaUtils::convertToInitDataType(initDataTypes[i]);
return result;
}
static WebVector<WebMediaKeySystemMediaCapability> convertCapabilities(const HeapVector<MediaKeySystemMediaCapability>& capabilities)
{
WebVector<WebMediaKeySystemMediaCapability> result(capabilities.size());
for (size_t i = 0; i < capabilities.size(); ++i) {
const WebString& contentType = capabilities[i].contentType();
result[i].contentType = contentType;
if (isValidContentType(contentType)) {
// FIXME: Fail if there are unrecognized parameters.
ParsedContentType type(capabilities[i].contentType());
result[i].mimeType = type.mimeType();
result[i].codecs = type.parameterValueForName("codecs");
}
result[i].robustness = capabilities[i].robustness();
}
return result;
}
static WebMediaKeySystemConfiguration::Requirement convertMediaKeysRequirement(const String& requirement)
{
if (requirement == "required")
return WebMediaKeySystemConfiguration::Requirement::Required;
if (requirement == "optional")
return WebMediaKeySystemConfiguration::Requirement::Optional;
if (requirement == "not-allowed")
return WebMediaKeySystemConfiguration::Requirement::NotAllowed;
// Everything else gets the default value.
ASSERT_NOT_REACHED();
return WebMediaKeySystemConfiguration::Requirement::Optional;
}
static WebVector<WebEncryptedMediaSessionType> convertSessionTypes(const Vector<String>& sessionTypes)
{
WebVector<WebEncryptedMediaSessionType> result(sessionTypes.size());
for (size_t i = 0; i < sessionTypes.size(); ++i)
result[i] = EncryptedMediaUtils::convertToSessionType(sessionTypes[i]);
return result;
}
static bool AreCodecsSpecified(const WebMediaKeySystemMediaCapability& capability)
{
return !capability.codecs.isEmpty();
}
// This class allows capabilities to be checked and a MediaKeySystemAccess
// object to be created asynchronously.
class MediaKeySystemAccessInitializer final : public EncryptedMediaRequest {
WTF_MAKE_NONCOPYABLE(MediaKeySystemAccessInitializer);
public:
MediaKeySystemAccessInitializer(ScriptState*, const String& keySystem, const HeapVector<MediaKeySystemConfiguration>& supportedConfigurations);
~MediaKeySystemAccessInitializer() override { }
// EncryptedMediaRequest implementation.
WebString keySystem() const override { return m_keySystem; }
const WebVector<WebMediaKeySystemConfiguration>& supportedConfigurations() const override { return m_supportedConfigurations; }
SecurityOrigin* getSecurityOrigin() const override { return m_resolver->getExecutionContext()->getSecurityOrigin(); }
void requestSucceeded(WebContentDecryptionModuleAccess*) override;
void requestNotSupported(const WebString& errorMessage) override;
ScriptPromise promise() { return m_resolver->promise(); }
DEFINE_INLINE_VIRTUAL_TRACE()
{
visitor->trace(m_resolver);
EncryptedMediaRequest::trace(visitor);
}
private:
// For widevine key system, generate warning and report to UMA if
// |m_supportedConfigurations| contains any video capability with empty
// robustness string.
// TODO(xhwang): Remove after we handle empty robustness correctly.
// See http://crbug.com/482277
void checkVideoCapabilityRobustness() const;
// Generate deprecation warning and log UseCounter if configuration
// contains only container-only contentType strings.
// TODO(jrummell): Remove once this is no longer allowed.
// See http://crbug.com/605661.
void checkEmptyCodecs(const WebMediaKeySystemConfiguration&);
Member<ScriptPromiseResolver> m_resolver;
const String m_keySystem;
WebVector<WebMediaKeySystemConfiguration> m_supportedConfigurations;
};
MediaKeySystemAccessInitializer::MediaKeySystemAccessInitializer(ScriptState* scriptState, const String& keySystem, const HeapVector<MediaKeySystemConfiguration>& supportedConfigurations)
: m_resolver(ScriptPromiseResolver::create(scriptState))
, m_keySystem(keySystem)
, m_supportedConfigurations(supportedConfigurations.size())
{
for (size_t i = 0; i < supportedConfigurations.size(); ++i) {
const MediaKeySystemConfiguration& config = supportedConfigurations[i];
WebMediaKeySystemConfiguration webConfig;
if (config.hasInitDataTypes()) {
webConfig.hasInitDataTypes = true;
webConfig.initDataTypes = convertInitDataTypes(config.initDataTypes());
}
if (config.hasAudioCapabilities()) {
webConfig.hasAudioCapabilities = true;
webConfig.audioCapabilities = convertCapabilities(config.audioCapabilities());
}
if (config.hasVideoCapabilities()) {
webConfig.hasVideoCapabilities = true;
webConfig.videoCapabilities = convertCapabilities(config.videoCapabilities());
}
ASSERT(config.hasDistinctiveIdentifier());
webConfig.distinctiveIdentifier = convertMediaKeysRequirement(config.distinctiveIdentifier());
ASSERT(config.hasPersistentState());
webConfig.persistentState = convertMediaKeysRequirement(config.persistentState());
if (config.hasSessionTypes()) {
webConfig.hasSessionTypes = true;
webConfig.sessionTypes = convertSessionTypes(config.sessionTypes());
}
// If |label| is not present, it will be a null string.
webConfig.label = config.label();
m_supportedConfigurations[i] = webConfig;
}
checkVideoCapabilityRobustness();
}
void MediaKeySystemAccessInitializer::requestSucceeded(WebContentDecryptionModuleAccess* access)
{
checkEmptyCodecs(access->getConfiguration());
m_resolver->resolve(new MediaKeySystemAccess(m_keySystem, adoptPtr(access)));
m_resolver.clear();
}
void MediaKeySystemAccessInitializer::requestNotSupported(const WebString& errorMessage)
{
m_resolver->reject(DOMException::create(NotSupportedError, errorMessage));
m_resolver.clear();
}
void MediaKeySystemAccessInitializer::checkVideoCapabilityRobustness() const
{
// Only check for widevine key system.
if (keySystem() != "com.widevine.alpha")
return;
bool hasVideoCapabilities = false;
bool hasEmptyRobustness = false;
for (const auto& config : m_supportedConfigurations) {
if (!config.hasVideoCapabilities)
continue;
hasVideoCapabilities = true;
for (const auto& capability : config.videoCapabilities) {
if (capability.robustness.isEmpty()) {
hasEmptyRobustness = true;
break;
}
}
if (hasEmptyRobustness)
break;
}
if (hasVideoCapabilities) {
DEFINE_THREAD_SAFE_STATIC_LOCAL(EnumerationHistogram, emptyRobustnessHistogram, new EnumerationHistogram("Media.EME.Widevine.VideoCapability.HasEmptyRobustness", 2));
emptyRobustnessHistogram.count(hasEmptyRobustness);
}
if (hasEmptyRobustness) {
m_resolver->getExecutionContext()->addConsoleMessage(ConsoleMessage::create(JSMessageSource, WarningMessageLevel,
"It is recommended that a robustness level be specified. Not specifying the robustness level could "
"result in unexpected behavior in the future, potentially including failure to play."));
}
}
void MediaKeySystemAccessInitializer::checkEmptyCodecs(const WebMediaKeySystemConfiguration& config)
{
// This is only checking for empty codecs in the selected configuration,
// as apps may pass container only contentType strings for compatibility
// with other implementations.
// This will only check that all returned capabilities do not contain
// codecs. This avoids alerting on configurations that will continue
// to succeed in the future once strict checking is enforced.
bool areAllAudioCodecsEmpty = false;
if (config.hasAudioCapabilities && !config.audioCapabilities.isEmpty()) {
areAllAudioCodecsEmpty = std::find_if(config.audioCapabilities.begin(), config.audioCapabilities.end(), AreCodecsSpecified)
== config.audioCapabilities.end();
}
bool areAllVideoCodecsEmpty = false;
if (config.hasVideoCapabilities && !config.videoCapabilities.isEmpty()) {
areAllVideoCodecsEmpty = std::find_if(config.videoCapabilities.begin(), config.videoCapabilities.end(), AreCodecsSpecified)
== config.videoCapabilities.end();
}
if (areAllAudioCodecsEmpty || areAllVideoCodecsEmpty) {
Deprecation::countDeprecation(m_resolver->getExecutionContext(), UseCounter::EncryptedMediaAllSelectedContentTypesMissingCodecs);
} else {
UseCounter::count(m_resolver->getExecutionContext(), UseCounter::EncryptedMediaAllSelectedContentTypesHaveCodecs);
}
}
} // namespace
ScriptPromise NavigatorRequestMediaKeySystemAccess::requestMediaKeySystemAccess(
ScriptState* scriptState,
Navigator& navigator,
const String& keySystem,
const HeapVector<MediaKeySystemConfiguration>& supportedConfigurations)
{
WTF_LOG(Media, "NavigatorRequestMediaKeySystemAccess::requestMediaKeySystemAccess()");
// From https://w3c.github.io/encrypted-media/#requestMediaKeySystemAccess
// When this method is invoked, the user agent must run the following steps:
// 1. If keySystem is an empty string, return a promise rejected with a
// new DOMException whose name is InvalidAccessError.
if (keySystem.isEmpty()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The keySystem parameter is empty."));
}
// 2. If supportedConfigurations was provided and is empty, return a
// promise rejected with a new DOMException whose name is
// InvalidAccessError.
if (!supportedConfigurations.size()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The supportedConfigurations parameter is empty."));
}
// 3-4. 'May Document use powerful features?' check.
ExecutionContext* executionContext = scriptState->getExecutionContext();
String errorMessage;
if (executionContext->isSecureContext(errorMessage)) {
UseCounter::count(executionContext, UseCounter::EncryptedMediaSecureOrigin);
} else {
Deprecation::countDeprecation(executionContext, UseCounter::EncryptedMediaInsecureOrigin);
// TODO(ddorwin): Implement the following:
// Reject promise with a new DOMException whose name is NotSupportedError.
}
// 5. Let origin be the origin of document.
// (Passed with the execution context in step 7.)
// 6. Let promise be a new promise.
Document* document = toDocument(executionContext);
if (!document->page()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The context provided is not associated with a page."));
}
MediaKeySystemAccessInitializer* initializer = new MediaKeySystemAccessInitializer(scriptState, keySystem, supportedConfigurations);
ScriptPromise promise = initializer->promise();
// 7. Asynchronously determine support, and if allowed, create and
// initialize the MediaKeySystemAccess object.
MediaKeysController* controller = MediaKeysController::from(document->page());
WebEncryptedMediaClient* mediaClient = controller->encryptedMediaClient(executionContext);
mediaClient->requestMediaKeySystemAccess(WebEncryptedMediaRequest(initializer));
// 8. Return promise.
return promise;
}
} // namespace blink