| // 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 <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/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" |
| |
| 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( |
| 0, memcmp(codec_parameters->extradata, &decoder_config->extra_data()[0], |
| decoder_config->extra_data().size())); |
| |
| // 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.data(), file.length(), false); |
| FFmpegGlue glue(&protocol); |
| ASSERT_TRUE(glue.OpenContext()); |
| AVFormatContext* format_context = glue.format_context(); |
| |
| for (size_t i = 0; i < format_context->nb_streams; ++i) { |
| AVStream* stream = format_context->streams[i]; |
| 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, 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.data(), file.length(), 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 (size_t i = 0; |
| i < format_context->nb_streams && (!found_audio || !found_video); |
| ++i) { |
| AVStream* stream = format_context->streams[i]; |
| 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.data(), file.length(), 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.data(), file.length(), 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.data(), file.length(), 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 int64_t test_data[][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.data(), file.length(), 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().has_value()); |
| const auto& color_volume_metadata = |
| video_config.hdr_metadata()->color_volume_metadata; |
| EXPECT_EQ(30.0, color_volume_metadata.luminance_min); |
| EXPECT_EQ(40.0, color_volume_metadata.luminance_max); |
| EXPECT_EQ(0.1f, color_volume_metadata.primaries.fRX); |
| EXPECT_EQ(0.2f, color_volume_metadata.primaries.fRY); |
| EXPECT_EQ(0.1f, color_volume_metadata.primaries.fGX); |
| EXPECT_EQ(0.2f, color_volume_metadata.primaries.fGY); |
| EXPECT_EQ(0.1f, color_volume_metadata.primaries.fBX); |
| EXPECT_EQ(0.2f, color_volume_metadata.primaries.fBY); |
| EXPECT_EQ(0.1f, color_volume_metadata.primaries.fWX); |
| EXPECT_EQ(0.2f, color_volume_metadata.primaries.fWY); |
| 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 |
| |
| } // namespace media |