blob: cacf51f860d419b5949eaa2bb573f56a74e3ce6a [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/cdm/common/cdm_manifest.h"
#include <stdint.h>
#include <memory>
#include <string>
#include <vector>
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_file_value_serializer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "base/version.h"
#include "content/public/common/cdm_info.h"
#include "media/base/cdm_capability.h"
#include "media/cdm/api/content_decryption_module.h"
#include "media/cdm/supported_cdm_versions.h"
#include "media/media_buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"
using media::CdmCapability;
namespace {
// These names must match what is used in cdm_manifest.cc.
const char kCdmVersion[] = "version";
const char kCdmModuleVersionsName[] = "x-cdm-module-versions";
const char kCdmInterfaceVersionsName[] = "x-cdm-interface-versions";
const char kCdmHostVersionsName[] = "x-cdm-host-versions";
const char kCdmCodecsListName[] = "x-cdm-codecs";
const char kCdmPersistentLicenseSupportName[] =
"x-cdm-persistent-license-support";
const char kCdmSupportedEncryptionSchemesName[] =
"x-cdm-supported-encryption-schemes";
// Version checking does change over time. Deriving these values from constants
// in the code to ensure they change when the CDM interface changes.
// |kSupportedCdmInterfaceVersion| and |kSupportedCdmHostVersion| are the
// minimum versions supported. There may be versions after them that are also
// supported.
constexpr int kSupportedCdmModuleVersion = CDM_MODULE_VERSION;
constexpr int kSupportedCdmInterfaceVersion =
media::kSupportedCdmInterfaceVersions[0].version;
static_assert(media::kSupportedCdmInterfaceVersions[0].enabled,
"kSupportedCdmInterfaceVersion is not enabled by default.");
constexpr int kSupportedCdmHostVersion = media::kMinSupportedCdmHostVersion;
const char kVersion[] = "1.2.3.4";
// Make a string of the values from 0 up to and including |item|.
std::string MakeStringList(int item) {
DCHECK_GT(item, 0);
std::vector<std::string> parts;
for (int i = 0; i <= item; ++i) {
parts.push_back(base::NumberToString(i));
}
return base::JoinString(parts, ",");
}
base::Value::List MakeList(const std::string& item) {
base::Value::List list;
list.Append(item);
return list;
}
base::Value::List MakeList(const std::string& item1, const std::string& item2) {
base::Value::List list;
list.Append(item1);
list.Append(item2);
return list;
}
// Create a default manifest with valid values for all entries.
base::Value::Dict DefaultManifest() {
base::Value::Dict dict;
dict.Set(kCdmCodecsListName, "vp8,vp09,av01");
dict.Set(kCdmPersistentLicenseSupportName, true);
dict.Set(kCdmSupportedEncryptionSchemesName, MakeList("cenc", "cbcs"));
// The following are dependent on what the current code supports.
EXPECT_TRUE(media::IsSupportedCdmModuleVersion(kSupportedCdmModuleVersion));
EXPECT_TRUE(media::IsSupportedAndEnabledCdmInterfaceVersion(
kSupportedCdmInterfaceVersion));
EXPECT_TRUE(media::IsSupportedCdmHostVersion(kSupportedCdmHostVersion));
dict.Set(kCdmModuleVersionsName,
base::NumberToString(kSupportedCdmModuleVersion));
dict.Set(kCdmInterfaceVersionsName,
base::NumberToString(kSupportedCdmInterfaceVersion));
dict.Set(kCdmHostVersionsName,
base::NumberToString(kSupportedCdmHostVersion));
dict.Set(kCdmVersion, kVersion);
return dict;
}
void CheckVideoCodecs(const media::CdmCapability::VideoCodecMap& actual,
const std::vector<media::VideoCodec>& expected) {
EXPECT_EQ(expected.size(), actual.size());
for (const auto& [video_codec, video_codec_info] : actual) {
EXPECT_TRUE(base::Contains(expected, video_codec));
// As the manifest only specifies codecs and not profiles, the list of
// profiles should be empty to indicate that all profiles are supported.
EXPECT_TRUE(video_codec_info.supported_profiles.empty());
}
}
void CheckAudioCodecs(const base::flat_set<media::AudioCodec>& actual,
const std::vector<media::AudioCodec>& expected) {
EXPECT_EQ(actual, expected);
}
void CheckEncryptionSchemes(
const base::flat_set<media::EncryptionScheme>& actual,
const std::vector<media::EncryptionScheme>& expected) {
EXPECT_EQ(actual, expected);
}
void CheckSessionTypes(const base::flat_set<media::CdmSessionType>& actual,
const std::vector<media::CdmSessionType>& expected) {
EXPECT_EQ(actual, expected);
}
void WriteManifestToFile(const base::ValueView manifest,
const base::FilePath& file_path) {
EXPECT_FALSE(base::PathExists(file_path));
JSONFileValueSerializer serializer(file_path);
EXPECT_TRUE(serializer.Serialize(std::move(manifest)));
EXPECT_TRUE(base::PathExists(file_path));
}
} // namespace
TEST(CdmManifestTest, IsCompatibleWithChrome) {
base::Value::Dict manifest(DefaultManifest());
EXPECT_TRUE(IsCdmManifestCompatibleWithChrome(manifest));
}
TEST(CdmManifestTest, InCompatibleModuleVersion) {
const int kUnsupportedModuleVersion = 0;
EXPECT_FALSE(media::IsSupportedCdmModuleVersion(kUnsupportedModuleVersion));
auto manifest = DefaultManifest();
manifest.Set(kCdmModuleVersionsName,
base::NumberToString(kUnsupportedModuleVersion));
EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, InCompatibleInterfaceVersion) {
const int kUnsupportedInterfaceVersion = kSupportedCdmInterfaceVersion - 1;
EXPECT_FALSE(media::IsSupportedAndEnabledCdmInterfaceVersion(
kUnsupportedInterfaceVersion));
auto manifest = DefaultManifest();
manifest.Set(kCdmInterfaceVersionsName,
base::NumberToString(kUnsupportedInterfaceVersion));
EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, InCompatibleHostVersion) {
const int kUnsupportedHostVersion = kSupportedCdmHostVersion - 1;
EXPECT_FALSE(media::IsSupportedCdmHostVersion(kUnsupportedHostVersion));
auto manifest = DefaultManifest();
manifest.Set(kCdmHostVersionsName,
base::NumberToString(kUnsupportedHostVersion));
EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, IsCompatibleWithMultipleValues) {
auto manifest = DefaultManifest();
manifest.Set(kCdmModuleVersionsName,
MakeStringList(kSupportedCdmModuleVersion));
manifest.Set(kCdmInterfaceVersionsName,
MakeStringList(kSupportedCdmInterfaceVersion));
manifest.Set(kCdmHostVersionsName, MakeStringList(kSupportedCdmHostVersion));
EXPECT_TRUE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, ValidManifest) {
auto manifest = DefaultManifest();
CdmCapability capability;
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs,
{media::VideoCodec::kVP8, media::VideoCodec::kVP9,
media::VideoCodec::kAV1});
CheckAudioCodecs(capability.audio_codecs, {
media::AudioCodec::kOpus,
media::AudioCodec::kVorbis,
media::AudioCodec::kFLAC,
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
media::AudioCodec::kAAC,
#if BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
media::AudioCodec::kDTS,
media::AudioCodec::kDTSE,
media::AudioCodec::kDTSXP2,
#endif // BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
#if BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
media::AudioCodec::kAC3,
media::AudioCodec::kEAC3,
#endif // BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
#endif // BUILDFLAG(USE_PROPRIETARY_CODECS)
});
CheckEncryptionSchemes(
capability.encryption_schemes,
{media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary,
media::CdmSessionType::kPersistentLicense});
}
TEST(CdmManifestTest, EmptyManifest) {
base::Value::Dict manifest;
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
// Manifests require a version.
manifest.Set(kCdmVersion, kVersion);
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {});
CheckAudioCodecs(capability.audio_codecs, {
media::AudioCodec::kOpus,
media::AudioCodec::kVorbis,
media::AudioCodec::kFLAC,
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
media::AudioCodec::kAAC,
#if BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
media::AudioCodec::kDTS,
media::AudioCodec::kDTSE,
media::AudioCodec::kDTSXP2,
#endif // BUILDFLAG(ENABLE_PLATFORM_DTS_AUDIO)
#if BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
media::AudioCodec::kAC3,
media::AudioCodec::kEAC3,
#endif // BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
#endif // BUILDFLAG(USE_PROPRIETARY_CODECS)
});
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
}
TEST(CdmManifestTest, ManifestCodecs) {
auto manifest = DefaultManifest();
// Try each valid value individually.
{
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "vp8");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kVP8});
}
{
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "vp09");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kVP9});
}
{
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "av01");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kAV1});
}
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
{
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "avc1");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kH264});
}
#endif
{
// Try list of everything (except proprietary codecs).
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "vp8,vp09,av01");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs,
{media::VideoCodec::kVP8, media::VideoCodec::kVP9,
media::VideoCodec::kAV1});
}
{
// Empty codecs list result in empty list.
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {});
}
{
// Note that invalid codec values are simply skipped.
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "invalid,av01");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {media::VideoCodec::kAV1});
}
{
// Legacy: "vp9.0" was used to support VP9 profile 0 (no profile 2 support).
// Now this has been deprecated and "vp9.0" becomes an invalid codec value.
CdmCapability capability;
manifest.Set(kCdmCodecsListName, "vp9.0");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {});
}
{
// Wrong types are an error.
CdmCapability capability;
manifest.Set(kCdmCodecsListName, true);
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
// Missing entry is OK, but list is empty.
CdmCapability capability;
EXPECT_TRUE(manifest.Remove(kCdmCodecsListName));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckVideoCodecs(capability.video_codecs, {});
}
}
TEST(CdmManifestTest, ManifestEncryptionSchemes) {
auto manifest = DefaultManifest();
// Try each valid value individually.
{
CdmCapability capability;
manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("cenc"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
}
{
CdmCapability capability;
manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("cbcs"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCbcs});
}
{
// Try multiple valid entries.
CdmCapability capability;
manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("cenc", "cbcs"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(
capability.encryption_schemes,
{media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
}
{
// Invalid encryption schemes are ignored. However, if value specified then
// there must be at least 1 valid value.
CdmCapability capability;
manifest.Set(kCdmSupportedEncryptionSchemesName, MakeList("invalid"));
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
CdmCapability capability;
manifest.Set(kCdmSupportedEncryptionSchemesName,
MakeList("invalid", "cenc"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
}
{
// Wrong types are an error.
CdmCapability capability;
manifest.Set(kCdmSupportedEncryptionSchemesName, true);
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
// Missing values default to "cenc".
CdmCapability capability;
EXPECT_TRUE(manifest.Remove(kCdmSupportedEncryptionSchemesName));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
}
}
TEST(CdmManifestTest, ManifestSessionTypes) {
auto manifest = DefaultManifest();
{
// Try false (persistent license not supported).
CdmCapability capability;
manifest.Set(kCdmPersistentLicenseSupportName, false);
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
}
{
// Try true (persistent license is supported).
CdmCapability capability;
manifest.Set(kCdmPersistentLicenseSupportName, true);
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary,
media::CdmSessionType::kPersistentLicense});
}
{
// Wrong types are an error.
CdmCapability capability;
manifest.Set(kCdmPersistentLicenseSupportName, "true");
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
// Missing values default to "temporary".
CdmCapability capability;
EXPECT_TRUE(manifest.Remove(kCdmPersistentLicenseSupportName));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
}
}
TEST(CdmManifestTest, FileManifest) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
// Manifests read from a file also need a version.
auto manifest = DefaultManifest();
WriteManifestToFile(manifest, manifest_path);
CdmCapability capability;
EXPECT_TRUE(ParseCdmManifestFromPath(manifest_path, &capability));
EXPECT_TRUE(capability.version.IsValid());
EXPECT_EQ(capability.version.GetString(), kVersion);
CheckVideoCodecs(capability.video_codecs,
{media::VideoCodec::kVP8, media::VideoCodec::kVP9,
media::VideoCodec::kAV1});
CheckEncryptionSchemes(
capability.encryption_schemes,
{media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary,
media::CdmSessionType::kPersistentLicense});
}
TEST(CdmManifestTest, FileManifestNoVersion) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
auto manifest = DefaultManifest();
manifest.Remove(kCdmVersion);
WriteManifestToFile(manifest, manifest_path);
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}
TEST(CdmManifestTest, FileManifestBadVersion) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
auto manifest = DefaultManifest();
manifest.Set(kCdmVersion, "bad version");
WriteManifestToFile(manifest, manifest_path);
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}
TEST(CdmManifestTest, FileManifestDoesNotExist) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}
TEST(CdmManifestTest, FileManifestEmpty) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
base::Value::Dict manifest;
WriteManifestToFile(manifest, manifest_path);
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}
TEST(CdmManifestTest, FileManifestLite) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
// Only a version plus fields to satisfy compatibility are required in the
// manifest to parse correctly.
base::Value::Dict manifest;
manifest.Set(kCdmVersion, "1.2.3.4");
manifest.Set(kCdmModuleVersionsName,
base::NumberToString(kSupportedCdmModuleVersion));
manifest.Set(kCdmInterfaceVersionsName,
base::NumberToString(kSupportedCdmInterfaceVersion));
manifest.Set(kCdmHostVersionsName,
base::NumberToString(kSupportedCdmHostVersion));
WriteManifestToFile(manifest, manifest_path);
CdmCapability capability;
EXPECT_TRUE(ParseCdmManifestFromPath(manifest_path, &capability));
CheckVideoCodecs(capability.video_codecs, {});
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
}
TEST(CdmManifestTest, FileManifestNotDictionary) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
base::Value manifest("not a dictionary");
WriteManifestToFile(manifest, manifest_path);
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &capability));
}