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

#include <utility>
#include <vector>

#include "EncoderConfig.h"
#include "GMPLog.h"
#include "GMPUtils.h"
#include "MainThreadUtils.h"
#include "MediaMIMETypes.h"
#include "PlatformDecoderModule.h"
#include "VideoConduit.h"
#include "api/video/video_frame_type.h"
#include "common_video/h264/h264_common.h"
#include "common_video/include/video_frame_buffer.h"
#include "gmp-video-frame-encoded.h"
#include "gmp-video-frame-i420.h"
#include "media/base/media_constants.h"
#include "modules/video_coding/include/video_codec_interface.h"
#include "modules/video_coding/svc/create_scalability_structure.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/media/webrtc/H264FmtpParser.h"
#include "nsServiceManagerUtils.h"

namespace mozilla {

using detail::InputImageData;

// OpenH264 officially supports both decode and encode up to level 5.2.
static constexpr H264_LEVEL kOpenH264MaxLevel = H264_LEVEL::H264_LEVEL_5_2;

media::DecodeSupportSet WebrtcGmpDecoderSupports(
    const MediaExtendedMIMEType& aMime, const SupportDecoderParams&) {
  if (!HaveGMPFor("decode-video"_ns, {"h264"_ns})) {
    return {};
  }
  const auto fmtp = ParseH264Fmtp(aMime.OriginalString());
  if (fmtp.mProfileLevel.isErr() &&
      fmtp.mProfileLevel.inspectErr() == H264FmtpParseError::Invalid) {
    return {};
  }
  if (fmtp.mProfileLevel.isOk()) {
    const auto& pl = fmtp.mProfileLevel.inspect();
    if (pl.mProfile > H264_PROFILE::H264_PROFILE_HIGH) {
      return {};
    }
    if (pl.mLevel > kOpenH264MaxLevel) {
      return {};
    }
  }
  return media::DecodeSupport::SoftwareDecode;
}

media::EncodeSupportSet WebrtcGmpEncoderSupports(const EncoderConfig& aConfig) {
  if (aConfig.mCodec != CodecType::H264) {
    return {};
  }
  if (aConfig.mHardwarePreference == HardwarePreference::RequireHardware) {
    return {};
  }
  // L1T2/L1T3 require the moz-h264-temporal-svc GMP tag.
  switch (aConfig.mScalabilityMode) {
    case ScalabilityMode::None:
      break;
    case ScalabilityMode::L1T2:
    case ScalabilityMode::L1T3:
      if (!HaveGMPFor("encode-video"_ns, {"moz-h264-temporal-svc"_ns})) {
        return {};
      }
      break;
    default:
      return {};
  }
  if (!HaveGMPFor("encode-video"_ns, {"h264"_ns})) {
    return {};
  }
  if (aConfig.mCodecSpecific.is<H264Specific>()) {
    const auto& codecSpecific = aConfig.mCodecSpecific.as<H264Specific>();
    // Profiles beyond Baseline need the "moz-h264-advanced" GMP tag. See
    // GMPEncoderModule::Supports for the playback equivalent.
    if (codecSpecific.mProfile != H264_PROFILE::H264_PROFILE_UNKNOWN &&
        codecSpecific.mProfile != H264_PROFILE::H264_PROFILE_BASE &&
        !HaveGMPFor("encode-video"_ns, {"moz-h264-advanced"_ns})) {
      return {};
    }
    if (codecSpecific.mLevel > kOpenH264MaxLevel) {
      return {};
    }
  }
  return media::EncodeSupport::SoftwareEncode;
}

// QP scaling thresholds.
static const int kLowH264QpThreshold = 24;
static const int kHighH264QpThreshold = 37;

// Encoder.
WebrtcGmpVideoEncoder::WebrtcGmpVideoEncoder(
    const webrtc::SdpVideoFormat& aFormat, std::string aPCHandle)
    : mGMP(nullptr),
      mInitting(false),
      mConfiguredBitrateKbps(0),
      mHost(nullptr),
      mMaxPayloadSize(0),
      mNeedKeyframe(true),
      mSyncLayerCap(webrtc::kMaxTemporalStreams),
      mFormatParams(aFormat.parameters),
      mCallbackMutex("WebrtcGmpVideoEncoder encoded callback mutex"),
      mCallback(nullptr),
      mPCHandle(std::move(aPCHandle)) {
  mCodecParams.mCodecType = kGMPVideoCodecInvalid;
  mCodecParams.mMode = kGMPCodecModeInvalid;
  mCodecParams.mLogLevel = GetGMPLibraryLogLevel();
  MOZ_ASSERT(!mPCHandle.empty());
}

WebrtcGmpVideoEncoder::~WebrtcGmpVideoEncoder() {
  // We should not have been destroyed if we never closed our GMP
  MOZ_ASSERT(!mGMP);
}

static int WebrtcFrameTypeToGmpFrameType(webrtc::VideoFrameType aIn,
                                         GMPVideoFrameType* aOut) {
  MOZ_ASSERT(aOut);
  switch (aIn) {
    case webrtc::VideoFrameType::kVideoFrameKey:
      *aOut = kGMPKeyFrame;
      break;
    case webrtc::VideoFrameType::kVideoFrameDelta:
      *aOut = kGMPDeltaFrame;
      break;
    case webrtc::VideoFrameType::kEmptyFrame:
      *aOut = kGMPSkipFrame;
      break;
    default:
      MOZ_CRASH("Unexpected webrtc::FrameType");
  }

  return WEBRTC_VIDEO_CODEC_OK;
}

static int GmpFrameTypeToWebrtcFrameType(GMPVideoFrameType aIn,
                                         webrtc::VideoFrameType* aOut) {
  MOZ_ASSERT(aOut);
  switch (aIn) {
    case kGMPKeyFrame:
      *aOut = webrtc::VideoFrameType::kVideoFrameKey;
      break;
    case kGMPDeltaFrame:
      *aOut = webrtc::VideoFrameType::kVideoFrameDelta;
      break;
    case kGMPSkipFrame:
      *aOut = webrtc::VideoFrameType::kEmptyFrame;
      break;
    default:
      MOZ_CRASH("Unexpected GMPVideoFrameType");
  }

  return WEBRTC_VIDEO_CODEC_OK;
}

static webrtc::ScalabilityMode GmpCodecParamsToScalabilityMode(
    const GMPVideoCodec& aParams) {
  switch (aParams.mTemporalLayerNum) {
    case 1:
      return webrtc::ScalabilityMode::kL1T1;
    case 2:
      return webrtc::ScalabilityMode::kL1T2;
    case 3:
      return webrtc::ScalabilityMode::kL1T3;
    default:
      NS_WARNING(nsPrintfCString("Expected 1-3 temporal layers but got %d.",
                                 aParams.mTemporalLayerNum)
                     .get());
      MOZ_CRASH("Unexpected number of temporal layers");
  }
}

int32_t WebrtcGmpVideoEncoder::InitEncode(
    const webrtc::VideoCodec* aCodecSettings,
    const webrtc::VideoEncoder::Settings& aSettings) {
  if (!mEncodeQueue) {
    mEncodeQueue.emplace(GetCurrentSerialEventTarget());
  }
  mEncodeQueue->AssertOnCurrentThread();

  if (!mMPS) {
    mMPS = do_GetService("@mozilla.org/gecko-media-plugin-service;1");
  }
  MOZ_ASSERT(mMPS);

  if (!mGMPThread) {
    if (NS_WARN_IF(NS_FAILED(mMPS->GetThread(getter_AddRefs(mGMPThread))))) {
      return WEBRTC_VIDEO_CODEC_ERROR;
    }
  }

  if (aCodecSettings->numberOfSimulcastStreams > 1) {
    // Simulcast not implemented for GMP-H264
    return WEBRTC_VIDEO_CODEC_ERR_SIMULCAST_PARAMETERS_NOT_SUPPORTED;
  }

  if (aCodecSettings->simulcastStream[0].numberOfTemporalLayers > 1 &&
      !HaveGMPFor("encode-video"_ns, {"moz-h264-temporal-svc"_ns})) {
    return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
  }

  GMPVideoCodec codecParams{};
  codecParams.mGMPApiVersion = kGMPVersion36;
  codecParams.mLogLevel = GetGMPLibraryLogLevel();
  codecParams.mStartBitrate = aCodecSettings->startBitrate;
  codecParams.mMinBitrate = aCodecSettings->minBitrate;
  codecParams.mMaxBitrate = aCodecSettings->maxBitrate;
  codecParams.mMaxFramerate = aCodecSettings->maxFramerate;
  codecParams.mFrameDroppingOn = aCodecSettings->GetFrameDropEnabled();
  codecParams.mTemporalLayerNum =
      aCodecSettings->simulcastStream[0].GetNumberOfTemporalLayers();
  if (aCodecSettings->mode == webrtc::VideoCodecMode::kScreensharing) {
    codecParams.mMode = kGMPScreensharing;
  } else {
    codecParams.mMode = kGMPRealtimeVideo;
  }
  codecParams.mWidth = aCodecSettings->width;
  codecParams.mHeight = aCodecSettings->height;

  uint32_t maxPayloadSize = aSettings.max_payload_size;
  if (mFormatParams.count(webrtc::kH264FmtpPacketizationMode) == 1 &&
      mFormatParams.at(webrtc::kH264FmtpPacketizationMode) == "1") {
    maxPayloadSize = 0;  // No limit, use FUAs
  }

  mConfiguredBitrateKbps = codecParams.mMaxBitrate;

  MOZ_ALWAYS_SUCCEEDS(
      mGMPThread->Dispatch(NewRunnableMethod<GMPVideoCodec, int32_t, uint32_t>(
          __func__, this, &WebrtcGmpVideoEncoder::InitEncode_g, codecParams,
          aSettings.number_of_cores, maxPayloadSize)));

  // Since init of the GMP encoder is a multi-step async dispatch (including
  // dispatches to main), and since this function is invoked on main, there's
  // no safe way to block until this init is done. If an error occurs, we'll
  // handle it later.
  return WEBRTC_VIDEO_CODEC_OK;
}

void WebrtcGmpVideoEncoder::InitEncode_g(const GMPVideoCodec& aCodecParams,
                                         int32_t aNumberOfCores,
                                         uint32_t aMaxPayloadSize) {
  nsTArray<nsCString> tags;
  tags.AppendElement("h264"_ns);
  auto callback = MakeUnique<InitDoneCallback>(this, aCodecParams);
  mInitting = true;
  mMaxPayloadSize = aMaxPayloadSize;
  mSyncLayerCap = aCodecParams.mTemporalLayerNum;
  mSvcController = webrtc::CreateScalabilityStructure(
      GmpCodecParamsToScalabilityMode(aCodecParams));
  if (!mSvcController) {
    GMP_LOG_DEBUG(
        "GMP Encode: CreateScalabilityStructure for {} temporal layers failed",
        aCodecParams.mTemporalLayerNum);
    Close_g();
    NotifyGmpInitDone(mPCHandle, WEBRTC_VIDEO_CODEC_ERROR,
                      "GMP Encode: CreateScalabilityStructure failed");
    return;
  }
  nsresult rv =
      mMPS->GetGMPVideoEncoder(nullptr, &tags, ""_ns, std::move(callback));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    GMP_LOG_DEBUG("GMP Encode: GetGMPVideoEncoder failed");
    Close_g();
    NotifyGmpInitDone(mPCHandle, WEBRTC_VIDEO_CODEC_ERROR,
                      "GMP Encode: GetGMPVideoEncoder failed");
  }
}

int32_t WebrtcGmpVideoEncoder::GmpInitDone_g(GMPVideoEncoderProxy* aGMP,
                                             GMPVideoHost* aHost,
                                             std::string* aErrorOut) {
  if (!mInitting || !aGMP || !aHost) {
    *aErrorOut =
        "GMP Encode: Either init was aborted, "
        "or init failed to supply either a GMP Encoder or GMP host.";
    if (aGMP) {
      // This could destroy us, since aGMP may be the last thing holding a ref
      // Return immediately.
      aGMP->Close();
    }
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  mInitting = false;

  if (mGMP && mGMP != aGMP) {
    Close_g();
  }

  mGMP = aGMP;
  mHost = aHost;
  mCachedPluginId = Some(mGMP->GetPluginId());
  mInitPluginEvent.Notify(*mCachedPluginId);
  return WEBRTC_VIDEO_CODEC_OK;
}

int32_t WebrtcGmpVideoEncoder::GmpInitDone_g(GMPVideoEncoderProxy* aGMP,
                                             GMPVideoHost* aHost,
                                             const GMPVideoCodec& aCodecParams,
                                             std::string* aErrorOut) {
  int32_t r = GmpInitDone_g(aGMP, aHost, aErrorOut);
  if (r != WEBRTC_VIDEO_CODEC_OK) {
    // We might have been destroyed if GmpInitDone failed.
    // Return immediately.
    return r;
  }
  mCodecParams = aCodecParams;
  return InitEncoderForSize(aCodecParams.mWidth, aCodecParams.mHeight,
                            aErrorOut);
}

void WebrtcGmpVideoEncoder::Close_g() {
  GMPVideoEncoderProxy* gmp(mGMP);
  mGMP = nullptr;
  mHost = nullptr;
  mInitting = false;
  mInputImageMap.Clear();

  if (mCachedPluginId) {
    mReleasePluginEvent.Notify(*mCachedPluginId);
  }
  mCachedPluginId = Nothing();

  if (gmp) {
    // Do this last, since this could cause us to be destroyed
    gmp->Close();
  }
}

int32_t WebrtcGmpVideoEncoder::InitEncoderForSize(unsigned short aWidth,
                                                  unsigned short aHeight,
                                                  std::string* aErrorOut) {
  mCodecParams.mWidth = aWidth;
  mCodecParams.mHeight = aHeight;
  // Pass dummy codecSpecific data for now...
  nsTArray<uint8_t> codecSpecific;

  GMPErr err =
      mGMP->InitEncode(mCodecParams, codecSpecific, this, 1, mMaxPayloadSize);
  if (err != GMPNoErr) {
    *aErrorOut = "GMP Encode: InitEncode failed";
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  return WEBRTC_VIDEO_CODEC_OK;
}

int32_t WebrtcGmpVideoEncoder::Encode(
    const webrtc::VideoFrame& aInputImage,
    const std::vector<webrtc::VideoFrameType>* aFrameTypes) {
  mEncodeQueue->AssertOnCurrentThread();
  MOZ_ASSERT(aInputImage.width() >= 0 && aInputImage.height() >= 0);
  if (!aFrameTypes) {
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  if (mConfiguredBitrateKbps == 0) {
    GMP_LOG_VERBOSE("GMP Encode: not enabled");
    MutexAutoLock lock(mCallbackMutex);
    if (mCallback) {
      mCallback->OnFrameDropped(aInputImage.rtp_timestamp(),
                                /* spatial_id = */ 0,
                                /* is_end_of_temporal_unit = */ true);
    }
    return WEBRTC_VIDEO_CODEC_OK;
  }

  // It is safe to copy aInputImage here because the frame buffer is held by
  // a refptr.
  MOZ_ALWAYS_SUCCEEDS(mGMPThread->Dispatch(
      NewRunnableMethod<webrtc::VideoFrame,
                        std::vector<webrtc::VideoFrameType>>(
          __func__, this, &WebrtcGmpVideoEncoder::Encode_g, aInputImage,
          *aFrameTypes)));

  return WEBRTC_VIDEO_CODEC_OK;
}

void WebrtcGmpVideoEncoder::RegetEncoderForResolutionChange(uint32_t aWidth,
                                                            uint32_t aHeight) {
  Close_g();

  UniquePtr<GetGMPVideoEncoderCallback> callback =
      MakeUnique<InitDoneForResolutionChangeCallback>(this, aWidth, aHeight);

  // OpenH264 codec (at least) can't handle dynamic input resolution changes
  // re-init the plugin when the resolution changes
  // XXX allow codec to indicate it doesn't need re-init!
  nsTArray<nsCString> tags;
  tags.AppendElement("h264"_ns);
  mInitting = true;
  if (NS_WARN_IF(NS_FAILED(mMPS->GetGMPVideoEncoder(nullptr, &tags, ""_ns,
                                                    std::move(callback))))) {
    NotifyGmpInitDone(mPCHandle, WEBRTC_VIDEO_CODEC_ERROR,
                      "GMP Encode: GetGMPVideoEncoder failed");
  }
}

void WebrtcGmpVideoEncoder::Encode_g(
    const webrtc::VideoFrame& aInputImage,
    std::vector<webrtc::VideoFrameType> aFrameTypes) {
  auto reportDroppedOnExit = MakeScopeExit([&] {
    MutexAutoLock lock(mCallbackMutex);
    if (mCallback) {
      mCallback->OnFrameDropped(aInputImage.rtp_timestamp(),
                                /* spatial_id = */ 0,
                                /* is_end_of_temporal_unit = */ true);
    }
  });

  if (!mGMP) {
    // destroyed via Terminate(), failed to init, or just not initted yet
    GMP_LOG_DEBUG("GMP Encode: not initted yet");
    return;
  }
  MOZ_ASSERT(mHost);

  if (mInputImageMap.Length() >= kMaxImagesInFlight) {
    GMP_LOG_WARNING(
        "GMP Encode: Max number of frames already in flight. Dropping this "
        "one.");
    return;
  }

  if (static_cast<uint32_t>(aInputImage.width()) != mCodecParams.mWidth ||
      static_cast<uint32_t>(aInputImage.height()) != mCodecParams.mHeight) {
    GMP_LOG_DEBUG("GMP Encode: resolution change from {}x{} to {}x{}",
                  mCodecParams.mWidth, mCodecParams.mHeight,
                  aInputImage.width(), aInputImage.height());

    mNeedKeyframe = true;
    RegetEncoderForResolutionChange(aInputImage.width(), aInputImage.height());
    if (!mGMP) {
      // We needed to go async to re-get the encoder. Bail.
      return;
    }
  }

  GMPVideoFrame* ftmp = nullptr;
  GMPErr err = mHost->CreateFrame(kGMPI420VideoFrame, &ftmp);
  if (err != GMPNoErr) {
    GMP_LOG_DEBUG("GMP Encode: failed to create frame on host");
    return;
  }
  GMPUniquePtr<GMPVideoi420Frame> frame(static_cast<GMPVideoi420Frame*>(ftmp));
  const webrtc::I420BufferInterface* input_image =
      aInputImage.video_frame_buffer()->GetI420();
  // check for overflow of stride * height
  CheckedInt32 ysize =
      CheckedInt32(input_image->StrideY()) * input_image->height();
  MOZ_RELEASE_ASSERT(ysize.isValid());
  // I will assume that if that doesn't overflow, the others case - YUV
  // 4:2:0 has U/V widths <= Y, even with alignment issues.
  err = frame->CreateFrame(
      ysize.value(), input_image->DataY(),
      input_image->StrideU() * ((input_image->height() + 1) / 2),
      input_image->DataU(),
      input_image->StrideV() * ((input_image->height() + 1) / 2),
      input_image->DataV(), input_image->width(), input_image->height(),
      input_image->StrideY(), input_image->StrideU(), input_image->StrideV());
  if (err != GMPNoErr) {
    GMP_LOG_DEBUG("GMP Encode: failed to create frame");
    return;
  }
  const auto gmpTimestamp =
      AssertedCast<uint64_t>(aInputImage.ntp_time_ms() * 1000);
  frame->SetTimestamp(gmpTimestamp);

  GMPCodecSpecificInfo info{};
  info.mCodecType = kGMPVideoCodecH264;
  nsTArray<uint8_t> codecSpecificInfo;
  codecSpecificInfo.AppendElements((uint8_t*)&info,
                                   sizeof(GMPCodecSpecificInfo));

  nsTArray<GMPVideoFrameType> gmp_frame_types;
  for (const auto& frameType : aFrameTypes) {
    GMPVideoFrameType ft;

    if (mNeedKeyframe) {
      ft = kGMPKeyFrame;
    } else {
      int32_t ret = WebrtcFrameTypeToGmpFrameType(frameType, &ft);
      if (ret != WEBRTC_VIDEO_CODEC_OK) {
        GMP_LOG_DEBUG(
            "GMP Encode: failed to map webrtc frame type to gmp frame type");
        return;
      }
    }

    gmp_frame_types.AppendElement(ft);
  }
  mNeedKeyframe = false;

  auto frameConfigs =
      mSvcController->NextFrameConfig(gmp_frame_types[0] == kGMPKeyFrame);
  MOZ_ASSERT(frameConfigs.size() == 1);

  MOZ_RELEASE_ASSERT(mInputImageMap.IsEmpty() ||
                     mInputImageMap.LastElement().ntp_timestamp_ms <
                         aInputImage.ntp_time_ms());

  GMP_LOG_DEBUG("GMP Encode: {}", (frame->Timestamp()));
  err = mGMP->Encode(std::move(frame), codecSpecificInfo, gmp_frame_types);
  if (err != GMPNoErr) {
    GMP_LOG_DEBUG("GMP Encode: failed to encode frame");
    return;
  }

  // Once in mInputImageMap, frame drops are reported by GMP callbacks
  // (Encoded/Dropped).
  reportDroppedOnExit.release();
  mInputImageMap.AppendElement(
      InputImageData{.gmp_timestamp_us = gmpTimestamp,
                     .ntp_timestamp_ms = aInputImage.ntp_time_ms(),
                     .timestamp_us = aInputImage.timestamp_us(),
                     .rtp_timestamp = aInputImage.rtp_timestamp(),
                     .frame_config = frameConfigs[0]});
}

int32_t WebrtcGmpVideoEncoder::RegisterEncodeCompleteCallback(
    webrtc::EncodedImageCallback* aCallback) {
  MutexAutoLock lock(mCallbackMutex);
  mCallback = aCallback;

  return WEBRTC_VIDEO_CODEC_OK;
}

int32_t WebrtcGmpVideoEncoder::Shutdown() {
  GMP_LOG_DEBUG("GMP Released:");
  RegisterEncodeCompleteCallback(nullptr);

  if (mGMPThread) {
    MOZ_ALWAYS_SUCCEEDS(mGMPThread->Dispatch(
        NewRunnableMethod(__func__, this, &WebrtcGmpVideoEncoder::Close_g)));
  }
  return WEBRTC_VIDEO_CODEC_OK;
}

int32_t WebrtcGmpVideoEncoder::SetRates(
    const webrtc::VideoEncoder::RateControlParameters& aParameters) {
  mEncodeQueue->AssertOnCurrentThread();
  MOZ_ASSERT(mGMPThread);
  MOZ_ASSERT(!aParameters.bitrate.IsSpatialLayerUsed(1),
             "No simulcast support for H264");
  auto old = mConfiguredBitrateKbps;
  mConfiguredBitrateKbps = aParameters.bitrate.GetSpatialLayerSum(0) / 1000;
  MOZ_ALWAYS_SUCCEEDS(
      mGMPThread->Dispatch(NewRunnableMethod<uint32_t, uint32_t, Maybe<double>>(
          __func__, this, &WebrtcGmpVideoEncoder::SetRates_g, old,
          mConfiguredBitrateKbps,
          aParameters.framerate_fps > 0.0 ? Some(aParameters.framerate_fps)
                                          : Nothing())));

  return WEBRTC_VIDEO_CODEC_OK;
}

WebrtcVideoEncoder::EncoderInfo WebrtcGmpVideoEncoder::GetEncoderInfo() const {
  WebrtcVideoEncoder::EncoderInfo info;
  info.supports_native_handle = false;
  info.implementation_name = "GMPOpenH264";
  info.scaling_settings = WebrtcVideoEncoder::ScalingSettings(
      kLowH264QpThreshold, kHighH264QpThreshold);
  info.is_hardware_accelerated = false;
  info.supports_simulcast = false;
  return info;
}

int32_t WebrtcGmpVideoEncoder::SetRates_g(uint32_t aOldBitRateKbps,
                                          uint32_t aNewBitRateKbps,
                                          Maybe<double> aFrameRate) {
  if (!mGMP) {
    // destroyed via Terminate()
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  GMP_LOG_DEBUG("GMP Encoder {} setting rate {}kbps", fmt::ptr(this),
                aNewBitRateKbps);

  mNeedKeyframe |= (aOldBitRateKbps == 0 && aNewBitRateKbps != 0);

  GMPErr err = mGMP->SetRates(
      aNewBitRateKbps, aFrameRate
                           .map([](double aFr) {
                             // Avoid rounding to 0
                             return std::max(1U, static_cast<uint32_t>(aFr));
                           })
                           .valueOr(mCodecParams.mMaxFramerate));
  if (err != GMPNoErr) {
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  return WEBRTC_VIDEO_CODEC_OK;
}

// GMPVideoEncoderCallback virtual functions.
void WebrtcGmpVideoEncoder::Terminated() {
  GMP_LOG_DEBUG("GMP Encoder Terminated: {}", fmt::ptr((void*)this));

  GMPVideoEncoderProxy* gmp(mGMP);
  mGMP = nullptr;
  mHost = nullptr;
  mInitting = false;
  mInputImageMap.Clear();

  if (gmp) {
    // Do this last, since this could cause us to be destroyed
    gmp->Close();
  }

  // Could now notify that it's dead
}

static int32_t GmpTimestampComparator(const InputImageData& aA,
                                      const InputImageData& aB) {
  const auto& a = aA.gmp_timestamp_us;
  const auto& b = aB.gmp_timestamp_us;
  return a < b ? -1 : a != b;
}

void WebrtcGmpVideoEncoder::Encoded(
    GMPVideoEncodedFrame* aEncodedFrame,
    const nsTArray<uint8_t>& aCodecSpecificInfo) {
  MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
  Maybe<InputImageData> data;
  MOZ_ASSERT(!mInputImageMap.IsEmpty());
  MOZ_ASSERT(mInputImageMap.Length() <= kMaxImagesInFlight);
  size_t nextIdx = mInputImageMap.IndexOfFirstElementGt(
      InputImageData{.gmp_timestamp_us = aEncodedFrame->TimeStamp()},
      GmpTimestampComparator);
  const size_t numToRemove = nextIdx;
  size_t numFramesDropped = numToRemove;
  MOZ_ASSERT(nextIdx != 0);
  if (nextIdx != 0 && mInputImageMap.ElementAt(nextIdx - 1).gmp_timestamp_us ==
                          aEncodedFrame->TimeStamp()) {
    --numFramesDropped;
    data = Some(mInputImageMap.ElementAt(nextIdx - 1));
  }
  AutoTArray<uint32_t, kMaxImagesInFlight> droppedRtpTimestamps;
  for (size_t i = 0; i < numFramesDropped; ++i) {
    droppedRtpTimestamps.AppendElement(
        mInputImageMap.ElementAt(i).rtp_timestamp);
  }
  mInputImageMap.RemoveElementsAt(0, numToRemove);

  webrtc::VideoFrameType frt;
  GmpFrameTypeToWebrtcFrameType(aEncodedFrame->FrameType(), &frt);
  MOZ_ASSERT_IF(mCodecParams.mTemporalLayerNum > 1 &&
                    aEncodedFrame->FrameType() == kGMPKeyFrame,
                aEncodedFrame->GetTemporalLayerId() == 0);
  if (aEncodedFrame->FrameType() == kGMPKeyFrame &&
      !data->frame_config.IsKeyframe()) {
    GMP_LOG_WARNING("GMP Encoded non-requested keyframe at t={}",
                    aEncodedFrame->TimeStamp());
    // If there could be multiple encode jobs in flight this would be racy.
    auto frameConfigs = mSvcController->NextFrameConfig(/* restart =*/true);
    MOZ_ASSERT(frameConfigs.size() == 1);
    data->frame_config = frameConfigs[0];
  }

  MOZ_ASSERT((aEncodedFrame->FrameType() == kGMPKeyFrame) ==
             data->frame_config.IsKeyframe());
  MOZ_ASSERT_IF(
      mCodecParams.mTemporalLayerNum > 1,
      aEncodedFrame->GetTemporalLayerId() == data->frame_config.TemporalId());

  MutexAutoLock lock(mCallbackMutex);
  if (!mCallback) {
    return;
  }

  for (const uint32_t rtpTimestamp : droppedRtpTimestamps) {
    mCallback->OnFrameDropped(rtpTimestamp, /* spatial_id = */ 0,
                              /* is_end_of_temporal_unit = */ true);
  }

  if (data.isNothing()) {
    MOZ_ASSERT_UNREACHABLE(
        "Unexpectedly didn't find an input image for this encoded frame");
    return;
  }

  webrtc::VideoFrameType ft;
  GmpFrameTypeToWebrtcFrameType(aEncodedFrame->FrameType(), &ft);

  GMP_LOG_DEBUG("GMP Encoded: {}, type {}, len {}", aEncodedFrame->TimeStamp(),
                static_cast<int>(aEncodedFrame->BufferType()),
                aEncodedFrame->Size());

  // Libwebrtc's RtpPacketizerH264 expects a 3- or 4-byte NALU start sequence
  // before the start of the NALU payload. {0,0,1} or {0,0,0,1}. We set this
  // in-place. Any other length of the length field we reject.
  if (NS_WARN_IF(!AdjustOpenH264NALUSequence(aEncodedFrame))) {
    return;
  }

  webrtc::EncodedImage unit;
  unit.SetEncodedData(webrtc::EncodedImageBuffer::Create(
      aEncodedFrame->Buffer(), aEncodedFrame->Size()));
  unit._frameType = ft;
  unit.SetRtpTimestamp(data->rtp_timestamp);
  unit.capture_time_ms_ = webrtc::Timestamp::Micros(data->timestamp_us).ms();
  unit.ntp_time_ms_ = data->ntp_timestamp_ms;
  unit._encodedWidth = aEncodedFrame->EncodedWidth();
  unit._encodedHeight = aEncodedFrame->EncodedHeight();

  webrtc::CodecSpecificInfo info;
#ifdef __LP64__
  // Only do these checks on some common builds to avoid build issues on more
  // exotic flavors.
  static_assert(
      sizeof(info.codecSpecific.H264) == 8,
      "webrtc::CodecSpecificInfoH264 has changed. We must handle the changes.");
  static_assert(
      sizeof(info) - sizeof(info.codecSpecific) -
              sizeof(info.generic_frame_info) -
              sizeof(info.template_structure) -
              sizeof(info.frame_instrumentation_data) ==
          24,
      "webrtc::CodecSpecificInfo's generic bits have changed. We must handle "
      "the changes.");
#endif
  info.codecType = webrtc::kVideoCodecH264;
  info.codecSpecific = {};
  info.codecSpecific.H264.packetization_mode =
      mFormatParams.count(webrtc::kH264FmtpPacketizationMode) == 1 &&
              mFormatParams.at(webrtc::kH264FmtpPacketizationMode) == "1"
          ? webrtc::H264PacketizationMode::NonInterleaved
          : webrtc::H264PacketizationMode::SingleNalUnit;
  info.codecSpecific.H264.temporal_idx = webrtc::kNoTemporalIdx;
  info.codecSpecific.H264.base_layer_sync = false;
  info.codecSpecific.H264.idr_frame =
      ft == webrtc::VideoFrameType::kVideoFrameKey;
  info.generic_frame_info = mSvcController->OnEncodeDone(data->frame_config);
  if (info.codecSpecific.H264.idr_frame &&
      info.generic_frame_info.has_value()) {
    info.template_structure = mSvcController->DependencyStructure();
  }

  if (mCodecParams.mTemporalLayerNum > 1) {
    int temporalIdx = std::clamp(aEncodedFrame->GetTemporalLayerId(), 0,
                                 mCodecParams.mTemporalLayerNum - 1);
    unit.SetTemporalIndex(temporalIdx);
    info.codecSpecific.H264.temporal_idx = temporalIdx;
    info.scalability_mode = GmpCodecParamsToScalabilityMode(mCodecParams);

    if (temporalIdx == 0) {
      // Base layer. Reset the sync layer tracking.
      mSyncLayerCap = mCodecParams.mTemporalLayerNum;
    } else {
      // Decrease the sync layer tracking. base_layer_sync per upstream code
      // shall be true iff the layer in question only depends on layer 0, i.e.
      // the base layer. Note in L1T3 the frame dependencies (and cap) are:
      //       | Temporal | Dependency |       |
      // Frame | Layer    | Frame      | Sync? |  Cap
      // ===============================================
      //     0 |        0 |          0 | False | _ -> 3
      //     1 |        2 |          0 | True  | 3 -> 2
      //     2 |        1 |          0 | True  | 2 -> 1
      //     3 |        2 |          1 | False | 1 -> 2
      info.codecSpecific.H264.base_layer_sync = temporalIdx < mSyncLayerCap;
      mSyncLayerCap = temporalIdx;
    }
  }

  // Parse QP.
  mH264BitstreamParser.ParseBitstream(unit);
  unit.qp_ = mH264BitstreamParser.GetLastSliceQp().value_or(-1);

  mCallback->OnEncodedImage(unit, &info);
}

void WebrtcGmpVideoEncoder::Dropped(uint64_t aTimestamp) {
  MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
  MOZ_ASSERT(!mInputImageMap.IsEmpty());
  MOZ_ASSERT(mInputImageMap.Length() <= kMaxImagesInFlight);

  size_t nextIdx = mInputImageMap.IndexOfFirstElementGt(
      InputImageData{.gmp_timestamp_us = aTimestamp}, GmpTimestampComparator);
  const size_t numDropped = nextIdx;
  MOZ_ASSERT(nextIdx != 0);
  MOZ_ASSERT(mInputImageMap.ElementAt(nextIdx - 1).gmp_timestamp_us ==
             aTimestamp);
  AutoTArray<uint32_t, kMaxImagesInFlight> droppedRtpTimestamps;
  for (size_t i = 0; i < numDropped; ++i) {
    droppedRtpTimestamps.AppendElement(
        mInputImageMap.ElementAt(i).rtp_timestamp);
  }
  mInputImageMap.RemoveElementsAt(0, numDropped);

  GMP_LOG_DEBUG(
      "GMP Dropped: {} dropped by encoder. Reporting {} frames dropped.",
      aTimestamp, numDropped);

  MutexAutoLock lock(mCallbackMutex);
  if (!mCallback) {
    return;
  }

  for (const uint32_t rtpTimestamp : droppedRtpTimestamps) {
    mCallback->OnFrameDropped(rtpTimestamp, /* spatial_id = */ 0,
                              /* is_end_of_temporal_unit = */ true);
  }
}

// Decoder.
WebrtcGmpVideoDecoder::WebrtcGmpVideoDecoder(std::string aPCHandle,
                                             TrackingId aTrackingId)
    : mGMP(nullptr),
      mInitting(false),
      mHost(nullptr),
      mCallbackMutex("WebrtcGmpVideoDecoder decoded callback mutex"),
      mCallback(nullptr),
      mDecoderStatus(GMPNoErr),
      mPCHandle(std::move(aPCHandle)),
      mTrackingId(std::move(aTrackingId)) {
  MOZ_ASSERT(!mPCHandle.empty());
}

WebrtcGmpVideoDecoder::~WebrtcGmpVideoDecoder() {
  // We should not have been destroyed if we never closed our GMP
  MOZ_ASSERT(!mGMP);
}

bool WebrtcGmpVideoDecoder::Configure(
    const webrtc::VideoDecoder::Settings& settings) {
  if (!mMPS) {
    mMPS = do_GetService("@mozilla.org/gecko-media-plugin-service;1");
  }
  MOZ_ASSERT(mMPS);

  if (!mGMPThread) {
    if (NS_WARN_IF(NS_FAILED(mMPS->GetThread(getter_AddRefs(mGMPThread))))) {
      return false;
    }
  }

  MOZ_ALWAYS_SUCCEEDS(
      mGMPThread->Dispatch(NewRunnableMethod<webrtc::VideoDecoder::Settings>(
          __func__, this, &WebrtcGmpVideoDecoder::Configure_g, settings)));

  return true;
}

void WebrtcGmpVideoDecoder::Configure_g(
    const webrtc::VideoDecoder::Settings& settings) {
  nsTArray<nsCString> tags;
  tags.AppendElement("h264"_ns);
  UniquePtr<GetGMPVideoDecoderCallback> callback =
      MakeUnique<InitDoneCallback>(this);
  mInitting = true;
  nsresult rv =
      mMPS->GetGMPVideoDecoder(nullptr, &tags, ""_ns, std::move(callback));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    GMP_LOG_DEBUG("GMP Decode: GetGMPVideoDecoder failed");
    Close_g();
    NotifyGmpInitDone(mPCHandle, WEBRTC_VIDEO_CODEC_ERROR,
                      "GMP Decode: GetGMPVideoDecoder failed.");
  }
}

int32_t WebrtcGmpVideoDecoder::GmpInitDone_g(GMPVideoDecoderProxy* aGMP,
                                             GMPVideoHost* aHost,
                                             std::string* aErrorOut) {
  if (!mInitting || !aGMP || !aHost) {
    *aErrorOut =
        "GMP Decode: Either init was aborted, "
        "or init failed to supply either a GMP decoder or GMP host.";
    if (aGMP) {
      // This could destroy us, since aGMP may be the last thing holding a ref
      // Return immediately.
      aGMP->Close();
    }
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  mInitting = false;

  if (mGMP && mGMP != aGMP) {
    Close_g();
  }

  mGMP = aGMP;
  mHost = aHost;
  mCachedPluginId = Some(mGMP->GetPluginId());
  mInitPluginEvent.Notify(*mCachedPluginId);

  GMPVideoCodec codec{};
  codec.mGMPApiVersion = kGMPVersion34;
  codec.mLogLevel = GetGMPLibraryLogLevel();

  // XXX this is currently a hack
  // GMPVideoCodecUnion codecSpecific;
  // memset(&codecSpecific, 0, sizeof(codecSpecific));
  nsTArray<uint8_t> codecSpecific;
  nsresult rv = mGMP->InitDecode(codec, codecSpecific, this, 1);
  if (NS_FAILED(rv)) {
    *aErrorOut = "GMP Decode: InitDecode failed";
    mQueuedFrames.Clear();
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  // now release any frames that got queued waiting for InitDone
  if (!mQueuedFrames.IsEmpty()) {
    // So we're safe to call Decode_g(), which asserts it's empty
    nsTArray<UniquePtr<GMPDecodeData>> temp = std::move(mQueuedFrames);
    for (auto& queued : temp) {
      Decode_g(std::move(queued));
    }
  }

  // This is an ugly solution to asynchronous decoding errors
  // from Decode_g() not being returned to the synchronous Decode() method.
  // If we don't return an error code at this point, our caller ultimately won't
  // know to request a PLI and the video stream will remain frozen unless an IDR
  // happens to arrive for other reasons. Bug 1492852 tracks implementing a
  // proper solution.
  if (mDecoderStatus != GMPNoErr) {
    GMP_LOG_ERROR("{}: Decoder status is bad ({})!", __PRETTY_FUNCTION__,
                  static_cast<unsigned>(mDecoderStatus));
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  return WEBRTC_VIDEO_CODEC_OK;
}

void WebrtcGmpVideoDecoder::Close_g() {
  GMPVideoDecoderProxy* gmp(mGMP);
  mGMP = nullptr;
  mHost = nullptr;
  mInitting = false;

  if (mCachedPluginId) {
    mReleasePluginEvent.Notify(*mCachedPluginId);
  }
  mCachedPluginId = Nothing();

  if (gmp) {
    // Do this last, since this could cause us to be destroyed
    gmp->Close();
  }
}

int32_t WebrtcGmpVideoDecoder::Decode(const webrtc::EncodedImage& aInputImage,
                                      bool aMissingFrames,
                                      int64_t aRenderTimeMs) {
  MOZ_ASSERT(mGMPThread);
  MOZ_ASSERT(!NS_IsMainThread());
  if (!aInputImage.size()) {
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  MediaInfoFlag flag = MediaInfoFlag::None;
  flag |= (aInputImage._frameType == webrtc::VideoFrameType::kVideoFrameKey
               ? MediaInfoFlag::KeyFrame
               : MediaInfoFlag::NonKeyFrame);
  flag |= MediaInfoFlag::SoftwareDecoding;
  flag |= MediaInfoFlag::VIDEO_H264;
  mPerformanceRecorder.Start((aInputImage.RtpTimestamp() * 1000ll) / 90,
                             "WebrtcGmpVideoDecoder"_ns, mTrackingId, flag);

  // This is an ugly solution to asynchronous decoding errors
  // from Decode_g() not being returned to the synchronous Decode() method.
  // If we don't return an error code at this point, our caller ultimately won't
  // know to request a PLI and the video stream will remain frozen unless an IDR
  // happens to arrive for other reasons. Bug 1492852 tracks implementing a
  // proper solution.
  auto decodeData =
      MakeUnique<GMPDecodeData>(aInputImage, aMissingFrames, aRenderTimeMs);

  MOZ_ALWAYS_SUCCEEDS(
      mGMPThread->Dispatch(NewRunnableMethod<UniquePtr<GMPDecodeData>&&>(
          __func__, this, &WebrtcGmpVideoDecoder::Decode_g,
          std::move(decodeData))));

  if (mDecoderStatus != GMPNoErr) {
    GMP_LOG_ERROR("{}: Decoder status is bad ({})!", __PRETTY_FUNCTION__,
                  static_cast<unsigned>(mDecoderStatus));
    return WEBRTC_VIDEO_CODEC_ERROR;
  }

  return WEBRTC_VIDEO_CODEC_OK;
}

// OpenH264 treats an Access Unit Delimiter (NALU type 9) as end-of-picture and
// discards the frame when encountered. Some senders (e.g. GeForce NOW) emit an
// AUD per access unit, so remove them from the Annex-B bitstream before
// decoding. Returns true when an AUD was present or false if not.
// aOut holds every non-empty non-AUD NALU (and is empty if there were none).
bool StripH264AccessUnitDelimiters(const uint8_t* aData, size_t aSize,
                                   nsTArray<uint8_t>& aOut) {
  MOZ_ASSERT(aOut.IsEmpty());

  const std::vector<webrtc::H264::NaluIndex> nalus =
      webrtc::H264::FindNaluIndices(std::span<const uint8_t>(aData, aSize));

  // Guard payload_size: FindNaluIndices can report an empty NALU (e.g. a
  // trailing start code) whose header byte is out of bounds.
  const auto isAud = [&](const webrtc::H264::NaluIndex& aNalu) {
    return aNalu.payload_size > 0 &&
           webrtc::H264::ParseNaluType(aData[aNalu.payload_start_offset]) ==
               webrtc::H264::NaluType::kAud;
  };

  bool hasAud = false;
  for (const auto& nalu : nalus) {
    if (isAud(nalu)) {
      hasAud = true;
      break;
    }
  }
  if (!hasAud) {
    return false;
  }

  aOut.SetCapacity(aSize);
  bool wroteFirstKeptNalu = false;
  for (const auto& nalu : nalus) {
    if (!nalu.payload_size || isAud(nalu)) {
      continue;
    }
    if (!wroteFirstKeptNalu) {
      // Normalize the first kept NALU to a 4-byte start code: the GMP packaging
      // overwrites the leading start code with a 4-byte length, so it must be
      // that size regardless of the original (possibly 3-byte) start code.
      static constexpr uint8_t kLongStartCode[] = {0, 0, 0, 1};
      aOut.AppendElements(kLongStartCode, sizeof(kLongStartCode));
      aOut.AppendElements(aData + nalu.payload_start_offset, nalu.payload_size);
      wroteFirstKeptNalu = true;
      continue;
    }
    aOut.AppendElements(
        aData + nalu.start_offset,
        nalu.payload_start_offset - nalu.start_offset + nalu.payload_size);
  }
  return true;
}

void WebrtcGmpVideoDecoder::Decode_g(UniquePtr<GMPDecodeData>&& aDecodeData) {
  CheckedInt<uint32_t> dataSize(aDecodeData->mImage.size());
  dataSize -= 4;
  if (!dataSize.isValid()) {
    GMP_LOG_ERROR("{}: bad input size ({})!", __PRETTY_FUNCTION__,
                  aDecodeData->mImage.size());
    mDecoderStatus = GMPInvalidArgErr;
    return;
  }

  if (!mGMP) {
    if (mInitting) {
      // InitDone hasn't been called yet (race)
      mQueuedFrames.AppendElement(std::move(aDecodeData));
      return;
    }
    // destroyed via Terminate(), failed to init, or just not initted yet
    GMP_LOG_DEBUG("GMP Decode: not initted yet");

    mDecoderStatus = GMPDecodeErr;
    return;
  }

  MOZ_ASSERT(mQueuedFrames.IsEmpty());
  MOZ_ASSERT(mHost);

  const uint8_t* data = aDecodeData->mImage.data();
  size_t size = aDecodeData->mImage.size();
  nsTArray<uint8_t> withoutAud;
  if (StripH264AccessUnitDelimiters(data, size, withoutAud)) {
    if (withoutAud.Length() < webrtc::H264::kNaluLongStartSequenceSize) {
      GMP_LOG_DEBUG("{}: dropping AUD-only H.264 access unit",
                    __PRETTY_FUNCTION__);
      mDecoderStatus = GMPNoErr;
      return;
    }
    data = withoutAud.Elements();
    size = withoutAud.Length();
  }

  GMPVideoFrame* ftmp = nullptr;
  GMPErr err = mHost->CreateFrame(kGMPEncodedVideoFrame, &ftmp);
  if (err != GMPNoErr) {
    GMP_LOG_ERROR("{}: CreateFrame failed ({})!", __PRETTY_FUNCTION__,
                  static_cast<unsigned>(err));
    mDecoderStatus = err;
    return;
  }

  GMPUniquePtr<GMPVideoEncodedFrame> frame(
      static_cast<GMPVideoEncodedFrame*>(ftmp));
  err = frame->CreateEmptyFrame(size);
  if (err != GMPNoErr) {
    GMP_LOG_ERROR("{}: CreateEmptyFrame failed ({})!", __PRETTY_FUNCTION__,
                  static_cast<unsigned>(err));
    mDecoderStatus = err;
    return;
  }

  // XXX At this point, we only will get mode1 data (a single length and a
  // buffer) Session_info.cc/etc code needs to change to support mode 0.
  *(reinterpret_cast<uint32_t*>(frame->Buffer())) = frame->Size();

  // XXX It'd be wonderful not to have to memcpy the encoded data!
  memcpy(frame->Buffer() + 4, data + 4, frame->Size() - 4);

  frame->SetEncodedWidth(aDecodeData->mImage._encodedWidth);
  frame->SetEncodedHeight(aDecodeData->mImage._encodedHeight);
  frame->SetTimeStamp((aDecodeData->mImage.RtpTimestamp() * 1000ll) /
                      90);  // rounds down
  frame->SetCompleteFrame(
      true);  // upstream no longer deals with incomplete frames
  frame->SetBufferType(GMP_BufferLength32);

  GMPVideoFrameType ft;
  int32_t ret =
      WebrtcFrameTypeToGmpFrameType(aDecodeData->mImage._frameType, &ft);
  if (ret != WEBRTC_VIDEO_CODEC_OK) {
    GMP_LOG_ERROR("{}: WebrtcFrameTypeToGmpFrameType failed ({})!",
                  __PRETTY_FUNCTION__, ret);
    mDecoderStatus = GMPDecodeErr;
    return;
  }

  GMPCodecSpecificInfo info{};
  info.mCodecType = kGMPVideoCodecH264;
  info.mCodecSpecific.mH264.mSimulcastIdx = 0;
  nsTArray<uint8_t> codecSpecificInfo;
  codecSpecificInfo.AppendElements((uint8_t*)&info,
                                   sizeof(GMPCodecSpecificInfo));

  GMP_LOG_DEBUG("GMP Decode: {}, len {}{}", frame->TimeStamp(),
                aDecodeData->mImage.size(),
                ft == kGMPKeyFrame ? ", KeyFrame" : "");

  nsresult rv = mGMP->Decode(std::move(frame), aDecodeData->mMissingFrames,
                             codecSpecificInfo, aDecodeData->mRenderTimeMs);
  if (NS_FAILED(rv)) {
    GMP_LOG_ERROR("{}: Decode failed (rv={})!", __PRETTY_FUNCTION__, rv);
    mDecoderStatus = GMPDecodeErr;
    return;
  }

  mDecoderStatus = GMPNoErr;
}

int32_t WebrtcGmpVideoDecoder::RegisterDecodeCompleteCallback(
    webrtc::DecodedImageCallback* aCallback) {
  MutexAutoLock lock(mCallbackMutex);
  mCallback = aCallback;

  return WEBRTC_VIDEO_CODEC_OK;
}

int32_t WebrtcGmpVideoDecoder::ReleaseGmp() {
  GMP_LOG_DEBUG("GMP Released:");
  RegisterDecodeCompleteCallback(nullptr);

  if (mGMPThread) {
    MOZ_ALWAYS_SUCCEEDS(mGMPThread->Dispatch(
        NewRunnableMethod(__func__, this, &WebrtcGmpVideoDecoder::Close_g)));
  }
  return WEBRTC_VIDEO_CODEC_OK;
}

void WebrtcGmpVideoDecoder::Terminated() {
  GMP_LOG_DEBUG("GMP Decoder Terminated: {}", fmt::ptr((void*)this));

  GMPVideoDecoderProxy* gmp(mGMP);
  mGMP = nullptr;
  mHost = nullptr;
  mInitting = false;

  if (gmp) {
    // Do this last, since this could cause us to be destroyed
    gmp->Close();
  }

  // Could now notify that it's dead
}

void WebrtcGmpVideoDecoder::Decoded(GMPVideoi420Frame* aDecodedFrame) {
  // we have two choices here: wrap the frame with a callback that frees
  // the data later (risking running out of shmems), or copy the data out
  // always.  Also, we can only Destroy() the frame on the gmp thread, so
  // copying is simplest if expensive.
  // I420 size including rounding...
  CheckedInt32 length = (CheckedInt32(aDecodedFrame->Stride(kGMPYPlane)) *
                         aDecodedFrame->Height()) +
                        (CheckedInt32(aDecodedFrame->Stride(kGMPVPlane)) +
                         aDecodedFrame->Stride(kGMPUPlane)) *
                            ((aDecodedFrame->Height() + 1) / 2);
  int32_t size = length.isValid() ? length.value() : 0;
  MOZ_RELEASE_ASSERT(length.isValid() && size > 0);

  // Don't use MakeUniqueFallible here, because UniquePtr isn't copyable, and
  // the closure below in WrapI420Buffer uses std::function which _is_ copyable.
  // We'll alloc the buffer here, so we preserve the "fallible" nature, and
  // then hand a shared_ptr, which is copyable, to WrapI420Buffer.
  auto* falliblebuffer = new (std::nothrow) uint8_t[size];
  if (falliblebuffer) {
    auto buffer = std::shared_ptr<uint8_t>(falliblebuffer);

    // This is 3 separate buffers currently anyways, no use in trying to
    // see if we can use a single memcpy.
    uint8_t* buffer_y = buffer.get();
    memcpy(buffer_y, aDecodedFrame->Buffer(kGMPYPlane),
           aDecodedFrame->Stride(kGMPYPlane) * aDecodedFrame->Height());
    // Should this be aligned, making it non-contiguous?  Assume no, this is
    // already factored into the strides.
    uint8_t* buffer_u =
        buffer_y + aDecodedFrame->Stride(kGMPYPlane) * aDecodedFrame->Height();
    memcpy(buffer_u, aDecodedFrame->Buffer(kGMPUPlane),
           aDecodedFrame->Stride(kGMPUPlane) *
               ((aDecodedFrame->Height() + 1) / 2));
    uint8_t* buffer_v = buffer_u + aDecodedFrame->Stride(kGMPUPlane) *
                                       ((aDecodedFrame->Height() + 1) / 2);
    memcpy(buffer_v, aDecodedFrame->Buffer(kGMPVPlane),
           aDecodedFrame->Stride(kGMPVPlane) *
               ((aDecodedFrame->Height() + 1) / 2));

    MutexAutoLock lock(mCallbackMutex);
    if (mCallback) {
      // Note: the last parameter to WrapI420Buffer is named no_longer_used,
      // but is currently called in the destructor of WrappedYuvBuffer when
      // the buffer is "no_longer_used".
      webrtc::scoped_refptr<webrtc::I420BufferInterface> video_frame_buffer =
          webrtc::WrapI420Buffer(
              aDecodedFrame->Width(), aDecodedFrame->Height(), buffer_y,
              aDecodedFrame->Stride(kGMPYPlane), buffer_u,
              aDecodedFrame->Stride(kGMPUPlane), buffer_v,
              aDecodedFrame->Stride(kGMPVPlane), [buffer] {});

      GMP_LOG_DEBUG("GMP Decoded: {}", aDecodedFrame->Timestamp());
      auto videoFrame =
          webrtc::VideoFrame::Builder()
              .set_video_frame_buffer(video_frame_buffer)
              .set_timestamp_rtp(
                  // round up
                  (aDecodedFrame->UpdatedTimestamp() * 90ll + 999) / 1000)
              .build();
      mPerformanceRecorder.Record(
          static_cast<int64_t>(aDecodedFrame->Timestamp()),
          [&](DecodeStage& aStage) {
            aStage.SetImageFormat(DecodeStage::YUV420P);
            aStage.SetResolution(aDecodedFrame->Width(),
                                 aDecodedFrame->Height());
            aStage.SetColorDepth(gfx::ColorDepth::COLOR_8);
          });
      mCallback->Decoded(videoFrame);
    }
  }
  aDecodedFrame->Destroy();
}

}  // namespace mozilla
