chromium/media/mojo/services/media_foundation_service.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/mojo/services/media_foundation_service.h"

#include <map>
#include <memory>
#include <optional>

#include "base/check.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "media/base/audio_codecs.h"
#include "media/base/cdm_capability.h"
#include "media/base/content_decryption_module.h"
#include "media/base/encryption_scheme.h"
#include "media/base/key_system_capability.h"
#include "media/base/key_systems.h"
#include "media/base/media_switches.h"
#include "media/base/video_codecs.h"
#include "media/cdm/win/media_foundation_cdm_module.h"
#include "media/cdm/win/media_foundation_cdm_util.h"
#include "media/media_buildflags.h"
#include "media/mojo/mojom/interface_factory.mojom.h"
#include "media/mojo/mojom/key_system_support.mojom.h"
#include "media/mojo/services/interface_factory_impl.h"

using Microsoft::WRL::ComPtr;

namespace media {

namespace {

// The feature parameters follow Windows API documentation:
// https://docs.microsoft.com/en-us/uwp/api/windows.media.protection.protectioncapabilities.istypesupported?view=winrt-19041
// This default feature string is required to query capability related to video
// decoder. Since we only care about the codec support rather than the specific
// resolution or bitrate capability, we use the following typical values which
// should be supported by most devices for a certain video codec.
const char kDefaultFeatures[] =
    "decode-bpp=8,decode-res-x=1920,decode-res-y=1080,decode-bitrate=10000000,"
    "decode-fps=30";

// These three parameters are an extension of the parameters supported
// in the above documentation to support the encryption capability query.
const char kRobustnessQueryName[] = "encryption-robustness";
const char kEncryptionSchemeQueryName[] = "encryption-type";
const char kEncryptionIvQueryName[] = "encryption-iv-size";

const char kSwSecureRobustness[] = "SW_SECURE_DECODE";
const char kHwSecureRobustness[] = "HW_SECURE_ALL";

// The followings define the supported codecs and encryption schemes that we try
// to query.
constexpr VideoCodec kAllVideoCodecs[] = {
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
    VideoCodec::kH264,
#if BUILDFLAG(ENABLE_PLATFORM_HEVC)
    VideoCodec::kHEVC,
#if BUILDFLAG(ENABLE_PLATFORM_DOLBY_VISION)
    VideoCodec::kDolbyVision,
#endif  // BUILDFLAG(ENABLE_PLATFORM_DOLBY_VISION)
#endif  // BUILDFLAG(ENABLE_PLATFORM_HEVC)
#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)
    VideoCodec::kVP9, VideoCodec::kAV1};

constexpr AudioCodec kAllAudioCodecs[] = {
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
    AudioCodec::kAAC,
#if BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
    AudioCodec::kEAC3,       AudioCodec::kAC3,
#endif  // BUILDFLAG(ENABLE_PLATFORM_AC3_EAC3_AUDIO)
#if BUILDFLAG(ENABLE_PLATFORM_AC4_AUDIO)
    AudioCodec::kAC4,
#endif  // BUILDFLAG(ENABLE_PLATFORM_AC4_AUDIO)
#if BUILDFLAG(ENABLE_PLATFORM_MPEG_H_AUDIO)
    AudioCodec::kMpegHAudio,
#endif  // BUILDFLAG(ENABLE_PLATFORM_MPEG_H_AUDIO)
#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)
    AudioCodec::kVorbis,     AudioCodec::kFLAC, AudioCodec::kOpus};

constexpr EncryptionScheme kAllEncryptionSchemes[] = {EncryptionScheme::kCenc,
                                                      EncryptionScheme::kCbcs};

bool IsTypeSupportedInternal(
    ComPtr<IMFContentDecryptionModuleFactory> cdm_factory,
    const std::string& key_system,
    bool is_hw_secure,
    const std::string& content_type) {
  const base::TimeTicks start_time = base::TimeTicks::Now();
  bool supported =
      cdm_factory->IsTypeSupported(base::UTF8ToWide(key_system).c_str(),
                                   base::UTF8ToWide(content_type).c_str());
  // The above function may take seconds to run. Report UMA to understand the
  // actual performance impact. Report UMA only for success cases.
  if (supported) {
    auto uma_name = "Media.EME.MediaFoundationService." +
                    GetKeySystemNameForUMA(key_system, is_hw_secure) +
                    ".IsTypeSupported";
    base::UmaHistogramTimes(uma_name, base::TimeTicks::Now() - start_time);
  }

  DVLOG(3) << __func__ << " " << (supported ? "[yes]" : "[no]") << ": "
           << key_system << ", " << content_type;

  return supported;
}

std::string GetFourCCString(VideoCodec codec) {
  switch (codec) {
    case VideoCodec::kH264:
      return "avc1";
    case VideoCodec::kVP9:
      return "vp09";
    case VideoCodec::kHEVC:
    case VideoCodec::kDolbyVision:
      return "hvc1";
    case VideoCodec::kAV1:
      return "av01";
    default:
      NOTREACHED_IN_MIGRATION()
          << "This video codec is not supported by MediaFoundationCDM. codec="
          << GetCodecName(codec);
  }
  return "";
}

// Returns an "ext-profile" feature query (with ending comma) for a video codec.
// Returns an empty string if "ext-profile" is not needed.
std::string GetExtProfile(VideoCodec codec) {
  if (codec == VideoCodec::kDolbyVision)
    return "ext-profile=dvhe.05,";

  return "";
}

std::string GetFourCCString(AudioCodec codec) {
  switch (codec) {
    case AudioCodec::kAAC:
      return "mp4a";
    case AudioCodec::kVorbis:
      return "vrbs";
    case AudioCodec::kFLAC:
      return "fLaC";
    case AudioCodec::kOpus:
      return "Opus";
    case AudioCodec::kEAC3:
      return "ec-3";
    case AudioCodec::kAC3:
      return "ac-3";
    case AudioCodec::kAC4:
      return "ac-4";
    case AudioCodec::kMpegHAudio:
      return "mhm1";
    default:
      NOTREACHED_IN_MIGRATION()
          << "This audio codec is not supported by MediaFoundationCDM. codec="
          << GetCodecName(codec);
  }
  return "";
}

std::string GetName(EncryptionScheme scheme) {
  switch (scheme) {
    case EncryptionScheme::kCenc:
      return "cenc";
    case EncryptionScheme::kCbcs:
      return "cbcs";
    default:
      NOTREACHED_IN_MIGRATION() << "Only cenc and cbcs are supported";
  }
  return "";
}

// According to the common encryption spec, both 8 and 16 bytes IV are allowed
// for CENC and CBCS. But some platforms may only support 8 byte IV CENC and
// Chromium does not differentiate IV size for each encryption scheme, so we use
// 8 for CENC and 16 for CBCS to provide the best coverage as those combination
// are recommended.
int GetIvSize(EncryptionScheme scheme) {
  switch (scheme) {
    case EncryptionScheme::kCenc:
      return 8;
    case EncryptionScheme::kCbcs:
      return 16;
    default:
      NOTREACHED_IN_MIGRATION() << "Only cenc and cbcs are supported";
  }
  return 0;
}

// Feature name:value mapping.
using FeatureMap = std::map<std::string, std::string>;

// Construct the query type string based on `video_codec`, optional
// `audio_codec`, `kDefaultFeatures` and `extra_features`.
std::string GetTypeString(VideoCodec video_codec,
                          std::optional<AudioCodec> audio_codec,
                          const FeatureMap& extra_features) {
  auto codec_string = GetFourCCString(video_codec);
  if (audio_codec.has_value())
    codec_string += "," + GetFourCCString(audio_codec.value());

  auto feature_string = GetExtProfile(video_codec) + kDefaultFeatures;
  DCHECK(!feature_string.empty()) << "default feature cannot be empty";
  for (const auto& feature : extra_features) {
    DCHECK(!feature.first.empty() && !feature.second.empty());
    feature_string += "," + feature.first + "=" + feature.second;
  }

  return base::ReplaceStringPlaceholders(
      "video/mp4;codecs=\"$1\";features=\"$2\"", {codec_string, feature_string},
      /*offsets=*/nullptr);
}

// Consolidates the information to construct the type string in only one place.
// This will help us avoid errors in faulty creation of the type string, and
// centralize from where we call IsTypeSupportedInternal()
bool IsTypeSupported(VideoCodec video_codec,
                     std::optional<AudioCodec> audio_codec,
                     const FeatureMap& extra_features,
                     ComPtr<IMFContentDecryptionModuleFactory> cdm_factory,
                     const std::string& key_system,
                     bool is_hw_secure) {
  auto type = GetTypeString(video_codec, audio_codec, extra_features);

  return IsTypeSupportedInternal(cdm_factory, key_system, is_hw_secure, type);
}

base::flat_set<EncryptionScheme> GetSupportedEncryptionSchemes(
    ComPtr<IMFContentDecryptionModuleFactory> cdm_factory,
    const std::string& key_system,
    bool is_hw_secure,
    VideoCodec video_codec,
    const std::string& robustness) {
  base::flat_set<EncryptionScheme> supported_schemes;
  for (const auto scheme : kAllEncryptionSchemes) {
    const FeatureMap extra_features = {
        {kEncryptionSchemeQueryName, GetName(scheme)},
        {kEncryptionIvQueryName, base::NumberToString(GetIvSize(scheme))},
        {kRobustnessQueryName, robustness.c_str()}};

    if (IsTypeSupported(video_codec, /*audio_codec=*/std::nullopt,
                        extra_features, cdm_factory, key_system,
                        is_hw_secure)) {
      supported_schemes.insert(scheme);
    }
  }
  return supported_schemes;
}

HRESULT CreateDummyMediaFoundationCdm(
    ComPtr<IMFContentDecryptionModuleFactory> cdm_factory,
    const std::string& key_system) {
  // Set `use_hw_secure_codecs` to indicate this for hardware secure mode,
  // which typically requires identifier and persistent storage.
  CdmConfig cdm_config = {key_system, /*allow_distinctive_identifier=*/true,
                          /*allow_persistent_state=*/true,
                          /*use_hw_secure_codecs=*/true};

  // Use a random CDM origin.
  auto cdm_origin_id = base::UnguessableToken::Create();

  // Use a dummy CDM store path root under the temp dir here. Since this code
  // runs in the LPAC process, the temp dir will be something like:
  //   C:\Users\<user>\AppData\Local\Packages\cr.sb.cdm<...>\AC\Temp
  // This folder is specifically for the CDM app container, so there's no need
  // to set ACL explicitly.
  base::FilePath temp_dir;
  base::PathService::Get(base::DIR_TEMP, &temp_dir);
  const char kDummyCdmStore[] = "DummyMediaFoundationCdmStore";
  auto dummy_cdm_store_path_root = temp_dir.AppendASCII(kDummyCdmStore);

  // Create the dummy CDM.
  Microsoft::WRL::ComPtr<IMFContentDecryptionModule> mf_cdm;
  auto hr = CreateMediaFoundationCdm(cdm_factory, cdm_config, cdm_origin_id,
                                     /*cdm_client_token=*/std::nullopt,
                                     dummy_cdm_store_path_root, mf_cdm);
  DLOG_IF(ERROR, FAILED(hr)) << __func__ << ": Failed for " << key_system;
  mf_cdm.Reset();

  // Delete the dummy CDM store folder so we don't leave files behind. This may
  // fail since the CDM and related objects may have the files open longer than
  // the total delete retry period or before the process terminates. This is
  // fine since they will be cleaned next time so files will not accumulate.
  // Ignore the `reply_callback` since nothing can be done with the result.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
      base::GetDeletePathRecursivelyCallback(dummy_cdm_store_path_root));

  return hr;
}

std::optional<CdmCapability> GetCdmCapability(
    ComPtr<IMFContentDecryptionModuleFactory> cdm_factory,
    const std::string& key_system,
    bool is_hw_secure) {
  DVLOG(2) << __func__ << ", is_hw_secure=" << is_hw_secure;

  // For hardware secure decryption, even when IsTypeSupportedInternal() says
  // it's supported, CDM creation could fail immediately. Therefore, create a
  // dummy CDM instance to detect this case.
  if (is_hw_secure &&
      FAILED(CreateDummyMediaFoundationCdm(cdm_factory, key_system))) {
    return std::nullopt;
  }

  // TODO(hmchen): make this generic for more key systems.
  const std::string robustness =
      is_hw_secure ? kHwSecureRobustness : kSwSecureRobustness;

  CdmCapability capability;

  // Query video codecs.
  for (const auto video_codec : kAllVideoCodecs) {
#if BUILDFLAG(ENABLE_PLATFORM_HEVC)
    // Only query encrypted HEVC when the feature is enabled.
    if (video_codec == VideoCodec::kHEVC &&
        !base::FeatureList::IsEnabled(kPlatformHEVCDecoderSupport)) {
      continue;
    }
#endif

#if BUILDFLAG(ENABLE_PLATFORM_ENCRYPTED_DOLBY_VISION)
    // Only query encrypted Dolby Vision when the feature is enabled.
    if (video_codec == VideoCodec::kDolbyVision &&
        !base::FeatureList::IsEnabled(kPlatformEncryptedDolbyVision)) {
      continue;
    }
#endif

    const FeatureMap extra_features = {{kRobustnessQueryName, robustness}};

    if (IsTypeSupported(video_codec, /*audio_codec=*/std::nullopt,
                        extra_features, cdm_factory, key_system,
                        is_hw_secure)) {
      // IsTypeSupported() does not support querying profiling, in general
      // assume all relevant profiles are supported.
      VideoCodecInfo video_codec_info;

      // `supports_clear_lead` should be set to false until detection for clear
      // lead support is fixed and the query works as expected.
      video_codec_info.supports_clear_lead = false;

#if BUILDFLAG(ENABLE_PLATFORM_DOLBY_VISION)
      // Dolby Vision on Windows only support profile 4/5/8 now. But profile 4
      // is rarely used and being deprecated, so only declare the support for
      // profile 5/8.
      if (video_codec == VideoCodec::kDolbyVision) {
        video_codec_info.supported_profiles = {
            VideoCodecProfile::DOLBYVISION_PROFILE5,
            VideoCodecProfile::DOLBYVISION_PROFILE8};
      }
#endif

      // We check for `!is_hw_secure` because clear lead should always be
      // supported for software security. When clear lead is supported
      // for hardware security (b/219818166), we will add a query to
      // set supports_clear_lead.
      if (!is_hw_secure) {
        video_codec_info.supports_clear_lead = true;
      }

      capability.video_codecs.emplace(video_codec, video_codec_info);
    }
  }

  // IsTypeSupported query string requires video codec, so stops if no video
  // codecs are supported.
  if (capability.video_codecs.empty()) {
    DVLOG(2) << "No video codecs supported for is_hw_secure=" << is_hw_secure;
    return std::nullopt;
  }

  // Query audio codecs.
  // Audio is usually independent to the video codec. So we use <one of the
  // supported video codecs> + <audio codec> to query the audio capability.
  for (const auto audio_codec : kAllAudioCodecs) {
    const auto& video_codec = capability.video_codecs.begin()->first;
    const FeatureMap extra_features = {{kRobustnessQueryName, robustness}};

    if (IsTypeSupported(video_codec, audio_codec, extra_features, cdm_factory,
                        key_system, is_hw_secure)) {
      capability.audio_codecs.emplace(audio_codec);
    }
  }

  // Query encryption scheme.

  // Note that the CdmCapability assumes all `video_codecs` + `encryption_
  // schemes` combinations are supported. However, in Media Foundation,
  // encryption scheme may be dependent on video codecs, so we query the
  // encryption scheme for all supported video codecs and get the intersection
  // of the encryption schemes which work for all codecs.
  base::flat_set<EncryptionScheme> intersection(
      std::begin(kAllEncryptionSchemes), std::end(kAllEncryptionSchemes));
  for (const auto& [video_codec, _] : capability.video_codecs) {
    const auto schemes = GetSupportedEncryptionSchemes(
        cdm_factory, key_system, is_hw_secure, video_codec, robustness);
    intersection = base::STLSetIntersection<base::flat_set<EncryptionScheme>>(
        intersection, schemes);
  }

  if (intersection.empty()) {
    // Fail if no supported encryption scheme.
    return std::nullopt;
  }

  capability.encryption_schemes = intersection;

  // IsTypeSupported does not support session type yet. So just use temporary
  // session which is required by EME spec.
  capability.session_types.insert(CdmSessionType::kTemporary);

  return capability;
}

}  // namespace

MediaFoundationService::MediaFoundationService(
    mojo::PendingReceiver<mojom::MediaFoundationService> receiver)
    : receiver_(this, std::move(receiver)) {
  DVLOG(1) << __func__;
  mojo_media_client_.Initialize();
}

MediaFoundationService::~MediaFoundationService() {
  DVLOG(1) << __func__;
}

void MediaFoundationService::IsKeySystemSupported(
    const std::string& key_system,
    IsKeySystemSupportedCallback callback) {
  DVLOG(2) << __func__ << ", key_system=" << key_system;

  SCOPED_UMA_HISTOGRAM_TIMER(
      "Media.EME.MediaFoundationService.IsKeySystemSupported");

  ComPtr<IMFContentDecryptionModuleFactory> cdm_factory;
  HRESULT hr = MediaFoundationCdmModule::GetInstance()->GetCdmFactory(
      key_system, cdm_factory);

  if (FAILED(hr)) {
    DLOG(ERROR) << "Failed to GetCdmFactory.";
    std::move(callback).Run(false, std::nullopt);
    return;
  }

  std::optional<CdmCapability> sw_secure_capability =
      GetCdmCapability(cdm_factory, key_system, /*is_hw_secure=*/false);
  std::optional<CdmCapability> hw_secure_capability =
      GetCdmCapability(cdm_factory, key_system, /*is_hw_secure=*/true);

  if (!sw_secure_capability && !hw_secure_capability) {
    DVLOG(2) << "Get empty CdmCapability.";
    std::move(callback).Run(false, std::nullopt);
    return;
  }

  auto capability = media::KeySystemCapability();
  capability.sw_secure_capability = sw_secure_capability;
  capability.hw_secure_capability = hw_secure_capability;
  std::move(callback).Run(true, std::move(capability));
}

void MediaFoundationService::CreateInterfaceFactory(
    mojo::PendingReceiver<mojom::InterfaceFactory> receiver,
    mojo::PendingRemote<mojom::FrameInterfaceFactory> frame_interfaces) {
  DVLOG(2) << __func__;
  interface_factory_receivers_.Add(
      std::make_unique<InterfaceFactoryImpl>(std::move(frame_interfaces),
                                             &mojo_media_client_),
      std::move(receiver));
}

}  // namespace media