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

#include <aom/aomcx.h>

#include <algorithm>

#include "AnnexB.h"
#include "BufferReader.h"
#include "EncoderConfig.h"
#include "FFmpegEncoderModule.h"
#include "FFmpegLog.h"
#include "FFmpegRuntimeLinker.h"
#include "FFmpegUtils.h"
#include "H264.h"
#include "ImageContainer.h"
#include "ImageConversion.h"
#include "libavutil/error.h"
#include "libavutil/pixfmt.h"
#include "libyuv.h"
#include "mozilla/StaticPrefs_media.h"
#include "mozilla/dom/ImageBitmapBinding.h"
#include "mozilla/dom/ImageUtils.h"
#include "mozilla/dom/VideoFrameBinding.h"
#ifdef MOZ_WIDGET_ANDROID
#  include "mozilla/gfx/gfxVars.h"
#endif
#include "nsPrintfCString.h"

// The ffmpeg namespace is introduced to avoid the PixelFormat's name conflicts
// with MediaDataEncoder::PixelFormat in MediaDataEncoder class scope.
namespace ffmpeg {

// TODO: WebCodecs' I420A should map to MediaDataEncoder::PixelFormat and then
// to AV_PIX_FMT_YUVA420P here.
#if LIBAVCODEC_VERSION_MAJOR < 54
using FFmpegPixelFormat = enum PixelFormat;
const FFmpegPixelFormat FFMPEG_PIX_FMT_NONE = FFmpegPixelFormat::PIX_FMT_NONE;
const FFmpegPixelFormat FFMPEG_PIX_FMT_RGBA = FFmpegPixelFormat::PIX_FMT_RGBA;
const FFmpegPixelFormat FFMPEG_PIX_FMT_BGRA = FFmpegPixelFormat::PIX_FMT_BGRA;
const FFmpegPixelFormat FFMPEG_PIX_FMT_RGB24 = FFmpegPixelFormat::PIX_FMT_RGB24;
const FFmpegPixelFormat FFMPEG_PIX_FMT_BGR24 = FFmpegPixelFormat::PIX_FMT_BGR24;
const FFmpegPixelFormat FFMPEG_PIX_FMT_YUV444P =
    FFmpegPixelFormat::PIX_FMT_YUV444P;
const FFmpegPixelFormat FFMPEG_PIX_FMT_YUV422P =
    FFmpegPixelFormat::PIX_FMT_YUV422P;
const FFmpegPixelFormat FFMPEG_PIX_FMT_YUV420P =
    FFmpegPixelFormat::PIX_FMT_YUV420P;
const FFmpegPixelFormat FFMPEG_PIX_FMT_NV12 = FFmpegPixelFormat::PIX_FMT_NV12;
const FFmpegPixelFormat FFMPEG_PIX_FMT_NV21 = FFmpegPixelFormat::PIX_FMT_NV21;
#else
using FFmpegPixelFormat = enum AVPixelFormat;
const FFmpegPixelFormat FFMPEG_PIX_FMT_NONE =
    FFmpegPixelFormat::AV_PIX_FMT_NONE;
const FFmpegPixelFormat FFMPEG_PIX_FMT_RGBA =
    FFmpegPixelFormat::AV_PIX_FMT_RGBA;
const FFmpegPixelFormat FFMPEG_PIX_FMT_BGRA =
    FFmpegPixelFormat::AV_PIX_FMT_BGRA;
const FFmpegPixelFormat FFMPEG_PIX_FMT_RGB24 =
    FFmpegPixelFormat::AV_PIX_FMT_RGB24;
const FFmpegPixelFormat FFMPEG_PIX_FMT_BGR24 =
    FFmpegPixelFormat::AV_PIX_FMT_BGR24;
const FFmpegPixelFormat FFMPEG_PIX_FMT_YUV444P =
    FFmpegPixelFormat::AV_PIX_FMT_YUV444P;
const FFmpegPixelFormat FFMPEG_PIX_FMT_YUV422P =
    FFmpegPixelFormat::AV_PIX_FMT_YUV422P;
const FFmpegPixelFormat FFMPEG_PIX_FMT_YUV420P =
    FFmpegPixelFormat::AV_PIX_FMT_YUV420P;
const FFmpegPixelFormat FFMPEG_PIX_FMT_NV12 =
    FFmpegPixelFormat::AV_PIX_FMT_NV12;
const FFmpegPixelFormat FFMPEG_PIX_FMT_NV21 =
    FFmpegPixelFormat::AV_PIX_FMT_NV21;
#endif

static const char* GetPixelFormatString(FFmpegPixelFormat aFormat) {
  switch (aFormat) {
    case FFMPEG_PIX_FMT_NONE:
      return "none";
    case FFMPEG_PIX_FMT_RGBA:
      return "packed RGBA 8:8:8:8 (32bpp, RGBARGBA...)";
    case FFMPEG_PIX_FMT_BGRA:
      return "packed BGRA 8:8:8:8 (32bpp, BGRABGRA...)";
    case FFMPEG_PIX_FMT_RGB24:
      return "packed RGB 8:8:8 (24bpp, RGBRGB...)";
    case FFMPEG_PIX_FMT_BGR24:
      return "packed RGB 8:8:8 (24bpp, BGRBGR...)";
    case FFMPEG_PIX_FMT_YUV444P:
      return "planar YUV 4:4:4 (24bpp, 1 Cr & Cb sample per 1x1 Y samples)";
    case FFMPEG_PIX_FMT_YUV422P:
      return "planar YUV 4:2:2 (16bpp, 1 Cr & Cb sample per 2x1 Y samples)";
    case FFMPEG_PIX_FMT_YUV420P:
      return "planar YUV 4:2:0 (12bpp, 1 Cr & Cb sample per 2x2 Y samples)";
    case FFMPEG_PIX_FMT_NV12:
      return "planar YUV 4:2:0 (12bpp, 1 interleaved UV components per 1x1 Y "
             "samples)";
    case FFMPEG_PIX_FMT_NV21:
      return "planar YUV 4:2:0 (12bpp, 1 interleaved VU components per 1x1 Y "
             "samples)";
    default:
      break;
  }
  MOZ_ASSERT_UNREACHABLE("Unsupported pixel format");
  return "unsupported";
}

};  // namespace ffmpeg

namespace mozilla {

struct H264Setting {
  int mValue;
  nsCString mString;
};

struct H264LiteralSetting {
  int mValue;
  nsLiteralCString mString;
  H264Setting get() const { return {mValue, mString.AsString()}; }
};

#if LIBAVCODEC_VERSION_MAJOR < 62
static constexpr H264LiteralSetting H264Profiles[]{
    {FF_PROFILE_H264_BASELINE, "baseline"_ns},
    {FF_PROFILE_H264_MAIN, "main"_ns},
    {FF_PROFILE_H264_EXTENDED, ""_ns},
    {FF_PROFILE_H264_HIGH, "high"_ns}};
#else
static constexpr H264LiteralSetting H264Profiles[]{
    {AV_PROFILE_H264_BASELINE, "baseline"_ns},
    {AV_PROFILE_H264_MAIN, "main"_ns},
    {AV_PROFILE_H264_EXTENDED, ""_ns},
    {AV_PROFILE_H264_HIGH, "high"_ns}};
#endif

static Maybe<H264Setting> GetH264Profile(const H264_PROFILE& aProfile) {
  switch (aProfile) {
    case H264_PROFILE::H264_PROFILE_UNKNOWN:
      return Nothing();
    case H264_PROFILE::H264_PROFILE_BASE:
      return Some(H264Profiles[0].get());
    case H264_PROFILE::H264_PROFILE_MAIN:
      return Some(H264Profiles[1].get());
    case H264_PROFILE::H264_PROFILE_EXTENDED:
      return Some(H264Profiles[2].get());
    case H264_PROFILE::H264_PROFILE_HIGH:
      return Some(H264Profiles[3].get());
    default:
      break;
  }
  MOZ_ASSERT_UNREACHABLE("undefined profile");
  return Nothing();
}

static Maybe<H264Setting> GetH264Level(const H264_LEVEL& aLevel) {
  int val = static_cast<int>(aLevel);
  nsPrintfCString str("%d", val);
  str.Insert('.', 1);
  return Some(H264Setting{val, std::move(str)});
}

struct VPXSVCAppendix {
  uint8_t mLayeringMode;
};

struct SVCLayerSettings {
  using CodecAppendix = Variant<VPXSVCAppendix, aom_svc_params_t>;
  size_t mNumberSpatialLayers;
  size_t mNumberTemporalLayers;
  uint8_t mPeriodicity;
  nsTArray<uint8_t> mLayerIds;
  // libvpx: ts_rate_decimator, libaom: framerate_factor
  nsTArray<uint8_t> mRateDecimators;
  nsTArray<uint32_t> mTargetBitrates;
  Maybe<CodecAppendix> mCodecAppendix;
};

static SVCLayerSettings GetSVCLayerSettings(CodecType aCodec,
                                            const ScalabilityMode& aMode,
                                            uint32_t aBitPerSec) {
  // TODO: Apply more sophisticated bitrate allocation, like SvcRateAllocator:
  // https://searchfox.org/mozilla-central/rev/3bd65516eb9b3a9568806d846ba8c81a9402a885/third_party/libwebrtc/modules/video_coding/svc/svc_rate_allocator.h#26

  size_t layers = 0;
  const uint32_t kbps = aBitPerSec / 1000;  // ts_target_bitrate requies kbps.

  uint8_t periodicity;
  nsTArray<uint8_t> layerIds;
  nsTArray<uint8_t> rateDecimators;
  nsTArray<uint32_t> bitrates;

  Maybe<SVCLayerSettings::CodecAppendix> appendix;

  if (aMode == ScalabilityMode::L1T2) {
    // Two temporal layers. 0-1...
    //
    // Frame pattern:
    // Layer 0: |0| |2| |4| |6| |8|
    // Layer 1: | |1| |3| |5| |7| |

    layers = 2;

    // 2 frames per period.
    periodicity = 2;

    // Assign layer ids.
    layerIds.AppendElement(0);
    layerIds.AppendElement(1);

    // Set rate decimators.
    rateDecimators.AppendElement(2);
    rateDecimators.AppendElement(1);

    // Bitrate allocation: L0 - 60%, L1 - 40%.
    bitrates.AppendElement(kbps * 3 / 5);
    bitrates.AppendElement(kbps);

    if (aCodec == CodecType::VP8 || aCodec == CodecType::VP9) {
      appendix.emplace(VPXSVCAppendix{
          .mLayeringMode = 2 /* VP9E_TEMPORAL_LAYERING_MODE_0101 */
      });
    }
  } else {
    MOZ_ASSERT(aMode == ScalabilityMode::L1T3);
    // Three temporal layers. 0-2-1-2...
    //
    // Frame pattern:
    // Layer 0: |0| | | |4| | | |8| |  |  |12|
    // Layer 1: | | |2| | | |6| | | |10|  |  |
    // Layer 2: | |1| |3| |5| |7| |9|  |11|  |

    layers = 3;

    // 4 frames per period
    periodicity = 4;

    // Assign layer ids.
    layerIds.AppendElement(0);
    layerIds.AppendElement(2);
    layerIds.AppendElement(1);
    layerIds.AppendElement(2);

    // Set rate decimators.
    rateDecimators.AppendElement(4);
    rateDecimators.AppendElement(2);
    rateDecimators.AppendElement(1);

    // Bitrate allocation: L0 - 50%, L1 - 20%, L2 - 30%.
    bitrates.AppendElement(kbps / 2);
    bitrates.AppendElement(kbps * 7 / 10);
    bitrates.AppendElement(kbps);

    if (aCodec == CodecType::VP8 || aCodec == CodecType::VP9) {
      appendix.emplace(VPXSVCAppendix{
          .mLayeringMode = 3 /* VP9E_TEMPORAL_LAYERING_MODE_0212 */
      });
    }
  }

  MOZ_ASSERT(layers == bitrates.Length(),
             "Bitrate must be assigned to each layer");
  return SVCLayerSettings{1,
                          layers,
                          periodicity,
                          std::move(layerIds),
                          std::move(rateDecimators),
                          std::move(bitrates),
                          appendix};
}

void FFmpegVideoEncoder<LIBAV_VER>::SVCInfo::UpdateTemporalLayerId() {
  MOZ_ASSERT(!mTemporalLayerIds.IsEmpty());
  mCurrentIndex = (mCurrentIndex + 1) % mTemporalLayerIds.Length();
}

uint8_t FFmpegVideoEncoder<LIBAV_VER>::SVCInfo::CurrentTemporalLayerId() {
  MOZ_ASSERT(!mTemporalLayerIds.IsEmpty());
  return mTemporalLayerIds[mCurrentIndex];
}

void FFmpegVideoEncoder<LIBAV_VER>::SVCInfo::ResetTemporalLayerId() {
  MOZ_ASSERT(!mTemporalLayerIds.IsEmpty());
  mCurrentIndex = 0;
}

FFmpegVideoEncoder<LIBAV_VER>::FFmpegVideoEncoder(
    const FFmpegLibWrapper* aLib, AVCodecID aCodecID,
    const RefPtr<TaskQueue>& aTaskQueue, const EncoderConfig& aConfig)
    : FFmpegDataEncoder(aLib, aCodecID, aTaskQueue, aConfig) {}

RefPtr<MediaDataEncoder::InitPromise> FFmpegVideoEncoder<LIBAV_VER>::Init() {
  FFMPEGV_LOG("Init");
  return InvokeAsync(mTaskQueue, __func__, [self = RefPtr(this)]() {
    MediaResult r = self->InitEncoder();
    if (NS_FAILED(r.Code())) {
      FFMPEGV_LOG("{}", r.Description().get());
      return InitPromise::CreateAndReject(r, __func__);
    }
    return InitPromise::CreateAndResolve(true, __func__);
  });
}

nsCString FFmpegVideoEncoder<LIBAV_VER>::GetDescriptionName() const {
#ifdef USING_MOZFFVPX
  return "ffvpx video encoder"_ns;
#else
  const char* lib =
#  if defined(MOZ_FFMPEG)
      FFmpegRuntimeLinker::LinkStatusLibraryName();
#  else
      "no library: ffmpeg disabled during build";
#  endif
  return nsPrintfCString("ffmpeg video encoder (%s)", lib);
#endif
}

bool FFmpegVideoEncoder<LIBAV_VER>::SvcEnabled() const {
  return mConfig.mScalabilityMode != ScalabilityMode::None;
}

bool FFmpegVideoEncoder<LIBAV_VER>::ShouldTryHardware() const {
#ifdef MOZ_WIDGET_ANDROID
  // On Android, the MediaCodec encoders are the only ones available to us,
  // which may be implemented in hardware or software.
  if (mCodecID == AV_CODEC_ID_H264 || mCodecID == AV_CODEC_ID_HEVC) {
    return StaticPrefs::media_ffvpx_hw_enabled();
  }

  if (StaticPrefs::media_ffvpx_hw_minimal()) {
    return false;
  }
#endif

  if (mConfig.mHardwarePreference == HardwarePreference::RequireSoftware) {
    return false;
  }

  RefPtr<PlatformEncoderModule> pem =
      FFmpegEncoderModule<LIBAV_VER>::Create(mLib);
  return pem->SupportsCodec(mConfig.mCodec)
      .contains(media::EncodeSupport::HardwareEncode);
}

MediaResult FFmpegVideoEncoder<LIBAV_VER>::InitEncoder() {
  MediaResult result(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR);
  if (ShouldTryHardware()) {
    result = InitEncoderInternal(/* aHardware */ true);
  }
  // TODO(aosmond): We should be checking here for RequireHardware, but we fail
  // encoding tests if we don't allow fallback to software on Linux in CI.
  if (NS_FAILED(result.Code())) {
    result = InitEncoderInternal(/* aHardware */ false);
  }
  return result;
}

MediaResult FFmpegVideoEncoder<LIBAV_VER>::InitEncoderInternal(bool aHardware) {
  MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());

  FFMPEGV_LOG("FFmpegVideoEncoder::InitEncoder");

  // Initialize the common members of the encoder instance
  auto r = AllocateCodecContext(aHardware);
  if (r.isErr()) {
    return r.inspectErr();
  }
  mCodecContext = r.unwrap();
  mCodecName = mCodecContext->codec->name;

#ifdef MOZ_WIDGET_ANDROID
  // We need to create a MediaCodec encoder for H264/HEVC but it may or may not
  // be backed by actual hardware.
  switch (mCodecID) {
    case AV_CODEC_ID_H264:
      mIsHardwareAccelerated = aHardware && gfx::gfxVars::UseH264HwEncode();
      break;
    case AV_CODEC_ID_HEVC:
      mIsHardwareAccelerated = aHardware && gfx::gfxVars::UseHEVCHwEncode();
      break;
    default:
      mIsHardwareAccelerated = aHardware;
      break;
  }
  mUseDurationMap = aHardware;
#else
#  ifdef MOZ_FFMPEG_ENCODER_USE_DURATION_MAP
  mUseDurationMap = true;
#  endif
  mIsHardwareAccelerated = aHardware;
#endif

  // And now the video-specific part
#ifdef MOZ_WIDGET_ANDROID
  // COLOR_FormatYUV420SemiPlanar(NV12) is the most widely supported
  // format by the Android hardware encoders.
  mCodecContext->pix_fmt =
      aHardware ? ffmpeg::FFMPEG_PIX_FMT_NV12 : ffmpeg::FFMPEG_PIX_FMT_YUV420P;
#else
  mCodecContext->pix_fmt = ffmpeg::FFMPEG_PIX_FMT_YUV420P;
  // // TODO: do this properly, based on the colorspace of the frame. Setting
  // this like that crashes encoders. if (mConfig.mCodec != CodecType::AV1) {
  //     if (mConfig.mPixelFormat == dom::ImageBitmapFormat::RGBA32 ||
  //         mConfig.mPixelFormat == dom::ImageBitmapFormat::BGRA32) {
  //       mCodecContext->color_primaries = AVCOL_PRI_BT709;
  //       mCodecContext->colorspace = AVCOL_SPC_RGB;
  //   #ifdef FFVPX_VERSION
  //       mCodecContext->color_trc = AVCOL_TRC_IEC61966_2_1;
  //   #endif
  //     } else {
  //       mCodecContext->color_primaries = AVCOL_PRI_BT709;
  //       mCodecContext->colorspace = AVCOL_SPC_BT709;
  //       mCodecContext->color_trc = AVCOL_TRC_BT709;
  //     }
  // }
#endif

  mCodecContext->width = static_cast<int>(mConfig.mSize.width);
  mCodecContext->height = static_cast<int>(mConfig.mSize.height);
  // Reasonnable default for the quantization range.
  mCodecContext->qmin =
      AssertedCast<int>(StaticPrefs::media_ffmpeg_encoder_quantizer_min());
  mCodecContext->qmax =
      AssertedCast<int>(StaticPrefs::media_ffmpeg_encoder_quantizer_max());
  if (mConfig.mUsage == Usage::Realtime) {
    mCodecContext->thread_count = 1;
  } else {
    int64_t pixels = mCodecContext->width * mCodecContext->height;
    int threads = 1;
    // Select a thread count that depends on the frame size, and cap to the
    // number of available threads minus one
    if (pixels >= 3840 * 2160) {
      threads = 16;
    } else if (pixels >= 1920 * 1080) {
      threads = 8;
    } else if (pixels >= 1280 * 720) {
      threads = 4;
    } else if (pixels >= 640 * 480) {
      threads = 2;
    }
    mCodecContext->thread_count = std::clamp<int>(
        threads, 1, AssertedCast<int>(GetNumberOfProcessors()) - 1);
  }
  // TODO(bug 1869560): The recommended time_base is the reciprocal of the frame
  // rate, but we set it to microsecond for now.
  mCodecContext->time_base =
      AVRational{.num = 1, .den = AssertedCast<int>(USECS_PER_S)};
#if LIBAVCODEC_VERSION_MAJOR >= 57
  // Note that sometimes framerate can be zero (from webcodecs).
  mCodecContext->framerate =
      AVRational{.num = AssertedCast<int>(mConfig.mFramerate), .den = 1};
#endif

#if LIBAVCODEC_VERSION_MAJOR >= 60
  mCodecContext->flags |= AV_CODEC_FLAG_FRAME_DURATION;
#endif

  // Setting 0 here disable inter-frames: all frames are keyframes
  mCodecContext->gop_size = mConfig.mKeyframeInterval
                                ? AssertedCast<int>(mConfig.mKeyframeInterval)
                                : 10000;
  mCodecContext->keyint_min = 0;

  // When either real-time or SVC is enabled via config, the general settings of
  // the encoder are set to be more appropriate for real-time usage
  if (mConfig.mUsage == Usage::Realtime || SvcEnabled()) {
    if (mConfig.mUsage != Usage::Realtime) {
      FFMPEGV_LOG(
          "SVC enabled but low latency encoding mode not enabled, forcing low "
          "latency mode");
    }
    mLib->av_opt_set(mCodecContext->priv_data, "deadline", "realtime", 0);
    // Explicitly ask encoder do not keep in flight at any one time for
    // lookahead purposes.
    mLib->av_opt_set(mCodecContext->priv_data, "lag-in-frames", "0", 0);

    if (mConfig.mCodec == CodecType::VP8 || mConfig.mCodec == CodecType::VP9) {
      mLib->av_opt_set(mCodecContext->priv_data, "error-resilient", "1", 0);
    }
    if (mConfig.mCodec == CodecType::AV1) {
      mLib->av_opt_set(mCodecContext->priv_data, "error-resilience", "1", 0);
      // This sets usage to AOM_USAGE_REALTIME
      mLib->av_opt_set(mCodecContext->priv_data, "usage", "1", 0);
      // Allow the bitrate to swing 50% up and down the target
      mLib->av_opt_set(mCodecContext->priv_data, "rc_undershoot_percent", "50",
                       0);
      mLib->av_opt_set(mCodecContext->priv_data, "rc_overshoot_percent", "50",
                       0);
      // Row multithreading -- note that we do single threaded encoding for now,
      // so this doesn't do much
      mLib->av_opt_set(mCodecContext->priv_data, "row_mt", "1", 0);
      // Cyclic refresh adaptive quantization
      mLib->av_opt_set(mCodecContext->priv_data, "aq-mode", "3", 0);
      // optimized for real-time, 7 for regular, lower: more cpu use -> higher
      // compression ratio
      mLib->av_opt_set(mCodecContext->priv_data, "cpu-used", "9", 0);
      // disable, this is to handle camera motion, unlikely for our use case
      mLib->av_opt_set(mCodecContext->priv_data, "enable-global-motion", "0",
                       0);
      mLib->av_opt_set(mCodecContext->priv_data, "enable-cfl-intra", "0", 0);
      // TODO: Set a number of tiles appropriate for the number of threads used
      // -- disable tiling if using a single thread.
      mLib->av_opt_set(mCodecContext->priv_data, "tile-columns", "0", 0);
      mLib->av_opt_set(mCodecContext->priv_data, "tile-rows", "0", 0);
    }
  } else {
    if (mConfig.mCodec == CodecType::AV1) {
      mLib->av_opt_set_int(
          mCodecContext->priv_data, "cpu-used",
          AssertedCast<int>(StaticPrefs::media_ffmpeg_encoder_cpu_used()), 0);
    }
  }

  if (SvcEnabled()) {
    if (Maybe<SVCSettings> settings = GetSVCSettings()) {
      if (mCodecName == "libaom-av1") {
        if (mConfig.mBitrateMode != BitrateMode::Constant) {
          return MediaResult(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR,
                             "AV1 with SVC only supports constant bitrate"_ns);
        }
      }

      SVCSettings s = settings.extract();
      FFMPEGV_LOG("SVC options string: {}={}", s.mSettingKeyValue.first.get(),
                  s.mSettingKeyValue.second.get());
      mLib->av_opt_set(mCodecContext->priv_data, s.mSettingKeyValue.first.get(),
                       s.mSettingKeyValue.second.get(), 0);

      // FFmpegVideoEncoder is reset after Drain(), so mSVCInfo should be
      // reset() before emplace().
      mSVCInfo.reset();
      mSVCInfo.emplace(std::move(s.mTemporalLayerIds));

      // TODO: layer settings should be changed dynamically when the frame's
      // color space changed.
    }
  }

#ifdef MOZ_WIDGET_ANDROID
  // If we are using MediaCodec, we can set more options.
  if (aHardware) {
    if (mConfig.mBitrateMode == BitrateMode::Constant) {
      mLib->av_opt_set(mCodecContext->priv_data, "bitrate_mode", "cbr", 0);
    } else {
      mLib->av_opt_set(mCodecContext->priv_data, "bitrate_mode", "vbr", 0);
    }
  }
#endif

  nsAutoCString h264Log;
  if (mConfig.mCodecSpecific.is<H264Specific>()) {
    const H264Specific& h264Specific =
        mConfig.mCodecSpecific.as<H264Specific>();
    H264Settings s = GetH264Settings(h264Specific);
    mCodecContext->profile = s.mProfile;
    mCodecContext->level = s.mLevel;
    for (const auto& pair : s.mSettingKeyValuePairs) {
      mLib->av_opt_set(mCodecContext->priv_data, pair.first.get(),
                       pair.second.get(), 0);
    }

    // Log the settings.
    const char* formatStr =
        h264Specific.mFormat == H264BitStreamFormat::AVC ? "AVCC" : "AnnexB";
    if (mCodecName.Equals("libx264"_ns)) {
      // When using profile other than EXTENDED, the profile string is in the
      // first element of mSettingKeyValuePairs, while EXTENDED profile has no
      // profile string.

      MOZ_ASSERT_IF(
          s.mSettingKeyValuePairs.Length() != 3,
          h264Specific.mProfile == H264_PROFILE::H264_PROFILE_EXTENDED);
      const char* profileStr = s.mSettingKeyValuePairs.Length() == 3
                                   ? s.mSettingKeyValuePairs[0].second.get()
                                   : "extended";
      const char* levelStr = s.mSettingKeyValuePairs.Length() == 3
                                 ? s.mSettingKeyValuePairs[1].second.get()
                                 : s.mSettingKeyValuePairs[0].second.get();
      h264Log.AppendPrintf(", H264: profile - %d (%s), level %d (%s), %s",
                           mCodecContext->profile, profileStr,
                           mCodecContext->level, levelStr, formatStr);
    } else {
      h264Log.AppendPrintf(", H264: profile - %d, level %d, %s",
                           mCodecContext->profile, mCodecContext->level,
                           formatStr);
    }
  }

  // - if mConfig.mDenoising is set: av_opt_set_int(mCodecContext->priv_data,
  // "noise_sensitivity", x, 0), where the x is from 0(disabled) to 6.
  // - if mConfig.mAdaptiveQp is set: av_opt_set_int(mCodecContext->priv_data,
  // "aq_mode", x, 0), where x is from 0 to 3: 0 - Disabled, 1 - Variance
  // AQ(default), 2 - Complexity AQ, 3 - Cycle AQ.

  // Our old version of libaom-av1 is considered experimental by the recent
  // ffmpeg we use. Allow experimental codecs for now until we decide on an AV1
  // encoder.
  mCodecContext->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;

  SetContextBitrate();

  AVDictionary* options = nullptr;
  if (int ret = OpenCodecContext(mCodecContext->codec, &options); ret < 0) {
    return MediaResult(
        NS_ERROR_DOM_MEDIA_FATAL_ERR,
        RESULT_DETAIL("failed to open %s avcodec: %s", mCodecName.get(),
                      MakeErrorString(mLib, ret).get()));
  }
  mLib->av_dict_free(&options);

  FFMPEGV_LOG(
      "{} has been initialized with format: {}, bitrate: {}, width: {}, "
      "height: {}, quantizer: [{}, {}], time_base: {}/{}{}",
      mCodecName.get(), ffmpeg::GetPixelFormatString(mCodecContext->pix_fmt),
      static_cast<int64_t>(mCodecContext->bit_rate), mCodecContext->width,
      mCodecContext->height, mCodecContext->qmin, mCodecContext->qmax,
      mCodecContext->time_base.num, mCodecContext->time_base.den,
      h264Log.IsEmpty() ? "" : h264Log.get());

  return NS_OK;
}

// avcodec_send_frame and avcodec_receive_packet were introduced in version 58.
#if LIBAVCODEC_VERSION_MAJOR >= 58
Result<MediaDataEncoder::EncodedData, MediaResult> FFmpegVideoEncoder<
    LIBAV_VER>::EncodeInputWithModernAPIs(RefPtr<const MediaData> aSample) {
  MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
  MOZ_ASSERT(mCodecContext);
  MOZ_ASSERT(aSample);

  RefPtr<const VideoData> sample(aSample->As<VideoData>());

  // Validate input.
  if (!sample->mImage) {
    return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "No image"_ns));
  }
  if (sample->mImage->GetSize().IsEmpty()) {
    return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                           "image width or height is invalid"_ns));
  }

  // Allocate AVFrame.
  if (!PrepareFrame()) {
    return Err(
        MediaResult(NS_ERROR_OUT_OF_MEMORY, "failed to allocate frame"_ns));
  }

  // Set AVFrame properties for its internal data allocation. For now, we always
  // convert into ffmpeg's buffer.
  mFrame->format = mCodecContext->pix_fmt;
  mFrame->width = static_cast<int>(mConfig.mSize.width);
  mFrame->height = static_cast<int>(mConfig.mSize.height);
  mFrame->pict_type =
      sample->mKeyframe ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_NONE;

  // Allocate AVFrame data.
  if (int ret = mLib->av_frame_get_buffer(mFrame, 0); ret < 0) {
    return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                           RESULT_DETAIL("failed to allocate frame data: %s",
                                         MakeErrorString(mLib, ret).get())));
  }

  // Make sure AVFrame is writable.
  if (int ret = mLib->av_frame_make_writable(mFrame); ret < 0) {
    return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                           RESULT_DETAIL("failed to make frame writable: %s",
                                         MakeErrorString(mLib, ret).get())));
  }

  nsresult rv;
  switch (mFrame->format) {
    case ffmpeg::FFMPEG_PIX_FMT_YUV420P:
      rv = ConvertToI420(sample->mImage, mFrame->data[0], mFrame->linesize[0],
                         mFrame->data[1], mFrame->linesize[1], mFrame->data[2],
                         mFrame->linesize[2], mConfig.mSize);
      break;
    case ffmpeg::FFMPEG_PIX_FMT_NV12:
      rv = ConvertToNV12(sample->mImage, mFrame->data[0], mFrame->linesize[0],
                         mFrame->data[1], mFrame->linesize[1], mConfig.mSize);
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("unhandled ffmpeg format!");
      rv = NS_ERROR_DOM_MEDIA_FATAL_ERR;
      break;
  }
  if (NS_FAILED(rv)) {
    return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                           "failed to convert format to ffmpeg format"_ns));
  }

  // Set presentation timestamp and duration of the AVFrame. The unit of pts is
  // time_base.
  // TODO(bug 1869560): The recommended time_base is the reciprocal of the frame
  // rate, but we set it to microsecond for now.
#  if LIBAVCODEC_VERSION_MAJOR >= 59
  mFrame->time_base =
      AVRational{.num = 1, .den = AssertedCast<int>(USECS_PER_S)};
#  endif
  // Provide fake pts, see header file.
  if (mConfig.mCodec == CodecType::AV1) {
    mFrame->pts = mFakePts;
    mPtsMap.Insert(mFakePts, aSample->mTime.ToMicroseconds());
    mFakePts += aSample->mDuration.ToMicroseconds();
    mCurrentFramePts = aSample->mTime.ToMicroseconds();
  } else {
    mFrame->pts = aSample->mTime.ToMicroseconds();
  }
#  ifdef MOZ_FFMPEG_ENCODER_USE_DURATION_MAP
  if (mUseDurationMap) {
    mDurationMap.Insert(mFrame->pts, aSample->mDuration.ToMicroseconds());
  }
#  else
  mFrame->duration = aSample->mDuration.ToMicroseconds();
#  endif
  Duration(mFrame) = aSample->mDuration.ToMicroseconds();

  AVDictionary* dict = nullptr;
  // VP8/VP9 use a mode that handles the temporal layer id sequence internally,
  // and don't require setting explicitly setting the metadata. Other codecs
  // such as AV1 via libaom however requires manual frame tagging.
  if (SvcEnabled() && mConfig.mCodec != CodecType::VP8 &&
      mConfig.mCodec != CodecType::VP9) {
    if (aSample->mKeyframe) {
      FFMPEGV_LOG("Key frame requested, reseting temporal layer id");
      mSVCInfo->ResetTemporalLayerId();
    }
    nsPrintfCString str("%d", mSVCInfo->CurrentTemporalLayerId());
    mLib->av_dict_set(&dict, "temporal_id", str.get(), 0);
    mFrame->metadata = dict;
  }

  // Now send the AVFrame to ffmpeg for encoding, same code for audio and video.
  return FFmpegDataEncoder<LIBAV_VER>::EncodeWithModernAPIs();
}
#endif  // if LIBAVCODEC_VERSION_MAJOR >= 58

Result<RefPtr<MediaRawData>, MediaResult>
FFmpegVideoEncoder<LIBAV_VER>::ToMediaRawData(AVPacket* aPacket) {
  MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
  MOZ_ASSERT(aPacket);

  auto extradataResult = GetExtraData(aPacket);
  if (extradataResult.isOk()) {
    mLastExtraData = extradataResult.unwrap();
  } else if (extradataResult.isErr()) {
    MediaResult e = extradataResult.unwrapErr();
    if (e.Code() != NS_ERROR_NOT_AVAILABLE &&
        e.Code() != NS_ERROR_NOT_IMPLEMENTED) {
      return Err(e);
    }
    FFMPEGV_LOG("GetExtraData failed with {}, but we can ignore it for now",
                e.Description().get());
  }

  auto data = MakeRefPtr<MediaRawData>();
  data->mKeyframe = (aPacket->flags & AV_PKT_FLAG_KEY) != 0;
  UniquePtr<MediaRawDataWriter> writer(data->CreateWriter());

  const bool isH264 =
      mCodecID == AV_CODEC_ID_H264 && mConfig.mCodecSpecific.is<H264Specific>();

#ifdef MOZ_WIDGET_ANDROID
  // If we have an AnnexB keyframe, and we failed to extract extradata from the
  // packet, then we know that the SPS/PPS data is missing from the data. In
  // that case we need to prepend our cached extradata to supply it in-band.
  //
  // When our minimum supported Android version is >= 9, then we can just use
  // the prepend-sps-pps-to-idr-frames parameter with AMediaFormat.
  if (data->mKeyframe && !extradataResult.isOk() && mLastExtraData && isH264 &&
      mConfig.mCodecSpecific.as<H264Specific>().mFormat ==
          H264BitStreamFormat::ANNEXB) {
    if (!writer->Append(mLastExtraData->Elements(), mLastExtraData->Length())) {
      return Err(
          MediaResult(NS_ERROR_OUT_OF_MEMORY,
                      "fail to append extradata to MediaRawData buffer"_ns));
    }
  }
#endif

  if (!writer->Append(aPacket->data, aPacket->size)) {
    return Err(MediaResult(NS_ERROR_OUT_OF_MEMORY,
                           "fail to append packet to MediaRawData buffer"_ns));
  }

  // With AVC, we are expected to provide the extradata out-of-band. This will
  // either be done as part of our conversion from AnnexB, or we will attach the
  // extradata we cached/extracted from the packet earlier.
  if (isH264 && mConfig.mCodecSpecific.as<H264Specific>().mFormat ==
                    H264BitStreamFormat::AVC) {
    if (!mCodecName.Equals("libx264"_ns) && AnnexB::IsAnnexB(*data)) {
      if (!AnnexB::ConvertSampleToAVCC(data, mLastExtraData)) {
        return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                               "Failed to convert to AVCC"_ns));
      }
    } else {
      data->mExtraData = mLastExtraData;
    }
  }

  // TODO(bug 1869560): The unit of pts, dts, and duration is time_base, which
  // is recommended to be the reciprocal of the frame rate, but we set it to
  // microsecond for now.
  data->mTime = media::TimeUnit::FromMicroseconds(aPacket->pts);
#ifdef MOZ_FFMPEG_ENCODER_USE_DURATION_MAP
  Maybe<int64_t> duration;
  if (mUseDurationMap && (duration = mDurationMap.Take(aPacket->pts))) {
    data->mDuration = media::TimeUnit::FromMicroseconds(*duration);
  } else
#endif
  {
    data->mDuration = media::TimeUnit::FromMicroseconds(aPacket->duration);
  }
  data->mTimecode = media::TimeUnit::FromMicroseconds(aPacket->dts);

  if (mConfig.mCodec == CodecType::AV1) {
    auto found = mPtsMap.Take(aPacket->pts);
    data->mTime = media::TimeUnit::FromMicroseconds(found.value());
  }

  if (mSVCInfo) {
    if (data->mKeyframe) {
      FFMPEGV_LOG(
          "Encoded packet is key frame, reseting temporal layer id sequence");
      mSVCInfo->ResetTemporalLayerId();
    }
    uint8_t temporalLayerId = mSVCInfo->CurrentTemporalLayerId();
    data->mTemporalLayerId.emplace(temporalLayerId);
    mSVCInfo->UpdateTemporalLayerId();
  }

  return data;
}

Result<already_AddRefed<MediaByteBuffer>, MediaResult>
FFmpegVideoEncoder<LIBAV_VER>::GetExtraData(AVPacket* aPacket) {
  MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
  MOZ_ASSERT(aPacket);

  // We only extract the extra data when encoding into AVCC format.
  if (mCodecID != AV_CODEC_ID_H264 ||
      !mConfig.mCodecSpecific.is<H264Specific>()) {
    return Err(
        MediaResult(NS_ERROR_NOT_AVAILABLE, "Extra data unnecessary"_ns));
  }

  const bool wantAVCC = mConfig.mCodecSpecific.as<H264Specific>().mFormat ==
                        H264BitStreamFormat::AVC;

  Span<const uint8_t> packetBuf(aPacket->data,
                                AssertedCast<size_t>(aPacket->size));
  if (!mCodecName.Equals("libx264"_ns) && AnnexB::IsAnnexB(packetBuf)) {
    auto extraData = wantAVCC ? AnnexB::ExtractExtraDataForAVCC(packetBuf)
                              : AnnexB::ExtractExtraData(packetBuf);
    if (!extraData) {
      return Err(MediaResult(NS_ERROR_NOT_AVAILABLE,
                             "Extra data missing from packet"_ns));
    }
    return extraData.forget();
  }

  if (!(aPacket->flags & AV_PKT_FLAG_KEY)) {
    return Err(MediaResult(NS_ERROR_NOT_AVAILABLE,
                           "Extra data only comes with key frame"_ns));
  }

  if (!mCodecName.Equals("libx264"_ns)) {
    return Err(MediaResult(
        NS_ERROR_NOT_IMPLEMENTED,
        RESULT_DETAIL(
            "Get extra data from codec %s has not been implemented yet",
            mCodecName.get())));
  }

  if (!wantAVCC) {
    return Err(
        MediaResult(NS_ERROR_NOT_AVAILABLE, "Extra data unnecessary"_ns));
  }

  bool useGlobalHeader =
#if LIBAVCODEC_VERSION_MAJOR >= 57
      mCodecContext->flags & AV_CODEC_FLAG_GLOBAL_HEADER;
#else
      false;
#endif

  Span<const uint8_t> buf;
  if (useGlobalHeader) {
    buf = Span<const uint8_t>(
        mCodecContext->extradata,
        AssertedCast<size_t>(mCodecContext->extradata_size));
  } else {
    buf = packetBuf;
  }
  if (buf.empty()) {
    return Err(MediaResult(NS_ERROR_UNEXPECTED,
                           "fail to get H264 AVCC header in key frame!"_ns));
  }

  BufferReader reader(buf);

  // The first part is sps.
  uint32_t spsSize = MOZ_TRY(reader.ReadU32());
  Span<const uint8_t> spsData =
      MOZ_TRY(reader.ReadSpan<const uint8_t>(static_cast<size_t>(spsSize)));

  // The second part is pps.
  uint32_t ppsSize = MOZ_TRY(reader.ReadU32());
  Span<const uint8_t> ppsData =
      MOZ_TRY(reader.ReadSpan<const uint8_t>(static_cast<size_t>(ppsSize)));

  // Ensure we have profile, constraints and level needed to create the extra
  // data.
  if (spsData.Length() < 4) {
    return Err(MediaResult(NS_ERROR_UNEXPECTED, "spsData is too short"_ns));
  }

  FFMPEGV_LOG(
      "Generate extra data: profile - {}, constraints: {}, level: {} for pts @ "
      "{}",
      spsData[1], spsData[2], spsData[3], aPacket->pts);

  // Create extra data.
  auto extraData = MakeRefPtr<MediaByteBuffer>();
  H264::WriteExtraData(extraData, spsData[1], spsData[2], spsData[3], spsData,
                       ppsData);
  MOZ_ASSERT(extraData);
  return extraData.forget();
}

Maybe<FFmpegVideoEncoder<LIBAV_VER>::SVCSettings>
FFmpegVideoEncoder<LIBAV_VER>::GetSVCSettings() {
  MOZ_ASSERT(!mCodecName.IsEmpty());
  MOZ_ASSERT(SvcEnabled());

  CodecType codecType = CodecType::Unknown;
  if (mCodecName == "libvpx") {
    codecType = CodecType::VP8;
  } else if (mCodecName == "libvpx-vp9") {
    codecType = CodecType::VP9;
  } else if (mCodecName == "libaom-av1") {
    codecType = CodecType::AV1;
  }

  if (codecType == CodecType::Unknown) {
    FFMPEGV_LOG("SVC setting is not implemented for {} codec",
                mCodecName.get());
    return Nothing();
  }

  SVCLayerSettings svc = GetSVCLayerSettings(
      codecType, mConfig.mScalabilityMode, mConfig.mBitrate);

  nsAutoCString name;
  nsAutoCString parameters;

  if (codecType == CodecType::VP8 || codecType == CodecType::VP9) {
    // Check if the number of temporal layers in codec specific settings
    // matches
    // the number of layers for the given scalability mode.
    if (mConfig.mCodecSpecific.is<VP8Specific>()) {
      MOZ_ASSERT(mConfig.mCodecSpecific.as<VP8Specific>().mNumTemporalLayers ==
                 svc.mNumberTemporalLayers);
    } else if (mConfig.mCodecSpecific.is<VP9Specific>()) {
      MOZ_ASSERT(mConfig.mCodecSpecific.as<VP9Specific>().mNumTemporalLayers ==
                 svc.mNumberTemporalLayers);
    }

    // Form an SVC setting string for libvpx.
    name = "ts-parameters"_ns;
    parameters.Append("ts_target_bitrate=");
    for (size_t i = 0; i < svc.mTargetBitrates.Length(); ++i) {
      if (i > 0) {
        parameters.Append(",");
      }
      parameters.AppendPrintf("%d", svc.mTargetBitrates[i]);
    }
    parameters.AppendPrintf(
        ":ts_layering_mode=%u",
        svc.mCodecAppendix->as<VPXSVCAppendix>().mLayeringMode);
  }

  if (codecType == CodecType::AV1) {
    // Form an SVC setting string for libaom.
    name = "svc-parameters"_ns;
    parameters.AppendPrintf("number_spatial_layers=%zu",
                            svc.mNumberSpatialLayers);
    parameters.AppendPrintf(":number_temporal_layers=%zu",
                            svc.mNumberTemporalLayers);
    parameters.Append(":framerate_factor=");
    for (size_t i = 0; i < svc.mRateDecimators.Length(); ++i) {
      if (i > 0) {
        parameters.Append(",");
      }
      parameters.AppendPrintf("%d", svc.mRateDecimators[i]);
    }
    parameters.Append(":layer_target_bitrate=");
    for (size_t i = 0; i < svc.mTargetBitrates.Length(); ++i) {
      if (i > 0) {
        parameters.Append(",");
      }
      parameters.AppendPrintf("%d", svc.mTargetBitrates[i]);
    }
  }

  return Some(
      SVCSettings{std::move(svc.mLayerIds),
                  std::make_pair(std::move(name), std::move(parameters))});
}

FFmpegVideoEncoder<LIBAV_VER>::H264Settings FFmpegVideoEncoder<
    LIBAV_VER>::GetH264Settings(const H264Specific& aH264Specific) {
  nsTArray<std::pair<nsCString, nsCString>> keyValuePairs;

  Maybe<H264Setting> profile = GetH264Profile(aH264Specific.mProfile);
  MOZ_RELEASE_ASSERT(profile.isSome());
  MOZ_RELEASE_ASSERT(!profile->mString.IsEmpty() ||
                     aH264Specific.mProfile ==
                         H264_PROFILE::H264_PROFILE_EXTENDED);

  Maybe<H264Setting> level = GetH264Level(aH264Specific.mLevel);
  MOZ_RELEASE_ASSERT(level.isSome());
  MOZ_RELEASE_ASSERT(!level->mString.IsEmpty());

  if (!profile->mString.IsEmpty()) {
    keyValuePairs.AppendElement(std::make_pair("profile"_ns, profile->mString));
  }
  keyValuePairs.AppendElement(std::make_pair("level"_ns, level->mString));

  if (mCodecName.Equals("libx264"_ns)) {
    // Set format: libx264's default format is annexb.
    if (aH264Specific.mFormat == H264BitStreamFormat::AVC) {
      keyValuePairs.AppendElement(std::make_pair("x264-params"_ns, "annexb=0"));
      // mCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER
      // if we don't want to append SPS/PPS data in all keyframe
      // (LIBAVCODEC_VERSION_MAJOR >= 57 only).
    } else {
      // Set annexb explicitly even if it's default format.
      keyValuePairs.AppendElement(std::make_pair("x264-params"_ns, "annexb=1"));
    }
  }

  return H264Settings{.mProfile = profile->mValue,
                      .mLevel = level->mValue,
                      .mSettingKeyValuePairs = std::move(keyValuePairs)};
}

}  // namespace mozilla
