chromium/media/muxers/mp4_muxer_unittest.cc

// Copyright 2023 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/muxers/mp4_muxer.h"

#include <algorithm>
#include <cstddef>
#include <memory>
#include <vector>

#include "base/big_endian.h"
#include "base/files/file_path.h"
#include "base/files/memory_mapped_file.h"
#include "base/path_service.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "media/base/audio_decoder_config.h"
#include "media/base/audio_parameters.h"
#include "media/base/media_tracks.h"
#include "media/base/mock_media_log.h"
#include "media/base/stream_parser.h"
#include "media/base/video_decoder_config.h"
#include "media/formats/mp4/es_descriptor.h"
#include "media/formats/mp4/mp4_stream_parser.h"
#include "media/muxers/mp4_muxer_delegate.h"
#include "media/muxers/mp4_type_conversion.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(USE_PROPRIETARY_CODECS)

namespace media {
namespace {

using ::testing::_;
using ::testing::AllOf;
using ::testing::Eq;
using ::testing::Field;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::Optional;
using ::testing::Property;
using ::testing::Return;
using ::testing::StrEq;

constexpr uint32_t kAudioSampleRate = 44100u;

class MockDelegate : public Mp4MuxerDelegateInterface {
 public:
  MOCK_METHOD(void,
              AddVideoFrame,
              (const Muxer::VideoParameters& params,
               std::string encoded_data,
               std::optional<VideoEncoder::CodecDescription> codec_description,
               base::TimeTicks timestamp,
               bool is_key_frame),
              (override));
  MOCK_METHOD(void,
              AddAudioFrame,
              (const AudioParameters& params,
               std::string encoded_data,
               std::optional<AudioEncoder::CodecDescription> codec_description,
               base::TimeTicks timestamp),
              (override));
  MOCK_METHOD(bool, Flush, (), (override));
  MOCK_METHOD(bool, FlushFragment, (), (override));
};

AudioEncoder::CodecDescription GetAudioCodecDescription(uint8_t data) {
  return {data};
}

VideoEncoder::CodecDescription GetVideoCodecDescription(uint8_t data) {
  return {data};
}

struct TestParam {
  bool has_video;
  bool has_audio;
};

class Mp4MuxerTest : public ::testing::TestWithParam<TestParam> {
 public:
  Mp4MuxerTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
        delegate_(std::make_unique<MockDelegate>()),
        delegate_ptr_(delegate_.get()) {}
  ~Mp4MuxerTest() override { delegate_ptr_ = nullptr; }

  void CreateMuxer(
      std::optional<base::TimeDelta> max_data_output_interval = std::nullopt) {
    muxer_ = std::make_unique<Mp4Muxer>(
        AudioCodec::kAAC, GetParam().has_video, GetParam().has_audio,
        std::move(delegate_), max_data_output_interval);
  }

  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<MockDelegate> delegate_;
  raw_ptr<MockDelegate> delegate_ptr_;
  std::unique_ptr<Mp4Muxer> muxer_;
};

TEST_P(Mp4MuxerTest, ForwardsFlush) {
  CreateMuxer();
  InSequence s;
  EXPECT_CALL(*delegate_ptr_, Flush).WillOnce(Return(false));
  EXPECT_CALL(*delegate_ptr_, Flush).WillOnce(Return(true));
  EXPECT_FALSE(muxer_->Flush());
  EXPECT_TRUE(muxer_->Flush());
}

TEST_P(Mp4MuxerTest, ForwardsFrames) {
  CreateMuxer();
  InSequence s;
  if (GetParam().has_audio) {
    AudioParameters audio_params(AudioParameters::AUDIO_PCM_LOW_LATENCY,
                                 ChannelLayoutConfig::Stereo(),
                                 kAudioSampleRate, 1000);
    EXPECT_CALL(
        *delegate_ptr_,
        AddAudioFrame(
            AllOf(Property(&AudioParameters::format,
                           AudioParameters::AUDIO_PCM_LOW_LATENCY),
                  Property(&AudioParameters::sample_rate, kAudioSampleRate)),
            StrEq("a1"), Optional(GetAudioCodecDescription(99)),
            base::TimeTicks() + base::Milliseconds(10)));
    EXPECT_CALL(*delegate_ptr_,
                AddAudioFrame(_, StrEq("a2"), Eq(std::nullopt),
                              base::TimeTicks() + base::Milliseconds(20)));
    muxer_->PutFrame(
        Muxer::EncodedFrame{audio_params, GetAudioCodecDescription(99), "a1",
                            std::string(), true},
        base::Milliseconds(10));
    muxer_->PutFrame(Muxer::EncodedFrame{audio_params, std::nullopt, "a2",
                                         std::string(), true},
                     base::Milliseconds(20));
  }
  if (GetParam().has_video) {
    Muxer::VideoParameters video_params(gfx::Size(40, 30), 30,
                                        VideoCodec::kH264, gfx::ColorSpace());
    EXPECT_CALL(
        *delegate_ptr_,
        AddVideoFrame(
            AllOf(Field(&Muxer::VideoParameters::frame_rate, 30),
                  Field(&Muxer::VideoParameters::codec, VideoCodec::kH264)),
            StrEq("v1"), Optional(GetVideoCodecDescription(66)),
            base::TimeTicks() + base::Milliseconds(30), true));
    EXPECT_CALL(
        *delegate_ptr_,
        AddVideoFrame(_, StrEq("v2"), Eq(std::nullopt),
                      base::TimeTicks() + base::Milliseconds(40), false));
    muxer_->PutFrame(
        Muxer::EncodedFrame{video_params, GetVideoCodecDescription(66), "v1",
                            std::string(), true},
        base::Milliseconds(30));
    muxer_->PutFrame(Muxer::EncodedFrame{video_params, std::nullopt, "v2",
                                         std::string(), false},
                     base::Milliseconds(40));
  }
}

TEST_P(Mp4MuxerTest, DoesntFlushOnInsufficientlySpacedFrames) {
  // This test needs video.
  if (!GetParam().has_video) {
    return;
  }
  CreateMuxer(base::Seconds(2));
  Muxer::VideoParameters video_params(gfx::Size(40, 30), 30, VideoCodec::kH264,
                                      gfx::ColorSpace());
  muxer_->PutFrame(
      Muxer::EncodedFrame{video_params, GetVideoCodecDescription(1), "v1",
                          std::string(), true},
      base::Milliseconds(0));
  task_environment_.AdvanceClock(base::Seconds(1));
  muxer_->PutFrame(Muxer::EncodedFrame{video_params, std::nullopt, "v2",
                                       std::string(), false},
                   base::Milliseconds(0));
  // Insert a frame just before the duration limit set initially in the test.
  // Expect that Flush isn't invoked.
  task_environment_.AdvanceClock(base::Seconds(1) - base::Milliseconds(1));
  EXPECT_CALL(*delegate_ptr_, Flush).Times(0);
  muxer_->PutFrame(
      Muxer::EncodedFrame{video_params, GetVideoCodecDescription(1), "v2",
                          std::string(), true},
      base::Milliseconds(0));
  Mock::VerifyAndClearExpectations(delegate_ptr_);
}

TEST_P(Mp4MuxerTest, FlushesOnSufficientlySpacedFramesForVideo) {
  // This test needs video.
  if (!GetParam().has_video) {
    return;
  }
  CreateMuxer(base::Seconds(2));
  Muxer::VideoParameters video_params(gfx::Size(40, 30), 30, VideoCodec::kH264,
                                      gfx::ColorSpace());
  muxer_->PutFrame(
      Muxer::EncodedFrame{video_params, GetVideoCodecDescription(1), "v1",
                          std::string(), true},
      base::Milliseconds(0));
  task_environment_.AdvanceClock(base::Seconds(1));
  muxer_->PutFrame(Muxer::EncodedFrame{video_params, std::nullopt, "v2",
                                       std::string(), false},
                   base::Milliseconds(0));
  // Time will advance to the time for the next flush, so expect Flush called
  // on the next keyframe.
  task_environment_.AdvanceClock(base::Seconds(1));
  EXPECT_CALL(*delegate_ptr_, FlushFragment);
  muxer_->PutFrame(
      Muxer::EncodedFrame{video_params, GetVideoCodecDescription(1), "v2",
                          std::string(), true},
      base::Milliseconds(0));
  Mock::VerifyAndClearExpectations(delegate_ptr_);
}

TEST_P(Mp4MuxerTest, FlushesOnSufficientlySpacedFramesForAudioOnly) {
  if (GetParam().has_video) {
    return;
  }
  CreateMuxer(base::Seconds(2));
  AudioParameters audio_params(AudioParameters::AUDIO_PCM_LOW_LATENCY,
                               ChannelLayoutConfig::Stereo(), kAudioSampleRate,
                               1000);
  muxer_->PutFrame(
      Muxer::EncodedFrame{audio_params, GetVideoCodecDescription(1), "a1",
                          std::string(), true},
      base::Milliseconds(0));
  task_environment_.AdvanceClock(base::Seconds(1));
  muxer_->PutFrame(Muxer::EncodedFrame{audio_params, std::nullopt, "a2",
                                       std::string(), false},
                   base::Milliseconds(0));
  // Time will advance to the time for the next flush, so expect Flush called
  // on the next keyframe.
  task_environment_.AdvanceClock(base::Seconds(1));
  EXPECT_CALL(*delegate_ptr_, FlushFragment);
  muxer_->PutFrame(
      Muxer::EncodedFrame{audio_params, GetAudioCodecDescription(1), "a2",
                          std::string(), true},
      base::Milliseconds(0));
  Mock::VerifyAndClearExpectations(delegate_ptr_);
}

static const TestParam kTestCases[] = {
    {true, false},
    {false, true},
    {true, true},
};

INSTANTIATE_TEST_SUITE_P(, Mp4MuxerTest, testing::ValuesIn(kTestCases));

}  // namespace
}  // namespace media
#endif  // #if BUILDFLAG(USE_PROPRIETARY_CODECS)