/* 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 "MediaCapabilities.h"

#include <utility>

#include "AllocationPolicy.h"
#include "DecoderTraits.h"
#include "MP4Decoder.h"
#include "MediaCapabilitiesValidation.h"
#include "MediaInfo.h"
#include "MediaRecorder.h"
#include "PDMFactorySupport.h"
#include "VPXDecoder.h"
#include "WindowRenderer.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/EMEUtils.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/StaticPrefs_media.h"
#include "mozilla/TaskQueue.h"
#include "mozilla/dom/DOMMozPromiseRequestHolder.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/MediaCapabilitiesBinding.h"
#include "mozilla/dom/MediaKeySystemAccess.h"
#include "mozilla/dom/MediaSource.h"
#include "mozilla/dom/Navigator.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/layers/KnowsCompositor.h"
#include "mozilla/media/MediaUtils.h"
#include "mozilla/media/webrtc/CodecInfo.h"
#include "mozilla/media/webrtc/H264FmtpParser.h"
#include "nsContentUtils.h"
#include "nsIPrincipal.h"

namespace mozilla::dom {
enum class CodecSupport : uint8_t { Supported, Unsupported, Unknown };
static const char* EnumValueToString(const CodecSupport& aEnum) {
  static constexpr const char* kStrings[] = {"Supported", "Unsupported",
                                             "Unknown"};
  return kStrings[static_cast<size_t>(aEnum)];
}
using CodecSupportPromise =
    MozPromise<CodecSupport, nsresult, /* IsExclusive = */ true>;
// Low-resolution heuristic baseline: 640x480 = 307200 pixels.
// Compared by total pixel count (not per-dimension) so that, e.g.,
// 720x360 (259200 pixels) is correctly classified as low-resolution.
constexpr uint32_t kLowResolutionPixelCount = 640 * 480;
struct VideoConfiguration;
struct AudioConfiguration;
bool MediaCapabilitiesKeySystemConfigurationToMediaKeySystemConfiguration(
    const MediaDecodingConfiguration& aInConfig,
    MediaKeySystemConfiguration& aOutConfig);

static mediacaps::BehaviorConfig GetBehaviorConfig(nsIGlobalObject* aParent) {
  nsAutoCString host;
  if (nsIPrincipal* p = aParent ? aParent->PrincipalOrNull() : nullptr) {
    p->GetAsciiHost(host);
  }
  // DataMutex operator* is deleted on rvalues; the lock must be a named
  // variable.
  auto legacyAllowlist =
      StaticPrefs::media_mediacapabilities_legacy_allowlist();
  auto webrtcAllowlist =
      StaticPrefs::media_mediacapabilities_webrtc_enabled_allowlist();
  return {
      .mLegacy = StaticPrefs::media_mediacapabilities_legacy_enabled() ||
                 media::HostnameInValue(*legacyAllowlist, host),
      .mWebRTCEnabled = StaticPrefs::media_mediacapabilities_webrtc_enabled() ||
                        media::HostnameInValue(*webrtcAllowlist, host),
  };
}
}  // namespace mozilla::dom

template <>
struct fmt::formatter<mozilla::dom::CodecSupport>
    : fmt::formatter<std::string_view> {
  auto format(mozilla::dom::CodecSupport aSupport,
              fmt::format_context& aCtx) const {
    return fmt::format_to(aCtx.out(), "{}", EnumValueToString(aSupport));
  }
};

template <>
struct fmt::formatter<mozilla::dom::VideoConfiguration>
    : fmt::formatter<std::string_view> {
  auto format(const mozilla::dom::VideoConfiguration& aConfig,
              fmt::format_context& aCtx) const {
    return fmt::format_to(
        aCtx.out(),
        "[contentType:{} width:{} height:{} bitrate:{} framerate:{} "
        "hasAlphaChannel:{} hdrMetadataType:{} colorGamut:{} "
        "transferFunction:{} scalabilityMode:{}]",
        NS_ConvertUTF16toUTF8(aConfig.mContentType).get(), aConfig.mWidth,
        aConfig.mHeight, aConfig.mBitrate, aConfig.mFramerate,
        aConfig.mHasAlphaChannel.WasPassed()
            ? (aConfig.mHasAlphaChannel.Value() ? "true" : "false")
            : "?",
        aConfig.mHdrMetadataType.WasPassed()
            ? GetEnumString(aConfig.mHdrMetadataType.Value()).get()
            : "?",
        aConfig.mColorGamut.WasPassed()
            ? GetEnumString(aConfig.mColorGamut.Value()).get()
            : "?",
        aConfig.mTransferFunction.WasPassed()
            ? GetEnumString(aConfig.mTransferFunction.Value()).get()
            : "?",
        aConfig.mScalabilityMode.WasPassed()
            ? NS_ConvertUTF16toUTF8(aConfig.mScalabilityMode.Value()).get()
            : "?");
  }
};

template <>
struct fmt::formatter<mozilla::dom::AudioConfiguration>
    : fmt::formatter<std::string_view> {
  auto format(const mozilla::dom::AudioConfiguration& aConfig,
              fmt::format_context& aCtx) const {
    return fmt::format_to(
        aCtx.out(), "[contentType:{} channels:{} bitrate:{} samplerate:{}]",
        NS_ConvertUTF16toUTF8(aConfig.mContentType).get(),
        aConfig.mChannels.WasPassed()
            ? NS_ConvertUTF16toUTF8(aConfig.mChannels.Value()).get()
            : "?",
        aConfig.mBitrate.WasPassed() ? aConfig.mBitrate.Value() : 0,
        aConfig.mSamplerate.WasPassed() ? aConfig.mSamplerate.Value() : 0);
  }
};

template <>
struct fmt::formatter<mozilla::dom::MediaCapabilitiesInfo>
    : fmt::formatter<std::string_view> {
  auto format(const mozilla::dom::MediaCapabilitiesInfo& aInfo,
              fmt::format_context& aCtx) const {
    return fmt::format_to(
        aCtx.out(), "[supported:{} smooth:{} powerEfficient:{}]",
        aInfo.mSupported ? "true" : "false", aInfo.mSmooth ? "true" : "false",
        aInfo.mPowerEfficient ? "true" : "false");
  }
};

template <>
struct fmt::formatter<mozilla::dom::MediaEncodingConfiguration>
    : fmt::formatter<std::string_view> {
  auto format(const mozilla::dom::MediaEncodingConfiguration& aConfig,
              fmt::format_context& aCtx) const {
    auto out = aCtx.out();
    out = fmt::format_to(out, "[video: ");
    if (aConfig.mVideo.WasPassed()) {
      out = fmt::format_to(out, "{}", aConfig.mVideo.Value());
    } else {
      out = fmt::format_to(out, "None");
    }
    out = fmt::format_to(out, ", audio: ");
    if (aConfig.mAudio.WasPassed()) {
      out = fmt::format_to(out, "{}", aConfig.mAudio.Value());
    } else {
      out = fmt::format_to(out, "None");
    }
    out = fmt::format_to(out, "]");
    return out;
  }
};

template <>
struct fmt::formatter<mozilla::dom::MediaDecodingConfiguration>
    : fmt::formatter<std::string_view> {
  auto format(const mozilla::dom::MediaDecodingConfiguration& aConfig,
              fmt::format_context& aCtx) const {
    auto out = aCtx.out();
    out = fmt::format_to(out, "[");

    if (aConfig.mVideo.WasPassed()) {
      out = fmt::format_to(out, "video:{}", aConfig.mVideo.Value());
      if (aConfig.mAudio.WasPassed()) {
        out = fmt::format_to(out, " ");
      }
    }

    if (aConfig.mAudio.WasPassed()) {
      out = fmt::format_to(out, "audio:{}", aConfig.mAudio.Value());
    }

    if (aConfig.mKeySystemConfiguration.WasPassed()) {
      out =
          fmt::format_to(out, "[keySystem:{}, ",
                         NS_ConvertUTF16toUTF8(
                             aConfig.mKeySystemConfiguration.Value().mKeySystem)
                             .get());

      mozilla::dom::MediaKeySystemConfiguration emeConfig;
      if (mozilla::dom::
              MediaCapabilitiesKeySystemConfigurationToMediaKeySystemConfiguration(
                  aConfig, emeConfig)) {
        nsCString emeStr =
            mozilla::dom::MediaKeySystemAccess::ToCString(emeConfig);
        out = std::copy(emeStr.BeginReading(), emeStr.EndReading(), out);
      }
      out = fmt::format_to(out, "]");
    }

    out = fmt::format_to(out, "]");
    return out;
  }
};

template <>
struct fmt::formatter<mozilla::dom::MediaCapabilitiesDecodingInfo>
    : fmt::formatter<std::string_view> {
  auto format(const mozilla::dom::MediaCapabilitiesDecodingInfo& aInfo,
              fmt::format_context& aCtx) const {
    return fmt::format_to(
        aCtx.out(),
        "[supported:{} smooth:{} powerEfficient:{} keySystemAccess:{}]",
        aInfo.mSupported ? "true" : "false", aInfo.mSmooth ? "true" : "false",
        aInfo.mPowerEfficient ? "true" : "false",
        aInfo.mKeySystemAccess ? "present" : "null");
  }
};

mozilla::LazyLogModule sMediaCapabilitiesLog("MediaCapabilities");

#define LOG(fmt, ...)                                          \
  MOZ_LOG_FMT(sMediaCapabilitiesLog, mozilla::LogLevel::Debug, \
              "[MediaCapabilities] {}: " fmt, __func__, __VA_ARGS__)

namespace mozilla::dom {
using mediacaps::IsValidMediaDecodingConfiguration;
using mediacaps::IsValidMediaEncodingConfiguration;

static gfx::IntSize ClampedIntSize(uint32_t aWidth, uint32_t aHeight) {
  return gfx::IntSize(
      static_cast<int32_t>(std::min<uint32_t>(aWidth, INT32_MAX)),
      static_cast<int32_t>(std::min<uint32_t>(aHeight, INT32_MAX)));
}

static CodecType WebrtcMimeToCodecType(const MediaExtendedMIMEType& aMime) {
  const nsCString& mime = aMime.Type().AsString();
  if (mime.EqualsLiteral("video/h264")) {
    return CodecType::H264;
  }
  if (mime.EqualsLiteral("video/vp8")) {
    return CodecType::VP8;
  }
  if (mime.EqualsLiteral("video/vp9")) {
    return CodecType::VP9;
  }
  if (mime.EqualsLiteral("video/av1")) {
    return CodecType::AV1;
  }
  return CodecType::Unknown;
}

// Returns an EncoderConfig for use with PEMFactory::Supports.
static EncoderConfig BuildEncoderConfig(const MediaExtendedMIMEType& aMime,
                                        const VideoConfiguration& aConfig) {
  const auto codec = WebrtcMimeToCodecType(aMime);
  MOZ_ASSERT(codec != CodecType::Unknown);
  const gfx::IntSize size = ClampedIntSize(aConfig.mWidth, aConfig.mHeight);
  MOZ_ASSERT(size.width > 0 && size.height > 0);

  EncoderConfig::CodecSpecific specific(void_t{});
  if (codec == CodecType::H264) {
    // Default to Baseline / level 3.1 (the closest we can get to WebRTC's
    // default 0x42e01f Constrained Baseline; bug 2040726).

    const auto fmtp = ParseH264Fmtp(aMime.OriginalString());
    const H264ProfileLevel pl =
        fmtp.mProfileLevel.isOk()
            ? fmtp.mProfileLevel.inspect()
            : H264ProfileLevel{H264_PROFILE::H264_PROFILE_BASE,
                               H264_LEVEL::H264_LEVEL_3_1};
    specific = AsVariant(
        H264Specific(pl.mProfile, pl.mLevel, H264BitStreamFormat::ANNEXB));
  }
  const float framerate = static_cast<float>(aConfig.mFramerate);
  const uint32_t fr =
      framerate > 1.0f ? SaturatingCast<uint32_t>(std::ceil(framerate)) : 1;
  const uint32_t bitrate = SaturatingCast<uint32_t>(aConfig.mBitrate);
  // PEMFactory::Supports() does not check bitrate, but we include it here
  // for future use.
  return EncoderConfig(
      codec, size, Usage::Realtime,
      EncoderConfig::SampleFormat(dom::ImageBitmapFormat::YUV420P), fr,
      /* kf interval*/ 0, bitrate, /* br min */ 0, /* br max */ 0,
      mozilla::BitrateMode::Variable, HardwarePreference::None,
      ScalabilityMode::None, specific);
}

// Caches codec support state (e.g., WebrtcCodecInfo) for reuse across
// audio and video support queries within a single MediaCapabilities request.
class MOZ_STACK_CLASS CodecSupportState final {
 public:
  // The MediaCapabilities pointer is held as a raw pointer to avoid
  // refcount-thread-mismatch: MediaCapabilities uses main-thread-only
  // refcounting, while CodecSupportState is thread-safe-refcounted and may
  // be released on a non-main thread by an InvokeAsync continuation.
  // The caller (a MediaCapabilities member function) keeps itself alive
  // via the outer promise chain's `self` capture for the full duration
  // of any synchronous CheckTypeFor* calls below.
  explicit CodecSupportState(const MediaCapabilities& aCaps,
                             const mediacaps::BehaviorConfig& aBehavior)
      : mCaps(aCaps), mBehavior(aBehavior) {}

  const mozilla::WebrtcCodecInfo& WebrtcCodecInfo() const {
    if (!mWebrtcCodecInfo) {
      mWebrtcCodecInfo = mozilla::WebrtcCodecInfo::Create();
    }
    return *mWebrtcCodecInfo;
  }

  [[nodiscard]]
  CodecSupport CheckVideoDecodeSupport(
      const MediaDecodingConfiguration& aConfig,
      const MediaExtendedMIMEType& aMime) const {
    const VideoConfiguration& videoConfig = aConfig.mVideo.Value();
    Maybe<ColorGamut> gamut = videoConfig.mColorGamut.WasPassed()
                                  ? Some(videoConfig.mColorGamut.Value())
                                  : Nothing();
    Maybe<TransferFunction> transfer =
        videoConfig.mTransferFunction.WasPassed()
            ? Some(videoConfig.mTransferFunction.Value())
            : Nothing();
    return CheckCodecSupport(aMime, aConfig.mType, gamut, transfer);
  }

  [[nodiscard]]
  CodecSupport CheckVideoEncodeSupport(
      const MediaEncodingConfiguration& aConfig,
      const MediaExtendedMIMEType& aMime) const {
    const VideoConfiguration& videoConfig = aConfig.mVideo.Value();
    Maybe<ColorGamut> gamut = videoConfig.mColorGamut.WasPassed()
                                  ? Some(videoConfig.mColorGamut.Value())
                                  : Nothing();
    Maybe<TransferFunction> transfer =
        videoConfig.mTransferFunction.WasPassed()
            ? Some(videoConfig.mTransferFunction.Value())
            : Nothing();
    return CheckCodecSupport(aMime, aConfig.mType, gamut, transfer);
  }

  [[nodiscard]]
  CodecSupport CheckAudioDecodeSupport(
      const MediaDecodingConfiguration& aConfig,
      const MediaExtendedMIMEType& aMime) const {
    return CheckCodecSupport(aMime, aConfig.mType, Nothing(), Nothing());
  }

  [[nodiscard]]
  CodecSupport CheckAudioEncodeSupport(
      const MediaEncodingConfiguration& aConfig,
      const MediaExtendedMIMEType& aMime) const {
    return CheckCodecSupport(aMime, aConfig.mType, Nothing(), Nothing());
  }

 private:
  const MediaCapabilities& mCaps;
  mediacaps::BehaviorConfig mBehavior;
  mutable std::unique_ptr<mozilla::WebrtcCodecInfo> mWebrtcCodecInfo;

  [[nodiscard]] CodecSupport CheckCodecSupport(
      const MediaExtendedMIMEType& aMime, MediaDecodingType aType,
      const Maybe<ColorGamut>& aColorGamut,
      const Maybe<TransferFunction>& aTransferFunction) const {
    if (mediacaps::CheckMIMETypeSupport(aMime, AsVariant(aType), aColorGamut,
                                        aTransferFunction, mBehavior)
            .isErr()) {
      return CodecSupport::Unsupported;
    }
    switch (aType) {
      case MediaDecodingType::File:
        return mCaps.CheckTypeForFile(aMime) ? CodecSupport::Supported
                                             : CodecSupport::Unsupported;
      case MediaDecodingType::Media_source:
        return mCaps.CheckTypeForMediaSource(aMime) ? CodecSupport::Supported
                                                    : CodecSupport::Unsupported;
      case MediaDecodingType::Webrtc:
        return WebrtcCodecInfo().CheckDecodeType(aMime)
                   ? CodecSupport::Supported
                   : CodecSupport::Unsupported;
      default:
        MOZ_ASSERT_UNREACHABLE("Unhandled MediaDecodingType");
        return CodecSupport::Unsupported;
    }
  }

  [[nodiscard]] CodecSupport CheckCodecSupport(
      const MediaExtendedMIMEType& aMime, MediaEncodingType aType,
      const Maybe<ColorGamut>& aColorGamut,
      const Maybe<TransferFunction>& aTransferFunction) const {
    if (mediacaps::CheckMIMETypeSupport(aMime, AsVariant(aType), aColorGamut,
                                        aTransferFunction, mBehavior)
            .isErr()) {
      return CodecSupport::Unsupported;
    }
    switch (aType) {
      case MediaEncodingType::Record:
        return mCaps.CheckTypeForEncoder(aMime) ? CodecSupport::Supported
                                                : CodecSupport::Unsupported;
      case MediaEncodingType::Webrtc:
        return WebrtcCodecInfo().CheckEncodeType(aMime)
                   ? CodecSupport::Supported
                   : CodecSupport::Unsupported;
      default:
        MOZ_ASSERT_UNREACHABLE("Unhandled MediaEncodingType");
        return CodecSupport::Unsupported;
    }
  }
};

// Thread allocation matching libwebrtc's NumberOfThreads() per encoder.
// Sources: third_party/libwebrtc/modules/video_coding/codecs/*/
static uint32_t Av1EncoderThreads(const uint32_t aPixels,
                                  const uint32_t aCores) {
  // libaom_av1_encoder_v2.cc GetThreadingTilesAndSuperblockSize
  // https://searchfox.org/firefox-main/rev/8352bcb6d75d53f3e2190221b71190e47afa0bfc/third_party/libwebrtc/modules/video_coding/codecs/av1/libaom_av1_encoder_v2.cc#122-144
  if ((aPixels >= 1920u * 1080u) && (aCores > 8)) {
    return 8;
  } else if ((aPixels >= 640u * 360u) && (aCores > 4)) {
    return 4;
  } else if ((aPixels >= 320u * 180u) && (aCores > 2)) {
    return 2;
  }
  return 1;
}
static uint32_t Vp9EncoderThreads(const uint32_t aPixels,
                                  const uint32_t aCores) {
  // libvpx_vp9_encoder.cc NumberOfThreads
  // https://searchfox.org/firefox-main/rev/8352bcb6d75d53f3e2190221b71190e47afa0bfc/third_party/libwebrtc/modules/video_coding/codecs/vp9/libvpx_vp9_encoder.cc#762-781
  if ((aPixels >= 1280u * 720u) && (aCores > 4)) {
    return 4;
  } else if ((aPixels >= 640u * 360u) && (aCores > 2)) {
    return 2;
  }
  return 1;
}
static uint32_t Vp8EncoderThreads(const uint32_t aPixels,
                                  const uint32_t aCores) {
  // libvpx_vp8_encoder.cc NumberOfThreads
  // https://searchfox.org/firefox-main/rev/8352bcb6d75d53f3e2190221b71190e47afa0bfc/third_party/libwebrtc/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc#821-872
#if defined(MOZ_WIDGET_ANDROID)
  if ((aPixels >= 320u * 180u)) {
    if (aCores >= 4) {
      return 3;
    } else if (aCores >= 2) {
      return 2;
    }
  }
  return 1;
#else
  if ((aPixels >= 1920u * 1080u) && (aCores > 8)) {
    return 8;
  } else if ((aPixels > 1280u * 960u) && (aCores >= 6)) {
    return 3;
  } else if ((aPixels > 640u * 480u) && (aCores >= 3)) {
    return (aCores >= 6 ? 3 : 2);
  }
  return 1;
#endif
}
static bool IsWebRTCSWEncodeSmooth(const VideoConfiguration& aConfig) {
  const auto shouldForceSmooth =
      StaticPrefs::media_mediacapabilities_webrtc_encode_smooth_override();
  if (shouldForceSmooth == 1) {
    return true;
  } else if (shouldForceSmooth == 2) {
    return false;
  }

  const NS_ConvertUTF16toUTF8 mimeStr(aConfig.mContentType);
  const int32_t slash = mimeStr.FindChar('/');
  if (slash < 0) {
    return false;
  }
  const auto afterSlash = Substring(mimeStr, slash + 1);
  const int32_t semi = afterSlash.FindChar(';');
  nsAutoCString codecStr(semi >= 0 ? Substring(afterSlash, 0, semi)
                                   : afterSlash);
  codecStr.Trim(" \t");

  // ratio = clip_duration / wall_time at 60fps: >1.0 means faster than
  // real-time. threads = libwebrtc thread count used during measurement. For
  // non-standard resolutions: rounds up to nearest standard bucket
  // Approximates scaling linearly by framerate and thread count.
  static const struct {
    const char* codec;
    uint32_t w, h;
    float ratio;  // realtime ratio at 60fps; >1.0 = faster than real-time
    uint32_t threads;
  } kMeasured[] = {
      {"h264", 426, 240, 2.06f, 1},   {"h264", 854, 480, 1.71f, 1},
      {"h264", 1280, 720, 1.51f, 1},  {"h264", 1920, 1080, 1.37f, 1},
      {"h264", 3840, 2160, 0.47f, 1},  // not smooth at 60fps
      {"av1", 426, 240, 2.10f, 2},    {"av1", 854, 480, 1.43f, 4},
      {"av1", 1280, 720, 0.98f, 4},   // not smooth at 60fps
      {"av1", 1920, 1080, 0.73f, 4},  // not smooth at 60fps
      {"av1", 3840, 2160, 0.26f, 4},  // not smooth at 60fps
      {"vp9", 426, 240, 1.94f, 1},    {"vp9", 854, 480, 1.85f, 2},
      {"vp9", 1280, 720, 1.64f, 4},   {"vp9", 1920, 1080, 1.20f, 4},
      {"vp9", 3840, 2160, 0.50f, 4},  // not smooth at 60fps
      {"vp8", 426, 240, 2.01f, 1},    {"vp8", 854, 480, 1.80f, 3},
      {"vp8", 1280, 720, 1.54f, 3},   {"vp8", 1920, 1080, 1.31f, 3},
      {"vp8", 3840, 2160, 0.55f, 3},  // not smooth at 60fps
  };

  const CheckedInt<uint32_t> pixelCount =
      CheckedInt<uint32_t>(aConfig.mWidth) * aConfig.mHeight;
  if (!pixelCount.isValid() || !std::isfinite(aConfig.mFramerate) ||
      aConfig.mFramerate <= 0) {
    return false;
  }
  const uint32_t pixels = pixelCount.value();
  const uint32_t rfps =
      std::max(1u, static_cast<uint32_t>(aConfig.mFramerate + 0.5));
  const uint32_t cores =
      std::max(1u, static_cast<uint32_t>(GetNumberOfProcessors()));

  // Actual thread count for this machine and codec.
  uint32_t actualThreads = 1;  // h264 (openh264) is always single-threaded
  if (codecStr.EqualsIgnoreCase("av1")) {
    actualThreads = Av1EncoderThreads(pixels, cores);
  } else if (codecStr.EqualsIgnoreCase("vp9")) {
    actualThreads = Vp9EncoderThreads(pixels, cores);
  } else if (codecStr.EqualsIgnoreCase("vp8")) {
    actualThreads = Vp8EncoderThreads(pixels, cores);
  }

  // kMeasured is sorted ascending by resolution within each codec.
  // First entry with bucket_pixels >= pixels is the smallest valid bucket
  // (rounds up, conservative).
  int32_t bucketIdx = -1;
  for (int32_t i = 0; i < static_cast<int32_t>(std::size(kMeasured)); i++) {
    if (!codecStr.EqualsIgnoreCase(kMeasured[i].codec)) {
      continue;
    } else if (kMeasured[i].w * kMeasured[i].h >= pixels) {
      bucketIdx = i;
      break;
    }
  }
  if (bucketIdx < 0) {
    return false;  // exceeds largest bucket (> 4K)
  }

  const auto& bucket = kMeasured[bucketIdx];
  const float scaledRatio =
      bucket.ratio * (60.0f / static_cast<float>(rfps)) *
      (static_cast<float>(actualThreads) / static_cast<float>(bucket.threads));
  return scaledRatio >= 1.0f;
}

// Gets the global's event target and creates a new DOMMozPromiseRequestHolder
// for that target. Must be called on the global's event target. On workers,
// acquires a StrongWorkerRef to block shutdown while the promise is in-flight.
// Returns false if the worker is already shutting down.
template <typename T>
[[nodiscard]]
static bool GetThreadForAsyncRequest(
    nsIGlobalObject* aParent, RefPtr<DOMMozPromiseRequestHolder<T>>* aHolderOut,
    RefPtr<nsISerialEventTarget>* aTargetThreadOut,
    RefPtr<StrongWorkerRef>* aWorkerRefOut, const char* aTag) {
  auto holder = MakeRefPtr<DOMMozPromiseRequestHolder<T>>(aParent);
  RefPtr<nsISerialEventTarget> target = aParent->SerialEventTarget();
  MOZ_ASSERT(target->IsOnCurrentThread());

  if (NS_IsMainThread()) {
    *aHolderOut = std::move(holder);
    *aTargetThreadOut = std::move(target);
    return true;
  }

  WorkerPrivate* wp = GetCurrentThreadWorkerPrivate();
  MOZ_ASSERT(wp, "Must be called from a worker thread");

  RefPtr<StrongWorkerRef> ref = StrongWorkerRef::Create(
      wp, aTag, [holder]() { holder->DisconnectIfExists(); });
  if (NS_WARN_IF(!ref)) {
    return false;
  }

  *aHolderOut = std::move(holder);
  *aTargetThreadOut = std::move(target);
  *aWorkerRefOut = std::move(ref);
  return true;
}

bool MediaCapabilitiesKeySystemConfigurationToMediaKeySystemConfiguration(
    const MediaDecodingConfiguration& aInConfig,
    MediaKeySystemConfiguration& aOutConfig) {
  if (!aInConfig.mKeySystemConfiguration.WasPassed()) {
    return false;
  }

  const auto& keySystemConfig = aInConfig.mKeySystemConfiguration.Value();
  if (!keySystemConfig.mInitDataType.IsEmpty()) {
    if (NS_WARN_IF(!aOutConfig.mInitDataTypes.AppendElement(
            keySystemConfig.mInitDataType, fallible))) {
      return false;
    }
  }
  if (keySystemConfig.mSessionTypes.WasPassed() &&
      !keySystemConfig.mSessionTypes.Value().IsEmpty()) {
    aOutConfig.mSessionTypes.Construct();
    for (const auto& type : keySystemConfig.mSessionTypes.Value()) {
      if (NS_WARN_IF(!aOutConfig.mSessionTypes.Value().AppendElement(
              type, fallible))) {
        return false;
      }
    }
  }
  aOutConfig.mDistinctiveIdentifier = keySystemConfig.mDistinctiveIdentifier;
  aOutConfig.mPersistentState = keySystemConfig.mPersistentState;

  if (aInConfig.mAudio.WasPassed()) {
    auto* capabilitiy = aOutConfig.mAudioCapabilities.AppendElement(fallible);
    if (NS_WARN_IF(!capabilitiy)) {
      return false;
    }
    capabilitiy->mContentType = aInConfig.mAudio.Value().mContentType;
    if (keySystemConfig.mAudio.WasPassed()) {
      const auto& config = keySystemConfig.mAudio.Value();
      capabilitiy->mRobustness = config.mRobustness;
      capabilitiy->mEncryptionScheme = config.mEncryptionScheme;
    }
  }
  if (aInConfig.mVideo.WasPassed()) {
    auto* capabilitiy = aOutConfig.mVideoCapabilities.AppendElement(fallible);
    if (NS_WARN_IF(!capabilitiy)) {
      return false;
    }
    capabilitiy->mContentType = aInConfig.mVideo.Value().mContentType;
    if (keySystemConfig.mVideo.WasPassed()) {
      const auto& config = keySystemConfig.mVideo.Value();
      capabilitiy->mRobustness = config.mRobustness;
      capabilitiy->mEncryptionScheme = config.mEncryptionScheme;
    }
  }
  return true;
}

MediaCapabilities::MediaCapabilities(nsIGlobalObject* aParent)
    : mParent(aParent) {}

void MediaCapabilities::CreateWebRTCDecodingInfo(
    const MediaDecodingConfiguration& aConfiguration, Promise* aPromise,
    Maybe<MediaContainerType> aVideoContainer,
    Maybe<MediaContainerType> aAudioContainer) {
  using PromiseType =
      MozPromise<MediaCapabilitiesDecodingInfo, bool, /*IsExclusive=*/true>;
  RefPtr<DOMMozPromiseRequestHolder<PromiseType>> holder;
  RefPtr<nsISerialEventTarget> targetThread;
  RefPtr<StrongWorkerRef> workerRef;
  if (!GetThreadForAsyncRequest<PromiseType>(
          mParent, &holder, &targetThread, &workerRef,
          "MediaCapabilities::DecodingInfo")) {
    aPromise->MaybeRejectWithInvalidStateError("The worker is shutting down");
    return;
  }

  RefPtr<TaskQueue> taskQueue =
      TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
                        "MediaCapabilities::TaskQueue");
  InvokeAsync(
      taskQueue, __func__,
      [aConfiguration, videoContainer = std::move(aVideoContainer),
       audioContainer = std::move(aAudioContainer)] {
        MOZ_ASSERT(videoContainer || audioContainer);

        // Step 7 returns early if neither audio nor video are supported.
        // If video isn't supported, audio must be - they can't both be
        // unknown. We can assume audio decoding and playback, which should be
        // smooth and powerEfficient.
        MediaCapabilitiesDecodingInfo info;
        info.mSupported = true;  // Passed previous support check
        info.mSmooth = true;
        info.mPowerEfficient = true;

        if (videoContainer) {
          const auto& v = aConfiguration.mVideo.Value();
          const auto& mime = videoContainer->ExtendedType();
          if (WebrtcMimeToCodecType(mime) == CodecType::H264) {
            const auto fmtp = ParseH264Fmtp(mime.OriginalString());
            const bool invalidFmtp =
                fmtp.mProfileLevel.isErr() &&
                fmtp.mProfileLevel.inspectErr() == H264FmtpParseError::Invalid;
            const bool levelTooLow =
                fmtp.mProfileLevel.isOk() &&
                !H264LevelFits(fmtp.mProfileLevel.inspect().mLevel, v.mWidth,
                               v.mHeight, static_cast<double>(v.mFramerate));
            if (invalidFmtp || levelTooLow) {
              MediaCapabilitiesDecodingInfo unsupported;
              unsupported.mSupported = false;
              unsupported.mSmooth = false;
              unsupported.mPowerEfficient = false;
              LOG("{} -> {}", aConfiguration, unsupported);
              return PromiseType::CreateAndResolve(
                  std::move(unsupported), "MediaCapabilities::DecodingInfo");
            }
          }
          const CheckedInt<uint32_t> pixels =
              CheckedInt<uint32_t>(v.mWidth) * CheckedInt<uint32_t>(v.mHeight);
          const bool lowResolution =
              pixels.isValid() && pixels.value() <= kLowResolutionPixelCount;
          // Normalize for PDMs that expect "video/avc"
          nsCString trackMime(videoContainer->Type().AsString());
          if (trackMime.LowerCaseEqualsLiteral("video/h264")) {
            trackMime.AssignLiteral("video/avc");
          }
          auto trackInfo =
              CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
                  trackMime, *videoContainer);
          if (!trackInfo) {
            MediaCapabilitiesDecodingInfo unsupported;
            unsupported.mSupported = false;
            unsupported.mSmooth = false;
            unsupported.mPowerEfficient = false;
            LOG("{} -> {}", aConfiguration, unsupported);
            return PromiseType::CreateAndResolve(
                std::move(unsupported), "MediaCapabilities::DecodingInfo");
          }
          SupportDecoderParams videoParameters(
              *trackInfo,
              media::VideoFrameRate(static_cast<float>(v.mFramerate)));
          auto videoSupport = SupportsVideoDecodeForWebrtc(
              videoContainer->ExtendedType(), videoParameters);
          if (videoSupport.isEmpty()) {
            MediaCapabilitiesDecodingInfo unsupported;
            unsupported.mSupported = false;
            unsupported.mSmooth = false;
            unsupported.mPowerEfficient = false;
            LOG("{} -> {}", aConfiguration, unsupported);
            return PromiseType::CreateAndResolve(
                std::move(unsupported), "MediaCapabilities::DecodingInfo");
          }
          const bool hwSupported =
              videoSupport.contains(media::DecodeSupport::HardwareDecode);
          info.mPowerEfficient = hwSupported || lowResolution;
        }

        return PromiseType::CreateAndResolve(
            std::move(info), "MediaCapabilities::CreateWebRTCDecodingInfo");
      })
      ->Then(
          targetThread, __func__,
          [promise = RefPtr(aPromise), workerRef,
           holder](MediaCapabilitiesDecodingInfo&& aInfo) {
            holder->Complete();
            nsIGlobalObject* global = holder->GetParentObject();
            NS_ENSURE_TRUE_VOID(global);
            promise->MaybeResolve(std::move(aInfo));
          },
          [] { MOZ_CRASH("Unexpected"); })
      ->Track(*holder);
}

// https://w3c.github.io/media-capabilities/#dom-mediacapabilities-decodinginfo
// Section 2.5.2 DecodingInfo() Method
already_AddRefed<Promise> MediaCapabilities::DecodingInfo(
    const MediaDecodingConfiguration& aConfiguration, ErrorResult& aRv) {
  RefPtr<Promise> promise = Promise::Create(mParent, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  const auto behavior = GetBehaviorConfig(mParent);

  // If WebRTC type is used and WebRTC is not enabled for this origin, reject.
  if (aConfiguration.mType == MediaDecodingType::Webrtc &&
      !behavior.mWebRTCEnabled) {
    promise->MaybeRejectWithTypeError<MSG_INVALID_ENUM_VALUE>(
        "type", "webrtc", "MediaDecodingType");
    return promise.forget();
  }

  // Step 1: If configuration is not a valid MediaDecodingConfiguration,
  // return a Promise rejected with a newly created TypeError.
  if (auto configCheck =
          IsValidMediaDecodingConfiguration(aConfiguration, behavior);
      configCheck.isErr()) {
    RejectWithValidationResult(promise, configCheck.unwrapErr());
    return promise.forget();
  }

  // Step 2: If configuration.keySystemConfiguration exists,
  // run the following substeps:
  if (aConfiguration.mKeySystemConfiguration.WasPassed()) {
    // Step 2.1: If the global object is of type WorkerGlobalScope,
    //           return a Promise rejected with a newly created DOMException
    //           whose name is InvalidStateError.
    if (IsWorkerGlobal(mParent->GetGlobalJSObject())) {
      promise->MaybeRejectWithInvalidStateError(
          "key system configuration is not allowed in the worker scope");
      return promise.forget();
    }
    // Step 2.2 If the global object’s relevant settings object is a
    //          non-secure context, return a Promise rejected with a newly
    //          created DOMException whose name is SecurityError.
    if (auto* window = mParent->GetAsInnerWindow();
        window && !window->IsSecureContext()) {
      promise->MaybeRejectWithSecurityError(
          "key system configuration is not allowed in a non-secure context");
      return promise.forget();
    }
  }

  // Step 3: Let p be a new Promise (already have it!)
  // Step 4: In parallel, run the Create a MediaCapabilitiesDecodingInfo
  //         algorithm with configuration and resolve p with its result.
  CreateMediaCapabilitiesDecodingInfo(aConfiguration, aRv, promise, behavior);
  return promise.forget();
}

// https://w3c.github.io/media-capabilities/#create-media-capabilities-decoding-info
void MediaCapabilities::CreateMediaCapabilitiesDecodingInfo(
    const MediaDecodingConfiguration& aConfiguration, ErrorResult& aRv,
    Promise* aPromise, const mediacaps::BehaviorConfig& aBehavior) {
  LOG("Processing {}", aConfiguration);

  const bool isWebRTC =
      mediacaps::IsMediaTypeWebRTC(AsVariant(aConfiguration.mType));
  CodecSupport videoSupported = CodecSupport::Unknown;
  CodecSupport audioSupported = CodecSupport::Unknown;
  CodecSupportState state(*this, aBehavior);

  Maybe<MediaContainerType> videoContainer;
  Maybe<MediaContainerType> audioContainer;

  // If configuration.video is present and is not a valid video configuration,
  // return a Promise rejected with a TypeError.
  if (aConfiguration.mVideo.WasPassed()) {
    auto videoMime = MakeMediaExtendedMIMEType(aConfiguration.mVideo.Value());
    if (!videoMime) {
      aPromise->MaybeRejectWithTypeError("Invalid VideoConfiguration");
      return;
    }
    videoSupported = state.CheckVideoDecodeSupport(aConfiguration, *videoMime);
    if (videoSupported == CodecSupport::Supported) {
      videoContainer = Some(MediaContainerType(std::move(*videoMime)));
    }
  }

  if (aConfiguration.mAudio.WasPassed()) {
    auto audioMime = MakeMediaExtendedMIMEType(aConfiguration.mAudio.Value());
    if (!audioMime) {
      aPromise->MaybeRejectWithTypeError("Invalid AudioConfiguration");
      return;
    }
    audioSupported = state.CheckAudioDecodeSupport(aConfiguration, *audioMime);
    if (audioSupported == CodecSupport::Supported) {
      audioContainer = Some(MediaContainerType(std::move(*audioMime)));
    }
  }
  const bool bothSupportUnknown = videoSupported == CodecSupport::Unknown &&
                                  audioSupported == CodecSupport::Unknown;

  // Step 4.6: If either videoSupported or audioSupported is unsupported, set
  // supported to false, smooth to false, powerEfficient to false, and return
  // info.
  if (videoSupported == CodecSupport::Unsupported ||
      audioSupported == CodecSupport::Unsupported || bothSupportUnknown) {
    MediaCapabilitiesDecodingInfo info;
    info.mSupported = false;
    info.mSmooth = false;
    info.mPowerEfficient = false;
    aPromise->MaybeResolve(std::move(info));
    return;
  }

  if (isWebRTC) {
    CreateWebRTCDecodingInfo(aConfiguration, aPromise,
                             std::move(videoContainer),
                             std::move(audioContainer));
  } else {
    CreateNonWebRTCDecodingInfo(aConfiguration, aPromise,
                                std::move(videoContainer),
                                std::move(audioContainer));
  }
}

static MediaCapabilitiesDecodingInfo CreateVideoDecodingInfo(
    const TrackInfo& aConfig, const bool aShouldResistFingerprinting,
    const bool aHardwareAccelerated) {
  MediaCapabilitiesDecodingInfo info;
  info.mSupported = true;
  info.mSmooth = true;
  info.mPowerEfficient = false;
  if (aShouldResistFingerprinting) {
    return info;
  }
  MOZ_ASSERT(aConfig.IsVideo());
  // mImage dimensions are int32_t from gfx::IntSize. CheckedInt rejects
  // negative inputs (mapping to !isValid()) and rejects width*height
  // overflow, in either case treating the result as not-low-resolution.
  const auto& image = aConfig.GetAsVideoInfo()->mImage;
  const CheckedInt<uint32_t> pixels =
      CheckedInt<uint32_t>(image.width) * CheckedInt<uint32_t>(image.height);
  const bool lowResolution =
      pixels.isValid() && pixels.value() <= kLowResolutionPixelCount;
  info.mPowerEfficient = aHardwareAccelerated || lowResolution;
  return info;
}

void MediaCapabilities::CreateNonWebRTCDecodingInfo(
    const MediaDecodingConfiguration& aConfiguration, Promise* aPromise,
    Maybe<MediaContainerType> aVideoContainer,
    Maybe<MediaContainerType> aAudioContainer) {
  nsTArray<UniquePtr<TrackInfo>> tracks;
  if (aConfiguration.mVideo.WasPassed()) {
    MOZ_ASSERT(aVideoContainer.isSome(),
               "configuration is valid and supported");
    auto videoTracks = DecoderTraits::GetTracksInfo(*aVideoContainer);
    // If the MIME type does not imply a codec, the string MUST
    // also have one and only one parameter that is named codecs with a value
    // describing a single media codec. Otherwise, it MUST contain no
    // parameters.
    if (videoTracks.Length() != 1) {
      aPromise->MaybeRejectWithTypeError(nsPrintfCString(
          "The provided type '%s' does not have a 'codecs' parameter.",
          aVideoContainer->OriginalString().get()));
      return;
    }
    MOZ_DIAGNOSTIC_ASSERT(videoTracks.ElementAt(0),
                          "must contain a valid trackinfo");
    // If the type refers to an audio codec, reject now.
    if (videoTracks[0]->GetType() != TrackInfo::kVideoTrack) {
      aPromise->MaybeRejectWithTypeError("Invalid VideoConfiguration");
      return;
    }
    tracks.AppendElements(std::move(videoTracks));
  }
  if (aConfiguration.mAudio.WasPassed()) {
    MOZ_ASSERT(aAudioContainer.isSome(),
               "configuration is valid and supported");
    auto audioTracks = DecoderTraits::GetTracksInfo(*aAudioContainer);
    // If the MIME type does not imply a codec, the string MUST
    // also have one and only one parameter that is named codecs with a value
    // describing a single media codec. Otherwise, it MUST contain no
    // parameters.
    if (audioTracks.Length() != 1) {
      aPromise->MaybeRejectWithTypeError(nsPrintfCString(
          "The provided type '%s' does not have a 'codecs' parameter.",
          aAudioContainer->OriginalString().get()));
      return;
    }
    MOZ_DIAGNOSTIC_ASSERT(audioTracks.ElementAt(0),
                          "must contain a valid trackinfo");
    // If the type refers to a video codec, reject now.
    if (audioTracks[0]->GetType() != TrackInfo::kAudioTrack) {
      aPromise->MaybeRejectWithTypeError("Invalid AudioConfiguration");
      return;
    }
    tracks.AppendElements(std::move(audioTracks));
  }

  // On Windows, the MediaDataDecoder expects to be created on a thread
  // supporting MTA, which the main thread doesn't. So we use our task queue
  // to create such decoder and perform initialization.
  RefPtr<TaskQueue> taskQueue =
      TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
                        "MediaCapabilities::TaskQueue");
  RefPtr<layers::KnowsCompositor> compositor = GetCompositor();
  const bool shouldResistFingerprinting =
      mParent->ShouldResistFingerprinting(RFPTarget::MediaCapabilities);
  float frameRate =
      aConfiguration.mVideo.WasPassed() && aVideoContainer.isSome()
          ? static_cast<float>(
                aVideoContainer->ExtendedType().GetFramerate().ref())
          : 0.0f;

  // Step 3: If configuration.keySystemConfiguration exists:
  if (aConfiguration.mKeySystemConfiguration.WasPassed()) {
    MOZ_ASSERT(
        NS_IsMainThread(),
        "Key system configuration qurey can not run on the worker thread!");

    RefPtr<nsISerialEventTarget> mainThread = GetMainThreadSerialEventTarget();
    if (!mainThread) {
      aPromise->MaybeRejectWithInvalidStateError(
          "The main thread is shutted down");
      return;
    }

    // This check isn't defined in the spec but exists in web platform tests,
    // so we perform the check as well in order to reduce the web
    // compatibility issues.
    // https://github.com/w3c/media-capabilities/issues/220
    const auto& keySystemConfig =
        aConfiguration.mKeySystemConfiguration.Value();
    if ((keySystemConfig.mVideo.WasPassed() &&
         !aConfiguration.mVideo.WasPassed()) ||
        (keySystemConfig.mAudio.WasPassed() &&
         !aConfiguration.mAudio.WasPassed())) {
      aPromise->MaybeRejectWithTypeError(
          "The type of decoding config doesn't match the type of key system "
          "config");
      return;
    }
    UniquePtr<TrackInfo> videoInfo;
    if (aConfiguration.mVideo.WasPassed() && aVideoContainer.isSome()) {
      videoInfo = std::move(tracks[0]);
    }
    CheckEncryptedDecodingSupport(aConfiguration)
        ->Then(
            mainThread, __func__,
            [promise = RefPtr<Promise>{aPromise}, aConfiguration, mainThread,
             taskQueue, compositor, shouldResistFingerprinting, frameRate,
             videoInfo = std::move(videoInfo)](
                MediaKeySystemAccessManager::MediaKeySystemAccessPromise::
                    ResolveOrRejectValue&& aValue) mutable {
              if (aValue.IsReject()) {
                MediaCapabilitiesDecodingInfo info;
                info.mSupported = false;
                info.mSmooth = false;
                info.mPowerEfficient = false;
                LOG("DRM support check rejected: {} -> {}", aConfiguration,
                    info);
                promise->MaybeResolve(std::move(info));
                return;
              }

              MediaCapabilitiesDecodingInfo drmInfo;
              drmInfo.mSupported = true;
              drmInfo.mSmooth = true;
              drmInfo.mPowerEfficient = false;
              drmInfo.mKeySystemAccess = aValue.ResolveValue();
              MOZ_ASSERT(drmInfo.mKeySystemAccess);
              MediaKeySystemConfiguration config;
              drmInfo.mKeySystemAccess->GetConfiguration(config);
              const bool hwDRM = IsHardwareDecryptionSupported(config);

              if (shouldResistFingerprinting) {
                if (hwDRM) {
                  drmInfo.mSupported = false;
                  drmInfo.mSmooth = false;
                  drmInfo.mPowerEfficient = false;
                } else {
                  drmInfo.mPowerEfficient = false;
                }
                LOG("RFP: suppressing DRM capabilities: {} -> {}",
                    aConfiguration, drmInfo);
                promise->MaybeResolve(std::move(drmInfo));
                return;
              }

              if (hwDRM || !videoInfo) {
                drmInfo.mPowerEfficient = hwDRM && !!videoInfo;
                LOG("DRM hardware decrypt or no video track: {} -> {}",
                    aConfiguration, drmInfo);
                promise->MaybeResolve(std::move(drmInfo));
                return;
              }

              // Software DRM: query the video decoder for powerEfficient.
              CheckVideoDecodingInfo(taskQueue, compositor, frameRate,
                                     false /* RFP already handled */,
                                     std::move(videoInfo))
                  ->Then(
                      mainThread, __func__,
                      [promise, drmInfo = std::move(drmInfo), aConfiguration](
                          CapabilitiesPromise::ResolveOrRejectValue&&
                              aDecoderResult) mutable {
                        if (aDecoderResult.IsResolve()) {
                          drmInfo.mPowerEfficient =
                              aDecoderResult.ResolveValue().mPowerEfficient;
                        } else {
                          drmInfo.mPowerEfficient = false;
                        }
                        LOG("Software DRM decoder check: {} -> {}",
                            aConfiguration, drmInfo);
                        promise->MaybeResolve(std::move(drmInfo));
                      });
            });
    return;
  }

  // Step 4: Otherwise, run the following steps:
  nsTArray<RefPtr<CapabilitiesPromise>> promises;

  for (auto&& config : tracks) {
    TrackInfo::TrackType type =
        config->IsVideo() ? TrackInfo::kVideoTrack : TrackInfo::kAudioTrack;

    MOZ_ASSERT(type == TrackInfo::kAudioTrack ||
                   aVideoContainer->ExtendedType().GetFramerate().isSome(),
               "framerate is a required member of VideoConfiguration");

    if (type == TrackInfo::kAudioTrack) {
      // There's no need to create an audio decoder has we only want to know
      // if such codec is supported. We do need to call the
      // PDMFactory::Supports API outside the main thread to get accurate
      // results.
      promises.AppendElement(
          InvokeAsync(taskQueue, __func__, [config = std::move(config)]() {
            SupportDecoderParams params{*config};
            if (PDMFactorySupport::IsSupported(params,
                                               nullptr /* decoder doctor */)
                    .isEmpty()) {
              return CapabilitiesPromise::CreateAndReject(NS_ERROR_FAILURE,
                                                          __func__);
            }
            MediaCapabilitiesDecodingInfo info;
            info.mSupported = true;
            info.mSmooth = true;
            info.mPowerEfficient = true;
            return CapabilitiesPromise::CreateAndResolve(std::move(info),
                                                         __func__);
          }));
      continue;
    }

    promises.AppendElement(
        CheckVideoDecodingInfo(taskQueue, compositor, frameRate,
                               shouldResistFingerprinting, std::move(config)));
  }

  MOZ_ASSERT(tracks.Length() <= 2);

  RefPtr<DOMMozPromiseRequestHolder<CapabilitiesPromise::AllPromiseType>>
      holder;
  RefPtr<nsISerialEventTarget> targetThread;
  RefPtr<StrongWorkerRef> workerRef;
  if (!GetThreadForAsyncRequest<CapabilitiesPromise::AllPromiseType>(
          mParent, &holder, &targetThread, &workerRef,
          "MediaCapabilities::DecodingInfo")) {
    aPromise->MaybeRejectWithInvalidStateError("The worker is shutting down");
    return;
  }

  CapabilitiesPromise::All(taskQueue, promises)
      ->Then(targetThread, __func__,
             [promise = RefPtr{aPromise}, tracks = std::move(tracks), workerRef,
              holder, aConfiguration](
                 CapabilitiesPromise::AllPromiseType::ResolveOrRejectValue&&
                     aValue) {
               holder->Complete();
               nsIGlobalObject* global = holder->GetParentObject();
               NS_ENSURE_TRUE_VOID(global);
               if (aValue.IsReject()) {
                 MediaCapabilitiesDecodingInfo info;
                 info.mSupported = false;
                 info.mSmooth = false;
                 info.mPowerEfficient = false;
                 LOG("{} -> {}", aConfiguration, info);
                 promise->MaybeResolve(std::move(info));
                 return;
               }
               bool powerEfficient = true;
               bool smooth = true;
               for (auto&& capability : aValue.ResolveValue()) {
                 smooth &= capability.mSmooth;
                 powerEfficient &= capability.mPowerEfficient;
               }
               MediaCapabilitiesDecodingInfo info;
               info.mSupported = true;
               info.mSmooth = smooth;
               info.mPowerEfficient = powerEfficient;
               LOG("{} -> {}", aConfiguration, info);
               promise->MaybeResolve(std::move(info));
             })
      ->Track(*holder);
}

// static
RefPtr<MediaCapabilities::CapabilitiesPromise>
MediaCapabilities::CheckVideoDecodingInfo(
    RefPtr<TaskQueue> aTaskQueue, RefPtr<layers::KnowsCompositor> aCompositor,
    float aFrameRate, bool aShouldResistFingerprinting,
    UniquePtr<TrackInfo> aConfig) {
  MOZ_ASSERT(aConfig && aConfig->IsVideo());
  MOZ_ASSERT(aTaskQueue);
  RefPtr<nsISerialEventTarget> target = aTaskQueue;
  return InvokeAsync(
      target, __func__,
      [taskQueue = std::move(aTaskQueue), compositor = std::move(aCompositor),
       frameRate = aFrameRate,
       shouldResistFingerprinting = aShouldResistFingerprinting,
       config = std::move(aConfig)]() mutable -> RefPtr<CapabilitiesPromise> {
        // MediaDataDecoder keeps a reference to the config object, so we must
        // keep it alive until the decoder has been shutdown.
        static Atomic<uint32_t> sTrackingIdCounter(0);
        TrackingId trackingId(TrackingId::Source::MediaCapabilities,
                              sTrackingIdCounter++,
                              TrackingId::TrackAcrossProcesses::Yes);
        CreateDecoderParams params{
            *config, compositor, CreateDecoderParams::VideoFrameRate(frameRate),
            TrackInfo::kVideoTrack, Some(std::move(trackingId))};
        // We want to ensure that all decoder's queries are occurring only
        // once at a time as it can quickly exhaust the system resources
        // otherwise.
        static RefPtr<AllocPolicy> sVideoAllocPolicy = [&taskQueue]() {
          SchedulerGroup::Dispatch(NS_NewRunnableFunction(
              "MediaCapabilities::AllocPolicy:Video", []() {
                ClearOnShutdown(&sVideoAllocPolicy,
                                ShutdownPhase::XPCOMShutdownThreads);
              }));
          return new SingleAllocPolicy(TrackInfo::TrackType::kVideoTrack,
                                       taskQueue);
        }();
        return AllocationWrapper::CreateDecoder(params, sVideoAllocPolicy)
            ->Then(
                taskQueue, __func__,
                [taskQueue, shouldResistFingerprinting,
                 config = std::move(config)](
                    AllocationWrapper::AllocateDecoderPromise::
                        ResolveOrRejectValue&& aValue) mutable {
                  if (aValue.IsReject()) {
                    return CapabilitiesPromise::CreateAndReject(
                        std::move(aValue.RejectValue()), __func__);
                  }
                  RefPtr<MediaDataDecoder> decoder =
                      std::move(aValue.ResolveValue());
                  RefPtr<CapabilitiesPromise> p = decoder->Init()->Then(
                      taskQueue, __func__,
                      [taskQueue, decoder, shouldResistFingerprinting,
                       config = std::move(config)](
                          MediaDataDecoder::InitPromise::ResolveOrRejectValue&&
                              aValue) mutable {
                        RefPtr<CapabilitiesPromise> p;
                        if (aValue.IsReject()) {
                          p = CapabilitiesPromise::CreateAndReject(
                              std::move(aValue.RejectValue()), __func__);
                        } else {
                          nsAutoCString reason;
                          bool hwAccel = decoder->IsHardwareAccelerated(reason);
                          auto info = CreateVideoDecodingInfo(
                              *config, shouldResistFingerprinting, hwAccel);
                          p = CapabilitiesPromise::CreateAndResolve(
                              std::move(info), __func__);
                        }
                        MOZ_ASSERT(p.get(), "the promise has been created");
                        // Let's keep alive the decoder and the config object
                        // until the decoder has been shutdown.
                        decoder->Shutdown()->Then(
                            taskQueue, __func__,
                            [taskQueue, decoder, config = std::move(config)](
                                const ShutdownPromise::ResolveOrRejectValue&
                                    aValue) {});
                        return p;
                      });
                  return p;
                });
      });
}

// https://www.w3.org/TR/media-capabilities/#is-encrypted-decode-supported
RefPtr<MediaKeySystemAccessManager::MediaKeySystemAccessPromise>
MediaCapabilities::CheckEncryptedDecodingSupport(
    const MediaDecodingConfiguration& aConfiguration) {
  using MediaKeySystemAccessPromise =
      MediaKeySystemAccessManager::MediaKeySystemAccessPromise;
  auto* window = mParent->GetAsInnerWindow();
  if (NS_WARN_IF(!window)) {
    return MediaKeySystemAccessPromise::CreateAndReject(NS_ERROR_FAILURE,
                                                        __func__);
  }

  auto* manager = window->Navigator()->GetOrCreateMediaKeySystemAccessManager();
  if (NS_WARN_IF(!manager)) {
    return MediaKeySystemAccessPromise::CreateAndReject(NS_ERROR_FAILURE,
                                                        __func__);
  }

  // Let emeConfiguration be a new MediaKeySystemConfiguration, and initialize
  // it as follows
  Sequence<MediaKeySystemConfiguration> configs;
  auto* emeConfig = configs.AppendElement(fallible);
  if (NS_WARN_IF(!emeConfig)) {
    return MediaKeySystemAccessPromise::CreateAndReject(NS_ERROR_FAILURE,
                                                        __func__);
  }

  if (!MediaCapabilitiesKeySystemConfigurationToMediaKeySystemConfiguration(
          aConfiguration, *emeConfig)) {
    return MediaKeySystemAccessPromise::CreateAndReject(NS_ERROR_FAILURE,
                                                        __func__);
  }
  return manager->Request(
      aConfiguration.mKeySystemConfiguration.Value().mKeySystem, configs);
}

// https://w3c.github.io/media-capabilities/#abstract-opdef-create-a-mediacapabilitiesencodinginfo
already_AddRefed<Promise> MediaCapabilities::EncodingInfo(
    const MediaEncodingConfiguration& aConfiguration, ErrorResult& aRv) {
  RefPtr<Promise> encodePromise = Promise::Create(mParent, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  const auto behavior = GetBehaviorConfig(mParent);

  // If WebRTC type is used and WebRTC is not enabled for this origin, reject.
  if (aConfiguration.mType == MediaEncodingType::Webrtc &&
      !behavior.mWebRTCEnabled) {
    encodePromise->MaybeRejectWithTypeError<MSG_INVALID_ENUM_VALUE>(
        "type", "webrtc", "MediaEncodingType");
    return encodePromise.forget();
  }

  // If configuration is not a valid MediaConfiguration, return a Promise
  // rejected with a TypeError.
  if (auto configCheck =
          IsValidMediaEncodingConfiguration(aConfiguration, behavior);
      configCheck.isErr()) {
    RejectWithValidationResult(encodePromise, configCheck.unwrapErr());
    return encodePromise.forget();
  }

  LOG("Processing EncodingInfo for: {}", aConfiguration);

  // Step 1: Let info be a new MediaCapabilitiesEncodingInfo instance.
  // Step 2: Set configuration to be a new MediaEncodingConfiguration.
  // For every property in configuration create a new property with the same
  // name and value in configuration.
  // (Both steps handled when object created during async support check)

  // Step 3: Let videoSupported be unknown.
  CodecSupport videoSupported = CodecSupport::Unknown;
  CodecSupportState state(*this, behavior);

  // Step 4: If video is present in configuration, run the following steps:
  // Step 4.1: Let videoMimeType be the result of running parse a MIME type
  // with configuration's contentType.
  Maybe<MediaExtendedMIMEType> videoMime;
  // Step 4.2: Set videoSupported to the result of running check MIME type
  // support with videoMimeType configuration's type.
  if (aConfiguration.mVideo.WasPassed()) {
    videoMime =
        MakeMediaExtendedMIMEType(aConfiguration.mVideo.Value().mContentType);
    MOZ_ASSERT(videoMime, "Validation already succeeded");
    if (videoMime) {
      videoSupported =
          state.CheckVideoEncodeSupport(aConfiguration, *videoMime);
    }
  }

  // Step 5: Let audioSupported be unknown.
  CodecSupport audioSupported = CodecSupport::Unknown;

  // Step 6: If audio is present in configuration, run the following steps:
  Maybe<MediaExtendedMIMEType> audioMime;
  if (aConfiguration.mAudio.WasPassed()) {
    // Step 6.1: Let audioMimeType be the result of running parse a MIME type
    // with configuration's contentType.
    audioMime =
        MakeMediaExtendedMIMEType(aConfiguration.mAudio.Value().mContentType);
    // Step 6.2: Set audioSupported to the result of running check MIME type
    // support with audioMimeType configuration's type.
    audioSupported =
        audioMime ? state.CheckAudioEncodeSupport(aConfiguration, *audioMime)
                  : CodecSupport::Unknown;
  }

  MediaCapabilitiesInfo info;
  const bool bothSupportUnknown = videoSupported == CodecSupport::Unknown &&
                                  audioSupported == CodecSupport::Unknown;

  // Step 7: If either videoSupported or audioSupported is unsupported, set
  // supported to false, smooth to false, powerEfficient to false, and return
  // info.
  if (videoSupported == CodecSupport::Unsupported ||
      audioSupported == CodecSupport::Unsupported || bothSupportUnknown) {
    info.mSupported = false;
    info.mSmooth = false;
    info.mPowerEfficient = false;
    encodePromise->MaybeResolve(std::move(info));
    return encodePromise.forget();
  }

  // Step 8: Otherwise, set supported to true.
  info.mSupported = true;

  // We defer checking specific encoder support to a background TaskQueue, and
  // continue the algorithm async in the then handler later.
  using PromiseType =
      MozPromise<MediaCapabilitiesInfo, bool, /*IsExclusive=*/true>;
  RefPtr<DOMMozPromiseRequestHolder<PromiseType>> holder;
  RefPtr<nsISerialEventTarget> targetThread;
  RefPtr<StrongWorkerRef> workerRef;
  if (!GetThreadForAsyncRequest<PromiseType>(
          mParent, &holder, &targetThread, &workerRef,
          "MediaCapabilities::EncodingInfo")) {
    // Worker is shutting down. Per spec, leave the promise pending; it will
    // be cleaned up by GC when the worker is torn down.
    return encodePromise.forget();
  }

  RefPtr<TaskQueue> taskQueue =
      TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_ENCODER),
                        "MediaCapabilities::TaskQueue");
  InvokeAsync(
      taskQueue, __func__,
      [aConfiguration, videoMime, videoSupported, audioMime, audioSupported,
       info = std::move(info)]() mutable {
        // Step 7 returns early if neither audio nor video are
        // supported. If video isn't supported, audio must be - they
        // can't both be unknown. We can assume audio encoding, which
        // should be smooth and powerEfficient.
        MOZ_ASSERT(audioSupported == CodecSupport::Supported ||
                   videoSupported == CodecSupport::Supported);
        (void)audioSupported;
        info.mSmooth = true;
        info.mPowerEfficient = true;

        bool lowResolution = false;
        if (videoSupported == CodecSupport::Supported) {
          MOZ_ASSERT(aConfiguration.mVideo.WasPassed());
          const auto& v = aConfiguration.mVideo.Value();
          if (WebrtcMimeToCodecType(*videoMime) == CodecType::H264) {
            const auto fmtp = ParseH264Fmtp(videoMime->OriginalString());
            const bool invalidFmtp =
                fmtp.mProfileLevel.isErr() &&
                fmtp.mProfileLevel.inspectErr() == H264FmtpParseError::Invalid;
            const bool levelTooLow =
                fmtp.mProfileLevel.isOk() &&
                !H264LevelFits(fmtp.mProfileLevel.inspect().mLevel, v.mWidth,
                               v.mHeight, static_cast<double>(v.mFramerate));
            if (invalidFmtp || levelTooLow) {
              MediaCapabilitiesInfo unsupported;
              unsupported.mSupported = false;
              unsupported.mSmooth = false;
              unsupported.mPowerEfficient = false;
              LOG("{} -> {}", aConfiguration, unsupported);
              return PromiseType::CreateAndResolve(
                  std::move(unsupported), "MediaCapabilities::EncodingInfo");
            }
          }
          auto encoderConfig = BuildEncoderConfig(*videoMime, v);
          const auto videoSupport = SupportsVideoEncodeForWebrtc(encoderConfig);
          if (videoSupport.isEmpty()) {
            MediaCapabilitiesInfo unsupported;
            unsupported.mSupported = false;
            unsupported.mSmooth = false;
            unsupported.mPowerEfficient = false;
            LOG("{} -> {}", aConfiguration, unsupported);
            return PromiseType::CreateAndResolve(
                std::move(unsupported), "MediaCapabilities::EncodingInfo");
          }
          const bool hwSupported =
              videoSupport.contains(media::EncodeSupport::HardwareEncode);
          const CheckedInt<uint32_t> pixels =
              CheckedInt<uint32_t>(v.mWidth) * CheckedInt<uint32_t>(v.mHeight);
          lowResolution =
              pixels.isValid() && pixels.value() <= kLowResolutionPixelCount;

          // Step 9: If the user agent is able to encode the media
          // represented by configuration at the indicated framerate,
          // set smooth to true. Otherwise set it to false.
          //
          // NOTE: The spec doesn't give hard guidelines for smooth.
          // We will hardware encode or low resolution encoding counts
          // as "smooth". For the highest accuracy we'd want to use
          // benchmarking code similar to what we had in the tree
          // earlier for decoding which was removed due to maintenance
          // concerns.
          info.mSmooth &= hwSupported || IsWebRTCSWEncodeSmooth(v);

          // Step 10: If the user agent is able to encode the media
          // represented by configuration in a power efficient manner,
          // set powerEfficient to true. Otherwise set it to false.
          //
          // Encoding or decoding is considered power efficient when the
          // power draw is optimal. The definition of optimal power draw
          // for encoding or decoding is left to the user agent.
          // However, a common implementation strategy is to consider
          // hardware usage as indicative of optimal power draw. User
          // agents SHOULD NOT mark hardware encoding or decoding as
          // power efficient by default, as non-hardware-accelerated
          // codecs can be just as efficient, particularly with
          // low-resolution video. User agents SHOULD NOT take the
          // device's power source into consideration when determining
          // encoding power efficiency unless the device's power source
          // has side effects such as enabling different encoding or
          // decoding modules.
          info.mPowerEfficient &= (hwSupported || lowResolution);
        }

        LOG("{} -> {}", aConfiguration, info);

        return PromiseType::CreateAndResolve(std::move(info),
                                             "MediaCapabilities::EncodingInfo");
      })
      ->Then(
          targetThread, __func__,
          [encodePromise, workerRef, holder,
           aConfiguration](MediaCapabilitiesInfo aInfo) {
            holder->Complete();
            nsIGlobalObject* global = holder->GetParentObject();
            NS_ENSURE_TRUE_VOID(global);
            // Step 11: Return info.
            encodePromise->MaybeResolve(std::move(aInfo));
          },
          [] { MOZ_CRASH("Unexpected"); })
      ->Track(*holder);
  return encodePromise.forget();
}

bool MediaCapabilities::CheckTypeForMediaSource(
    const MediaExtendedMIMEType& aType) const {
  IgnoredErrorResult rv;
  MediaSource::IsTypeSupported(
      NS_ConvertUTF8toUTF16(aType.OriginalString()),
      nullptr /* DecoderDoctorDiagnostics */, rv,
      Some(mParent->ShouldResistFingerprinting(RFPTarget::MediaCapabilities)));

  return !rv.Failed();
}

bool MediaCapabilities::CheckTypeForFile(
    const MediaExtendedMIMEType& aType) const {
  MediaContainerType containerType(aType);

  return DecoderTraits::CanHandleContainerType(
             containerType, nullptr /* DecoderDoctorDiagnostics */) !=
         CANPLAY_NO;
}

bool MediaCapabilities::CheckTypeForEncoder(
    const MediaExtendedMIMEType& aType) const {
  return MediaRecorder::IsTypeSupported(
      NS_ConvertUTF8toUTF16(aType.OriginalString()));
}

already_AddRefed<layers::KnowsCompositor> MediaCapabilities::GetCompositor() {
  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(GetParentObject());
  if (NS_WARN_IF(!window)) {
    return nullptr;
  }

  nsCOMPtr<Document> doc = window->GetExtantDoc();
  if (NS_WARN_IF(!doc)) {
    return nullptr;
  }
  WindowRenderer* renderer = nsContentUtils::WindowRendererForDocument(doc);
  if (NS_WARN_IF(!renderer)) {
    return nullptr;
  }
  RefPtr<layers::KnowsCompositor> knows = renderer->AsKnowsCompositor();
  if (NS_WARN_IF(!knows)) {
    return nullptr;
  }
  return knows->GetForMedia().forget();
}

JSObject* MediaCapabilities::WrapObject(JSContext* aCx,
                                        JS::Handle<JSObject*> aGivenProto) {
  return MediaCapabilities_Binding::Wrap(aCx, this, aGivenProto);
}

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaCapabilities)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaCapabilities)
NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaCapabilities)

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaCapabilities, mParent)

}  // namespace mozilla::dom
#undef LOG
