chromium/media/filters/ffmpeg_demuxer_unittest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "media/filters/ffmpeg_demuxer.h"

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

#include <algorithm>
#include <memory>
#include <string>
#include <utility>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "media/base/decrypt_config.h"
#include "media/base/demuxer_stream.h"
#include "media/base/media_switches.h"
#include "media/base/media_tracks.h"
#include "media/base/media_util.h"
#include "media/base/mock_demuxer_host.h"
#include "media/base/mock_media_log.h"
#include "media/base/supported_types.h"
#include "media/base/test_helpers.h"
#include "media/base/timestamp_constants.h"
#include "media/base/video_codecs.h"
#include "media/base/video_color_space.h"
#include "media/ffmpeg/ffmpeg_common.h"
#include "media/filters/file_data_source.h"
#include "media/formats/mp4/avc.h"
#include "media/formats/mp4/bitstream_converter.h"
#include "media/media_buildflags.h"
#include "media/mojo/services/gpu_mojo_media_client_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/color_space.h"

_;
AnyNumber;
DoAll;
Exactly;
InSequence;
Invoke;
NotNull;
Return;
SaveArg;
SetArgPointee;
StrictMock;
WithArgs;

namespace media {

// This does not verify any of the codec parameters that may be included in the
// log entry.
MATCHER_P(SimpleCreatedFFmpegDemuxerStream, stream_type, "") {}

MATCHER_P(FailedToCreateValidDecoderConfigFromStream, stream_type, "") {}

MATCHER_P(SkippingUnsupportedStream, stream_type, "") {}

const uint8_t kEncryptedMediaInitData[] =;

static void EosOnReadDone(bool* got_eos_buffer,
                          base::OnceClosure quit_closure,
                          DemuxerStream::Status status,
                          DemuxerStream::DecoderBufferVector buffers) {}

// Fixture class to facilitate writing tests.  Takes care of setting up the
// FFmpeg, pipeline and filter host mocks.
class FFmpegDemuxerTest : public testing::Test {};

TEST_F(FFmpegDemuxerTest, Initialize_OpenFails) {}

TEST_F(FFmpegDemuxerTest, Initialize_NoStreams) {}

TEST_F(FFmpegDemuxerTest, Initialize_NoAudioVideo) {}

TEST_F(FFmpegDemuxerTest, Initialize_Successful) {}

TEST_F(FFmpegDemuxerTest, Initialize_Multitrack) {}

#if BUILDFLAG(USE_PROPRIETARY_CODECS)
TEST_F(FFmpegDemuxerTest, Initialize_Multitrack_Disabled) {
  // Open a file containing the following streams:
  //   Stream #0: Video (AVC), disabled
  //   Stream #1: Video (AVC)
  CreateDemuxer("multitrack-disabled.mp4");
  InitializeDemuxer();

  ASSERT_EQ(2u, media_tracks_->tracks().size());
  EXPECT_FALSE(media_tracks_->tracks()[0]->enabled());
  EXPECT_TRUE(media_tracks_->tracks()[1]->enabled());
}

TEST_F(FFmpegDemuxerTest, Initialize_Track_Disabled) {
  // Open a file containing the following streams:
  //   Stream #0: Video (AVC), disabled
  CreateDemuxer("track-disabled.mp4");
  InitializeDemuxer();

  // The track enabled flag should be ignored when all tracks are disabled.
  ASSERT_EQ(1u, media_tracks_->tracks().size());
  EXPECT_TRUE(media_tracks_->tracks()[0]->enabled());
}
#endif

TEST_F(FFmpegDemuxerTest, Initialize_Encrypted) {}

TEST_F(FFmpegDemuxerTest, Initialize_NoConfigChangeSupport) {}

TEST_F(FFmpegDemuxerTest, AbortPendingReads) {}

TEST_F(FFmpegDemuxerTest, Read_Audio) {}

TEST_F(FFmpegDemuxerTest, Read_Video) {}

TEST_F(FFmpegDemuxerTest, SeekInitialized_NoVideoStartTime) {}

TEST_F(FFmpegDemuxerTest, Seeking_PreferredStreamSelection) {}

TEST_F(FFmpegDemuxerTest, Read_VideoPositiveStartTime) {}

TEST_F(FFmpegDemuxerTest, Read_AudioNoStartTime) {}

// Same test above, but using sync2.ogv which has video stream muxed before the
// audio stream, so seeking based only on start time will fail since ffmpeg is
// essentially just seeking based on file position.
TEST_F(FFmpegDemuxerTest, Read_AudioNegativeStartTimeAndOggDiscard_Sync) {}

// Similar to the test above, but using an opus clip with a large amount of
// pre-skip, which ffmpeg encodes as negative timestamps.
TEST_F(FFmpegDemuxerTest, Read_AudioNegativeStartTimeAndOpusDiscard_Sync) {}

#if BUILDFLAG(USE_PROPRIETARY_CODECS)

// Similar to the test above, but using an opus clip plus h264 b-frames to
// ensure we don't apply chained ogg workarounds to other content.
TEST_F(FFmpegDemuxerTest,
       Read_AudioNegativeStartTimeAndOpusDiscardH264Mp4_Sync) {
  CreateDemuxer("tos-h264-opus.mp4");
  InitializeDemuxer();

  // Attempt a read from the video stream and run the message loop until done.
  DemuxerStream* video = GetStream(DemuxerStream::VIDEO);
  DemuxerStream* audio = GetStream(DemuxerStream::AUDIO);
  EXPECT_EQ(audio->audio_decoder_config().codec_delay(), 312);

  // Packet size to timestamp (in microseconds) mapping for the first N packets
  // which should be fully discarded.
  static const int kTestExpectations[][2] = {
      {234, 20000}, {228, 40000}, {340, 60000}};

  // Run the test twice with a seek in between.
  for (int i = 0; i < 2; ++i) {
    Read(audio, FROM_HERE, 408, 0, true, DemuxerStream::Status::kOk,
         base::Microseconds(6500));

    for (size_t j = 0; j < std::size(kTestExpectations); ++j) {
      Read(audio, FROM_HERE, kTestExpectations[j][0], kTestExpectations[j][1],
           true);
    }

    // Though the internal start time may be below zero, the exposed media time
    // must always be >= zero.
    EXPECT_EQ(base::TimeDelta(), demuxer_->GetStartTime());

    Read(video, FROM_HERE, 185105, 0, true);
    Read(video, FROM_HERE, 35941, 125000, false);

    // If things aren't working correctly, this expectation will fail because
    // the chained ogg workaround breaks out of order timestamps.
    Read(video, FROM_HERE, 8129, 84000, false);

    // Seek back to the beginning and repeat the test.
    WaitableMessageLoopEvent event;
    demuxer_->Seek(base::TimeDelta(), event.GetPipelineStatusCB());
    event.RunAndWaitForStatus(PIPELINE_OK);
  }
}
// Similar to the above cases, but with mixed audio/video trimming.
TEST_F(FFmpegDemuxerTest, Read_AudioVideoNegativeStartTime) {
  CreateDemuxer("sync2-trimmed.mp4");
  InitializeDemuxer();

  // Attempt a read from the video stream and run the message loop until done.
  DemuxerStream* video = GetStream(DemuxerStream::VIDEO);
  DemuxerStream* audio = GetStream(DemuxerStream::AUDIO);

  Read(audio, FROM_HERE, 10, 0, true, DemuxerStream::Status::kOk,
       base::Microseconds(1005464));  // ~ 43 * 23220
  Read(audio, FROM_HERE, 10, 23220, true, DemuxerStream::Status::kOk,
       kInfiniteDuration);

  // The rest are all similar, just verify that discard padding is correct.
  base::RunLoop run_loop;
  audio->Read(41, base::BindLambdaForTesting(
                      [&](DemuxerStream::Status status,
                          DemuxerStream::DecoderBufferVector buffers) {
                        for (const auto& buffer : buffers) {
                          EXPECT_EQ(buffer->discard_padding().first,
                                    kInfiniteDuration);
                        }
                        run_loop.QuitWhenIdle();
                      }));
  run_loop.Run();
  task_environment_.RunUntilIdle();
  Read(audio, FROM_HERE, 10, 998458, true);  // First audible audio.

  // Note: Frames are in decode (not presentation) order at this point.
  Read(video, FROM_HERE, 26791, -66733, true, DemuxerStream::Status::kOk,
       kInfiniteDuration);
  Read(video, FROM_HERE, 4233, 33367, false);
  Read(video, FROM_HERE, 3257, 0, false);
  Read(video, FROM_HERE, 2467, -33367, false, DemuxerStream::Status::kOk,
       kInfiniteDuration);
  Read(video, FROM_HERE, 4049, 133467, false);
}
#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)

// Similar to the test above, but using sfx-opus.ogg, which has a much smaller
// amount of discard padding and no |start_time| set on the AVStream.
TEST_F(FFmpegDemuxerTest, Read_AudioNegativeStartTimeAndOpusSfxDiscard_Sync) {}

TEST_F(FFmpegDemuxerTest, Read_DiscardDisabledVideoStream) {}

TEST_F(FFmpegDemuxerTest, Read_EndOfStream) {}

TEST_F(FFmpegDemuxerTest, Read_EndOfStream_NoDuration) {}

TEST_F(FFmpegDemuxerTest, Read_EndOfStream_NoDuration_VideoOnly) {}

TEST_F(FFmpegDemuxerTest, Read_EndOfStream_NoDuration_AudioOnly) {}

TEST_F(FFmpegDemuxerTest, Read_EndOfStream_NoDuration_UnsupportedStream) {}

TEST_F(FFmpegDemuxerTest, Seek) {}

TEST_F(FFmpegDemuxerTest, CancelledSeek) {}

TEST_F(FFmpegDemuxerTest, Stop) {}

// Verify that seek works properly when the WebM cues data is at the start of
// the file instead of at the end.
TEST_F(FFmpegDemuxerTest, SeekWithCuesBeforeFirstCluster) {}

// Ensure ID3v1 tag reading is disabled.  id3_test.mp3 has an ID3v1 tag with the
// field "title" set to "sample for id3 test".
TEST_F(FFmpegDemuxerTest, NoID3TagData) {}

// Ensure MP3 files with large image/video based ID3 tags demux okay.  FFmpeg
// will hand us a video stream to the data which will likely be in a format we
// don't accept as video; e.g. PNG.
TEST_F(FFmpegDemuxerTest, Mp3WithVideoStreamID3TagData) {}

// Ensure a video with an unsupported audio track still results in the video
// stream being demuxed. Because we disable the speex parser for ogg, the audio
// track won't even show up to the demuxer.
TEST_F(FFmpegDemuxerTest, UnsupportedAudioSupportedVideoDemux) {}

// Ensure a video with an unsupported video track still results in the audio
// stream being demuxed.
TEST_F(FFmpegDemuxerTest, UnsupportedVideoSupportedAudioDemux) {}

#if BUILDFLAG(USE_PROPRIETARY_CODECS)
// FFmpeg returns null data pointers when samples have zero size, leading to
// mistakenly creating end of stream buffers http://crbug.com/169133
TEST_F(FFmpegDemuxerTest, MP4_ZeroStszEntry) {
  CreateDemuxer("bear-1280x720-zero-stsz-entry.mp4");
  InitializeDemuxer();
  ReadUntilEndOfStream(GetStream(DemuxerStream::AUDIO));
}
#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)

class Mp3SeekFFmpegDemuxerTest
    : public FFmpegDemuxerTest,
      public testing::WithParamInterface<const char*> {};
TEST_P(Mp3SeekFFmpegDemuxerTest, TestFastSeek) {}

// MP3s should seek quickly without sequentially reading up to the seek point.
// VBR vs CBR and the presence/absence of TOC influence the seeking algorithm.
// See http://crbug.com/530043 and FFmpeg flag AVFMT_FLAG_FAST_SEEK.
INSTANTIATE_TEST_SUITE_P();

#if BUILDFLAG(USE_PROPRIETARY_CODECS)
static void ValidateAnnexB(DemuxerStream* stream,
                           base::OnceClosure quit_closure,
                           DemuxerStream::Status status,
                           DemuxerStream::DecoderBufferVector buffers) {
  EXPECT_EQ(status, DemuxerStream::kOk);
  EXPECT_EQ(buffers.size(), 1u);
  scoped_refptr<DecoderBuffer> buffer = std::move(buffers[0]);
  if (buffer->end_of_stream()) {
    std::move(quit_closure).Run();
    return;
  }

  std::vector<SubsampleEntry> subsamples;

  if (buffer->decrypt_config())
    subsamples = buffer->decrypt_config()->subsamples();

  bool is_valid =
      mp4::AVC::AnalyzeAnnexB(buffer->data(), buffer->size(), subsamples)
          .is_conformant.value_or(false);
  EXPECT_TRUE(is_valid);

  if (!is_valid) {
    LOG(ERROR) << "Buffer contains invalid Annex B data.";
    std::move(quit_closure).Run();
    return;
  }
  stream->Read(
      1, base::BindOnce(&ValidateAnnexB, stream, std::move(quit_closure)));
}

TEST_F(FFmpegDemuxerTest, IsValidAnnexB) {
  const char* files[] = {"bear-1280x720-av_frag.mp4",
                         "bear-1280x720-av_with-aud-nalus_frag.mp4"};

  for (size_t i = 0; i < std::size(files); ++i) {
    DVLOG(1) << "Testing " << files[i];
    CreateDemuxer(files[i]);
    InitializeDemuxer();

    // Ensure the expected streams are present.
    DemuxerStream* stream = GetStream(DemuxerStream::VIDEO);
    ASSERT_TRUE(stream);
    stream->EnableBitstreamConverter();

    base::RunLoop loop;
    stream->Read(
        1, base::BindOnce(&ValidateAnnexB, stream, loop.QuitWhenIdleClosure()));
    loop.Run();

    demuxer_->Stop();
    demuxer_.reset();
    data_source_.reset();
  }
}

TEST_F(FFmpegDemuxerTest, Rotate_Metadata_0) {
  CreateDemuxer("bear_rotate_0.mp4");
  InitializeDemuxer();

  DemuxerStream* stream = GetStream(DemuxerStream::VIDEO);
  ASSERT_TRUE(stream);

  const VideoDecoderConfig& video_config = stream->video_decoder_config();
  ASSERT_EQ(VIDEO_ROTATION_0, video_config.video_transformation().rotation);
}

TEST_F(FFmpegDemuxerTest, Rotate_Metadata_90) {
  CreateDemuxer("bear_rotate_90.mp4");
  InitializeDemuxer();

  DemuxerStream* stream = GetStream(DemuxerStream::VIDEO);
  ASSERT_TRUE(stream);

  const VideoDecoderConfig& video_config = stream->video_decoder_config();
  ASSERT_EQ(VIDEO_ROTATION_90, video_config.video_transformation().rotation);
}

TEST_F(FFmpegDemuxerTest, Rotate_Metadata_180) {
  CreateDemuxer("bear_rotate_180.mp4");
  InitializeDemuxer();

  DemuxerStream* stream = GetStream(DemuxerStream::VIDEO);
  ASSERT_TRUE(stream);

  const VideoDecoderConfig& video_config = stream->video_decoder_config();
  ASSERT_EQ(VIDEO_ROTATION_180, video_config.video_transformation().rotation);
}

TEST_F(FFmpegDemuxerTest, Rotate_Metadata_270) {
  CreateDemuxer("bear_rotate_270.mp4");
  InitializeDemuxer();

  DemuxerStream* stream = GetStream(DemuxerStream::VIDEO);
  ASSERT_TRUE(stream);

  const VideoDecoderConfig& video_config = stream->video_decoder_config();
  ASSERT_EQ(VIDEO_ROTATION_270, video_config.video_transformation().rotation);
}

TEST_F(FFmpegDemuxerTest, NaturalSizeWithoutPASP) {
  CreateDemuxer("bear-640x360-non_square_pixel-without_pasp.mp4");
  InitializeDemuxer();

  DemuxerStream* stream = GetStream(DemuxerStream::VIDEO);
  ASSERT_TRUE(stream);

  const VideoDecoderConfig& video_config = stream->video_decoder_config();
  EXPECT_EQ(gfx::Size(639, 360), video_config.natural_size());
}

TEST_F(FFmpegDemuxerTest, NaturalSizeWithPASP) {
  CreateDemuxer("bear-640x360-non_square_pixel-with_pasp.mp4");
  InitializeDemuxer();

  DemuxerStream* stream = GetStream(DemuxerStream::VIDEO);
  ASSERT_TRUE(stream);

  const VideoDecoderConfig& video_config = stream->video_decoder_config();
  EXPECT_EQ(gfx::Size(639, 360), video_config.natural_size());
}

TEST_F(FFmpegDemuxerTest, HEVC_in_MP4_container) {
  CreateDemuxer("bear-hevc-frag.mp4");

  const VideoType kHevc{
      .codec = VideoCodec::kHEVC,
      .profile = HEVCPROFILE_MIN,
      .color_space = VideoColorSpace::REC709(),
  };
  if (IsSupportedVideoType(kHevc)) {
    InitializeDemuxer();

    DemuxerStream* video = GetStream(DemuxerStream::VIDEO);
    ASSERT_TRUE(video);

    Read(video, FROM_HERE, 3569, 66733, true);
    Read(video, FROM_HERE, 1042, 200200, false);
  } else {
    InitializeDemuxerAndExpectPipelineStatus(
        DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
  }
}

TEST_F(FFmpegDemuxerTest, Read_AC3_Audio) {
  CreateDemuxer("bear-ac3-only-frag.mp4");

  constexpr AudioType kAc3{
      .codec = AudioCodec::kAC3,
      .spatial_rendering = false,
  };
  if (IsSupportedAudioType(kAc3)) {
    InitializeDemuxer();

    // Attempt a read from the audio stream and run the message loop until done.
    DemuxerStream* audio = GetStream(DemuxerStream::AUDIO);

    // Read the first two frames and check that we are getting expected data
    Read(audio, FROM_HERE, 834, 0, true);
    Read(audio, FROM_HERE, 836, 34830, true);
  } else {
    InitializeDemuxerAndExpectPipelineStatus(
        DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
  }
}

TEST_F(FFmpegDemuxerTest, Read_EAC3_Audio) {
  CreateDemuxer("bear-eac3-only-frag.mp4");

  constexpr AudioType kEac3{
      .codec = AudioCodec::kEAC3,
      .spatial_rendering = false,
  };
  if (IsSupportedAudioType(kEac3)) {
    InitializeDemuxer();

    // Attempt a read from the audio stream and run the message loop until done.
    DemuxerStream* audio = GetStream(DemuxerStream::AUDIO);

    // Read the first two frames and check that we are getting expected data
    Read(audio, FROM_HERE, 870, 0, true);
    Read(audio, FROM_HERE, 872, 34830, true);
  } else {
    InitializeDemuxerAndExpectPipelineStatus(
        DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
  }
}

TEST_F(FFmpegDemuxerTest, Read_Mp4_Media_Track_Info) {
  CreateDemuxer("bear.mp4");
  InitializeDemuxer();

  EXPECT_EQ(media_tracks_->tracks().size(), 2u);

  const MediaTrack& audio_track = *(media_tracks_->tracks()[1]);
  EXPECT_EQ(audio_track.type(), MediaTrack::Type::kAudio);
  EXPECT_EQ(audio_track.bytestream_track_id(), 2);
  EXPECT_EQ(audio_track.kind().value(), "main");
  EXPECT_EQ(audio_track.label().value(), "SoundHandler");
  EXPECT_EQ(audio_track.language().value(), "und");

  const MediaTrack& video_track = *(media_tracks_->tracks()[0]);
  EXPECT_EQ(video_track.type(), MediaTrack::Type::kVideo);
  EXPECT_EQ(video_track.bytestream_track_id(), 1);
  EXPECT_EQ(video_track.kind().value(), "main");
  EXPECT_EQ(video_track.label().value(), "VideoHandler");
  EXPECT_EQ(video_track.language().value(), "und");
}

TEST_F(FFmpegDemuxerTest, Read_Mp4_Multiple_Tracks) {
  CreateDemuxer("bbb-320x240-2video-2audio.mp4");
  InitializeDemuxer();

  EXPECT_EQ(media_tracks_->tracks().size(), 4u);

  const MediaTrack& video_track = *(media_tracks_->tracks()[0]);
  EXPECT_EQ(video_track.type(), MediaTrack::Type::kVideo);
  EXPECT_EQ(video_track.bytestream_track_id(), 1);
  EXPECT_EQ(video_track.kind().value(), "main");
  EXPECT_EQ(video_track.label().value(), "VideoHandler");
  EXPECT_EQ(video_track.language().value(), "und");

  const MediaTrack& audio_track = *(media_tracks_->tracks()[1]);
  EXPECT_EQ(audio_track.type(), MediaTrack::Type::kAudio);
  EXPECT_EQ(audio_track.bytestream_track_id(), 2);
  EXPECT_EQ(audio_track.kind().value(), "main");
  EXPECT_EQ(audio_track.label().value(), "SoundHandler");
  EXPECT_EQ(audio_track.language().value(), "und");

  const MediaTrack& video_track2 = *(media_tracks_->tracks()[2]);
  EXPECT_EQ(video_track2.type(), MediaTrack::Type::kVideo);
  EXPECT_EQ(video_track2.bytestream_track_id(), 3);
  EXPECT_EQ(video_track2.kind().value(), "main");
  EXPECT_EQ(video_track2.label().value(), "VideoHandler");
  EXPECT_EQ(video_track2.language().value(), "und");

  const MediaTrack& audio_track2 = *(media_tracks_->tracks()[3]);
  EXPECT_EQ(audio_track2.type(), MediaTrack::Type::kAudio);
  EXPECT_EQ(audio_track2.bytestream_track_id(), 4);
  EXPECT_EQ(audio_track2.kind().value(), "main");
  EXPECT_EQ(audio_track2.label().value(), "SoundHandler");
  EXPECT_EQ(audio_track2.language().value(), "und");
}

// Note: This test has multiple mp4 files concatenated. It should succeed or
// there may be a regression in mp4 file handling. https://crbug.com/1506906
TEST_F(FFmpegDemuxerTest, Read_Mp4_Crbug657437) {
  CreateDemuxer("crbug657437.mp4");
  InitializeDemuxer();
}

TEST_F(FFmpegDemuxerTest, XHE_AAC) {
  if (!IsSupportedAudioType(
          {AudioCodec::kAAC, AudioCodecProfile::kXHE_AAC, false})) {
    GTEST_SKIP() << "Unsupported platform.";
  }

  CreateDemuxer("noise-xhe-aac.mp4");
  InitializeDemuxer();

  DemuxerStream* audio = GetStream(DemuxerStream::AUDIO);
  ASSERT_TRUE(audio);

  EXPECT_EQ(audio->audio_decoder_config().profile(),
            AudioCodecProfile::kXHE_AAC);

  // ADTS bitstream conversion shouldn't be enabled for xHE-AAC since it can't
  // be represented with only two bits for the profile.
  audio->EnableBitstreamConverter();
  EXPECT_FALSE(HasBitstreamConverter(audio));

  // Even though FFmpeg can't decode xHE-AAC content, it should be demuxing it
  // just fine.
  Read(audio, FROM_HERE, 1796, 0, true);
}

#endif  // BUILDFLAG(USE_PROPRIETARY_CODECS)

TEST_F(FFmpegDemuxerTest, Read_Webm_Multiple_Tracks) {}

TEST_F(FFmpegDemuxerTest, Read_Webm_Media_Track_Info) {}

// UTCDateToTime_* tests here assume FFmpegDemuxer's ExtractTimelineOffset
// helper uses base::Time::FromUTCString() for conversion.
TEST_F(FFmpegDemuxerTest, UTCDateToTime_Valid) {}

TEST_F(FFmpegDemuxerTest, UTCDateToTime_Invalid) {}

static void VerifyFlacStream(DemuxerStream* stream,
                             int expected_bytes_per_channel,
                             ChannelLayout expected_channel_layout,
                             int expected_samples_per_second,
                             SampleFormat expected_sample_format) {}

TEST_F(FFmpegDemuxerTest, Read_Flac) {}

TEST_F(FFmpegDemuxerTest, Read_Flac_Mp4) {}

TEST_F(FFmpegDemuxerTest, Read_Flac_192kHz_Mp4) {}

// Verify that FFmpeg demuxer falls back to choosing disabled streams for
// seeking if there's no suitable enabled stream found.
TEST_F(FFmpegDemuxerTest, Seek_FallbackToDisabledVideoStream) {}

TEST_F(FFmpegDemuxerTest, Seek_FallbackToDisabledAudioStream) {}

namespace {
void QuitLoop(base::OnceClosure quit_closure,
              DemuxerStream::Type type,
              const std::vector<DemuxerStream*>& streams) {}

void DisableAndEnableDemuxerTracks(
    FFmpegDemuxer* demuxer,
    base::test::TaskEnvironment* task_environment) {}

void OnReadDoneExpectEos(DemuxerStream::Status status,
                         DemuxerStream::DecoderBufferVector buffers) {}
}  // namespace

TEST_F(FFmpegDemuxerTest, StreamStatusNotifications) {}

TEST_F(FFmpegDemuxerTest, MultitrackMemoryUsage) {}

TEST_F(FFmpegDemuxerTest, SeekOnVideoTrackChangeWontSeekIfEmpty) {}

}  // namespace media