#include "base/bind.h"
#include "base/files/file_path.h"
+#include "base/logging.h"
#include "base/path_service.h"
#include "base/threading/thread.h"
#include "media/base/decrypt_config.h"
#include "media/ffmpeg/ffmpeg_common.h"
#include "media/filters/ffmpeg_demuxer.h"
#include "media/filters/file_data_source.h"
+#include "media/formats/mp4/avc.h"
#include "media/formats/webm/webm_crypto_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
void CreateDemuxer(const std::string& name) {
CHECK(!demuxer_);
- EXPECT_CALL(host_, SetTotalBytes(_)).Times(AnyNumber());
- EXPECT_CALL(host_, AddBufferedByteRange(_, _)).Times(AnyNumber());
EXPECT_CALL(host_, AddBufferedTimeRange(_, _)).Times(AnyNumber());
CreateDataSource(name);
MOCK_METHOD1(CheckPoint, void(int v));
- void InitializeDemuxerText(bool enable_text) {
+ void InitializeDemuxerWithTimelineOffset(bool enable_text,
+ base::Time timeline_offset) {
EXPECT_CALL(host_, SetDuration(_));
WaitableMessageLoopEvent event;
demuxer_->Initialize(&host_, event.GetPipelineStatusCB(), enable_text);
+ demuxer_->timeline_offset_ = timeline_offset;
event.RunAndWaitForStatus(PIPELINE_OK);
}
+ void InitializeDemuxerText(bool enable_text) {
+ InitializeDemuxerWithTimelineOffset(enable_text, base::Time());
+ }
+
void InitializeDemuxer() {
InitializeDemuxerText(false);
}
// |location| simply indicates where the call to this function was made.
// This makes it easier to track down where test failures occur.
void OnReadDone(const tracked_objects::Location& location,
- int size, int64 timestampInMicroseconds,
+ int size,
+ int64 timestamp_us,
+ base::TimeDelta discard_front_padding,
DemuxerStream::Status status,
const scoped_refptr<DecoderBuffer>& buffer) {
std::string location_str;
location_str += "\n";
SCOPED_TRACE(location_str);
EXPECT_EQ(status, DemuxerStream::kOk);
- OnReadDoneCalled(size, timestampInMicroseconds);
+ OnReadDoneCalled(size, timestamp_us);
EXPECT_TRUE(buffer.get() != NULL);
EXPECT_EQ(size, buffer->data_size());
- EXPECT_EQ(base::TimeDelta::FromMicroseconds(timestampInMicroseconds),
- buffer->timestamp());
-
+ EXPECT_EQ(timestamp_us, buffer->timestamp().InMicroseconds());
+ EXPECT_EQ(discard_front_padding, buffer->discard_padding().first);
DCHECK_EQ(&message_loop_, base::MessageLoop::current());
message_loop_.PostTask(FROM_HERE, base::MessageLoop::QuitWhenIdleClosure());
}
DemuxerStream::ReadCB NewReadCB(const tracked_objects::Location& location,
- int size, int64 timestampInMicroseconds) {
- EXPECT_CALL(*this, OnReadDoneCalled(size, timestampInMicroseconds));
- return base::Bind(&FFmpegDemuxerTest::OnReadDone, base::Unretained(this),
- location, size, timestampInMicroseconds);
+ int size,
+ int64 timestamp_us) {
+ EXPECT_CALL(*this, OnReadDoneCalled(size, timestamp_us));
+ return base::Bind(&FFmpegDemuxerTest::OnReadDone,
+ base::Unretained(this),
+ location,
+ size,
+ timestamp_us,
+ base::TimeDelta());
+ }
+
+ DemuxerStream::ReadCB NewReadCBWithCheckedDiscard(
+ const tracked_objects::Location& location,
+ int size,
+ int64 timestamp_us,
+ base::TimeDelta discard_front_padding) {
+ EXPECT_CALL(*this, OnReadDoneCalled(size, timestamp_us));
+ return base::Bind(&FFmpegDemuxerTest::OnReadDone,
+ base::Unretained(this),
+ location,
+ size,
+ timestamp_us,
+ discard_front_padding);
}
// TODO(xhwang): This is a workaround of the issue that move-only parameters
return demuxer_->glue_->format_context();
}
+ int preferred_seeking_stream_index() const {
+ return demuxer_->preferred_stream_for_seeking_.first;
+ }
+
void ReadUntilEndOfStream(DemuxerStream* stream) {
bool got_eos_buffer = false;
const int kMaxBuffers = 170;
message_loop_.Run();
}
-TEST_F(FFmpegDemuxerTest, Read_VideoNonZeroStart) {
+TEST_F(FFmpegDemuxerTest, SeekInitialized_NoVideoStartTime) {
+ CreateDemuxer("audio-start-time-only.webm");
+ InitializeDemuxer();
+ EXPECT_EQ(0, preferred_seeking_stream_index());
+}
+
+TEST_F(FFmpegDemuxerTest, Read_VideoPositiveStartTime) {
+ const int64 kTimelineOffsetMs = 1352550896000LL;
+
// Test the start time is the first timestamp of the video and audio stream.
CreateDemuxer("nonzero-start-time.webm");
+ InitializeDemuxerWithTimelineOffset(
+ false, base::Time::FromJsTime(kTimelineOffsetMs));
+
+ // Attempt a read from the video stream and run the message loop until done.
+ DemuxerStream* video = demuxer_->GetStream(DemuxerStream::VIDEO);
+ DemuxerStream* audio = demuxer_->GetStream(DemuxerStream::AUDIO);
+
+ const base::TimeDelta video_start_time =
+ base::TimeDelta::FromMicroseconds(400000);
+ const base::TimeDelta audio_start_time =
+ base::TimeDelta::FromMicroseconds(396000);
+
+ // Run the test twice with a seek in between.
+ for (int i = 0; i < 2; ++i) {
+ video->Read(NewReadCB(FROM_HERE, 5636, video_start_time.InMicroseconds()));
+ message_loop_.Run();
+ audio->Read(NewReadCB(FROM_HERE, 165, audio_start_time.InMicroseconds()));
+ message_loop_.Run();
+
+ // Verify that the start time is equal to the lowest timestamp (ie the
+ // audio).
+ EXPECT_EQ(audio_start_time, demuxer_->start_time());
+
+ // Verify that the timeline offset has been adjusted by the start time.
+ EXPECT_EQ(kTimelineOffsetMs + audio_start_time.InMilliseconds(),
+ demuxer_->GetTimelineOffset().ToJavaTime());
+
+ // Seek back to the beginning and repeat the test.
+ WaitableMessageLoopEvent event;
+ demuxer_->Seek(base::TimeDelta(), event.GetPipelineStatusCB());
+ event.RunAndWaitForStatus(PIPELINE_OK);
+ }
+}
+
+TEST_F(FFmpegDemuxerTest, Read_AudioNoStartTime) {
+ // FFmpeg does not set timestamps when demuxing wave files. Ensure that the
+ // demuxer sets a start time of zero in this case.
+ CreateDemuxer("sfx_s24le.wav");
+ InitializeDemuxer();
+
+ // Run the test twice with a seek in between.
+ for (int i = 0; i < 2; ++i) {
+ demuxer_->GetStream(DemuxerStream::AUDIO)
+ ->Read(NewReadCB(FROM_HERE, 4095, 0));
+ message_loop_.Run();
+ EXPECT_EQ(base::TimeDelta(), demuxer_->start_time());
+
+ // Seek back to the beginning and repeat the test.
+ WaitableMessageLoopEvent event;
+ demuxer_->Seek(base::TimeDelta(), event.GetPipelineStatusCB());
+ event.RunAndWaitForStatus(PIPELINE_OK);
+ }
+}
+
+// TODO(dalecurtis): Test is disabled since FFmpeg does not currently guarantee
+// the order of demuxed packets in OGG containers. Re-enable once we decide to
+// either workaround it or attempt a fix upstream. See http://crbug.com/387996.
+TEST_F(FFmpegDemuxerTest,
+ DISABLED_Read_AudioNegativeStartTimeAndOggDiscard_Bear) {
+ // Many ogg files have negative starting timestamps, so ensure demuxing and
+ // seeking work correctly with a negative start time.
+ CreateDemuxer("bear.ogv");
InitializeDemuxer();
// Attempt a read from the video stream and run the message loop until done.
DemuxerStream* video = demuxer_->GetStream(DemuxerStream::VIDEO);
DemuxerStream* audio = demuxer_->GetStream(DemuxerStream::AUDIO);
- // Check first buffer in video stream.
- video->Read(NewReadCB(FROM_HERE, 5636, 400000));
- message_loop_.Run();
+ // Run the test twice with a seek in between.
+ for (int i = 0; i < 2; ++i) {
+ audio->Read(
+ NewReadCBWithCheckedDiscard(FROM_HERE, 40, 0, kInfiniteDuration()));
+ message_loop_.Run();
+ audio->Read(
+ NewReadCBWithCheckedDiscard(FROM_HERE, 41, 2903, kInfiniteDuration()));
+ message_loop_.Run();
+ audio->Read(NewReadCBWithCheckedDiscard(
+ FROM_HERE, 173, 5805, base::TimeDelta::FromMicroseconds(10159)));
+ message_loop_.Run();
- // Check first buffer in audio stream.
- audio->Read(NewReadCB(FROM_HERE, 165, 396000));
- message_loop_.Run();
+ audio->Read(NewReadCB(FROM_HERE, 148, 18866));
+ message_loop_.Run();
+ EXPECT_EQ(base::TimeDelta::FromMicroseconds(-15964),
+ demuxer_->start_time());
+
+ video->Read(NewReadCB(FROM_HERE, 5751, 0));
+ message_loop_.Run();
+
+ video->Read(NewReadCB(FROM_HERE, 846, 33367));
+ message_loop_.Run();
- // Verify that the start time is equal to the lowest timestamp (ie the audio).
- EXPECT_EQ(demuxer_->GetStartTime().InMicroseconds(), 396000);
+ video->Read(NewReadCB(FROM_HERE, 1255, 66733));
+ message_loop_.Run();
+
+ // Seek back to the beginning and repeat the test.
+ WaitableMessageLoopEvent event;
+ demuxer_->Seek(base::TimeDelta(), event.GetPipelineStatusCB());
+ event.RunAndWaitForStatus(PIPELINE_OK);
+ }
+}
+
+// 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) {
+ // Many ogg files have negative starting timestamps, so ensure demuxing and
+ // seeking work correctly with a negative start time.
+ CreateDemuxer("sync2.ogv");
+ InitializeDemuxer();
+
+ // Attempt a read from the video stream and run the message loop until done.
+ DemuxerStream* video = demuxer_->GetStream(DemuxerStream::VIDEO);
+ DemuxerStream* audio = demuxer_->GetStream(DemuxerStream::AUDIO);
+
+ // Run the test twice with a seek in between.
+ for (int i = 0; i < 2; ++i) {
+ audio->Read(NewReadCBWithCheckedDiscard(
+ FROM_HERE, 1, 0, base::TimeDelta::FromMicroseconds(2902)));
+ message_loop_.Run();
+
+ audio->Read(NewReadCB(FROM_HERE, 1, 2902));
+ message_loop_.Run();
+ EXPECT_EQ(base::TimeDelta::FromMicroseconds(-2902),
+ demuxer_->start_time());
+
+ video->Read(NewReadCB(FROM_HERE, 9997, 0));
+ message_loop_.Run();
+
+ video->Read(NewReadCB(FROM_HERE, 16, 33241));
+ message_loop_.Run();
+
+ video->Read(NewReadCB(FROM_HERE, 631, 66482));
+ message_loop_.Run();
+
+ // Seek back to the beginning and repeat the test.
+ WaitableMessageLoopEvent event;
+ demuxer_->Seek(base::TimeDelta(), event.GetPipelineStatusCB());
+ event.RunAndWaitForStatus(PIPELINE_OK);
+ }
}
TEST_F(FFmpegDemuxerTest, Read_EndOfStream) {
demuxer_.reset();
}
-TEST_F(FFmpegDemuxerTest, DisableAudioStream) {
- // We are doing the following things here:
- // 1. Initialize the demuxer with audio and video stream.
- // 2. Send a "disable audio stream" message to the demuxer.
- // 3. Demuxer will free audio packets even if audio stream was initialized.
- CreateDemuxer("bear-320x240.webm");
- InitializeDemuxer();
-
- // Submit a "disable audio stream" message to the demuxer.
- demuxer_->OnAudioRendererDisabled();
- message_loop_.RunUntilIdle();
-
- // Get our streams.
- DemuxerStream* video = demuxer_->GetStream(DemuxerStream::VIDEO);
- DemuxerStream* audio = demuxer_->GetStream(DemuxerStream::AUDIO);
- ASSERT_TRUE(video);
- ASSERT_TRUE(audio);
-
- // The audio stream should have been prematurely stopped.
- EXPECT_FALSE(IsStreamStopped(DemuxerStream::VIDEO));
- EXPECT_TRUE(IsStreamStopped(DemuxerStream::AUDIO));
-
- // Attempt a read from the video stream: it should return valid data.
- video->Read(NewReadCB(FROM_HERE, 22084, 0));
- message_loop_.Run();
-
- // Attempt a read from the audio stream: it should immediately return end of
- // stream without requiring the message loop to read data.
- bool got_eos_buffer = false;
- audio->Read(base::Bind(&EosOnReadDone, &got_eos_buffer));
- message_loop_.RunUntilIdle();
- EXPECT_TRUE(got_eos_buffer);
-}
-
// 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) {
message_loop_.Run();
}
+#if defined(USE_PROPRIETARY_CODECS)
// 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) {
-#if !defined(USE_PROPRIETARY_CODECS)
- return;
-#endif
CreateDemuxer("id3_test.mp3");
InitializeDemuxer();
EXPECT_FALSE(av_dict_get(format_context()->metadata, "title", NULL, 0));
}
+#endif
+#if defined(USE_PROPRIETARY_CODECS)
// 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) {
-#if !defined(USE_PROPRIETARY_CODECS)
- return;
-#endif
CreateDemuxer("id3_png_test.mp3");
InitializeDemuxer();
EXPECT_FALSE(demuxer_->GetStream(DemuxerStream::VIDEO));
EXPECT_TRUE(demuxer_->GetStream(DemuxerStream::AUDIO));
}
+#endif
// Ensure a video with an unsupported audio track still results in the video
// stream being demuxed.
EXPECT_TRUE(demuxer_->GetStream(DemuxerStream::AUDIO));
}
+#if defined(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) {
-#if !defined(USE_PROPRIETARY_CODECS)
- return;
-#endif
CreateDemuxer("bear-1280x720-zero-stsz-entry.mp4");
InitializeDemuxer();
ReadUntilEndOfStream(demuxer_->GetStream(DemuxerStream::AUDIO));
}
+
+static void ValidateAnnexB(DemuxerStream* stream,
+ DemuxerStream::Status status,
+ const scoped_refptr<DecoderBuffer>& buffer) {
+ EXPECT_EQ(status, DemuxerStream::kOk);
+
+ if (buffer->end_of_stream()) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE, base::MessageLoop::QuitWhenIdleClosure());
+ return;
+ }
+
+ std::vector<SubsampleEntry> subsamples;
+
+ if (buffer->decrypt_config())
+ subsamples = buffer->decrypt_config()->subsamples();
+
+ bool is_valid =
+ mp4::AVC::IsValidAnnexB(buffer->data(), buffer->data_size(),
+ subsamples);
+ EXPECT_TRUE(is_valid);
+
+ if (!is_valid) {
+ LOG(ERROR) << "Buffer contains invalid Annex B data.";
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE, base::MessageLoop::QuitWhenIdleClosure());
+ return;
+ }
+
+ stream->Read(base::Bind(&ValidateAnnexB, stream));
+};
+
+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 < arraysize(files); ++i) {
+ DVLOG(1) << "Testing " << files[i];
+ CreateDemuxer(files[i]);
+ InitializeDemuxer();
+
+ // Ensure the expected streams are present.
+ DemuxerStream* stream = demuxer_->GetStream(DemuxerStream::VIDEO);
+ ASSERT_TRUE(stream);
+ stream->EnableBitstreamConverter();
+
+ stream->Read(base::Bind(&ValidateAnnexB, stream));
+ message_loop_.Run();
+
+ WaitableMessageLoopEvent event;
+ demuxer_->Stop(event.GetClosure());
+ event.RunAndWait();
+ demuxer_.reset();
+ data_source_.reset();
+ }
+}
+
+TEST_F(FFmpegDemuxerTest, Rotate_Metadata_0) {
+ CreateDemuxer("bear_rotate_0.mp4");
+ InitializeDemuxer();
+
+ DemuxerStream* stream = demuxer_->GetStream(DemuxerStream::VIDEO);
+ ASSERT_TRUE(stream);
+ ASSERT_EQ(VIDEO_ROTATION_0, stream->video_rotation());
+}
+
+TEST_F(FFmpegDemuxerTest, Rotate_Metadata_90) {
+ CreateDemuxer("bear_rotate_90.mp4");
+ InitializeDemuxer();
+
+ DemuxerStream* stream = demuxer_->GetStream(DemuxerStream::VIDEO);
+ ASSERT_TRUE(stream);
+ ASSERT_EQ(VIDEO_ROTATION_90, stream->video_rotation());
+}
+
+TEST_F(FFmpegDemuxerTest, Rotate_Metadata_180) {
+ CreateDemuxer("bear_rotate_180.mp4");
+ InitializeDemuxer();
+
+ DemuxerStream* stream = demuxer_->GetStream(DemuxerStream::VIDEO);
+ ASSERT_TRUE(stream);
+ ASSERT_EQ(VIDEO_ROTATION_180, stream->video_rotation());
+}
+
+TEST_F(FFmpegDemuxerTest, Rotate_Metadata_270) {
+ CreateDemuxer("bear_rotate_270.mp4");
+ InitializeDemuxer();
+
+ DemuxerStream* stream = demuxer_->GetStream(DemuxerStream::VIDEO);
+ ASSERT_TRUE(stream);
+ ASSERT_EQ(VIDEO_ROTATION_270, stream->video_rotation());
+}
+
+#endif
+
} // namespace media