blob: ec28f5992079364d957f35b7dbaf1d60d2664a0b [file] [log] [blame]
// Copyright 2019 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 "chrome/common/media/cdm_manifest.h"
#include <stddef.h>
#include <memory>
#include <string>
#include <vector>
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/json/json_file_value_serializer.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_split.h"
#include "base/values.h"
#include "base/version.h"
#include "content/public/common/cdm_info.h"
#include "extensions/common/manifest_constants.h"
#include "media/base/content_decryption_module.h"
#include "media/base/decrypt_config.h"
#include "media/base/video_codecs.h"
#include "media/cdm/cdm_proxy.h"
#include "media/cdm/supported_cdm_versions.h"
#include "media/media_buildflags.h"
#if !BUILDFLAG(ENABLE_LIBRARY_CDMS)
#error This file should only be compiled when library CDMs are enabled
#endif
namespace {
// The CDM manifest includes several custom values, all beginning with "x-cdm-".
// They are:
// x-cdm-module-versions
// x-cdm-interface-versions
// x-cdm-host-versions
// x-cdm-codecs
// x-cdm-persistent-license-support
// x-cdm-supported-encryption-schemes
// x-cdm-supported-cdm-proxy-protocols
// What they represent is listed below. They should never have non-backwards
// compatible changes. All values are strings. All values that are lists are
// delimited by commas. No trailing commas. For example, "1,2,4".
const char kCdmValueDelimiter[] = ",";
// The following entries are required.
// Interface versions are lists of integers (e.g. "1" or "1,2,4").
// All match the interface versions from content_decryption_module.h that the
// CDM supports.
// Matches CDM_MODULE_VERSION.
const char kCdmModuleVersionsName[] = "x-cdm-module-versions";
// Matches supported ContentDecryptionModule_* version(s).
const char kCdmInterfaceVersionsName[] = "x-cdm-interface-versions";
// Matches supported Host_* version(s).
const char kCdmHostVersionsName[] = "x-cdm-host-versions";
// The codecs list is a list of simple codec names (e.g. "vp8,vorbis").
const char kCdmCodecsListName[] = "x-cdm-codecs";
// Whether persistent license is supported by the CDM: "true" or "false".
const char kCdmPersistentLicenseSupportName[] =
"x-cdm-persistent-license-support";
// The list of supported encryption schemes (e.g. ["cenc","cbcs"]).
const char kCdmSupportedEncryptionSchemesName[] =
"x-cdm-supported-encryption-schemes";
// The list of supported proxy protocols (e.g. ["intel"]).
const char kCdmSupportedCdmProxyProtocolsName[] =
"x-cdm-supported-cdm-proxy-protocols";
// The following strings are used to specify supported codecs in the
// parameter |kCdmCodecsListName|.
const char kCdmSupportedCodecVp8[] = "vp8";
// Legacy VP9, which is equivalent to VP9 profile 0.
// TODO(xhwang): Newer CDMs should support "vp09" below. Remove this after older
// CDMs are obsolete.
const char kCdmSupportedCodecLegacyVp9[] = "vp9.0";
// Supports at least VP9 profile 0 and profile 2.
const char kCdmSupportedCodecVp9[] = "vp09";
const char kCdmSupportedCodecAv1[] = "av01";
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
const char kCdmSupportedCodecAvc1[] = "avc1";
#endif
// The following strings are used to specify supported encryption schemes in
// the parameter |kCdmSupportedEncryptionSchemesName|.
const char kCdmSupportedEncryptionSchemeCenc[] = "cenc";
const char kCdmSupportedEncryptionSchemeCbcs[] = "cbcs";
// The following string(s) are used to specify supported CdmProxy protocols in
// the parameter |kCdmSupportedCdmProxyProtocolsName|.
const char kCdmSupportedCdmProxyProtocolIntel[] = "intel";
typedef bool (*VersionCheckFunc)(int version);
// Returns whether the CDM's API version, as specified in the manifest by
// |version_name|, is supported in this Chrome binary and not disabled at run
// time by calling |version_check_func|. If the manifest entry contains multiple
// values, each one is checked sequentially, and if any one is supported, this
// function returns true. If all values in the manifest entry are not supported,
// then return false.
bool CheckForCompatibleVersion(const base::Value& manifest,
const std::string version_name,
VersionCheckFunc version_check_func) {
DCHECK(manifest.is_dict());
auto* version_string = manifest.FindStringKey(version_name);
if (!version_string) {
DVLOG(1) << "CDM manifest missing " << version_name;
return false;
}
DVLOG_IF(1, version_string->empty())
<< "CDM manifest has empty " << version_name;
for (const base::StringPiece& ver_str :
base::SplitStringPiece(*version_string, kCdmValueDelimiter,
base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
int version = 0;
if (base::StringToInt(ver_str, &version) && version_check_func(version))
return true;
}
DVLOG(1) << "CDM manifest has no supported " << version_name << " in '"
<< *version_string << "'";
return false;
}
// Returns true and updates |video_codecs| if the appropriate manifest entry is
// valid. When VP9 is supported, sets |supports_vp9_profile2| if profile 2 is
// supported. Older CDMs may only support profile 0. Returns false and does not
// modify |video_codecs| if the manifest entry is incorrectly formatted.
bool GetCodecs(const base::Value& manifest,
std::vector<media::VideoCodec>* video_codecs,
bool* supports_vp9_profile2) {
DCHECK(manifest.is_dict());
DCHECK(video_codecs);
const base::Value* value = manifest.FindKey(kCdmCodecsListName);
if (!value) {
DLOG(WARNING) << "CDM manifest is missing codecs.";
return true;
}
if (!value->is_string()) {
DLOG(ERROR) << "CDM manifest entry " << kCdmCodecsListName
<< " is not a string.";
return false;
}
const std::string& codecs = value->GetString();
if (codecs.empty()) {
DLOG(WARNING) << "CDM manifest has empty codecs list.";
return true;
}
std::vector<media::VideoCodec> result;
const std::vector<base::StringPiece> supported_codecs =
base::SplitStringPiece(codecs, kCdmValueDelimiter, base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
// Assuming VP9 profile 2 is not supported by default. Will only be set when
// kCdmSupportedCodecVp9 is available below.
*supports_vp9_profile2 = false;
for (const auto& codec : supported_codecs) {
if (codec == kCdmSupportedCodecVp8) {
result.push_back(media::VideoCodec::kCodecVP8);
} else if (codec == kCdmSupportedCodecLegacyVp9) {
result.push_back(media::VideoCodec::kCodecVP9);
} else if (codec == kCdmSupportedCodecVp9) {
result.push_back(media::VideoCodec::kCodecVP9);
*supports_vp9_profile2 = true;
} else if (codec == kCdmSupportedCodecAv1) {
result.push_back(media::VideoCodec::kCodecAV1);
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
} else if (codec == kCdmSupportedCodecAvc1) {
result.push_back(media::VideoCodec::kCodecH264);
#endif
}
}
video_codecs->swap(result);
return true;
}
// Returns true and updates |session_types| if the appropriate manifest entry is
// valid. Returns false if the manifest entry is incorrectly formatted.
bool GetSessionTypes(const base::Value& manifest,
base::flat_set<media::CdmSessionType>* session_types) {
DCHECK(manifest.is_dict());
DCHECK(session_types);
bool is_persistent_license_supported = false;
const base::Value* value = manifest.FindKey(kCdmPersistentLicenseSupportName);
if (value) {
if (!value->is_bool())
return false;
is_persistent_license_supported = value->GetBool();
}
// Temporary session is always supported.
session_types->insert(media::CdmSessionType::kTemporary);
if (is_persistent_license_supported)
session_types->insert(media::CdmSessionType::kPersistentLicense);
return true;
}
// Returns true and updates |encryption_schemes| if the appropriate manifest
// entry is valid. Returns false and does not modify |encryption_schemes| if the
// manifest entry is incorrectly formatted. It is assumed that all CDMs support
// 'cenc', so if the manifest entry is missing, the result will indicate support
// for 'cenc' only. Incorrect types in the manifest entry will log the error and
// fail. Unrecognized values will be reported but otherwise ignored.
bool GetEncryptionSchemes(
const base::Value& manifest,
base::flat_set<media::EncryptionScheme>* encryption_schemes) {
DCHECK(manifest.is_dict());
DCHECK(encryption_schemes);
const base::Value* value =
manifest.FindKey(kCdmSupportedEncryptionSchemesName);
if (!value) {
// No manifest entry found, so assume only 'cenc' supported for backwards
// compatibility.
encryption_schemes->insert(media::EncryptionScheme::kCenc);
return true;
}
if (!value->is_list()) {
DLOG(ERROR) << "CDM manifest entry " << kCdmSupportedEncryptionSchemesName
<< " is not a list.";
return false;
}
base::flat_set<media::EncryptionScheme> result;
for (const auto& item : value->GetList()) {
if (!item.is_string()) {
DLOG(ERROR) << "Unrecognized item type in CDM manifest entry "
<< kCdmSupportedEncryptionSchemesName;
return false;
}
const std::string& scheme = item.GetString();
if (scheme == kCdmSupportedEncryptionSchemeCenc) {
result.insert(media::EncryptionScheme::kCenc);
} else if (scheme == kCdmSupportedEncryptionSchemeCbcs) {
result.insert(media::EncryptionScheme::kCbcs);
} else {
DLOG(WARNING) << "Unrecognized encryption scheme '" << scheme
<< "' in CDM manifest entry "
<< kCdmSupportedEncryptionSchemesName;
}
}
// As the manifest entry exists, it must specify at least one valid value.
if (result.empty())
return false;
encryption_schemes->swap(result);
return true;
}
// Returns true and updates |cdm_proxy_protocols| if the appropriate manifest
// entry is valid. Returns false and does not modify |cdm_proxy_protocols| if
// the manifest entry is incorrectly formatted. Incorrect types in the manifest
// entry will log the error and fail. Unrecognized values will be reported but
// otherwise ignored.
bool GetCdmProxyProtocols(
const base::Value& manifest,
base::flat_set<media::CdmProxy::Protocol>* cdm_proxy_protocols) {
DCHECK(manifest.is_dict());
const auto* value = manifest.FindKey(kCdmSupportedCdmProxyProtocolsName);
if (!value)
return true;
if (!value->is_list()) {
DLOG(ERROR) << "CDM manifest entry " << kCdmSupportedCdmProxyProtocolsName
<< " is not a list.";
return false;
}
base::flat_set<media::CdmProxy::Protocol> result;
for (const auto& item : value->GetList()) {
if (!item.is_string()) {
DLOG(ERROR) << "Unrecognized item type in CDM manifest entry "
<< kCdmSupportedCdmProxyProtocolsName;
return false;
}
const std::string& protocol = item.GetString();
if (protocol == kCdmSupportedCdmProxyProtocolIntel) {
result.insert(media::CdmProxy::Protocol::kIntel);
} else {
DLOG(WARNING) << "Unrecognized CdmProxy protocol '" << protocol
<< "' in CDM manifest entry "
<< kCdmSupportedCdmProxyProtocolsName;
}
}
cdm_proxy_protocols->swap(result);
return true;
}
bool GetVersion(const base::Value& manifest, base::Version* version) {
DCHECK(manifest.is_dict());
auto* version_string =
manifest.FindStringKey(extensions::manifest_keys::kVersion);
if (!version_string) {
DLOG(ERROR) << "CDM manifest missing "
<< extensions::manifest_keys::kVersion;
return false;
}
*version = base::Version(*version_string);
if (!version->IsValid()) {
DLOG(ERROR) << "CDM manifest version " << version_string << " is invalid.";
return false;
}
return true;
}
} // namespace
bool IsCdmManifestCompatibleWithChrome(const base::Value& manifest) {
DCHECK(manifest.is_dict());
return CheckForCompatibleVersion(manifest, kCdmModuleVersionsName,
media::IsSupportedCdmModuleVersion) &&
CheckForCompatibleVersion(
manifest, kCdmInterfaceVersionsName,
media::IsSupportedAndEnabledCdmInterfaceVersion) &&
CheckForCompatibleVersion(manifest, kCdmHostVersionsName,
media::IsSupportedCdmHostVersion);
}
bool ParseCdmManifest(const base::Value& manifest,
content::CdmCapability* capability) {
DCHECK(manifest.is_dict());
return GetCodecs(manifest, &capability->video_codecs,
&capability->supports_vp9_profile2) &&
GetEncryptionSchemes(manifest, &capability->encryption_schemes) &&
GetSessionTypes(manifest, &capability->session_types) &&
GetCdmProxyProtocols(manifest, &capability->cdm_proxy_protocols);
}
bool ParseCdmManifestFromPath(const base::FilePath& manifest_path,
base::Version* version,
content::CdmCapability* capability) {
JSONFileValueDeserializer deserializer(manifest_path);
int error_code;
std::string error_message;
std::unique_ptr<base::Value> manifest =
deserializer.Deserialize(&error_code, &error_message);
if (!manifest || !manifest->is_dict()) {
DLOG(ERROR) << "Could not deserialize CDM manifest from " << manifest_path
<< ". Error: " << error_code << " / " << error_message;
return false;
}
return IsCdmManifestCompatibleWithChrome(*manifest) &&
GetVersion(*manifest, version) &&
ParseCdmManifest(*manifest, capability);
}