/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/MediaKeySystemAccess.h" #include #include "DecoderDoctorDiagnostics.h" #include "DecoderTraits.h" #include "MP4Decoder.h" #include "MediaContainerType.h" #include "WebMDecoder.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/EMEUtils.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_media.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/KeySystemNames.h" #include "mozilla/dom/MediaKeySession.h" #include "mozilla/dom/MediaKeySystemAccessBinding.h" #include "mozilla/dom/MediaKeySystemAccessManager.h" #include "mozilla/dom/MediaSource.h" #include "nsDOMString.h" #include "nsIObserverService.h" #include "nsMimeTypes.h" #include "nsReadableUtils.h" #include "nsServiceManagerUtils.h" #include "nsUnicharUtils.h" #ifdef XP_WIN # include "WMFDecoderModule.h" #endif #ifdef MOZ_WIDGET_ANDROID # include "mozilla/java/MediaDrmProxyWrappers.h" #endif namespace mozilla::dom { #define LOG(msg, ...) \ EME_LOG("MediaKeySystemAccess::%s " msg, __func__, ##__VA_ARGS__) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeySystemAccess, mParent) NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccess) NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccess) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccess) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END static nsCString ToCString(const MediaKeySystemConfiguration& aConfig); MediaKeySystemAccess::MediaKeySystemAccess( nsPIDOMWindowInner* aParent, const nsAString& aKeySystem, const MediaKeySystemConfiguration& aConfig) : mParent(aParent), mKeySystem(aKeySystem), mConfig(aConfig) { LOG("Created MediaKeySystemAccess for keysystem=%s config=%s", NS_ConvertUTF16toUTF8(mKeySystem).get(), mozilla::dom::ToCString(mConfig).get()); } MediaKeySystemAccess::~MediaKeySystemAccess() = default; JSObject* MediaKeySystemAccess::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return MediaKeySystemAccess_Binding::Wrap(aCx, this, aGivenProto); } nsPIDOMWindowInner* MediaKeySystemAccess::GetParentObject() const { return mParent; } void MediaKeySystemAccess::GetKeySystem(nsString& aOutKeySystem) const { aOutKeySystem.Assign(mKeySystem); } void MediaKeySystemAccess::GetConfiguration( MediaKeySystemConfiguration& aConfig) { aConfig = mConfig; } already_AddRefed MediaKeySystemAccess::CreateMediaKeys( ErrorResult& aRv) { RefPtr keys(new MediaKeys(mParent, mKeySystem, mConfig)); return keys->Init(aRv); } enum class SecureLevel { Software, Hardware, }; static MediaKeySystemStatus EnsureCDMInstalled(const nsAString& aKeySystem, const SecureLevel aSecure, nsACString& aOutMessage) { if (aSecure == SecureLevel::Software && !KeySystemConfig::Supports(aKeySystem)) { aOutMessage = "CDM is not installed"_ns; return MediaKeySystemStatus::Cdm_not_installed; } #ifdef MOZ_WMF_CDM if (aSecure == SecureLevel::Hardware) { // Ensure we check the hardware key system name. nsAutoString hardwareKeySystem; if (IsWidevineKeySystem(aKeySystem) || IsWidevineExperimentKeySystemAndSupported(aKeySystem)) { hardwareKeySystem = NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName); } else if (IsPlayReadyKeySystemAndSupported(aKeySystem)) { hardwareKeySystem = NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware); } else { MOZ_ASSERT_UNREACHABLE("Not supported key system for HWDRM!"); } if (!KeySystemConfig::Supports(hardwareKeySystem)) { aOutMessage = "CDM is not installed"_ns; return MediaKeySystemStatus::Cdm_not_installed; } } #endif return MediaKeySystemStatus::Available; } /* static */ MediaKeySystemStatus MediaKeySystemAccess::GetKeySystemStatus( const MediaKeySystemAccessRequest& aRequest, nsACString& aOutMessage) { const nsString& keySystem = aRequest.mKeySystem; MOZ_ASSERT(StaticPrefs::media_eme_enabled() || IsClearkeyKeySystem(keySystem)); LOG("checking if CDM is installed or disabled for %s", NS_ConvertUTF16toUTF8(keySystem).get()); if (IsClearkeyKeySystem(keySystem)) { return EnsureCDMInstalled(keySystem, SecureLevel::Software, aOutMessage); } // This is used to determine if we need to download Widevine L1. bool shouldCheckL1Installation = false; #ifdef MOZ_WMF_CDM if (StaticPrefs::media_eme_widevine_experiment_enabled()) { shouldCheckL1Installation = CheckIfHarewareDRMConfigExists(aRequest.mConfigs) || IsWidevineExperimentKeySystemAndSupported(keySystem); } #endif // Check Widevine L3 if (IsWidevineKeySystem(keySystem) && !shouldCheckL1Installation) { if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) { if (!Preferences::GetBool("media.gmp-widevinecdm.enabled", false)) { aOutMessage = "Widevine EME disabled"_ns; return MediaKeySystemStatus::Cdm_disabled; } return EnsureCDMInstalled(keySystem, SecureLevel::Software, aOutMessage); #ifdef MOZ_WIDGET_ANDROID } else if (Preferences::GetBool("media.mediadrm-widevinecdm.visible", false)) { bool supported = mozilla::java::MediaDrmProxy::IsSchemeSupported(keySystem); if (!supported) { aOutMessage = nsLiteralCString( "KeySystem or Minimum API level not met for Widevine EME"); return MediaKeySystemStatus::Cdm_not_supported; } return MediaKeySystemStatus::Available; #endif } } #ifdef MOZ_WMF_CDM // Check PlayReady, which is a built-in CDM on most Windows versions. if (IsPlayReadyKeySystemAndSupported(keySystem) && KeySystemConfig::Supports(keySystem)) { return MediaKeySystemStatus::Available; } // Check Widevine L1. This can be a request for experimental key systems, or // for a normal key system with hardware robustness. if ((IsWidevineExperimentKeySystemAndSupported(keySystem) || IsWidevineKeySystem(keySystem)) && shouldCheckL1Installation) { // TODO : if L3 hasn't been installed as well, should we fallback to install // L3? if (!Preferences::GetBool("media.gmp-widevinecdm-l1.enabled", false)) { aOutMessage = "Widevine L1 EME disabled"_ns; return MediaKeySystemStatus::Cdm_disabled; } return EnsureCDMInstalled(keySystem, SecureLevel::Hardware, aOutMessage); } #endif return MediaKeySystemStatus::Cdm_not_supported; } static KeySystemConfig::EMECodecString ToEMEAPICodecString( const nsString& aCodec) { if (IsAACCodecString(aCodec)) { return KeySystemConfig::EME_CODEC_AAC; } if (aCodec.EqualsLiteral("opus")) { return KeySystemConfig::EME_CODEC_OPUS; } if (aCodec.EqualsLiteral("vorbis")) { return KeySystemConfig::EME_CODEC_VORBIS; } if (aCodec.EqualsLiteral("flac")) { return KeySystemConfig::EME_CODEC_FLAC; } if (IsH264CodecString(aCodec)) { return KeySystemConfig::EME_CODEC_H264; } if (IsAV1CodecString(aCodec)) { return KeySystemConfig::EME_CODEC_AV1; } if (IsVP8CodecString(aCodec)) { return KeySystemConfig::EME_CODEC_VP8; } if (IsVP9CodecString(aCodec)) { return KeySystemConfig::EME_CODEC_VP9; } #ifdef MOZ_WMF if (IsH265CodecString(aCodec)) { return KeySystemConfig::EME_CODEC_HEVC; } #endif return ""_ns; } static RefPtr GetSupportedKeySystemConfigs(const nsAString& aKeySystem, bool aIsHardwareDecryption) { using DecryptionInfo = KeySystemConfig::DecryptionInfo; nsTArray requests; // Software Widevine and Clearkey if (IsWidevineKeySystem(aKeySystem) || IsClearkeyKeySystem(aKeySystem)) { requests.AppendElement( KeySystemConfigRequest{aKeySystem, DecryptionInfo::Software}); } #ifdef MOZ_WMF_CDM if (IsPlayReadyEnabled()) { // PlayReady software and hardware if (aKeySystem.EqualsLiteral(kPlayReadyKeySystemName) || aKeySystem.EqualsLiteral(kPlayReadyKeySystemHardware)) { requests.AppendElement( KeySystemConfigRequest{NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), DecryptionInfo::Software}); if (aIsHardwareDecryption) { requests.AppendElement(KeySystemConfigRequest{ NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), DecryptionInfo::Hardware}); requests.AppendElement(KeySystemConfigRequest{ NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware), DecryptionInfo::Hardware}); } } // PlayReady clearlead if (aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName)) { requests.AppendElement(KeySystemConfigRequest{ NS_ConvertUTF8toUTF16(kPlayReadyHardwareClearLeadKeySystemName), DecryptionInfo::Hardware}); } } if (IsWidevineHardwareDecryptionEnabled()) { // Widevine hardware if (aKeySystem.EqualsLiteral(kWidevineExperimentKeySystemName) || (IsWidevineKeySystem(aKeySystem) && aIsHardwareDecryption)) { requests.AppendElement(KeySystemConfigRequest{ NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName), DecryptionInfo::Hardware}); } // Widevine clearlead if (aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName)) { requests.AppendElement(KeySystemConfigRequest{ NS_ConvertUTF8toUTF16(kWidevineExperiment2KeySystemName), DecryptionInfo::Hardware}); } } #endif return KeySystemConfig::CreateKeySystemConfigs(requests); } /* static */ RefPtr MediaKeySystemAccess::KeySystemSupportsInitDataType( const nsAString& aKeySystem, const nsAString& aInitDataType, bool aIsHardwareDecryption) { RefPtr promise = new GenericPromise::Private(__func__); GetSupportedKeySystemConfigs(aKeySystem, aIsHardwareDecryption) ->Then(GetMainThreadSerialEventTarget(), __func__, [promise, initDataType = nsString{std::move(aInitDataType)}]( const KeySystemConfig::SupportedConfigsPromise:: ResolveOrRejectValue& aResult) { if (aResult.IsResolve()) { for (const auto& config : aResult.ResolveValue()) { if (config.mInitDataTypes.Contains(initDataType)) { promise->Resolve(true, __func__); return; } } } promise->Reject(NS_ERROR_DOM_MEDIA_CDM_ERR, __func__); }); return promise.forget(); } enum CodecType { Audio, Video, Invalid }; static bool CanDecryptAndDecode( const nsString& aKeySystem, const nsString& aContentType, CodecType aCodecType, const KeySystemConfig::ContainerSupport& aContainerSupport, const nsTArray& aCodecs, DecoderDoctorDiagnostics* aDiagnostics) { MOZ_ASSERT(aCodecType != Invalid); for (const KeySystemConfig::EMECodecString& codec : aCodecs) { MOZ_ASSERT(!codec.IsEmpty()); if (aContainerSupport.DecryptsAndDecodes(codec)) { // GMP can decrypt-and-decode this codec. continue; } if (aContainerSupport.Decrypts(codec)) { IgnoredErrorResult rv; MediaSource::IsTypeSupported(aContentType, aDiagnostics, rv); if (!rv.Failed()) { // GMP can decrypt and is allowed to return compressed samples to // Gecko to decode, and Gecko has a decoder. continue; } } // Neither the GMP nor Gecko can both decrypt and decode. We don't // support this codec. #if defined(XP_WIN) // Widevine CDM doesn't include an AAC decoder. So if WMF can't // decode AAC, and a codec wasn't specified, be conservative // and reject the MediaKeys request, since we assume Widevine // will be used with AAC. if (codec == KeySystemConfig::EME_CODEC_AAC && IsWidevineKeySystem(aKeySystem) && !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) { if (aDiagnostics) { aDiagnostics->SetKeySystemIssue( DecoderDoctorDiagnostics::eWidevineWithNoWMF); } } #endif return false; } return true; } // Returns if an encryption scheme is supported per: // https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md // To be supported the scheme should be one of: // - null // - missing (which will result in the nsString being set to void and thus null) // - 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& 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)) { // "A missing or null value indicates that any encryption scheme is // acceptable." return true; } return aSupportedEncryptionSchemes.Contains(aEncryptionScheme); } static bool ToSessionType(const nsAString& aSessionType, MediaKeySessionType& aOutType) { Maybe type = StringToEnum(aSessionType); if (type.isNothing()) { return false; } aOutType = type.value(); return true; } // 5.1.1 Is persistent session type? static bool IsPersistentSessionType(MediaKeySessionType aSessionType) { return aSessionType == MediaKeySessionType::Persistent_license; } static bool ContainsSessionType( const nsTArray& aTypes, const MediaKeySessionType& aSessionType) { return (aSessionType == MediaKeySessionType::Persistent_license && aTypes.Contains(KeySystemConfig::SessionType::PersistentLicense)) || (aSessionType == MediaKeySessionType::Temporary && aTypes.Contains(KeySystemConfig::SessionType::Temporary)); } CodecType GetMajorType(const MediaMIMEType& aMIMEType) { if (aMIMEType.HasAudioMajorType()) { return Audio; } if (aMIMEType.HasVideoMajorType()) { return Video; } return Invalid; } static CodecType GetCodecType(const KeySystemConfig::EMECodecString& aCodec) { if (aCodec.Equals(KeySystemConfig::EME_CODEC_AAC) || aCodec.Equals(KeySystemConfig::EME_CODEC_OPUS) || aCodec.Equals(KeySystemConfig::EME_CODEC_VORBIS) || aCodec.Equals(KeySystemConfig::EME_CODEC_FLAC)) { return Audio; } if (aCodec.Equals(KeySystemConfig::EME_CODEC_H264) || aCodec.Equals(KeySystemConfig::EME_CODEC_AV1) || aCodec.Equals(KeySystemConfig::EME_CODEC_VP8) || aCodec.Equals(KeySystemConfig::EME_CODEC_VP9) || aCodec.Equals(KeySystemConfig::EME_CODEC_HEVC)) { return Video; } return Invalid; } static bool AllCodecsOfType( const nsTArray& aCodecs, const CodecType aCodecType) { for (const KeySystemConfig::EMECodecString& codec : aCodecs) { if (GetCodecType(codec) != aCodecType) { return false; } } return true; } static bool IsParameterUnrecognized(const nsAString& aContentType) { nsAutoString contentType(aContentType); contentType.StripWhitespace(); nsTArray params; nsAString::const_iterator start, end, semicolon, equalSign; contentType.BeginReading(start); contentType.EndReading(end); semicolon = start; // Find any substring between ';' & '='. while (semicolon != end) { if (FindCharInReadable(';', semicolon, end)) { equalSign = ++semicolon; if (FindCharInReadable('=', equalSign, end)) { params.AppendElement(Substring(semicolon, equalSign)); semicolon = equalSign; } } } for (auto param : params) { if (!param.LowerCaseEqualsLiteral("codecs") && !param.LowerCaseEqualsLiteral("profiles")) { return true; } } return false; } // 3.1.1.3 Get Supported Capabilities for Audio/Video Type static Sequence GetSupportedCapabilities( const CodecType aCodecType, const nsTArray& aRequestedCapabilities, const MediaKeySystemConfiguration& aPartialConfig, const KeySystemConfig& aKeySystem, DecoderDoctorDiagnostics* aDiagnostics, const Document* aDocument) { // Let local accumulated configuration be a local copy of partial // configuration. (Note: It's not necessary for us to maintain a local copy, // as we don't need to test whether capabilites from previous calls to this // algorithm work with the capabilities currently being considered in this // call. ) // Let supported media capabilities be an empty sequence of // MediaKeySystemMediaCapability dictionaries. Sequence supportedCapabilities; // For each requested media capability in requested media capabilities: for (const MediaKeySystemMediaCapability& capabilities : aRequestedCapabilities) { // Let content type be requested media capability's contentType member. const nsString& contentTypeString = capabilities.mContentType; // Let robustness be requested media capability's robustness member. const nsString& robustness = capabilities.mRobustness; // Optional encryption scheme extension, see // https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md // This will only be exposed to JS if // media.eme.encrypted-media-encryption-scheme.enabled is preffed on. const nsString encryptionScheme = capabilities.mEncryptionScheme; // If content type is the empty string, return null. if (contentTypeString.IsEmpty()) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') rejected; " "audio or video capability has empty contentType.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); return Sequence(); } // If content type is an invalid or unrecognized MIME type, continue // to the next iteration. Maybe maybeContainerType = MakeMediaContainerType(contentTypeString); if (!maybeContainerType) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "failed to parse contentTypeString as MIME type.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } const MediaContainerType& containerType = *maybeContainerType; bool invalid = false; nsTArray codecs; for (const auto& codecString : containerType.ExtendedType().Codecs().Range()) { KeySystemConfig::EMECodecString emeCodec = ToEMEAPICodecString(nsString(codecString)); if (emeCodec.IsEmpty()) { invalid = true; EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "'%s' is an invalid codec string.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get(), NS_ConvertUTF16toUTF8(codecString).get()); break; } codecs.AppendElement(emeCodec); } if (invalid) { continue; } // If the user agent does not support container, continue to the next // iteration. The case-sensitivity of string comparisons is determined by // the appropriate RFC. (Note: Per RFC 6838 [RFC6838], "Both top-level type // and subtype names are case-insensitive."'. We're using // nsContentTypeParser and that is case-insensitive and converts all its // parameter outputs to lower case.) const bool supportedInMP4 = MP4Decoder::IsSupportedType(containerType, aDiagnostics); if (supportedInMP4 && !aKeySystem.mMP4.IsSupported()) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "MP4 requested but unsupported.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } const bool isWebM = WebMDecoder::IsSupportedType(containerType); if (isWebM && !aKeySystem.mWebM.IsSupported()) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s,'%s') unsupported; " "WebM requested but unsupported.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } if (!supportedInMP4 && !isWebM) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "Unsupported or unrecognized container requested.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } // Let parameters be the RFC 6381[RFC6381] parameters, if any, specified by // content type. // If the user agent does not recognize one or more parameters, continue to // the next iteration. if (IsParameterUnrecognized(contentTypeString)) { continue; } // Let media types be the set of codecs and codec constraints specified by // parameters. The case-sensitivity of string comparisons is determined by // the appropriate RFC or other specification. // (Note: codecs array is 'parameter'). // If media types is empty: if (codecs.IsEmpty()) { // Log deprecation warning to encourage authors to not do this! DeprecationWarningLog(aDocument, "MediaEMENoCodecsDeprecatedWarning"); // TODO: Remove this once we're sure it doesn't break the web. // If container normatively implies a specific set of codecs and codec // constraints: Let parameters be that set. if (supportedInMP4) { if (aCodecType == Audio) { codecs.AppendElement(KeySystemConfig::EME_CODEC_AAC); } else if (aCodecType == Video) { codecs.AppendElement(KeySystemConfig::EME_CODEC_H264); } } else if (isWebM) { if (aCodecType == Audio) { codecs.AppendElement(KeySystemConfig::EME_CODEC_VORBIS); } else if (aCodecType == Video) { codecs.AppendElement(KeySystemConfig::EME_CODEC_VP8); } } // Otherwise: Continue to the next iteration. // (Note: all containers we support have implied codecs, so don't continue // here.) } // If container type is not strictly a audio/video type, continue to the // next iteration. const auto majorType = GetMajorType(containerType.Type()); if (majorType == Invalid) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "MIME type is not an audio or video MIME type.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } if (majorType != aCodecType || !AllCodecsOfType(codecs, aCodecType)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "MIME type mixes audio codecs in video capabilities " "or video codecs in audio capabilities.", 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 // value or a value not supported by implementation, continue to the // next iteration. String comparison is case-sensitive. if (!robustness.IsEmpty()) { if (majorType == Audio && !aKeySystem.mAudioRobustness.Contains(robustness)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "unsupported robustness string.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } if (majorType == Video && !aKeySystem.mVideoRobustness.Contains(robustness)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "unsupported robustness string.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } // 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 // encrypted media data for the combination of container, media types, // robustness and local accumulated configuration in combination with // restrictions... const auto& containerSupport = supportedInMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM; if (!CanDecryptAndDecode(aKeySystem.mKeySystem, contentTypeString, majorType, containerSupport, codecs, aDiagnostics)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') " "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; " "codec unsupported by CDM requested.", NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), NS_ConvertUTF16toUTF8(contentTypeString).get(), NS_ConvertUTF16toUTF8(robustness).get(), NS_ConvertUTF16toUTF8(encryptionScheme).get()); continue; } // ... add requested media capability to supported media capabilities. if (!supportedCapabilities.AppendElement(capabilities, mozilla::fallible)) { NS_WARNING("GetSupportedCapabilities: Malloc failure"); return Sequence(); } // Note: omitting steps 3.13.2, our robustness is not sophisticated enough // to require considering all requirements together. } return supportedCapabilities; } // "Get Supported Configuration and Consent" algorithm, steps 4-7 for // distinctive identifier, and steps 8-11 for persistent state. The steps // are the same for both requirements/features, so we factor them out into // a single function. static bool CheckRequirement( const MediaKeysRequirement aRequirement, const KeySystemConfig::Requirement aKeySystemRequirement, MediaKeysRequirement& aOutRequirement) { // Let requirement be the value of candidate configuration's member. MediaKeysRequirement requirement = aRequirement; // If requirement is "optional" and feature is not allowed according to // restrictions, set requirement to "not-allowed". if (aRequirement == MediaKeysRequirement::Optional && aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) { requirement = MediaKeysRequirement::Not_allowed; } // Follow the steps for requirement from the following list: switch (requirement) { case MediaKeysRequirement::Required: { // If the implementation does not support use of requirement in // combination with accumulated configuration and restrictions, return // NotSupported. if (aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) { return false; } break; } case MediaKeysRequirement::Optional: { // Continue with the following steps. break; } case MediaKeysRequirement::Not_allowed: { // If the implementation requires use of feature in combination with // accumulated configuration and restrictions, return NotSupported. if (aKeySystemRequirement == KeySystemConfig::Requirement::Required) { return false; } break; } default: { return false; } } // Set the requirement member of accumulated configuration to equal // calculated requirement. aOutRequirement = requirement; return true; } // 3.1.1.2, step 12 // Follow the steps for the first matching condition from the following list: // If the sessionTypes member is present in candidate configuration. // Let session types be candidate configuration's sessionTypes member. // Otherwise let session types be ["temporary"]. // Note: This returns an empty array on malloc failure. static Sequence UnboxSessionTypes( const Optional>& aSessionTypes) { Sequence sessionTypes; if (aSessionTypes.WasPassed()) { sessionTypes = aSessionTypes.Value(); } else { // Note: fallible. Results in an empty array. (void)sessionTypes.AppendElement(ToString(MediaKeySessionType::Temporary), mozilla::fallible); } return sessionTypes; } // 3.1.1.2 Get Supported Configuration and Consent static bool GetSupportedConfig(const KeySystemConfig& aKeySystem, const MediaKeySystemConfiguration& aCandidate, MediaKeySystemConfiguration& aOutConfig, DecoderDoctorDiagnostics* aDiagnostics, bool aInPrivateBrowsing, const Document* aDocument) { EME_LOG("Compare implementation '%s'\n with request '%s'", NS_ConvertUTF16toUTF8(aKeySystem.GetDebugInfo()).get(), ToCString(aCandidate).get()); // Let accumulated configuration be a new MediaKeySystemConfiguration // dictionary. MediaKeySystemConfiguration config; // Set the label member of accumulated configuration to equal the label member // of candidate configuration. config.mLabel = aCandidate.mLabel; // If the initDataTypes member of candidate configuration is non-empty, run // the following steps: if (!aCandidate.mInitDataTypes.IsEmpty()) { // Let supported types be an empty sequence of DOMStrings. nsTArray supportedTypes; // For each value in candidate configuration's initDataTypes member: for (const nsString& initDataType : aCandidate.mInitDataTypes) { // Let initDataType be the value. // If the implementation supports generating requests based on // initDataType, add initDataType to supported types. String comparison is // case-sensitive. The empty string is never supported. if (aKeySystem.mInitDataTypes.Contains(initDataType)) { supportedTypes.AppendElement(initDataType); } } // If supported types is empty, return NotSupported. if (supportedTypes.IsEmpty()) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "no supported initDataTypes provided.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } // Set the initDataTypes member of accumulated configuration to supported // types. if (!config.mInitDataTypes.Assign(supportedTypes)) { return false; } } if (!CheckRequirement(aCandidate.mDistinctiveIdentifier, aKeySystem.mDistinctiveIdentifier, config.mDistinctiveIdentifier)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "distinctiveIdentifier requirement not satisfied.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } if (!CheckRequirement(aCandidate.mPersistentState, aKeySystem.mPersistentState, config.mPersistentState)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "persistentState requirement not satisfied.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } if (config.mPersistentState == MediaKeysRequirement::Required && aInPrivateBrowsing) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "persistentState requested in Private Browsing window.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } Sequence sessionTypes(UnboxSessionTypes(aCandidate.mSessionTypes)); if (sessionTypes.IsEmpty()) { // Malloc failure. return false; } // For each value in session types: for (const auto& sessionTypeString : sessionTypes) { // Let session type be the value. MediaKeySessionType sessionType; if (!ToSessionType(sessionTypeString, sessionType)) { // (Assume invalid sessionType is unsupported as per steps below). EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "invalid session type specified.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } // If accumulated configuration's persistentState value is "not-allowed" // and the Is persistent session type? algorithm returns true for session // type return NotSupported. if (config.mPersistentState == MediaKeysRequirement::Not_allowed && IsPersistentSessionType(sessionType)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "persistent session requested but keysystem doesn't" "support persistent state.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } // If the implementation does not support session type in combination // with accumulated configuration and restrictions for other reasons, // return NotSupported. if (!ContainsSessionType(aKeySystem.mSessionTypes, sessionType)) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "session type '%s' unsupported by keySystem.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get(), NS_ConvertUTF16toUTF8(sessionTypeString).get()); return false; } // If accumulated configuration's persistentState value is "optional" // and the result of running the Is persistent session type? algorithm // on session type is true, change accumulated configuration's // persistentState value to "required". if (config.mPersistentState == MediaKeysRequirement::Optional && IsPersistentSessionType(sessionType)) { config.mPersistentState = MediaKeysRequirement::Required; } } // Set the sessionTypes member of accumulated configuration to session types. config.mSessionTypes.Construct(std::move(sessionTypes)); // If the videoCapabilities and audioCapabilities members in candidate // configuration are both empty, return NotSupported. if (aCandidate.mAudioCapabilities.IsEmpty() && aCandidate.mVideoCapabilities.IsEmpty()) { // TODO: Most sites using EME still don't pass capabilities, so we // can't reject on it yet without breaking them. So add this later. // Log deprecation warning to encourage authors to not do this! DeprecationWarningLog(aDocument, "MediaEMENoCapabilitiesDeprecatedWarning"); } // If the videoCapabilities member in candidate configuration is non-empty: if (!aCandidate.mVideoCapabilities.IsEmpty()) { // Let video capabilities be the result of executing the Get Supported // Capabilities for Audio/Video Type algorithm on Video, candidate // configuration's videoCapabilities member, accumulated configuration, // and restrictions. Sequence caps = GetSupportedCapabilities(Video, aCandidate.mVideoCapabilities, config, aKeySystem, aDiagnostics, aDocument); // If video capabilities is null, return NotSupported. if (caps.IsEmpty()) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "no supported video capabilities.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } // Set the videoCapabilities member of accumulated configuration to video // capabilities. config.mVideoCapabilities = std::move(caps); } else { // Otherwise: // Set the videoCapabilities member of accumulated configuration to an empty // sequence. } // If the audioCapabilities member in candidate configuration is non-empty: if (!aCandidate.mAudioCapabilities.IsEmpty()) { // Let audio capabilities be the result of executing the Get Supported // Capabilities for Audio/Video Type algorithm on Audio, candidate // configuration's audioCapabilities member, accumulated configuration, and // restrictions. Sequence caps = GetSupportedCapabilities(Audio, aCandidate.mAudioCapabilities, config, aKeySystem, aDiagnostics, aDocument); // If audio capabilities is null, return NotSupported. if (caps.IsEmpty()) { EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "no supported audio capabilities.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } // Set the audioCapabilities member of accumulated configuration to audio // capabilities. config.mAudioCapabilities = std::move(caps); } else { // Otherwise: // Set the audioCapabilities member of accumulated configuration to an empty // sequence. } // If accumulated configuration's distinctiveIdentifier value is "optional", // follow the steps for the first matching condition from the following list: if (config.mDistinctiveIdentifier == MediaKeysRequirement::Optional) { // If the implementation requires use Distinctive Identifier(s) or // Distinctive Permanent Identifier(s) for any of the combinations // in accumulated configuration if (aKeySystem.mDistinctiveIdentifier == KeySystemConfig::Requirement::Required) { // Change accumulated configuration's distinctiveIdentifier value to // "required". config.mDistinctiveIdentifier = MediaKeysRequirement::Required; } else { // Otherwise, change accumulated configuration's distinctiveIdentifier // value to "not-allowed". config.mDistinctiveIdentifier = MediaKeysRequirement::Not_allowed; } } // If accumulated configuration's persistentState value is "optional", follow // the steps for the first matching condition from the following list: if (config.mPersistentState == MediaKeysRequirement::Optional) { // If the implementation requires persisting state for any of the // combinations in accumulated configuration if (aKeySystem.mPersistentState == KeySystemConfig::Requirement::Required) { // Change accumulated configuration's persistentState value to "required". config.mPersistentState = MediaKeysRequirement::Required; } else { // Otherwise, change accumulated configuration's persistentState // value to "not-allowed". config.mPersistentState = MediaKeysRequirement::Not_allowed; } } // Note: Omitting steps 20-22. We don't ask for consent. #if defined(XP_WIN) // Widevine CDM doesn't include an AAC decoder. So if WMF can't decode AAC, // and a codec wasn't specified, be conservative and reject the MediaKeys // request. if (IsWidevineKeySystem(aKeySystem.mKeySystem) && (aCandidate.mAudioCapabilities.IsEmpty() || aCandidate.mVideoCapabilities.IsEmpty()) && !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) { if (aDiagnostics) { aDiagnostics->SetKeySystemIssue( DecoderDoctorDiagnostics::eWidevineWithNoWMF); } EME_LOG( "MediaKeySystemConfiguration (label='%s') rejected; " "WMF required for Widevine decoding, but it's not available.", NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); return false; } #endif // Return accumulated configuration. aOutConfig = config; return true; } /* static */ RefPtr MediaKeySystemAccess::GetSupportedConfig(MediaKeySystemAccessRequest* aRequest, bool aIsPrivateBrowsing, const Document* aDocument) { nsTArray implementations; const bool isHardwareDecryptionRequest = CheckIfHarewareDRMConfigExists(aRequest->mConfigs) || DoesKeySystemSupportHardwareDecryption(aRequest->mKeySystem); RefPtr promise = new KeySystemConfig::KeySystemConfigPromise::Private(__func__); GetSupportedKeySystemConfigs(aRequest->mKeySystem, isHardwareDecryptionRequest) ->Then(GetMainThreadSerialEventTarget(), __func__, [promise, aRequest, aIsPrivateBrowsing, document = RefPtr{aDocument}]( const KeySystemConfig::SupportedConfigsPromise:: ResolveOrRejectValue& aResult) { if (aResult.IsResolve()) { MediaKeySystemConfiguration outConfig; for (const auto& implementation : aResult.ResolveValue()) { for (const MediaKeySystemConfiguration& candidate : aRequest->mConfigs) { if (mozilla::dom::GetSupportedConfig( implementation, candidate, outConfig, &aRequest->mDiagnostics, aIsPrivateBrowsing, document)) { promise->Resolve(std::move(outConfig), __func__); return; } } } } promise->Reject(false, __func__); }); return promise.forget(); } /* static */ void MediaKeySystemAccess::NotifyObservers(nsPIDOMWindowInner* aWindow, const nsAString& aKeySystem, MediaKeySystemStatus aStatus) { RequestMediaKeySystemAccessNotification data; data.mKeySystem = aKeySystem; data.mStatus = aStatus; nsAutoString json; data.ToJSON(json); EME_LOG("MediaKeySystemAccess::NotifyObservers() %s", NS_ConvertUTF16toUTF8(json).get()); nsCOMPtr obs = services::GetObserverService(); if (obs) { obs->NotifyObservers(aWindow, MediaKeys::kMediaKeysRequestTopic, json.get()); } } static nsCString ToCString(const nsString& aString) { nsCString str("'"); str.Append(NS_ConvertUTF16toUTF8(aString)); str.AppendLiteral("'"); return str; } static nsCString ToCString(const MediaKeysRequirement aValue) { nsCString str("'"); str.AppendASCII(GetEnumString(aValue)); str.AppendLiteral("'"); return str; } static nsCString ToCString(const MediaKeySystemMediaCapability& aValue) { nsCString str; str.AppendLiteral("{contentType="); str.Append(ToCString(aValue.mContentType)); str.AppendLiteral(", robustness="); str.Append(ToCString(aValue.mRobustness)); str.AppendLiteral(", encryptionScheme="); str.Append(ToCString(aValue.mEncryptionScheme)); str.AppendLiteral("}"); return str; } template static nsCString ToCString(const Sequence& aSequence) { nsCString str; str.AppendLiteral("["); StringJoinAppend(str, ","_ns, aSequence, [](nsACString& dest, const Type& element) { dest.Append(ToCString(element)); }); str.AppendLiteral("]"); return str; } template static nsCString ToCString(const Optional>& aOptional) { nsCString str; if (aOptional.WasPassed()) { str.Append(ToCString(aOptional.Value())); } else { str.AppendLiteral("[]"); } return str; } static nsCString ToCString(const MediaKeySystemConfiguration& aConfig) { nsCString str; str.AppendLiteral("{label="); str.Append(ToCString(aConfig.mLabel)); str.AppendLiteral(", initDataTypes="); str.Append(ToCString(aConfig.mInitDataTypes)); str.AppendLiteral(", audioCapabilities="); str.Append(ToCString(aConfig.mAudioCapabilities)); str.AppendLiteral(", videoCapabilities="); str.Append(ToCString(aConfig.mVideoCapabilities)); str.AppendLiteral(", distinctiveIdentifier="); str.Append(ToCString(aConfig.mDistinctiveIdentifier)); str.AppendLiteral(", persistentState="); str.Append(ToCString(aConfig.mPersistentState)); str.AppendLiteral(", sessionTypes="); str.Append(ToCString(aConfig.mSessionTypes)); str.AppendLiteral("}"); return str; } /* static */ nsCString MediaKeySystemAccess::ToCString( const Sequence& aConfig) { return mozilla::dom::ToCString(aConfig); } #undef LOG } // namespace mozilla::dom