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

#include "media/ffmpeg/ffmpeg_common.h"

#include <stddef.h>
#include <stdint.h>

#include <array>
#include <cstring>

#include "base/files/memory_mapped_file.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "media/base/audio_decoder_config.h"
#include "media/base/media.h"
#include "media/base/media_util.h"
#include "media/base/supported_types.h"
#include "media/base/test_data_util.h"
#include "media/base/video_decoder_config.h"
#include "media/filters/ffmpeg_glue.h"
#include "media/filters/in_memory_url_protocol.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(USE_PROPRIETARY_CODECS) && BUILDFLAG(ENABLE_PLATFORM_DOLBY_VISION)
#include "media/formats/mp4/dolby_vision.h"
#endif  // BUILDFLAG (USE_PROPRIETARY_CODECS) &&
        // BUILDFLAG(ENABLE_PLATFORM_DOLBY_VISION)

namespace media {

class FFmpegCommonTest : public testing::Test {
 public:
  FFmpegCommonTest() {}
  ~FFmpegCommonTest() override = default;
};

uint8_t kExtraData[5] = {0x00, 0x01, 0x02, 0x03, 0x04};

template <typename T>
void TestConfigConvertExtraData(
    AVStream* stream,
    T* decoder_config,
    const base::RepeatingCallback<bool(const AVStream*, T*)>& converter_fn) {
  // Should initially convert.
  EXPECT_TRUE(converter_fn.Run(stream, decoder_config));

  // Store orig to let FFmpeg free whatever it allocated.
  AVCodecParameters* codec_parameters = stream->codecpar;
  uint8_t* orig_extradata = codec_parameters->extradata;
  int orig_extradata_size = codec_parameters->extradata_size;

  // Valid combination: extra_data = nullptr && size = 0.
  codec_parameters->extradata = nullptr;
  codec_parameters->extradata_size = 0;
  EXPECT_TRUE(converter_fn.Run(stream, decoder_config));
  EXPECT_EQ(static_cast<size_t>(codec_parameters->extradata_size),
            decoder_config->extra_data().size());

  // Valid combination: extra_data = non-nullptr && size > 0.
  codec_parameters->extradata = &kExtraData[0];
  codec_parameters->extradata_size = std::size(kExtraData);
  EXPECT_TRUE(converter_fn.Run(stream, decoder_config));
  EXPECT_EQ(static_cast<size_t>(codec_parameters->extradata_size),
            decoder_config->extra_data().size());
  EXPECT_EQ(AVCodecParametersExtraDataToSpan(codec_parameters),
            base::span(decoder_config->extra_data()));

  // Possible combination: extra_data = nullptr && size != 0, but the converter
  // function considers this valid and having no extra_data, due to behavior of
  // avcodec_parameters_to_context().
  codec_parameters->extradata = nullptr;
  codec_parameters->extradata_size = 10;
  EXPECT_TRUE(converter_fn.Run(stream, decoder_config));
  EXPECT_EQ(0UL, decoder_config->extra_data().size());

  // Invalid combination: extra_data = non-nullptr && size = 0.
  codec_parameters->extradata = &kExtraData[0];
  codec_parameters->extradata_size = 0;
  EXPECT_FALSE(converter_fn.Run(stream, decoder_config));

  // Restore orig values for sane cleanup.
  codec_parameters->extradata = orig_extradata;
  codec_parameters->extradata_size = orig_extradata_size;
}

void VerifyProfileTest(const char* file_name,
                       VideoCodecProfile expected_profile) {
  // Open a file to get a real AVStreams from FFmpeg.
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(GetTestDataFilePath(file_name)));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());
  AVFormatContext* format_context = glue.format_context();

  for (auto* stream : AVFormatContextToSpan(format_context)) {
    AVCodecParameters* codec_parameters = stream->codecpar;
    AVMediaType codec_type = codec_parameters->codec_type;

    if (codec_type == AVMEDIA_TYPE_VIDEO) {
      VideoDecoderConfig video_config;
      EXPECT_TRUE(AVStreamToVideoDecoderConfig(stream, &video_config));
      EXPECT_EQ(expected_profile, video_config.profile());
    } else {
      // Only process video.
      continue;
    }
  }
}

TEST_F(FFmpegCommonTest,
       AVCodecContextToAudioDecoderConfig_HandleChannelMismatch) {
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(GetTestDataFilePath("4ch.wav")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());

  AVFormatContext* format_context = glue.format_context();
  AVStream* stream = format_context->streams[0];

  auto codec_context = AVStreamToAVCodecContext(stream);

  // Purposely have an incorrect mask.
  codec_context->ch_layout.u.mask = AV_CH_LAYOUT_MONO;

  AudioDecoderConfig audio_config;
  ASSERT_TRUE(AVCodecContextToAudioDecoderConfig(
      codec_context.get(), EncryptionScheme::kUnencrypted, &audio_config));

  EXPECT_EQ(AudioCodec::kPCM, audio_config.codec());
  // We correctly guess QUAD instead of MONO.
  EXPECT_EQ(CHANNEL_LAYOUT_QUAD, audio_config.channel_layout());
  EXPECT_EQ(4, audio_config.channels());
}

TEST_F(FFmpegCommonTest, AVStreamToDecoderConfig) {
  // Open a file to get a real AVStreams from FFmpeg.
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(GetTestDataFilePath("bear-320x240.webm")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());
  AVFormatContext* format_context = glue.format_context();

  // Find the audio and video streams and test valid and invalid combinations
  // for extradata and extradata_size.
  bool found_audio = false;
  bool found_video = false;
  for (AVStream* stream : AVFormatContextToSpan(format_context)) {
    if (found_audio && found_video) {
      break;
    }

    AVCodecParameters* codec_parameters = stream->codecpar;
    AVMediaType codec_type = codec_parameters->codec_type;

    if (codec_type == AVMEDIA_TYPE_AUDIO) {
      if (found_audio)
        continue;
      found_audio = true;
      AudioDecoderConfig audio_config;
      TestConfigConvertExtraData(
          stream, &audio_config,
          base::BindRepeating(&AVStreamToAudioDecoderConfig));
    } else if (codec_type == AVMEDIA_TYPE_VIDEO) {
      if (found_video)
        continue;
      found_video = true;
      VideoDecoderConfig video_config;
      TestConfigConvertExtraData(
          stream, &video_config,
          base::BindRepeating(&AVStreamToVideoDecoderConfig));
    } else {
      // Only process audio/video.
      continue;
    }
  }

  ASSERT_TRUE(found_audio);
  ASSERT_TRUE(found_video);
}

TEST_F(FFmpegCommonTest, AVStreamToAudioDecoderConfig_OpusAmbisonics_4ch) {
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(
      GetTestDataFilePath("bear-opus-end-trimming-4ch-channelmapping2.webm")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());

  AVFormatContext* format_context = glue.format_context();
  EXPECT_EQ(static_cast<unsigned int>(1), format_context->nb_streams);
  AVStream* stream = format_context->streams[0];

  AVCodecParameters* codec_parameters = stream->codecpar;
  EXPECT_EQ(AVMEDIA_TYPE_AUDIO, codec_parameters->codec_type);

  AudioDecoderConfig audio_config;
  ASSERT_TRUE(AVStreamToAudioDecoderConfig(stream, &audio_config));

  EXPECT_EQ(AudioCodec::kOpus, audio_config.codec());
  EXPECT_EQ(CHANNEL_LAYOUT_QUAD, audio_config.channel_layout());
  EXPECT_EQ(4, audio_config.channels());
}

TEST_F(FFmpegCommonTest, AVStreamToAudioDecoderConfig_OpusAmbisonics_11ch) {
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(
      GetTestDataFilePath("bear-opus-end-trimming-11ch-channelmapping2.webm")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());

  AVFormatContext* format_context = glue.format_context();
  EXPECT_EQ(static_cast<unsigned int>(1), format_context->nb_streams);
  AVStream* stream = format_context->streams[0];

  AVCodecParameters* codec_parameters = stream->codecpar;
  EXPECT_EQ(AVMEDIA_TYPE_AUDIO, codec_parameters->codec_type);

  AudioDecoderConfig audio_config;
  ASSERT_TRUE(AVStreamToAudioDecoderConfig(stream, &audio_config));

  EXPECT_EQ(AudioCodec::kOpus, audio_config.codec());
  EXPECT_EQ(CHANNEL_LAYOUT_DISCRETE, audio_config.channel_layout());
  EXPECT_EQ(11, audio_config.channels());
}

TEST_F(FFmpegCommonTest, AVStreamToAudioDecoderConfig_9ch_wav) {
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(GetTestDataFilePath("9ch.wav")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());

  AVFormatContext* format_context = glue.format_context();
  EXPECT_EQ(static_cast<unsigned int>(1), format_context->nb_streams);
  AVStream* stream = format_context->streams[0];

  AVCodecParameters* codec_parameters = stream->codecpar;
  EXPECT_EQ(AVMEDIA_TYPE_AUDIO, codec_parameters->codec_type);

  AudioDecoderConfig audio_config;
  ASSERT_TRUE(AVStreamToAudioDecoderConfig(stream, &audio_config));

  EXPECT_EQ(AudioCodec::kPCM, audio_config.codec());
  EXPECT_EQ(CHANNEL_LAYOUT_DISCRETE, audio_config.channel_layout());
  EXPECT_EQ(9, audio_config.channels());
}

TEST_F(FFmpegCommonTest, TimeBaseConversions) {
  const auto test_data = std::to_array<std::array<const int64_t, 5>>({
      {1, 2, 1, 500000, 1},
      {1, 3, 1, 333333, 1},
      {1, 3, 2, 666667, 2},
  });

  for (size_t i = 0; i < std::size(test_data); ++i) {
    SCOPED_TRACE(i);

    AVRational time_base;
    time_base.num = static_cast<int>(test_data[i][0]);
    time_base.den = static_cast<int>(test_data[i][1]);

    base::TimeDelta time_delta =
        ConvertFromTimeBase(time_base, test_data[i][2]);

    EXPECT_EQ(time_delta.InMicroseconds(), test_data[i][3]);
    EXPECT_EQ(ConvertToTimeBase(time_base, time_delta), test_data[i][4]);
  }
}

TEST_F(FFmpegCommonTest, VerifyFormatSizes) {
  for (AVSampleFormat format = AV_SAMPLE_FMT_NONE;
       format < AV_SAMPLE_FMT_NB;
       format = static_cast<AVSampleFormat>(format + 1)) {
    std::vector<AVCodecID> codec_ids(1, AV_CODEC_ID_NONE);
    if (format == AV_SAMPLE_FMT_S32)
      codec_ids.push_back(AV_CODEC_ID_PCM_S24LE);
    for (const auto& codec_id : codec_ids) {
      SampleFormat sample_format =
          AVSampleFormatToSampleFormat(format, codec_id);
      if (sample_format == kUnknownSampleFormat) {
        // This format not supported, so skip it.
        continue;
      }

      // Have FFMpeg compute the size of a buffer of 1 channel / 1 frame
      // with 1 byte alignment to make sure the sizes match.
      int single_buffer_size =
          av_samples_get_buffer_size(NULL, 1, 1, format, 1);
      int bytes_per_channel = SampleFormatToBytesPerChannel(sample_format);
      EXPECT_EQ(bytes_per_channel, single_buffer_size);
    }
  }
}

// Verifies there are no collisions of the codec name hashes used for UMA.  Also
// includes code for updating the histograms XML.
TEST_F(FFmpegCommonTest, VerifyUmaCodecHashes) {
  const AVCodecDescriptor* desc = avcodec_descriptor_next(nullptr);

  std::map<int32_t, const char*> sorted_hashes;
  while (desc) {
    const int32_t hash = HashCodecName(desc->name);
    // Ensure there are no collisions.
    ASSERT_TRUE(sorted_hashes.find(hash) == sorted_hashes.end());
    sorted_hashes[hash] = desc->name;

    desc = avcodec_descriptor_next(desc);
  }

  // Add a none entry for when no codec is detected.
  static const char kUnknownCodec[] = "none";
  const int32_t hash = HashCodecName(kUnknownCodec);
  ASSERT_TRUE(sorted_hashes.find(hash) == sorted_hashes.end());
  sorted_hashes[hash] = kUnknownCodec;

  // Uncomment the following lines to generate the "FFmpegCodecHashes" enum for
  // usage in the histogram metrics file.  While it regenerates *ALL* values, it
  // should only be used to *ADD* values to histograms file.  Never delete any
  // values; diff should verify.
#if 0
  static const std::vector<std::pair<std::string, int32_t>> kDeprecatedHashes =
      {
          {"brender_pix_deprecated", -1866047250},
          {"adpcm_vima_deprecated", -1782518388},
          {"pcm_s32le_planar_deprecated", -1328796639},
          {"webp_deprecated", -993429906},
          {"paf_video_deprecated", -881893142},
          {"vima_deprecated", -816209197},
          {"iff_byterun1", -777478450},
          {"paf_audio_deprecated", -630356729},

          {"exr_deprecated", -418117523},
          {"hevc_deprecated", -414733739},
          {"vp7_deprecated", -197551526},
          {"escape130_deprecated", 73149662},
          {"tak_deprecated", 1041617024},
          {"opus_deprecated", 1165132763},
          {"g2m_deprecated", 1194572884},

          {"pcm_s24le_planar_deprecated", 1535518292},
          {"sanm_deprecated", 2047102762},

          {"mpegvideo_xvmc_deprecated", 1550758811},
          {"voxware_deprecated", 1656834662}
      };

  for (auto& kv : kDeprecatedHashes)
    sorted_hashes[kv.second] = kv.first.c_str();
  printf("<enum name=\"FFmpegCodecHashes\">\n");
  for (const auto& kv : sorted_hashes)
    printf("  <int value=\"%d\" label=\"%s\"/>\n", kv.first, kv.second);
  printf("</enum>\n");
#endif
}

#if BUILDFLAG(USE_PROPRIETARY_CODECS)
TEST_F(FFmpegCommonTest, VerifyH264Profile) {
  VerifyProfileTest("bear-1280x720.mp4", H264PROFILE_HIGH);
}

#if BUILDFLAG(ENABLE_PLATFORM_HEVC)
TEST_F(FFmpegCommonTest, VerifyH265MainProfile) {
  VerifyProfileTest("bear-1280x720-hevc.mp4", HEVCPROFILE_MAIN);
}

TEST_F(FFmpegCommonTest, VerifyH265Main10Profile) {
  VerifyProfileTest("bear-1280x720-hevc-10bit.mp4", HEVCPROFILE_MAIN10);
}
#endif  // BUILDFLAG(ENABLE_PLATFORM_HEVC)
#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)

// Verifies that the HDR Metadata and VideoColorSpace are correctly parsed.
TEST_F(FFmpegCommonTest, VerifyHDRMetadataAndColorSpaceInfo) {
  // Open a file to get a real AVStreams from FFmpeg.
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(GetTestDataFilePath("colour.webm")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());
  AVFormatContext* format_context = glue.format_context();
  ASSERT_EQ(format_context->nb_streams, 1u);

  AVStream* stream = format_context->streams[0];
  AVCodecParameters* codec_parameters = stream->codecpar;
  AVMediaType codec_type = codec_parameters->codec_type;
  ASSERT_EQ(codec_type, AVMEDIA_TYPE_VIDEO);

  VideoDecoderConfig video_config;
  EXPECT_TRUE(AVStreamToVideoDecoderConfig(stream, &video_config));
  ASSERT_TRUE(video_config.hdr_metadata().HasMDCV());
  const auto& mdcv = video_config.hdr_metadata().GetMDCV();
  EXPECT_EQ(30.0, mdcv.fMinimumDisplayMasteringLuminance);
  EXPECT_EQ(40.0, mdcv.fMaximumDisplayMasteringLuminance);
  EXPECT_EQ(0.1f, mdcv.fDisplayPrimaries.fRX);
  EXPECT_EQ(0.2f, mdcv.fDisplayPrimaries.fRY);
  EXPECT_EQ(0.1f, mdcv.fDisplayPrimaries.fGX);
  EXPECT_EQ(0.2f, mdcv.fDisplayPrimaries.fGY);
  EXPECT_EQ(0.1f, mdcv.fDisplayPrimaries.fBX);
  EXPECT_EQ(0.2f, mdcv.fDisplayPrimaries.fBY);
  EXPECT_EQ(0.1f, mdcv.fDisplayPrimaries.fWX);
  EXPECT_EQ(0.2f, mdcv.fDisplayPrimaries.fWY);
  const auto& clli = video_config.hdr_metadata().GetCLLI();
  EXPECT_EQ(11.0f, clli.fMaxCLL);
  EXPECT_EQ(12.0f, clli.fMaxFALL);
  EXPECT_EQ(VideoColorSpace(VideoColorSpace::PrimaryID::SMPTEST428_1,
                            VideoColorSpace::TransferID::LOG,
                            VideoColorSpace::MatrixID::RGB,
                            gfx::ColorSpace::RangeID::FULL),
            video_config.color_space_info());
}

#if BUILDFLAG(ENABLE_AV1_DECODER)
TEST_F(FFmpegCommonTest, VerifyAv1Profiles) {
  VerifyProfileTest("blackwhite_yuv444p_av1.mp4", AV1PROFILE_PROFILE_HIGH);
  VerifyProfileTest("blackwhite_yuv444p_av1.webm", AV1PROFILE_PROFILE_HIGH);
  VerifyProfileTest("bear-av1.mp4", AV1PROFILE_PROFILE_MAIN);
}
#endif

#if BUILDFLAG(USE_PROPRIETARY_CODECS) && BUILDFLAG(ENABLE_PLATFORM_DOLBY_VISION)
TEST_F(FFmpegCommonTest, VerifyDolbyVisionColorSpaceInfo_Profile5) {
  // Open a file to get a real AVStreams from FFmpeg.
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(
      GetTestDataFilePath("glass-blowing2-dolby-vision-profile-5-frag.mp4")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());
  AVFormatContext* format_context = glue.format_context();
  ASSERT_EQ(format_context->nb_streams, 1u);

  AVStream* stream = format_context->streams[0];
  AVCodecParameters* codec_parameters = stream->codecpar;
  AVMediaType codec_type = codec_parameters->codec_type;
  ASSERT_EQ(codec_type, AVMEDIA_TYPE_VIDEO);

  UpdateDefaultDecoderSupportedVideoProfiles(
      {VideoCodecProfile::DOLBYVISION_PROFILE5});

  VideoDecoderConfig video_config;
  EXPECT_TRUE(AVStreamToVideoDecoderConfig(stream, &video_config));
  auto expected = mp4::ParseDolbyVisionColorSpace(
      VideoCodecProfile::DOLBYVISION_PROFILE5, 0);
  EXPECT_EQ(expected, video_config.color_space_info());
}

TEST_F(FFmpegCommonTest, VerifyDolbyVisionColorSpaceInfo_Profile8) {
  // Open a file to get a real AVStreams from FFmpeg.
  base::MemoryMappedFile file;
  ASSERT_TRUE(file.Initialize(
      GetTestDataFilePath("glass-blowing2-dolby-vision-profile-8-1-frag.mp4")));
  InMemoryUrlProtocol protocol(file.bytes(), false);
  FFmpegGlue glue(&protocol);
  ASSERT_TRUE(glue.OpenContext());
  AVFormatContext* format_context = glue.format_context();
  ASSERT_EQ(format_context->nb_streams, 1u);

  AVStream* stream = format_context->streams[0];
  AVCodecParameters* codec_parameters = stream->codecpar;
  AVMediaType codec_type = codec_parameters->codec_type;
  ASSERT_EQ(codec_type, AVMEDIA_TYPE_VIDEO);

  UpdateDefaultDecoderSupportedVideoProfiles(
      {VideoCodecProfile::DOLBYVISION_PROFILE8});

  VideoDecoderConfig video_config;
  EXPECT_TRUE(AVStreamToVideoDecoderConfig(stream, &video_config));
  auto expected =
      mp4::ParseDolbyVisionColorSpace(VideoCodecProfile::DOLBYVISION_PROFILE8,
                                      mp4::kDolbyVisionCompatibilityIdHDR10);
  EXPECT_EQ(expected, video_config.color_space_info());
}
#endif  // BUILDFLAG (USE_PROPRIETARY_CODECS) &&
        // BUILDFLAG(ENABLE_PLATFORM_DOLBY_VISION)

TEST_F(FFmpegCommonTest, ChannelLayoutToChromeChannelLayout_HighChannelCount) {
  AVChannelLayout layout_514;
  layout_514.order = AV_CHANNEL_ORDER_NATIVE;
  layout_514.nb_channels = 10;
  layout_514.u.mask = AV_CH_LAYOUT_5POINT1POINT4_BACK;

  AVChannelLayout layout_714;
  layout_714.order = AV_CHANNEL_ORDER_NATIVE;
  layout_714.nb_channels = 12;
  layout_714.u.mask = AV_CH_LAYOUT_7POINT1POINT4_BACK;

  EXPECT_EQ(CHANNEL_LAYOUT_5_1_4,
            ChannelLayoutToChromeChannelLayout(layout_514));
  EXPECT_EQ(CHANNEL_LAYOUT_7_1_4,
            ChannelLayoutToChromeChannelLayout(layout_714));
}

}  // namespace media
