| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/bind.h" |
| #include "media/base/audio_decoder_config.h" |
| #include "media/base/mock_callback.h" |
| #include "media/base/mock_demuxer_host.h" |
| #include "media/base/test_data_util.h" |
| #include "media/filters/chunk_demuxer.h" |
| #include "media/filters/chunk_demuxer_client.h" |
| #include "media/webm/cluster_builder.h" |
| #include "media/webm/webm_constants.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::testing::AnyNumber; |
| using ::testing::InSequence; |
| using ::testing::NotNull; |
| using ::testing::Return; |
| using ::testing::SaveArg; |
| using ::testing::SetArgumentPointee; |
| using ::testing::_; |
| |
| namespace media { |
| |
| static const uint8 kTracksHeader[] = { |
| 0x16, 0x54, 0xAE, 0x6B, // Tracks ID |
| 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // tracks(size = 0) |
| }; |
| |
| static const int kTracksHeaderSize = sizeof(kTracksHeader); |
| static const int kTracksSizeOffset = 4; |
| |
| // The size of TrackEntry element in test file "webm_vp8_track_entry" starts at |
| // index 1 and spans 8 bytes. |
| static const int kVideoTrackSizeOffset = 1; |
| static const int kVideoTrackSizeWidth = 8; |
| static const int kVideoTrackEntryHeaderSize = kVideoTrackSizeOffset + |
| kVideoTrackSizeWidth; |
| |
| static const int kVideoTrackNum = 1; |
| static const int kAudioTrackNum = 2; |
| |
| static const int kAudioBlockDuration = 23; |
| static const int kVideoBlockDuration = 33; |
| |
| static const char* kSourceId = "SourceId"; |
| |
| base::TimeDelta kDefaultDuration() { |
| return base::TimeDelta::FromMilliseconds(201224); |
| } |
| |
| // Write an integer into buffer in the form of vint that spans 8 bytes. |
| // The data pointed by |buffer| should be at least 8 bytes long. |
| // |number| should be in the range 0 <= number < 0x00FFFFFFFFFFFFFF. |
| static void WriteInt64(uint8* buffer, int64 number) { |
| DCHECK(number >= 0 && number < GG_LONGLONG(0x00FFFFFFFFFFFFFF)); |
| buffer[0] = 0x01; |
| int64 tmp = number; |
| for (int i = 7; i > 0; i--) { |
| buffer[i] = tmp & 0xff; |
| tmp >>= 8; |
| } |
| } |
| |
| MATCHER_P(HasTimestamp, timestamp_in_ms, "") { |
| return arg && !arg->IsEndOfStream() && |
| arg->GetTimestamp().InMilliseconds() == timestamp_in_ms; |
| } |
| |
| static void OnReadDone(const base::TimeDelta& expected_time, |
| bool* called, |
| const scoped_refptr<Buffer>& buffer) { |
| EXPECT_EQ(expected_time, buffer->GetTimestamp()); |
| *called = true; |
| } |
| |
| class MockChunkDemuxerClient : public ChunkDemuxerClient { |
| public: |
| MockChunkDemuxerClient() {} |
| virtual ~MockChunkDemuxerClient() {} |
| |
| MOCK_METHOD1(DemuxerOpened, void(ChunkDemuxer* demuxer)); |
| MOCK_METHOD0(DemuxerClosed, void()); |
| // TODO(xhwang): This is a workaround of the issue that move-only parameters |
| // are not supported in mocked methods. Remove this when the issue is fixed. |
| // See http://code.google.com/p/googletest/issues/detail?id=395 |
| MOCK_METHOD2(KeyNeededMock, void(const uint8* init_data, int init_data_size)); |
| void KeyNeeded(scoped_array<uint8> init_data, int init_data_size) { |
| KeyNeededMock(init_data.get(), init_data_size); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockChunkDemuxerClient); |
| }; |
| |
| class ChunkDemuxerTest : public testing::Test { |
| protected: |
| enum CodecsIndex { |
| AUDIO, |
| VIDEO, |
| MAX_CODECS_INDEX |
| }; |
| |
| ChunkDemuxerTest() |
| : buffered_bytes_(0), |
| client_(new MockChunkDemuxerClient()), |
| demuxer_(new ChunkDemuxer(client_.get())) { |
| } |
| |
| virtual ~ChunkDemuxerTest() { |
| ShutdownDemuxer(); |
| } |
| |
| void CreateInfoTracks(bool has_audio, bool has_video, |
| bool video_content_encoded, scoped_array<uint8>* buffer, |
| int* size) { |
| scoped_array<uint8> info; |
| int info_size = 0; |
| scoped_array<uint8> audio_track_entry; |
| int audio_track_entry_size = 0; |
| scoped_array<uint8> video_track_entry; |
| int video_track_entry_size = 0; |
| scoped_array<uint8> video_content_encodings; |
| int video_content_encodings_size = 0; |
| |
| ReadTestDataFile("webm_info_element", &info, &info_size); |
| |
| int tracks_element_size = 0; |
| |
| if (has_audio) { |
| ReadTestDataFile("webm_vorbis_track_entry", &audio_track_entry, |
| &audio_track_entry_size); |
| tracks_element_size += audio_track_entry_size; |
| } |
| |
| if (has_video) { |
| ReadTestDataFile("webm_vp8_track_entry", &video_track_entry, |
| &video_track_entry_size); |
| tracks_element_size += video_track_entry_size; |
| if (video_content_encoded) { |
| ReadTestDataFile("webm_content_encodings", &video_content_encodings, |
| &video_content_encodings_size); |
| tracks_element_size += video_content_encodings_size; |
| } |
| } |
| |
| *size = info_size + kTracksHeaderSize + tracks_element_size; |
| |
| buffer->reset(new uint8[*size]); |
| |
| uint8* buf = buffer->get(); |
| memcpy(buf, info.get(), info_size); |
| buf += info_size; |
| |
| memcpy(buf, kTracksHeader, kTracksHeaderSize); |
| WriteInt64(buf + kTracksSizeOffset, tracks_element_size); |
| buf += kTracksHeaderSize; |
| |
| if (has_audio) { |
| memcpy(buf, audio_track_entry.get(), audio_track_entry_size); |
| buf += audio_track_entry_size; |
| } |
| |
| if (has_video) { |
| memcpy(buf, video_track_entry.get(), video_track_entry_size); |
| if (video_content_encoded) { |
| memcpy(buf + video_track_entry_size, video_content_encodings.get(), |
| video_content_encodings_size); |
| video_track_entry_size += video_content_encodings_size; |
| WriteInt64(buf + kVideoTrackSizeOffset, |
| video_track_entry_size - kVideoTrackEntryHeaderSize); |
| } |
| buf += video_track_entry_size; |
| } |
| } |
| |
| ChunkDemuxer::Status AddId() { |
| std::vector<std::string> codecs(2); |
| codecs[0] = "vp8"; |
| codecs[1] = "vorbis"; |
| return demuxer_->AddId(kSourceId, "video/webm", codecs); |
| } |
| |
| bool AppendData(const uint8* data, size_t length) { |
| CHECK(length); |
| EXPECT_CALL(host_, AddBufferedByteRange(_, _)).Times(AnyNumber()) |
| .WillRepeatedly(SaveArg<1>(&buffered_bytes_)); |
| EXPECT_CALL(host_, SetNetworkActivity(true)) |
| .Times(AnyNumber()); |
| return demuxer_->AppendData(kSourceId, data, length); |
| } |
| |
| bool AppendDataInPieces(const uint8* data, size_t length) { |
| return AppendDataInPieces(data, length, 7); |
| } |
| |
| bool AppendDataInPieces(const uint8* data, size_t length, size_t piece_size) { |
| const uint8* start = data; |
| const uint8* end = data + length; |
| while (start < end) { |
| int64 old_buffered_bytes = buffered_bytes_; |
| size_t append_size = std::min(piece_size, |
| static_cast<size_t>(end - start)); |
| if (!AppendData(start, append_size)) |
| return false; |
| start += append_size; |
| |
| EXPECT_GT(buffered_bytes_, old_buffered_bytes); |
| } |
| return true; |
| } |
| |
| bool AppendInfoTracks(bool has_audio, bool has_video, |
| bool video_content_encoded) { |
| scoped_array<uint8> info_tracks; |
| int info_tracks_size = 0; |
| CreateInfoTracks(has_audio, has_video, video_content_encoded, |
| &info_tracks, &info_tracks_size); |
| return AppendData(info_tracks.get(), info_tracks_size); |
| } |
| |
| void InitDoneCalled(PipelineStatus expected_status, |
| PipelineStatus status) { |
| EXPECT_EQ(status, expected_status); |
| } |
| |
| PipelineStatusCB CreateInitDoneCB(const base::TimeDelta& expected_duration, |
| PipelineStatus expected_status) { |
| if (expected_status == PIPELINE_OK) |
| EXPECT_CALL(host_, SetDuration(expected_duration)); |
| |
| return base::Bind(&ChunkDemuxerTest::InitDoneCalled, |
| base::Unretained(this), |
| expected_status); |
| } |
| |
| bool InitDemuxer(bool has_audio, bool has_video, |
| bool video_content_encoded) { |
| PipelineStatus expected_status = |
| (has_audio || has_video) ? PIPELINE_OK : DEMUXER_ERROR_COULD_NOT_OPEN; |
| |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, CreateInitDoneCB(kDefaultDuration(), expected_status)); |
| |
| if (AddId() != ChunkDemuxer::kOk) |
| return false; |
| |
| return AppendInfoTracks(has_audio, has_video, video_content_encoded); |
| } |
| |
| void ShutdownDemuxer() { |
| if (demuxer_) { |
| EXPECT_CALL(*client_, DemuxerClosed()); |
| demuxer_->Shutdown(); |
| } |
| } |
| |
| void AddSimpleBlock(ClusterBuilder* cb, int track_num, int64 timecode) { |
| uint8 data[] = { 0x00 }; |
| cb->AddSimpleBlock(track_num, timecode, 0, data, sizeof(data)); |
| } |
| |
| scoped_ptr<Cluster> GenerateCluster(int timecode, int block_count) { |
| return GenerateCluster(timecode, timecode, block_count); |
| } |
| |
| scoped_ptr<Cluster> GenerateCluster(int audio_timecode, int video_timecode, |
| int block_count) { |
| CHECK_GT(block_count, 0); |
| |
| int size = 10; |
| scoped_array<uint8> data(new uint8[size]); |
| |
| ClusterBuilder cb; |
| cb.SetClusterTimecode(std::min(audio_timecode, video_timecode)); |
| |
| if (block_count == 1) { |
| cb.AddBlockGroup(kAudioTrackNum, audio_timecode, kAudioBlockDuration, |
| kWebMFlagKeyframe, data.get(), size); |
| return cb.Finish(); |
| } |
| |
| // Create simple blocks for everything except the last 2 blocks. |
| // The first video frame must be a keyframe. |
| uint8 video_flag = kWebMFlagKeyframe; |
| for (int i = 0; i < block_count - 2; i++) { |
| if (audio_timecode <= video_timecode) { |
| cb.AddSimpleBlock(kAudioTrackNum, audio_timecode, kWebMFlagKeyframe, |
| data.get(), size); |
| audio_timecode += kAudioBlockDuration; |
| continue; |
| } |
| |
| cb.AddSimpleBlock(kVideoTrackNum, video_timecode, video_flag, data.get(), |
| size); |
| video_timecode += kVideoBlockDuration; |
| video_flag = 0; |
| } |
| |
| // Make the last 2 blocks BlockGroups so that they don't get delayed by the |
| // block duration calculation logic. |
| if (audio_timecode <= video_timecode) { |
| cb.AddBlockGroup(kAudioTrackNum, audio_timecode, kAudioBlockDuration, |
| kWebMFlagKeyframe, data.get(), size); |
| cb.AddBlockGroup(kVideoTrackNum, video_timecode, kVideoBlockDuration, |
| video_flag, data.get(), size); |
| } else { |
| cb.AddBlockGroup(kVideoTrackNum, video_timecode, kVideoBlockDuration, |
| video_flag, data.get(), size); |
| cb.AddBlockGroup(kAudioTrackNum, audio_timecode, kAudioBlockDuration, |
| kWebMFlagKeyframe, data.get(), size); |
| } |
| |
| return cb.Finish(); |
| } |
| |
| void GenerateExpectedReads(int timecode, int block_count, |
| DemuxerStream* audio, |
| DemuxerStream* video) { |
| CHECK_GT(block_count, 0); |
| int audio_timecode = timecode; |
| int video_timecode = timecode; |
| |
| if (block_count == 1) { |
| ExpectRead(audio, audio_timecode); |
| return; |
| } |
| |
| for (int i = 0; i < block_count; i++) { |
| if (audio_timecode <= video_timecode) { |
| ExpectRead(audio, audio_timecode); |
| audio_timecode += kAudioBlockDuration; |
| continue; |
| } |
| |
| ExpectRead(video, video_timecode); |
| video_timecode += kVideoBlockDuration; |
| } |
| } |
| |
| MOCK_METHOD1(ReadDone, void(const scoped_refptr<Buffer>&)); |
| |
| void ExpectRead(DemuxerStream* stream, int64 timestamp_in_ms) { |
| EXPECT_CALL(*this, ReadDone(HasTimestamp(timestamp_in_ms))); |
| stream->Read(base::Bind(&ChunkDemuxerTest::ReadDone, |
| base::Unretained(this))); |
| } |
| |
| MOCK_METHOD1(Checkpoint, void(int id)); |
| |
| struct BufferTimestamps { |
| int video_time_ms; |
| int audio_time_ms; |
| }; |
| static const int kSkip = -1; |
| |
| // Test parsing a WebM file. |
| // |filename| - The name of the file in media/test/data to parse. |
| // |timestamps| - The expected timestamps on the parsed buffers. |
| // a timestamp of kSkip indicates that a Read() call for that stream |
| // shouldn't be made on that iteration of the loop. If both streams have |
| // a kSkip then the loop will terminate. |
| bool ParseWebMFile(const std::string& filename, |
| const BufferTimestamps* timestamps, |
| const base::TimeDelta& duration) { |
| scoped_array<uint8> buffer; |
| int buffer_size = 0; |
| |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, CreateInitDoneCB(duration, PIPELINE_OK)); |
| |
| if (AddId() != ChunkDemuxer::kOk) |
| return false; |
| |
| // Read a WebM file into memory and send the data to the demuxer. |
| ReadTestDataFile(filename, &buffer, &buffer_size); |
| if (!AppendDataInPieces(buffer.get(), buffer_size, 512)) |
| return false; |
| |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| // Verify that the timestamps on the first few packets match what we |
| // expect. |
| for (size_t i = 0; |
| (timestamps[i].audio_time_ms != kSkip || |
| timestamps[i].video_time_ms != kSkip); |
| i++) { |
| bool audio_read_done = false; |
| bool video_read_done = false; |
| |
| if (timestamps[i].audio_time_ms != kSkip) { |
| DCHECK(audio); |
| audio->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds( |
| timestamps[i].audio_time_ms), |
| &audio_read_done)); |
| EXPECT_TRUE(audio_read_done); |
| } |
| |
| if (timestamps[i].video_time_ms != kSkip) { |
| DCHECK(video); |
| video->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds( |
| timestamps[i].video_time_ms), |
| &video_read_done)); |
| |
| EXPECT_TRUE(video_read_done); |
| } |
| } |
| |
| return true; |
| } |
| |
| MockDemuxerHost host_; |
| int64 buffered_bytes_; |
| |
| scoped_ptr<MockChunkDemuxerClient> client_; |
| scoped_refptr<ChunkDemuxer> demuxer_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ChunkDemuxerTest); |
| }; |
| |
| TEST_F(ChunkDemuxerTest, TestInit) { |
| // Test no streams, audio-only, video-only, and audio & video scenarios, |
| // with video content encoded or not. |
| for (int i = 0; i < 8; i++) { |
| bool has_audio = (i & 0x1) != 0; |
| bool has_video = (i & 0x2) != 0; |
| bool video_content_encoded = (i & 0x4) != 0; |
| |
| // No test on invalid combination. |
| if (!has_video && video_content_encoded) |
| continue; |
| |
| client_.reset(new MockChunkDemuxerClient()); |
| demuxer_ = new ChunkDemuxer(client_.get()); |
| if (has_video && video_content_encoded) |
| EXPECT_CALL(*client_, KeyNeededMock(NotNull(), 16)); |
| |
| ASSERT_TRUE(InitDemuxer(has_audio, has_video, video_content_encoded)); |
| |
| scoped_refptr<DemuxerStream> audio_stream = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| if (has_audio) { |
| ASSERT_TRUE(audio_stream); |
| |
| const AudioDecoderConfig& config = audio_stream->audio_decoder_config(); |
| EXPECT_EQ(kCodecVorbis, config.codec()); |
| EXPECT_EQ(16, config.bits_per_channel()); |
| EXPECT_EQ(CHANNEL_LAYOUT_STEREO, config.channel_layout()); |
| EXPECT_EQ(44100, config.samples_per_second()); |
| EXPECT_TRUE(config.extra_data()); |
| EXPECT_GT(config.extra_data_size(), 0u); |
| } else { |
| EXPECT_FALSE(audio_stream); |
| } |
| |
| scoped_refptr<DemuxerStream> video_stream = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| if (has_video) { |
| EXPECT_TRUE(video_stream); |
| } else { |
| EXPECT_FALSE(video_stream); |
| } |
| |
| ShutdownDemuxer(); |
| demuxer_ = NULL; |
| } |
| } |
| |
| // Makes sure that Seek() reports an error if Shutdown() |
| // is called before the first cluster is passed to the demuxer. |
| TEST_F(ChunkDemuxerTest, TestShutdownBeforeFirstSeekCompletes) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| demuxer_->Seek(base::TimeDelta::FromSeconds(0), |
| NewExpectedStatusCB(PIPELINE_ERROR_ABORT)); |
| } |
| |
| // Test that Seek() completes successfully when the first cluster |
| // arrives. |
| TEST_F(ChunkDemuxerTest, TestAppendDataAfterSeek) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| InSequence s; |
| |
| EXPECT_CALL(*this, Checkpoint(1)); |
| |
| demuxer_->Seek(base::TimeDelta::FromSeconds(0), |
| NewExpectedStatusCB(PIPELINE_OK)); |
| |
| EXPECT_CALL(*this, Checkpoint(2)); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 4)); |
| |
| Checkpoint(1); |
| |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| |
| Checkpoint(2); |
| } |
| |
| // Test the case where a Seek() is requested while the parser |
| // is in the middle of cluster. This is to verify that the parser |
| // resets itself on seek and is in the right state when data from |
| // the new seek point arrives. |
| TEST_F(ChunkDemuxerTest, TestSeekWhileParsingCluster) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| InSequence s; |
| |
| scoped_ptr<Cluster> cluster_a(GenerateCluster(0, 6)); |
| scoped_ptr<Cluster> cluster_b(GenerateCluster(5000, 6)); |
| |
| // Append all but the last byte so that everything but |
| // the last block can be parsed. |
| ASSERT_TRUE(AppendData(cluster_a->data(), cluster_a->size() - 1)); |
| |
| ExpectRead(audio, 0); |
| ExpectRead(video, 0); |
| ExpectRead(audio, kAudioBlockDuration); |
| // Note: We skip trying to read a video buffer here because computing |
| // the duration for this block relies on successfully parsing the last block |
| // in the cluster the cluster. |
| ExpectRead(audio, 2 * kAudioBlockDuration); |
| |
| demuxer_->StartWaitingForSeek(); |
| demuxer_->Seek(base::TimeDelta::FromSeconds(5), |
| NewExpectedStatusCB(PIPELINE_OK)); |
| |
| |
| // Append the new cluster and verify that only the blocks |
| // in the new cluster are returned. |
| ASSERT_TRUE(AppendData(cluster_b->data(), cluster_b->size())); |
| GenerateExpectedReads(5000, 6, audio, video); |
| } |
| |
| // Test the case where AppendData() is called before Init(). |
| TEST_F(ChunkDemuxerTest, TestAppendDataBeforeInit) { |
| scoped_array<uint8> info_tracks; |
| int info_tracks_size = 0; |
| CreateInfoTracks(true, true, false, &info_tracks, &info_tracks_size); |
| |
| EXPECT_FALSE(demuxer_->AppendData(kSourceId, info_tracks.get(), |
| info_tracks_size)); |
| } |
| |
| // Make sure Read() callbacks are dispatched with the proper data. |
| TEST_F(ChunkDemuxerTest, TestRead) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| bool audio_read_done = false; |
| bool video_read_done = false; |
| audio->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &audio_read_done)); |
| |
| video->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &video_read_done)); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 4)); |
| |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| |
| EXPECT_TRUE(audio_read_done); |
| EXPECT_TRUE(video_read_done); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestOutOfOrderClusters) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_ptr<Cluster> cluster_a(GenerateCluster(10, 4)); |
| |
| ASSERT_TRUE(AppendData(cluster_a->data(), cluster_a->size())); |
| |
| // Cluster B starts before cluster_a and has data |
| // that overlaps. |
| scoped_ptr<Cluster> cluster_b(GenerateCluster(5, 4)); |
| |
| // Make sure that AppendData() does not fail. |
| ASSERT_TRUE(AppendData(cluster_b->data(), cluster_b->size())); |
| |
| // Verify that AppendData() can still accept more data. |
| scoped_ptr<Cluster> cluster_c(GenerateCluster(45, 2)); |
| ASSERT_TRUE(demuxer_->AppendData(kSourceId, cluster_c->data(), |
| cluster_c->size())); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestNonMonotonicButAboveClusterTimecode) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| ClusterBuilder cb; |
| |
| // Test the case where block timecodes are not monotonically |
| // increasing but stay above the cluster timecode. |
| cb.SetClusterTimecode(5); |
| AddSimpleBlock(&cb, kAudioTrackNum, 5); |
| AddSimpleBlock(&cb, kVideoTrackNum, 10); |
| AddSimpleBlock(&cb, kAudioTrackNum, 7); |
| AddSimpleBlock(&cb, kVideoTrackNum, 15); |
| scoped_ptr<Cluster> cluster_a(cb.Finish()); |
| |
| EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_DECODE)); |
| ASSERT_TRUE(AppendData(cluster_a->data(), cluster_a->size())); |
| |
| // Verify that AppendData() doesn't accept more data now. |
| scoped_ptr<Cluster> cluster_b(GenerateCluster(20, 2)); |
| EXPECT_FALSE(demuxer_->AppendData(kSourceId, cluster_b->data(), |
| cluster_b->size())); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestBackwardsAndBeforeClusterTimecode) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| ClusterBuilder cb; |
| |
| // Test timecodes going backwards and including values less than the cluster |
| // timecode. |
| cb.SetClusterTimecode(5); |
| AddSimpleBlock(&cb, kAudioTrackNum, 5); |
| AddSimpleBlock(&cb, kVideoTrackNum, 5); |
| AddSimpleBlock(&cb, kAudioTrackNum, 3); |
| AddSimpleBlock(&cb, kVideoTrackNum, 3); |
| scoped_ptr<Cluster> cluster_a(cb.Finish()); |
| |
| EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_DECODE)); |
| ASSERT_TRUE(AppendData(cluster_a->data(), cluster_a->size())); |
| |
| // Verify that AppendData() doesn't accept more data now. |
| scoped_ptr<Cluster> cluster_b(GenerateCluster(6, 2)); |
| EXPECT_FALSE(demuxer_->AppendData(kSourceId, cluster_b->data(), |
| cluster_b->size())); |
| } |
| |
| |
| TEST_F(ChunkDemuxerTest, TestPerStreamMonotonicallyIncreasingTimestamps) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| ClusterBuilder cb; |
| |
| // Test strict monotonic increasing timestamps on a per stream |
| // basis. |
| cb.SetClusterTimecode(5); |
| AddSimpleBlock(&cb, kAudioTrackNum, 5); |
| AddSimpleBlock(&cb, kVideoTrackNum, 5); |
| AddSimpleBlock(&cb, kAudioTrackNum, 5); |
| AddSimpleBlock(&cb, kVideoTrackNum, 7); |
| scoped_ptr<Cluster> cluster(cb.Finish()); |
| |
| EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_DECODE)); |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestMonotonicallyIncreasingTimestampsAcrossClusters) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| ClusterBuilder cb; |
| |
| // Test strict monotonic increasing timestamps on a per stream |
| // basis across clusters. |
| cb.SetClusterTimecode(5); |
| AddSimpleBlock(&cb, kAudioTrackNum, 5); |
| AddSimpleBlock(&cb, kVideoTrackNum, 5); |
| scoped_ptr<Cluster> cluster_a(cb.Finish()); |
| |
| ASSERT_TRUE(AppendData(cluster_a->data(), cluster_a->size())); |
| |
| cb.SetClusterTimecode(5); |
| AddSimpleBlock(&cb, kAudioTrackNum, 5); |
| AddSimpleBlock(&cb, kVideoTrackNum, 7); |
| scoped_ptr<Cluster> cluster_b(cb.Finish()); |
| |
| EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_DECODE)); |
| ASSERT_TRUE(AppendData(cluster_b->data(), cluster_b->size())); |
| |
| // Verify that AppendData() doesn't accept more data now. |
| scoped_ptr<Cluster> cluster_c(GenerateCluster(10, 2)); |
| EXPECT_FALSE(demuxer_->AppendData(kSourceId, cluster_c->data(), |
| cluster_c->size())); |
| } |
| |
| // Test the case where a cluster is passed to AppendData() before |
| // INFO & TRACKS data. |
| TEST_F(ChunkDemuxerTest, TestClusterBeforeInfoTracks) { |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN)); |
| |
| ASSERT_EQ(AddId(), ChunkDemuxer::kOk); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 1)); |
| |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| } |
| |
| // Test cases where we get an EndOfStream() call during initialization. |
| TEST_F(ChunkDemuxerTest, TestEOSDuringInit) { |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN)); |
| demuxer_->EndOfStream(PIPELINE_OK); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestDecodeErrorEndOfStream) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 4)); |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| |
| EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_DECODE)); |
| demuxer_->EndOfStream(PIPELINE_ERROR_DECODE); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestNetworkErrorEndOfStream) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 4)); |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| |
| EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_NETWORK)); |
| demuxer_->EndOfStream(PIPELINE_ERROR_NETWORK); |
| } |
| |
| // Helper class to reduce duplicate code when testing end of stream |
| // Read() behavior. |
| class EndOfStreamHelper { |
| public: |
| EndOfStreamHelper(const scoped_refptr<Demuxer> demuxer) |
| : demuxer_(demuxer), |
| audio_read_done_(false), |
| video_read_done_(false) { |
| } |
| |
| // Request a read on the audio and video streams. |
| void RequestReads() { |
| EXPECT_FALSE(audio_read_done_); |
| EXPECT_FALSE(video_read_done_); |
| |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| audio->Read(base::Bind(&OnEndOfStreamReadDone, |
| &audio_read_done_)); |
| |
| video->Read(base::Bind(&OnEndOfStreamReadDone, |
| &video_read_done_)); |
| } |
| |
| // Check to see if |audio_read_done_| and |video_read_done_| variables |
| // match |expected|. |
| void CheckIfReadDonesWereCalled(bool expected) { |
| EXPECT_EQ(expected, audio_read_done_); |
| EXPECT_EQ(expected, video_read_done_); |
| } |
| |
| private: |
| static void OnEndOfStreamReadDone(bool* called, |
| const scoped_refptr<Buffer>& buffer) { |
| EXPECT_TRUE(buffer->IsEndOfStream()); |
| *called = true; |
| } |
| |
| scoped_refptr<Demuxer> demuxer_; |
| bool audio_read_done_; |
| bool video_read_done_; |
| |
| DISALLOW_COPY_AND_ASSIGN(EndOfStreamHelper); |
| }; |
| |
| // Make sure that all pending reads that we don't have media data for get an |
| // "end of stream" buffer when EndOfStream() is called. |
| TEST_F(ChunkDemuxerTest, TestEndOfStreamWithPendingReads) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| bool audio_read_done_1 = false; |
| bool video_read_done_1 = false; |
| EndOfStreamHelper end_of_stream_helper_1(demuxer_); |
| EndOfStreamHelper end_of_stream_helper_2(demuxer_); |
| |
| audio->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &audio_read_done_1)); |
| |
| video->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &video_read_done_1)); |
| |
| end_of_stream_helper_1.RequestReads(); |
| end_of_stream_helper_2.RequestReads(); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 2)); |
| |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| |
| EXPECT_TRUE(audio_read_done_1); |
| EXPECT_TRUE(video_read_done_1); |
| end_of_stream_helper_1.CheckIfReadDonesWereCalled(false); |
| end_of_stream_helper_2.CheckIfReadDonesWereCalled(false); |
| |
| demuxer_->EndOfStream(PIPELINE_OK); |
| |
| end_of_stream_helper_1.CheckIfReadDonesWereCalled(true); |
| end_of_stream_helper_2.CheckIfReadDonesWereCalled(true); |
| } |
| |
| // Make sure that all Read() calls after we get an EndOfStream() |
| // call return an "end of stream" buffer. |
| TEST_F(ChunkDemuxerTest, TestReadsAfterEndOfStream) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| bool audio_read_done_1 = false; |
| bool video_read_done_1 = false; |
| EndOfStreamHelper end_of_stream_helper_1(demuxer_); |
| EndOfStreamHelper end_of_stream_helper_2(demuxer_); |
| EndOfStreamHelper end_of_stream_helper_3(demuxer_); |
| |
| audio->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &audio_read_done_1)); |
| |
| video->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &video_read_done_1)); |
| |
| end_of_stream_helper_1.RequestReads(); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 2)); |
| |
| ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); |
| |
| EXPECT_TRUE(audio_read_done_1); |
| EXPECT_TRUE(video_read_done_1); |
| end_of_stream_helper_1.CheckIfReadDonesWereCalled(false); |
| |
| EXPECT_TRUE(demuxer_->EndOfStream(PIPELINE_OK)); |
| |
| end_of_stream_helper_1.CheckIfReadDonesWereCalled(true); |
| |
| // Request a few more reads and make sure we immediately get |
| // end of stream buffers. |
| end_of_stream_helper_2.RequestReads(); |
| end_of_stream_helper_2.CheckIfReadDonesWereCalled(true); |
| |
| end_of_stream_helper_3.RequestReads(); |
| end_of_stream_helper_3.CheckIfReadDonesWereCalled(true); |
| } |
| |
| // Make sure AppendData() will accept elements that span multiple calls. |
| TEST_F(ChunkDemuxerTest, TestAppendingInPieces) { |
| |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK)); |
| |
| ASSERT_EQ(AddId(), ChunkDemuxer::kOk); |
| |
| scoped_array<uint8> info_tracks; |
| int info_tracks_size = 0; |
| CreateInfoTracks(true, true, false, &info_tracks, &info_tracks_size); |
| |
| scoped_ptr<Cluster> cluster_a(GenerateCluster(0, 4)); |
| scoped_ptr<Cluster> cluster_b(GenerateCluster(46, 66, 5)); |
| |
| size_t buffer_size = info_tracks_size + cluster_a->size() + cluster_b->size(); |
| scoped_array<uint8> buffer(new uint8[buffer_size]); |
| uint8* dst = buffer.get(); |
| memcpy(dst, info_tracks.get(), info_tracks_size); |
| dst += info_tracks_size; |
| |
| memcpy(dst, cluster_a->data(), cluster_a->size()); |
| dst += cluster_a->size(); |
| |
| memcpy(dst, cluster_b->data(), cluster_b->size()); |
| dst += cluster_b->size(); |
| |
| ASSERT_TRUE(AppendDataInPieces(buffer.get(), buffer_size)); |
| |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| ASSERT_TRUE(audio); |
| ASSERT_TRUE(video); |
| |
| GenerateExpectedReads(0, 9, audio, video); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestWebMFile_AudioAndVideo) { |
| struct BufferTimestamps buffer_timestamps[] = { |
| {0, 0}, |
| {33, 3}, |
| {67, 6}, |
| {100, 9}, |
| {133, 12}, |
| {kSkip, kSkip}, |
| }; |
| |
| ASSERT_TRUE(ParseWebMFile("bear-320x240.webm", buffer_timestamps, |
| base::TimeDelta::FromMilliseconds(2744))); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestWebMFile_LiveAudioAndVideo) { |
| struct BufferTimestamps buffer_timestamps[] = { |
| {0, 0}, |
| {33, 3}, |
| {67, 6}, |
| {100, 9}, |
| {133, 12}, |
| {kSkip, kSkip}, |
| }; |
| |
| ASSERT_TRUE(ParseWebMFile("bear-320x240-live.webm", buffer_timestamps, |
| kInfiniteDuration())); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestWebMFile_AudioOnly) { |
| struct BufferTimestamps buffer_timestamps[] = { |
| {kSkip, 0}, |
| {kSkip, 3}, |
| {kSkip, 6}, |
| {kSkip, 9}, |
| {kSkip, 12}, |
| {kSkip, kSkip}, |
| }; |
| |
| ASSERT_TRUE(ParseWebMFile("bear-320x240-audio-only.webm", buffer_timestamps, |
| base::TimeDelta::FromMilliseconds(2744))); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestWebMFile_VideoOnly) { |
| struct BufferTimestamps buffer_timestamps[] = { |
| {0, kSkip}, |
| {33, kSkip}, |
| {67, kSkip}, |
| {100, kSkip}, |
| {133, kSkip}, |
| {kSkip, kSkip}, |
| }; |
| |
| ASSERT_TRUE(ParseWebMFile("bear-320x240-video-only.webm", buffer_timestamps, |
| base::TimeDelta::FromMilliseconds(2703))); |
| } |
| |
| // Verify that we output buffers before the entire cluster has been parsed. |
| TEST_F(ChunkDemuxerTest, TestIncrementalClusterParsing) { |
| ASSERT_TRUE(InitDemuxer(true, true, false)); |
| |
| scoped_ptr<Cluster> cluster(GenerateCluster(0, 6)); |
| scoped_refptr<DemuxerStream> audio = |
| demuxer_->GetStream(DemuxerStream::AUDIO); |
| scoped_refptr<DemuxerStream> video = |
| demuxer_->GetStream(DemuxerStream::VIDEO); |
| |
| bool audio_read_done = false; |
| bool video_read_done = false; |
| audio->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &audio_read_done)); |
| |
| video->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(0), |
| &video_read_done)); |
| |
| // Make sure the reads haven't completed yet. |
| EXPECT_FALSE(audio_read_done); |
| EXPECT_FALSE(video_read_done); |
| |
| // Append data one byte at a time until the audio read completes. |
| int i = 0; |
| for (; i < cluster->size() && !audio_read_done; ++i) { |
| ASSERT_TRUE(AppendData(cluster->data() + i, 1)); |
| } |
| |
| EXPECT_TRUE(audio_read_done); |
| EXPECT_FALSE(video_read_done); |
| EXPECT_GT(i, 0); |
| EXPECT_LT(i, cluster->size()); |
| |
| // Append data one byte at a time until the video read completes. |
| for (; i < cluster->size() && !video_read_done; ++i) { |
| ASSERT_TRUE(AppendData(cluster->data() + i, 1)); |
| } |
| |
| EXPECT_TRUE(video_read_done); |
| EXPECT_LT(i, cluster->size()); |
| |
| audio_read_done = false; |
| video_read_done = false; |
| audio->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(23), |
| &audio_read_done)); |
| |
| video->Read(base::Bind(&OnReadDone, |
| base::TimeDelta::FromMilliseconds(33), |
| &video_read_done)); |
| |
| // Make sure the reads haven't completed yet. |
| EXPECT_FALSE(audio_read_done); |
| EXPECT_FALSE(video_read_done); |
| |
| // Append the remaining data. |
| ASSERT_LT(i, cluster->size()); |
| ASSERT_TRUE(AppendData(cluster->data() + i, cluster->size() - i)); |
| |
| EXPECT_TRUE(audio_read_done); |
| EXPECT_TRUE(video_read_done); |
| } |
| |
| |
| TEST_F(ChunkDemuxerTest, TestParseErrorDuringInit) { |
| EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_DECODE)); |
| |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK)); |
| |
| ASSERT_EQ(AddId(), ChunkDemuxer::kOk); |
| |
| ASSERT_TRUE(AppendInfoTracks(true, true, false)); |
| |
| uint8 tmp = 0; |
| ASSERT_TRUE(demuxer_->AppendData(kSourceId, &tmp, 1)); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestAVHeadersWithAudioOnlyType) { |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, CreateInitDoneCB(kDefaultDuration(), |
| DEMUXER_ERROR_COULD_NOT_OPEN)); |
| |
| std::vector<std::string> codecs(1); |
| codecs[0] = "vorbis"; |
| ASSERT_EQ(demuxer_->AddId(kSourceId, "audio/webm", codecs), |
| ChunkDemuxer::kOk); |
| |
| ASSERT_TRUE(AppendInfoTracks(true, true, false)); |
| } |
| |
| TEST_F(ChunkDemuxerTest, TestAVHeadersWithVideoOnlyType) { |
| EXPECT_CALL(*client_, DemuxerOpened(_)); |
| demuxer_->Initialize( |
| &host_, CreateInitDoneCB(kDefaultDuration(), |
| DEMUXER_ERROR_COULD_NOT_OPEN)); |
| |
| std::vector<std::string> codecs(1); |
| codecs[0] = "vp8"; |
| ASSERT_EQ(demuxer_->AddId(kSourceId, "video/webm", codecs), |
| ChunkDemuxer::kOk); |
| |
| ASSERT_TRUE(AppendInfoTracks(true, true, false)); |
| } |
| |
| } // namespace media |