forked from mirrors/gecko-dev
Bug 1898588 - part4 : check scheme per codec if encryptionScheme in MediaKeySystemMediaCapability exists. r=jolin
In this patch, we make ContainerSupport be able to store pairs of content type and encryption scheme so that we can check the encryptionScheme per type if that is mentioned in a EME request. Differential Revision: https://phabricator.services.mozilla.com/D211645
This commit is contained in:
parent
6dfee59a8f
commit
2ad5b06f21
5 changed files with 134 additions and 60 deletions
|
|
@ -610,6 +610,29 @@ const char* CryptoSchemeToString(const CryptoScheme& aScheme) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nsCString CryptoSchemeSetToString(const CryptoSchemeSet& aSchemes) {
|
||||||
|
nsAutoCString rv;
|
||||||
|
if (aSchemes.contains(CryptoScheme::Cenc)) {
|
||||||
|
rv.AppendLiteral("cenc");
|
||||||
|
}
|
||||||
|
if (aSchemes.contains(CryptoScheme::Cbcs)) {
|
||||||
|
if (!rv.IsEmpty()) {
|
||||||
|
rv.AppendLiteral("/");
|
||||||
|
}
|
||||||
|
rv.AppendLiteral("cbcs");
|
||||||
|
}
|
||||||
|
if (aSchemes.contains(CryptoScheme::Cbcs_1_9)) {
|
||||||
|
if (!rv.IsEmpty()) {
|
||||||
|
rv.AppendLiteral("/");
|
||||||
|
}
|
||||||
|
rv.AppendLiteral("cbcs-1-9");
|
||||||
|
}
|
||||||
|
if (rv.IsEmpty()) {
|
||||||
|
rv.AppendLiteral("none");
|
||||||
|
}
|
||||||
|
return std::move(rv);
|
||||||
|
}
|
||||||
|
|
||||||
CryptoScheme StringToCryptoScheme(const nsAString& aString) {
|
CryptoScheme StringToCryptoScheme(const nsAString& aString) {
|
||||||
if (aString.EqualsLiteral("cenc")) {
|
if (aString.EqualsLiteral("cenc")) {
|
||||||
return CryptoScheme::Cenc;
|
return CryptoScheme::Cenc;
|
||||||
|
|
|
||||||
|
|
@ -592,6 +592,7 @@ enum class CryptoScheme : uint8_t {
|
||||||
using CryptoSchemeSet = EnumSet<CryptoScheme, uint8_t>;
|
using CryptoSchemeSet = EnumSet<CryptoScheme, uint8_t>;
|
||||||
|
|
||||||
const char* CryptoSchemeToString(const CryptoScheme& aScheme);
|
const char* CryptoSchemeToString(const CryptoScheme& aScheme);
|
||||||
|
nsCString CryptoSchemeSetToString(const CryptoSchemeSet& aSchemes);
|
||||||
CryptoScheme StringToCryptoScheme(const nsAString& aString);
|
CryptoScheme StringToCryptoScheme(const nsAString& aString);
|
||||||
|
|
||||||
class CryptoTrack {
|
class CryptoTrack {
|
||||||
|
|
|
||||||
|
|
@ -171,16 +171,24 @@ void MFCDMCapabilitiesIPDLToKeySystemConfig(
|
||||||
!aKeySystemConfig.mVideoRobustness.Contains(c.robustness())) {
|
!aKeySystemConfig.mVideoRobustness.Contains(c.robustness())) {
|
||||||
aKeySystemConfig.mVideoRobustness.AppendElement(c.robustness());
|
aKeySystemConfig.mVideoRobustness.AppendElement(c.robustness());
|
||||||
}
|
}
|
||||||
|
CryptoSchemeSet schemes;
|
||||||
|
for (const auto& scheme : c.encryptionSchemes()) {
|
||||||
|
schemes += scheme;
|
||||||
|
}
|
||||||
aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
|
aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
|
||||||
NS_ConvertUTF16toUTF8(c.contentType()));
|
NS_ConvertUTF16toUTF8(c.contentType()), Some(schemes));
|
||||||
}
|
}
|
||||||
for (const auto& c : aCDMConfig.audioCapabilities()) {
|
for (const auto& c : aCDMConfig.audioCapabilities()) {
|
||||||
if (!c.robustness().IsEmpty() &&
|
if (!c.robustness().IsEmpty() &&
|
||||||
!aKeySystemConfig.mAudioRobustness.Contains(c.robustness())) {
|
!aKeySystemConfig.mAudioRobustness.Contains(c.robustness())) {
|
||||||
aKeySystemConfig.mAudioRobustness.AppendElement(c.robustness());
|
aKeySystemConfig.mAudioRobustness.AppendElement(c.robustness());
|
||||||
}
|
}
|
||||||
|
CryptoSchemeSet schemes;
|
||||||
|
for (const auto& scheme : c.encryptionSchemes()) {
|
||||||
|
schemes += scheme;
|
||||||
|
}
|
||||||
aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
|
aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
|
||||||
NS_ConvertUTF16toUTF8(c.contentType()));
|
NS_ConvertUTF16toUTF8(c.contentType()), Some(schemes));
|
||||||
}
|
}
|
||||||
aKeySystemConfig.mPersistentState = aCDMConfig.persistentState();
|
aKeySystemConfig.mPersistentState = aCDMConfig.persistentState();
|
||||||
aKeySystemConfig.mDistinctiveIdentifier = aCDMConfig.distinctiveID();
|
aKeySystemConfig.mDistinctiveIdentifier = aCDMConfig.distinctiveID();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#ifndef DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
|
#ifndef DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
|
||||||
#define DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
|
#define DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
|
||||||
|
|
||||||
|
#include "MediaData.h"
|
||||||
#include "nsString.h"
|
#include "nsString.h"
|
||||||
#include "nsTArray.h"
|
#include "nsTArray.h"
|
||||||
#include "mozilla/MozPromise.h"
|
#include "mozilla/MozPromise.h"
|
||||||
|
|
@ -80,38 +81,52 @@ struct KeySystemConfig {
|
||||||
return !mCodecsDecoded.IsEmpty() || !mCodecsDecrypted.IsEmpty();
|
return !mCodecsDecoded.IsEmpty() || !mCodecsDecrypted.IsEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// CDM decrypts and decodes using a DRM robust decoder, and passes decoded
|
// True if a codec and scheme pair can be decrypted and decoded
|
||||||
// samples back to Gecko for rendering.
|
bool DecryptsAndDecodes(
|
||||||
bool DecryptsAndDecodes(const EMECodecString& aCodec) const {
|
const EMECodecString& aCodec,
|
||||||
return mCodecsDecoded.Contains(aCodec);
|
const Maybe<CryptoScheme>& aScheme = Nothing()) const {
|
||||||
|
return CheckCodecAndSchemePair(mCodecsDecoded, aCodec, aScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CDM decrypts and passes the decrypted samples back to Gecko for decoding.
|
// True if a codec and scheme pair can be decrypted
|
||||||
bool Decrypts(const EMECodecString& aCodec) const {
|
bool Decrypts(const EMECodecString& aCodec,
|
||||||
return mCodecsDecrypted.Contains(aCodec);
|
const Maybe<CryptoScheme>& aScheme = Nothing()) const {
|
||||||
|
return CheckCodecAndSchemePair(mCodecsDecrypted, aCodec, aScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetCanDecryptAndDecode(const EMECodecString& aCodec) {
|
void SetCanDecryptAndDecode(
|
||||||
|
const EMECodecString& aCodec,
|
||||||
|
const Maybe<CryptoSchemeSet>& aSchemes = Nothing{}) {
|
||||||
// Can't both decrypt and decrypt-and-decode a codec.
|
// Can't both decrypt and decrypt-and-decode a codec.
|
||||||
MOZ_ASSERT(!Decrypts(aCodec));
|
MOZ_ASSERT(!ContainsDecryptedOnlyCodec(aCodec));
|
||||||
// Prevent duplicates.
|
// Prevent duplicates.
|
||||||
MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
|
MOZ_ASSERT(!ContainsDecryptedAndDecodedCodec(aCodec));
|
||||||
mCodecsDecoded.AppendElement(aCodec);
|
|
||||||
|
mCodecsDecoded.AppendElement(CodecSchemePair{aCodec, aSchemes});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetCanDecrypt(const EMECodecString& aCodec) {
|
void SetCanDecrypt(const EMECodecString& aCodec,
|
||||||
|
const Maybe<CryptoSchemeSet>& aSchemes = Nothing{}) {
|
||||||
// Prevent duplicates.
|
// Prevent duplicates.
|
||||||
MOZ_ASSERT(!Decrypts(aCodec));
|
MOZ_ASSERT(!ContainsDecryptedOnlyCodec(aCodec));
|
||||||
// Can't both decrypt and decrypt-and-decode a codec.
|
// Can't both decrypt and decrypt-and-decode a codec.
|
||||||
MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
|
MOZ_ASSERT(!ContainsDecryptedAndDecodedCodec(aCodec));
|
||||||
mCodecsDecrypted.AppendElement(aCodec);
|
mCodecsDecrypted.AppendElement(CodecSchemePair{aCodec, aSchemes});
|
||||||
}
|
}
|
||||||
|
|
||||||
EMECodecString GetDebugInfo() const {
|
EMECodecString GetDebugInfo() const {
|
||||||
EMECodecString info;
|
EMECodecString info;
|
||||||
info.AppendLiteral("decoding-and-decrypting:[");
|
info.AppendLiteral("decoding-and-decrypting:[");
|
||||||
for (size_t idx = 0; idx < mCodecsDecoded.Length(); idx++) {
|
for (size_t idx = 0; idx < mCodecsDecoded.Length(); idx++) {
|
||||||
info.Append(mCodecsDecoded[idx]);
|
const auto& cur = mCodecsDecoded[idx];
|
||||||
|
info.Append(cur.first);
|
||||||
|
if (cur.second) {
|
||||||
|
info.AppendLiteral("(");
|
||||||
|
info.Append(CryptoSchemeSetToString(*cur.second));
|
||||||
|
info.AppendLiteral(")");
|
||||||
|
} else {
|
||||||
|
info.AppendLiteral("(all)");
|
||||||
|
}
|
||||||
if (idx + 1 < mCodecsDecoded.Length()) {
|
if (idx + 1 < mCodecsDecoded.Length()) {
|
||||||
info.AppendLiteral(",");
|
info.AppendLiteral(",");
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +134,15 @@ struct KeySystemConfig {
|
||||||
info.AppendLiteral("],");
|
info.AppendLiteral("],");
|
||||||
info.AppendLiteral("decrypting-only:[");
|
info.AppendLiteral("decrypting-only:[");
|
||||||
for (size_t idx = 0; idx < mCodecsDecrypted.Length(); idx++) {
|
for (size_t idx = 0; idx < mCodecsDecrypted.Length(); idx++) {
|
||||||
info.Append(mCodecsDecrypted[idx]);
|
const auto& cur = mCodecsDecrypted[idx];
|
||||||
|
info.Append(cur.first);
|
||||||
|
if (cur.second) {
|
||||||
|
info.AppendLiteral("(");
|
||||||
|
info.Append(CryptoSchemeSetToString(*cur.second));
|
||||||
|
info.AppendLiteral(")");
|
||||||
|
} else {
|
||||||
|
info.AppendLiteral("(all)");
|
||||||
|
}
|
||||||
if (idx + 1 < mCodecsDecrypted.Length()) {
|
if (idx + 1 < mCodecsDecrypted.Length()) {
|
||||||
info.AppendLiteral(",");
|
info.AppendLiteral(",");
|
||||||
}
|
}
|
||||||
|
|
@ -129,8 +152,38 @@ struct KeySystemConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
nsTArray<EMECodecString> mCodecsDecoded;
|
using CodecSchemePair = std::pair<EMECodecString, Maybe<CryptoSchemeSet>>;
|
||||||
nsTArray<EMECodecString> mCodecsDecrypted;
|
// These two arrays are exclusive, the codec in one array can't appear on
|
||||||
|
// another array. If CryptoSchemeSet is nothing, that means the codec has
|
||||||
|
// support for all schemes, which is our default. Setting CryptoSchemeSet
|
||||||
|
// explicitly can restrict avaiable schemes for a codec.
|
||||||
|
nsTArray<CodecSchemePair> mCodecsDecoded;
|
||||||
|
nsTArray<CodecSchemePair> mCodecsDecrypted;
|
||||||
|
|
||||||
|
bool ContainsDecryptedOnlyCodec(const EMECodecString& aCodec) const {
|
||||||
|
return std::any_of(
|
||||||
|
mCodecsDecrypted.begin(), mCodecsDecrypted.end(),
|
||||||
|
[&](const auto& aPair) { return aPair.first.Equals(aCodec); });
|
||||||
|
}
|
||||||
|
bool ContainsDecryptedAndDecodedCodec(const EMECodecString& aCodec) const {
|
||||||
|
return std::any_of(
|
||||||
|
mCodecsDecoded.begin(), mCodecsDecoded.end(),
|
||||||
|
[&](const auto& aPair) { return aPair.first.Equals(aCodec); });
|
||||||
|
}
|
||||||
|
bool CheckCodecAndSchemePair(const nsTArray<CodecSchemePair>& aArray,
|
||||||
|
const EMECodecString& aCodec,
|
||||||
|
const Maybe<CryptoScheme>& aScheme) const {
|
||||||
|
return std::any_of(aArray.begin(), aArray.end(), [&](const auto& aPair) {
|
||||||
|
if (!aPair.first.Equals(aCodec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// No scheme is specified, which means accepting all schemes.
|
||||||
|
if (!aPair.second || !aScheme) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return aPair.second->contains(*aScheme);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return true if given key system is supported on the current device.
|
// Return true if given key system is supported on the current device.
|
||||||
|
|
|
||||||
|
|
@ -319,17 +319,18 @@ static bool CanDecryptAndDecode(
|
||||||
CodecType aCodecType,
|
CodecType aCodecType,
|
||||||
const KeySystemConfig::ContainerSupport& aContainerSupport,
|
const KeySystemConfig::ContainerSupport& aContainerSupport,
|
||||||
const nsTArray<KeySystemConfig::EMECodecString>& aCodecs,
|
const nsTArray<KeySystemConfig::EMECodecString>& aCodecs,
|
||||||
|
const Maybe<CryptoScheme>& aScheme,
|
||||||
DecoderDoctorDiagnostics* aDiagnostics) {
|
DecoderDoctorDiagnostics* aDiagnostics) {
|
||||||
MOZ_ASSERT(aCodecType != Invalid);
|
MOZ_ASSERT(aCodecType != Invalid);
|
||||||
for (const KeySystemConfig::EMECodecString& codec : aCodecs) {
|
for (const KeySystemConfig::EMECodecString& codec : aCodecs) {
|
||||||
MOZ_ASSERT(!codec.IsEmpty());
|
MOZ_ASSERT(!codec.IsEmpty());
|
||||||
|
|
||||||
if (aContainerSupport.DecryptsAndDecodes(codec)) {
|
if (aContainerSupport.DecryptsAndDecodes(codec, aScheme)) {
|
||||||
// GMP can decrypt-and-decode this codec.
|
// GMP can decrypt-and-decode this codec.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aContainerSupport.Decrypts(codec)) {
|
if (aContainerSupport.Decrypts(codec, aScheme)) {
|
||||||
IgnoredErrorResult rv;
|
IgnoredErrorResult rv;
|
||||||
MediaSource::IsTypeSupported(aContentType, aDiagnostics, rv);
|
MediaSource::IsTypeSupported(aContentType, aDiagnostics, rv);
|
||||||
if (!rv.Failed()) {
|
if (!rv.Failed()) {
|
||||||
|
|
@ -361,29 +362,18 @@ static bool CanDecryptAndDecode(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns if an encryption scheme is supported per:
|
// https://w3c.github.io/encrypted-media/#dom-mediakeysystemmediacapability-encryptionscheme
|
||||||
// https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md
|
// This convert `encryptionScheme` to the type of CryptoScheme, so that we can
|
||||||
// To be supported the scheme should be one of:
|
// further check whether the scheme is supported or not in our media pipeline.
|
||||||
// - null
|
Maybe<CryptoScheme> ConvertEncryptionSchemeStrToScheme(
|
||||||
// - missing (which will result in the nsString being set to void and thus null)
|
const nsString& aEncryptionScheme) {
|
||||||
// - one of the schemes supported by the CDM
|
|
||||||
// If the pref to enable this behavior is not set, then the value should be
|
|
||||||
// empty/null, as the dict member will not be exposed. In this case we will
|
|
||||||
// always report support as we would before this feature was implemented.
|
|
||||||
static bool SupportsEncryptionScheme(
|
|
||||||
const nsString& aEncryptionScheme,
|
|
||||||
const nsTArray<nsString>& aSupportedEncryptionSchemes) {
|
|
||||||
MOZ_ASSERT(
|
|
||||||
DOMStringIsNull(aEncryptionScheme) ||
|
|
||||||
StaticPrefs::media_eme_encrypted_media_encryption_scheme_enabled(),
|
|
||||||
"Encryption scheme checking support must be preffed on for "
|
|
||||||
"encryptionScheme to be a non-null string");
|
|
||||||
if (DOMStringIsNull(aEncryptionScheme)) {
|
if (DOMStringIsNull(aEncryptionScheme)) {
|
||||||
// "A missing or null value indicates that any encryption scheme is
|
// "A missing or null value indicates that any encryption scheme is
|
||||||
// acceptable."
|
// acceptable."
|
||||||
return true;
|
return Nothing();
|
||||||
}
|
}
|
||||||
return aSupportedEncryptionSchemes.Contains(aEncryptionScheme);
|
auto scheme = StringToCryptoScheme(aEncryptionScheme);
|
||||||
|
return Some(scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ToSessionType(const nsAString& aSessionType,
|
static bool ToSessionType(const nsAString& aSessionType,
|
||||||
|
|
@ -478,7 +468,8 @@ static bool IsParameterUnrecognized(const nsAString& aContentType) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.1.1.3 Get Supported Capabilities for Audio/Video Type
|
// 3.2.2.3 Get Supported Capabilities for Audio/Video Type
|
||||||
|
// https://w3c.github.io/encrypted-media/#get-supported-capabilities-for-audio-video-type
|
||||||
static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities(
|
static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities(
|
||||||
const CodecType aCodecType,
|
const CodecType aCodecType,
|
||||||
const nsTArray<MediaKeySystemMediaCapability>& aRequestedCapabilities,
|
const nsTArray<MediaKeySystemMediaCapability>& aRequestedCapabilities,
|
||||||
|
|
@ -667,6 +658,20 @@ static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities(
|
||||||
NS_ConvertUTF16toUTF8(encryptionScheme).get());
|
NS_ConvertUTF16toUTF8(encryptionScheme).get());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// If encryption scheme is non-null and is not recognized or not supported
|
||||||
|
// by implementation, continue to the next iteration.
|
||||||
|
const auto scheme = ConvertEncryptionSchemeStrToScheme(encryptionScheme);
|
||||||
|
if (scheme && *scheme == CryptoScheme::None) {
|
||||||
|
EME_LOG(
|
||||||
|
"MediaKeySystemConfiguration (label='%s') "
|
||||||
|
"MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
|
||||||
|
"unsupported scheme string.",
|
||||||
|
NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
|
||||||
|
NS_ConvertUTF16toUTF8(contentTypeString).get(),
|
||||||
|
NS_ConvertUTF16toUTF8(robustness).get(),
|
||||||
|
NS_ConvertUTF16toUTF8(encryptionScheme).get());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// If robustness is not the empty string and contains an unrecognized
|
// If robustness is not the empty string and contains an unrecognized
|
||||||
// value or a value not supported by implementation, continue to the
|
// value or a value not supported by implementation, continue to the
|
||||||
// next iteration. String comparison is case-sensitive.
|
// next iteration. String comparison is case-sensitive.
|
||||||
|
|
@ -698,22 +703,6 @@ static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities(
|
||||||
// Note: specified robustness requirements are satisfied.
|
// Note: specified robustness requirements are satisfied.
|
||||||
}
|
}
|
||||||
|
|
||||||
// If preffed on: "In the Get Supported Capabilities for Audio/Video Type
|
|
||||||
// algorithm, implementations must skip capabilities specifying unsupported
|
|
||||||
// encryption schemes."
|
|
||||||
if (!SupportsEncryptionScheme(encryptionScheme,
|
|
||||||
aKeySystem.mEncryptionSchemes)) {
|
|
||||||
EME_LOG(
|
|
||||||
"MediaKeySystemConfiguration (label='%s') "
|
|
||||||
"MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
|
|
||||||
"encryption scheme unsupported by CDM requested.",
|
|
||||||
NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
|
|
||||||
NS_ConvertUTF16toUTF8(contentTypeString).get(),
|
|
||||||
NS_ConvertUTF16toUTF8(robustness).get(),
|
|
||||||
NS_ConvertUTF16toUTF8(encryptionScheme).get());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user agent and implementation definitely support playback of
|
// If the user agent and implementation definitely support playback of
|
||||||
// encrypted media data for the combination of container, media types,
|
// encrypted media data for the combination of container, media types,
|
||||||
// robustness and local accumulated configuration in combination with
|
// robustness and local accumulated configuration in combination with
|
||||||
|
|
@ -721,7 +710,7 @@ static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities(
|
||||||
const auto& containerSupport =
|
const auto& containerSupport =
|
||||||
supportedInMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM;
|
supportedInMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM;
|
||||||
if (!CanDecryptAndDecode(aKeySystem.mKeySystem, contentTypeString,
|
if (!CanDecryptAndDecode(aKeySystem.mKeySystem, contentTypeString,
|
||||||
majorType, containerSupport, codecs,
|
majorType, containerSupport, codecs, scheme,
|
||||||
aDiagnostics)) {
|
aDiagnostics)) {
|
||||||
EME_LOG(
|
EME_LOG(
|
||||||
"MediaKeySystemConfiguration (label='%s') "
|
"MediaKeySystemConfiguration (label='%s') "
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue