diff options
author | vrk@chromium.org <vrk@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-07-14 00:01:04 +0000 |
---|---|---|
committer | vrk@chromium.org <vrk@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-07-14 00:01:04 +0000 |
commit | 2cc4fd29938b21378dfce7d075601ac3fa1ff753 (patch) | |
tree | 81ba555b1e1906cae3303ab7dd974f14886a439b | |
parent | 206387a513aa87efb15f8bd1122f85592b70fda4 (diff) | |
download | chromium_src-2cc4fd29938b21378dfce7d075601ac3fa1ff753.zip chromium_src-2cc4fd29938b21378dfce7d075601ac3fa1ff753.tar.gz chromium_src-2cc4fd29938b21378dfce7d075601ac3fa1ff753.tar.bz2 |
Implement ChunkDemuxer::GetStartTime()
This CL makes ChunkDemuxer wait until it gets the first OnNewMediaSegment()
signal before notifying the Pipeline that it is initialized. ChunkDemuxer uses
the first media segment start time reported as its |start_time_|, and seeks
all streams to this time. The "first seek" logic is thus removed from
SourceBufferStream.
BUG=132815
TEST=media_unittests, 'bug 5' plays
Review URL: https://chromiumcodereview.appspot.com/10690057
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@146687 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | media/base/pipeline.cc | 2 | ||||
-rw-r--r-- | media/base/pipeline_status.h | 1 | ||||
-rw-r--r-- | media/filters/chunk_demuxer.cc | 61 | ||||
-rw-r--r-- | media/filters/chunk_demuxer.h | 3 | ||||
-rw-r--r-- | media/filters/chunk_demuxer_unittest.cc | 165 | ||||
-rw-r--r-- | media/filters/source_buffer_stream.cc | 60 | ||||
-rw-r--r-- | media/filters/source_buffer_stream.h | 12 | ||||
-rw-r--r-- | media/filters/source_buffer_stream_unittest.cc | 95 | ||||
-rw-r--r-- | webkit/media/webmediaplayer_impl.cc | 7 |
9 files changed, 294 insertions, 112 deletions
diff --git a/media/base/pipeline.cc b/media/base/pipeline.cc index b585355..658b414 100644 --- a/media/base/pipeline.cc +++ b/media/base/pipeline.cc @@ -785,7 +785,7 @@ void Pipeline::SeekTask(TimeDelta time, const PipelineStatusCB& seek_cb) { // kStarting (for each filter) // kStarted SetState(kPausing); - seek_timestamp_ = time; + seek_timestamp_ = std::max(time, demuxer_->GetStartTime()); seek_cb_ = seek_cb; // Kick off seeking! diff --git a/media/base/pipeline_status.h b/media/base/pipeline_status.h index 8f1a677..cfce0b4 100644 --- a/media/base/pipeline_status.h +++ b/media/base/pipeline_status.h @@ -12,6 +12,7 @@ namespace media { // Status states for pipeline. All codes except PIPELINE_OK indicate errors. +// TODO(vrk/scherkus): Trim the unused status codes. (crbug.com/126070) enum PipelineStatus { PIPELINE_OK, PIPELINE_ERROR_URL_NOT_FOUND, diff --git a/media/filters/chunk_demuxer.cc b/media/filters/chunk_demuxer.cc index ead7956..06a8d629 100644 --- a/media/filters/chunk_demuxer.cc +++ b/media/filters/chunk_demuxer.cc @@ -5,6 +5,7 @@ #include "media/filters/chunk_demuxer.h" #include "base/bind.h" +#include "base/callback_helpers.h" #include "base/logging.h" #include "base/message_loop.h" #include "base/string_util.h" @@ -167,6 +168,9 @@ class ChunkDemuxerStream : public DemuxerStream { // Append() belong to a media segment that starts at |start_timestamp|. void OnNewMediaSegment(TimeDelta start_timestamp); + // Notifies the stream that it begins at |start_time|. + void SetStartTime(TimeDelta start_time); + // Called when mid-stream config updates occur. // Returns true if the new config is accepted. // Returns false if the new config should trigger an error. @@ -265,6 +269,11 @@ void ChunkDemuxerStream::OnNewMediaSegment(TimeDelta start_timestamp) { stream_->OnNewMediaSegment(start_timestamp); } +void ChunkDemuxerStream::SetStartTime(TimeDelta start_time) { + base::AutoLock auto_lock(lock_); + stream_->SetStartTime(start_time); +} + bool ChunkDemuxerStream::Append(const StreamParser::BufferQueue& buffers) { if (buffers.empty()) return false; @@ -459,7 +468,8 @@ void ChunkDemuxerStream::CreateReadDoneClosures_Locked(ClosureQueue* closures) { ChunkDemuxer::ChunkDemuxer(ChunkDemuxerClient* client) : state_(WAITING_FOR_INIT), host_(NULL), - client_(client) { + client_(client), + start_time_(kNoTimestamp()) { DCHECK(client); } @@ -486,6 +496,7 @@ void ChunkDemuxer::Stop(const base::Closure& callback) { void ChunkDemuxer::Seek(TimeDelta time, const PipelineStatusCB& cb) { DVLOG(1) << "Seek(" << time.InSecondsF() << ")"; + DCHECK(time >= start_time_); PipelineStatus status = PIPELINE_ERROR_INVALID_STATE; { @@ -534,10 +545,9 @@ scoped_refptr<DemuxerStream> ChunkDemuxer::GetStream( } TimeDelta ChunkDemuxer::GetStartTime() const { - DVLOG(1) << "GetStartTime()"; - // TODO(acolwell) : Fix this so it uses the time on the first packet. - // (crbug.com/132815) - return TimeDelta(); + DCHECK(start_time_ != kNoTimestamp()); + DVLOG(1) << "GetStartTime(): " << start_time_.InSecondsF(); + return start_time_; } void ChunkDemuxer::StartWaitingForSeek() { @@ -693,9 +703,9 @@ bool ChunkDemuxer::AppendData(const std::string& id, switch (state_) { case INITIALIZING: + case WAITING_FOR_START_TIME: DCHECK_GT(stream_parser_map_.count(id), 0u); if (!stream_parser_map_[id]->Parse(data, length)) { - DCHECK_EQ(state_, INITIALIZING); ReportError_Locked(DEMUXER_ERROR_COULD_NOT_OPEN); return true; } @@ -880,7 +890,7 @@ bool ChunkDemuxer::CanEndOfStream_Locked() const { } void ChunkDemuxer::OnStreamParserInitDone(bool success, TimeDelta duration) { - DVLOG(1) << "OnSourceBufferInitDone(" << success << ", " + DVLOG(1) << "OnStreamParserInitDone(" << success << ", " << duration.InSecondsF() << ")"; lock_.AssertAcquired(); DCHECK_EQ(state_, INITIALIZING); @@ -899,10 +909,7 @@ void ChunkDemuxer::OnStreamParserInitDone(bool success, TimeDelta duration) { host_->SetDuration(duration_); - ChangeState_Locked(INITIALIZED); - PipelineStatusCB cb; - std::swap(cb, init_cb_); - cb.Run(PIPELINE_OK); + ChangeState_Locked(WAITING_FOR_START_TIME); } bool ChunkDemuxer::OnNewConfigs(bool has_audio, bool has_video, @@ -950,6 +957,7 @@ bool ChunkDemuxer::OnNewConfigs(bool has_audio, bool has_video, } bool ChunkDemuxer::OnAudioBuffers(const StreamParser::BufferQueue& buffers) { + lock_.AssertAcquired(); DCHECK_NE(state_, SHUTDOWN); if (!audio_) @@ -959,6 +967,7 @@ bool ChunkDemuxer::OnAudioBuffers(const StreamParser::BufferQueue& buffers) { } bool ChunkDemuxer::OnVideoBuffers(const StreamParser::BufferQueue& buffers) { + lock_.AssertAcquired(); DCHECK_NE(state_, SHUTDOWN); if (!video_) @@ -975,13 +984,37 @@ bool ChunkDemuxer::OnNeedKey(scoped_array<uint8> init_data, void ChunkDemuxer::OnNewMediaSegment(const std::string& source_id, TimeDelta start_timestamp) { - // TODO(vrk): There should be a special case for the first appends where all - // streams (for both demuxed and muxed case) begin at the earliest stream - // timestamp. (crbug.com/132815) + DVLOG(2) << "OnNewMediaSegment(" << source_id << ", " + << start_timestamp.InSecondsF() << ")"; + lock_.AssertAcquired(); + + if (start_time_ == kNoTimestamp()) { + DCHECK(state_ == INITIALIZING || state_ == WAITING_FOR_START_TIME); + // Use the first reported media segment start time as the |start_time_| + // for the demuxer. + start_time_ = start_timestamp; + } + if (audio_ && source_id == source_id_audio_) audio_->OnNewMediaSegment(start_timestamp); if (video_ && source_id == source_id_video_) video_->OnNewMediaSegment(start_timestamp); + + if (state_ != WAITING_FOR_START_TIME) + return; + + if (audio_) { + audio_->SetStartTime(start_time_); + audio_->Seek(start_time_); + } + if (video_) { + video_->SetStartTime(start_time_); + video_->Seek(start_time_); + } + + // The demuxer is now initialized after the |start_timestamp_| was set. + ChangeState_Locked(INITIALIZED); + base::ResetAndReturn(&init_cb_).Run(PIPELINE_OK); } } // namespace media diff --git a/media/filters/chunk_demuxer.h b/media/filters/chunk_demuxer.h index ecd2d77..ced6d97 100644 --- a/media/filters/chunk_demuxer.h +++ b/media/filters/chunk_demuxer.h @@ -87,6 +87,7 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer { enum State { WAITING_FOR_INIT, INITIALIZING, + WAITING_FOR_START_TIME, INITIALIZED, ENDED, PARSE_ERROR, @@ -143,6 +144,8 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer { std::string source_id_audio_; std::string source_id_video_; + base::TimeDelta start_time_; + DISALLOW_COPY_AND_ASSIGN(ChunkDemuxer); }; diff --git a/media/filters/chunk_demuxer_unittest.cc b/media/filters/chunk_demuxer_unittest.cc index cde3417..85e98f6 100644 --- a/media/filters/chunk_demuxer_unittest.cc +++ b/media/filters/chunk_demuxer_unittest.cc @@ -273,14 +273,28 @@ class ChunkDemuxerTest : public testing::Test { return AppendData(source_id, info_tracks.get(), info_tracks_size); } + bool AppendGarbage() { + // Fill up an array with gibberish. + int garbage_cluster_size = 10; + scoped_array<uint8> garbage_cluster(new uint8[garbage_cluster_size]); + for (int i = 0; i < garbage_cluster_size; ++i) + garbage_cluster[i] = i; + return AppendData(garbage_cluster.get(), garbage_cluster_size); + } + void InitDoneCalled(PipelineStatus expected_status, PipelineStatus status) { EXPECT_EQ(status, expected_status); } + bool AppendEmptyCluster(int timecode) { + scoped_ptr<Cluster> empty_cluster = GenerateEmptyCluster(timecode); + return AppendData(empty_cluster->data(), empty_cluster->size()); + } + PipelineStatusCB CreateInitDoneCB(const base::TimeDelta& expected_duration, PipelineStatus expected_status) { - if (expected_status == PIPELINE_OK) + if (expected_duration != kNoTimestamp()) EXPECT_CALL(host_, SetDuration(expected_duration)); return CreateInitDoneCB(expected_status); } @@ -296,9 +310,13 @@ class ChunkDemuxerTest : public testing::Test { PipelineStatus expected_status = (has_audio || has_video) ? PIPELINE_OK : DEMUXER_ERROR_COULD_NOT_OPEN; + base::TimeDelta expected_duration = kNoTimestamp(); + if (expected_status == PIPELINE_OK) + expected_duration = kDefaultDuration(); + EXPECT_CALL(*client_, DemuxerOpened(_)); demuxer_->Initialize( - &host_, CreateInitDoneCB(kDefaultDuration(), expected_status)); + &host_, CreateInitDoneCB(expected_duration, expected_status)); if (AddId(kSourceId, has_audio, has_video) != ChunkDemuxer::kOk) return false; @@ -322,6 +340,18 @@ class ChunkDemuxerTest : public testing::Test { return success; } + bool InitDemuxer_ExpectInitFailure() { + EXPECT_CALL(*client_, DemuxerOpened(_)); + demuxer_->Initialize( + &host_, CreateInitDoneCB( + kDefaultDuration(), DEMUXER_ERROR_COULD_NOT_OPEN)); + + if (AddId(kSourceId, true, true) != ChunkDemuxer::kOk) + return false; + + return AppendInitSegment(true, true, false); + } + void ShutdownDemuxer() { if (demuxer_) { EXPECT_CALL(*client_, DemuxerClosed()); @@ -451,8 +481,9 @@ class ChunkDemuxerTest : public testing::Test { } } - void GenerateExpectedReads(int timecode, int block_count, - DemuxerStream* stream, int block_duration) { + void GenerateSingleStreamExpectedReads( + int timecode, int block_count, DemuxerStream* stream, + int block_duration) { CHECK_GT(block_count, 0); int stream_timecode = timecode; @@ -624,30 +655,29 @@ TEST_F(ChunkDemuxerTest, TestInit) { } } -// Makes sure that Seek() reports an error if Shutdown() +// Makes sure that pipeline 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_F(ChunkDemuxerTest, TestShutdownBeforeFirstCluster) { + ASSERT_TRUE(InitDemuxer_ExpectInitFailure()); } // Test that Seek() completes successfully when the first cluster // arrives. TEST_F(ChunkDemuxerTest, TestAppendDataAfterSeek) { ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> first_cluster(kDefaultFirstCluster()); + ASSERT_TRUE(AppendData(first_cluster->data(), first_cluster->size())); InSequence s; EXPECT_CALL(*this, Checkpoint(1)); - demuxer_->Seek(base::TimeDelta::FromSeconds(0), + demuxer_->Seek(base::TimeDelta::FromMilliseconds(46), NewExpectedStatusCB(PIPELINE_OK)); EXPECT_CALL(*this, Checkpoint(2)); - scoped_ptr<Cluster> cluster(kDefaultFirstCluster()); + scoped_ptr<Cluster> cluster(kDefaultSecondCluster()); Checkpoint(1); @@ -656,6 +686,24 @@ TEST_F(ChunkDemuxerTest, TestAppendDataAfterSeek) { Checkpoint(2); } +// Test that parsing errors are handled for clusters appended before init +// completes. +TEST_F(ChunkDemuxerTest, TestErrorWhileParsingClusterBeforeInitCompletes) { + ASSERT_TRUE(InitDemuxer_ExpectInitFailure()); + + ASSERT_TRUE(AppendGarbage()); +} + +// Test that parsing errors are handled for clusters appended after init. +TEST_F(ChunkDemuxerTest, TestErrorWhileParsingClusterAfterInit) { + ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> first_cluster(kDefaultFirstCluster()); + ASSERT_TRUE(AppendData(first_cluster->data(), first_cluster->size())); + + EXPECT_CALL(host_, OnDemuxerError(PIPELINE_ERROR_DECODE)); + ASSERT_TRUE(AppendGarbage()); +} + // Test the case where a Seek() is requested while the parser // is in the middle of cluster. This is to verify that the parser // does not reset itself on a seek. @@ -715,6 +763,9 @@ TEST_F(ChunkDemuxerTest, TestAppendDataBeforeInit) { TEST_F(ChunkDemuxerTest, TestRead) { ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> cluster(kDefaultFirstCluster()); + ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); + scoped_refptr<DemuxerStream> audio = demuxer_->GetStream(DemuxerStream::AUDIO); scoped_refptr<DemuxerStream> video = @@ -725,24 +776,20 @@ TEST_F(ChunkDemuxerTest, TestRead) { 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(kDefaultFirstCluster()); - - 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(kDefaultFirstCluster()); + ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); 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 @@ -760,6 +807,8 @@ TEST_F(ChunkDemuxerTest, TestOutOfOrderClusters) { TEST_F(ChunkDemuxerTest, TestNonMonotonicButAboveClusterTimecode) { ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> first_cluster(kDefaultFirstCluster()); + ASSERT_TRUE(AppendData(first_cluster->data(), first_cluster->size())); ClusterBuilder cb; @@ -783,6 +832,8 @@ TEST_F(ChunkDemuxerTest, TestNonMonotonicButAboveClusterTimecode) { TEST_F(ChunkDemuxerTest, TestBackwardsAndBeforeClusterTimecode) { ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> first_cluster(kDefaultFirstCluster()); + ASSERT_TRUE(AppendData(first_cluster->data(), first_cluster->size())); ClusterBuilder cb; @@ -807,6 +858,8 @@ TEST_F(ChunkDemuxerTest, TestBackwardsAndBeforeClusterTimecode) { TEST_F(ChunkDemuxerTest, TestPerStreamMonotonicallyIncreasingTimestamps) { ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> first_cluster(kDefaultFirstCluster()); + ASSERT_TRUE(AppendData(first_cluster->data(), first_cluster->size())); ClusterBuilder cb; @@ -929,6 +982,9 @@ class EndOfStreamHelper { TEST_F(ChunkDemuxerTest, TestEndOfStreamWithPendingReads) { ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> cluster(GenerateCluster(0, 2)); + ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); + scoped_refptr<DemuxerStream> audio = demuxer_->GetStream(DemuxerStream::AUDIO); scoped_refptr<DemuxerStream> video = @@ -950,10 +1006,6 @@ TEST_F(ChunkDemuxerTest, TestEndOfStreamWithPendingReads) { 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); @@ -970,6 +1022,9 @@ TEST_F(ChunkDemuxerTest, TestEndOfStreamWithPendingReads) { TEST_F(ChunkDemuxerTest, TestReadsAfterEndOfStream) { ASSERT_TRUE(InitDemuxer(true, true, false)); + scoped_ptr<Cluster> cluster(GenerateCluster(0, 2)); + ASSERT_TRUE(AppendData(cluster->data(), cluster->size())); + scoped_refptr<DemuxerStream> audio = demuxer_->GetStream(DemuxerStream::AUDIO); scoped_refptr<DemuxerStream> video = @@ -991,10 +1046,6 @@ TEST_F(ChunkDemuxerTest, TestReadsAfterEndOfStream) { 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); @@ -1130,6 +1181,7 @@ TEST_F(ChunkDemuxerTest, TestWebMFile_AltRefFrames) { // Verify that we output buffers before the entire cluster has been parsed. TEST_F(ChunkDemuxerTest, TestIncrementalClusterParsing) { ASSERT_TRUE(InitDemuxer(true, true, false)); + ASSERT_TRUE(AppendEmptyCluster(0)); scoped_ptr<Cluster> cluster(GenerateCluster(0, 6)); scoped_refptr<DemuxerStream> audio = @@ -1192,13 +1244,11 @@ TEST_F(ChunkDemuxerTest, TestIncrementalClusterParsing) { 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)); + &host_, CreateInitDoneCB( + kDefaultDuration(), DEMUXER_ERROR_COULD_NOT_OPEN)); ASSERT_EQ(AddId(), ChunkDemuxer::kOk); @@ -1211,7 +1261,7 @@ TEST_F(ChunkDemuxerTest, TestParseErrorDuringInit) { TEST_F(ChunkDemuxerTest, TestAVHeadersWithAudioOnlyType) { EXPECT_CALL(*client_, DemuxerOpened(_)); demuxer_->Initialize( - &host_, CreateInitDoneCB(kDefaultDuration(), + &host_, CreateInitDoneCB(kNoTimestamp(), DEMUXER_ERROR_COULD_NOT_OPEN)); std::vector<std::string> codecs(1); @@ -1225,7 +1275,7 @@ TEST_F(ChunkDemuxerTest, TestAVHeadersWithAudioOnlyType) { TEST_F(ChunkDemuxerTest, TestAVHeadersWithVideoOnlyType) { EXPECT_CALL(*client_, DemuxerOpened(_)); demuxer_->Initialize( - &host_, CreateInitDoneCB(kDefaultDuration(), + &host_, CreateInitDoneCB(kNoTimestamp(), DEMUXER_ERROR_COULD_NOT_OPEN)); std::vector<std::string> codecs(1); @@ -1274,9 +1324,9 @@ TEST_F(ChunkDemuxerTest, TestAddSeparateSourcesForAudioAndVideo) { // Append audio and video data into separate source ids. ASSERT_TRUE(AppendData(audio_id, cluster_a->data(), cluster_a->size())); - GenerateExpectedReads(0, 4, audio, kAudioBlockDuration); + GenerateSingleStreamExpectedReads(0, 4, audio, kAudioBlockDuration); ASSERT_TRUE(AppendData(video_id, cluster_v->data(), cluster_v->size())); - GenerateExpectedReads(0, 4, video, kVideoBlockDuration); + GenerateSingleStreamExpectedReads(0, 4, video, kVideoBlockDuration); } TEST_F(ChunkDemuxerTest, TestAddIdFailures) { @@ -1317,7 +1367,7 @@ TEST_F(ChunkDemuxerTest, TestRemoveId) { // Read() from audio should return normal buffers. scoped_refptr<DemuxerStream> audio = demuxer_->GetStream(DemuxerStream::AUDIO); - GenerateExpectedReads(0, 4, audio, kAudioBlockDuration); + GenerateSingleStreamExpectedReads(0, 4, audio, kAudioBlockDuration); // Remove the audio id. demuxer_->RemoveId(audio_id); @@ -1331,7 +1381,7 @@ TEST_F(ChunkDemuxerTest, TestRemoveId) { // Read() from video should still return normal buffers. scoped_refptr<DemuxerStream> video = demuxer_->GetStream(DemuxerStream::VIDEO); - GenerateExpectedReads(0, 4, video, kVideoBlockDuration); + GenerateSingleStreamExpectedReads(0, 4, video, kVideoBlockDuration); } // Test that Seek() successfully seeks to all source IDs. @@ -1571,14 +1621,12 @@ TEST_F(ChunkDemuxerTest, TestDifferentStreamTimecodes) { scoped_refptr<DemuxerStream> video = demuxer_->GetStream(DemuxerStream::VIDEO); - demuxer_->Seek(base::TimeDelta::FromSeconds(0), - NewExpectedStatusCB(PIPELINE_OK)); - // Create a cluster where the video timecode begins 25ms after the audio. - scoped_ptr<Cluster> start_cluster( - GenerateCluster(0, 25, 8)); - + scoped_ptr<Cluster> start_cluster(GenerateCluster(0, 25, 8)); ASSERT_TRUE(AppendData(start_cluster->data(), start_cluster->size())); + + demuxer_->Seek(base::TimeDelta::FromSeconds(0), + NewExpectedStatusCB(PIPELINE_OK)); GenerateExpectedReads(0, 25, 8, audio, video); // Seek to 5 seconds. @@ -1593,12 +1641,39 @@ TEST_F(ChunkDemuxerTest, TestDifferentStreamTimecodes) { GenerateExpectedReads(5025, 5000, 8, audio, video); } +TEST_F(ChunkDemuxerTest, TestDifferentStreamTimecodesSeparateSources) { + std::string audio_id = "audio1"; + std::string video_id = "video1"; + ASSERT_TRUE(InitDemuxerAudioAndVideoSources(audio_id, video_id)); + + scoped_refptr<DemuxerStream> audio = + demuxer_->GetStream(DemuxerStream::AUDIO); + scoped_refptr<DemuxerStream> video = + demuxer_->GetStream(DemuxerStream::VIDEO); + + // Generate two streams where the video stream starts 5ms after the audio + // stream and append them. + scoped_ptr<Cluster> cluster_v( + GenerateSingleStreamCluster(30, 4 * kVideoBlockDuration + 30, + kVideoTrackNum, kVideoBlockDuration)); + scoped_ptr<Cluster> cluster_a( + GenerateSingleStreamCluster(25, 4 * kAudioBlockDuration + 25, + kAudioTrackNum, kAudioBlockDuration)); + ASSERT_TRUE(AppendData(audio_id, cluster_a->data(), cluster_a->size())); + ASSERT_TRUE(AppendData(video_id, cluster_v->data(), cluster_v->size())); + + // Both streams should be able to fulfill a seek to 25. + demuxer_->Seek(base::TimeDelta::FromMilliseconds(25), + NewExpectedStatusCB(PIPELINE_OK)); + GenerateSingleStreamExpectedReads(25, 4, audio, kAudioBlockDuration); + GenerateSingleStreamExpectedReads(30, 4, video, kVideoBlockDuration); +} + TEST_F(ChunkDemuxerTest, TestClusterWithNoBuffers) { ASSERT_TRUE(InitDemuxer(true, true, false)); - // Create an empty cluster beginning at 0. - scoped_ptr<Cluster> empty_cluster(GenerateEmptyCluster(0)); - ASSERT_TRUE(AppendData(empty_cluster->data(), empty_cluster->size())); + // Generate and append an empty cluster beginning at 0. + ASSERT_TRUE(AppendEmptyCluster(0)); // Sanity check that data can be appended after this cluster correctly. scoped_ptr<Cluster> media_data(GenerateCluster(0, 2)); diff --git a/media/filters/source_buffer_stream.cc b/media/filters/source_buffer_stream.cc index c80504f..b72aeb5 100644 --- a/media/filters/source_buffer_stream.cc +++ b/media/filters/source_buffer_stream.cc @@ -59,6 +59,9 @@ class SourceBufferRange { // |timestamp|. void SeekAheadPast(base::TimeDelta timestamp); + // Seeks to the beginning of the range. + void SeekToStart(); + // Finds the next keyframe from |buffers_| after |timestamp|, and creates and // returns a new SourceBufferRange with the buffers from that keyframe onward. // The buffers in the new SourceBufferRange are moved out of this range. If @@ -226,7 +229,8 @@ static int kDefaultBufferDurationInMs = 125; namespace media { SourceBufferStream::SourceBufferStream(const AudioDecoderConfig& audio_config) - : seek_pending_(false), + : stream_start_time_(kNoTimestamp()), + seek_pending_(false), seek_buffer_timestamp_(kNoTimestamp()), selected_range_(NULL), end_of_stream_(false), @@ -239,7 +243,8 @@ SourceBufferStream::SourceBufferStream(const AudioDecoderConfig& audio_config) } SourceBufferStream::SourceBufferStream(const VideoDecoderConfig& video_config) - : seek_pending_(false), + : stream_start_time_(kNoTimestamp()), + seek_pending_(false), seek_buffer_timestamp_(kNoTimestamp()), selected_range_(NULL), end_of_stream_(false), @@ -269,6 +274,15 @@ void SourceBufferStream::OnNewMediaSegment( last_buffer_timestamp_ = kNoTimestamp(); } +void SourceBufferStream::SetStartTime(base::TimeDelta stream_start_time) { + DCHECK(stream_start_time_ == kNoTimestamp()); + DCHECK(stream_start_time != kNoTimestamp()); + stream_start_time_ = stream_start_time; + + DCHECK(ranges_.empty() || + ranges_.front()->GetStartTimestamp() >= stream_start_time_); +} + bool SourceBufferStream::Append( const SourceBufferStream::BufferQueue& buffers) { DCHECK(!buffers.empty()); @@ -286,10 +300,15 @@ bool SourceBufferStream::Append( return false; } + if (stream_start_time_ != kNoTimestamp() && + media_segment_start_time_ < stream_start_time_) { + DVLOG(1) << "Cannot append a media segment before the start of stream."; + return false; + } + UpdateMaxInterbufferDistance(buffers); - // Save a snapshot of the |selected_range_| state before range modifications - // are made. + // Save a snapshot of stream state before range modifications are made. base::TimeDelta next_buffer_timestamp = GetNextBufferTimestamp(); base::TimeDelta end_buffer_timestamp = GetEndBufferTimestamp(); @@ -322,14 +341,6 @@ bool SourceBufferStream::Append( range_for_new_buffers, &deleted_next_buffer, &deleted_buffers); MergeWithAdjacentRangeIfNecessary(range_for_new_buffers); - // If these were the first buffers appended to the stream, seek to the - // beginning of the range. - // TODO(vrk): This should be done by ChunkDemuxer. (crbug.com/132815) - if (!seek_pending_ && !selected_range_) { - SetSelectedRange(*range_for_new_buffers); - selected_range_->Seek(buffers.front()->GetDecodeTimestamp()); - } - // Seek to try to fulfill a previous call to Seek(). if (seek_pending_) { DCHECK(!selected_range_); @@ -360,8 +371,14 @@ bool SourceBufferStream::Append( return true; } +bool SourceBufferStream::IsBeforeFirstRange(base::TimeDelta timestamp) const { + if (ranges_.empty()) + return false; + return timestamp < ranges_.front()->GetStartTimestamp(); +} + bool SourceBufferStream::IsMonotonicallyIncreasing( - const BufferQueue& buffers) { + const BufferQueue& buffers) const { DCHECK(!buffers.empty()); base::TimeDelta prev_timestamp = last_buffer_timestamp_; for (BufferQueue::const_iterator itr = buffers.begin(); @@ -605,14 +622,24 @@ void SourceBufferStream::MergeWithAdjacentRangeIfNecessary( } void SourceBufferStream::Seek(base::TimeDelta timestamp) { + DCHECK(stream_start_time_ != kNoTimestamp()); + DCHECK(timestamp >= stream_start_time_); SetSelectedRange(NULL); track_buffer_.clear(); + if (IsBeforeFirstRange(timestamp)) { + SetSelectedRange(ranges_.front()); + ranges_.front()->SeekToStart(); + seek_pending_ = false; + end_of_stream_ = false; + return; + } + seek_buffer_timestamp_ = timestamp; seek_pending_ = true; RangeList::iterator itr = ranges_.end(); - for (itr = ranges_.begin(); itr != ranges_.end(); itr++) { + for (itr = ranges_.begin(); itr != ranges_.end(); ++itr) { if ((*itr)->CanSeekTo(timestamp)) break; } @@ -802,6 +829,11 @@ void SourceBufferRange::SeekAhead(base::TimeDelta timestamp, DCHECK_LT(next_buffer_index_, static_cast<int>(buffers_.size())); } +void SourceBufferRange::SeekToStart() { + DCHECK(!buffers_.empty()); + next_buffer_index_ = 0; +} + SourceBufferRange* SourceBufferRange::SplitRange(base::TimeDelta timestamp) { // Find the first keyframe after |timestamp|, not including |timestamp|. KeyframeMap::iterator new_beginning_keyframe = diff --git a/media/filters/source_buffer_stream.h b/media/filters/source_buffer_stream.h index e1b56e5..a0151ea 100644 --- a/media/filters/source_buffer_stream.h +++ b/media/filters/source_buffer_stream.h @@ -37,6 +37,9 @@ class MEDIA_EXPORT SourceBufferStream { // starting at |media_segment_start_time|. void OnNewMediaSegment(base::TimeDelta media_segment_start_time); + // Sets the start time of the stream. + void SetStartTime(base::TimeDelta stream_start_time); + // Add the |buffers| to the SourceBufferStream. Buffers within the queue are // expected to be in order, but multiple calls to Append() may add buffers out // of order or overlapping. Assumes all buffers within |buffers| are in @@ -159,9 +162,13 @@ class MEDIA_EXPORT SourceBufferStream { // for the previous |selected_range_|. void SetSelectedRange(SourceBufferRange* range); + // Returns true if |timestamp| occurs before the start timestamp of the first + // range in |ranges_|, false otherwise or if |ranges_| is empty. + bool IsBeforeFirstRange(base::TimeDelta timestamp) const; + // Returns true if the timestamps of |buffers| are monotonically increasing // since the previous append to the media segment, false otherwise. - bool IsMonotonicallyIncreasing(const BufferQueue& buffers); + bool IsMonotonicallyIncreasing(const BufferQueue& buffers) const; // Returns true if |selected_range_| is the only range in |ranges_| that // HasNextBufferPosition(). @@ -176,6 +183,9 @@ class MEDIA_EXPORT SourceBufferStream { AudioDecoderConfig audio_config_; VideoDecoderConfig video_config_; + // The starting time of the stream. + base::TimeDelta stream_start_time_; + // True if more data needs to be appended before the Seek() can complete, // false if no Seek() has been requested or the Seek() is completed. bool seek_pending_; diff --git a/media/filters/source_buffer_stream_unittest.cc b/media/filters/source_buffer_stream_unittest.cc index dfecbf3..95a81ff 100644 --- a/media/filters/source_buffer_stream_unittest.cc +++ b/media/filters/source_buffer_stream_unittest.cc @@ -21,6 +21,9 @@ class SourceBufferStreamTest : public testing::Test { SourceBufferStreamTest() { stream_.reset(new SourceBufferStream(config_)); SetStreamInfo(kDefaultFramesPerSecond, kDefaultKeyframesPerSecond); + + // Set start time to the beginning of the stream. + stream_->SetStartTime(base::TimeDelta()); } void SetStreamInfo(int frames_per_second, int keyframes_per_second) { @@ -1155,6 +1158,7 @@ TEST_F(SourceBufferStreamTest, Seek_StartOfSegment) { // Append 5 buffers at position (5 + |bump|) through 9, where the media // segment begins at position 5. + Seek(5); NewSegmentAppend_OffsetFirstBuffer(5, 5, bump); scoped_refptr<StreamParserBuffer> buffer; @@ -1180,6 +1184,17 @@ TEST_F(SourceBufferStreamTest, Seek_StartOfSegment) { CheckExpectedBuffers(16, 19); } +TEST_F(SourceBufferStreamTest, Seek_BeforeStartOfSegment) { + // Append 10 buffers at positions 5 through 14. + NewSegmentAppend(5, 10); + + // Seek to a time before the first buffer in the range. + Seek(0); + + // Should return buffers from the beginning of the range. + CheckExpectedBuffers(5, 14); +} + TEST_F(SourceBufferStreamTest, OldSeekPoint_CompleteOverlap) { // Append 5 buffers at positions 0 through 4. NewSegmentAppend(0, 4); @@ -1200,18 +1215,21 @@ TEST_F(SourceBufferStreamTest, OldSeekPoint_CompleteOverlap) { } TEST_F(SourceBufferStreamTest, OldSeekPoint_CompleteOverlap_Pending) { - // Append 5 buffers at positions 10 through 14 and seek to beginning of the + // Append 2 buffers at positions 0 through 1. + NewSegmentAppend(0, 2); + + // Append 5 buffers at positions 15 through 19 and seek to beginning of the // range. - NewSegmentAppend(10, 5); - Seek(10); + NewSegmentAppend(15, 5); + Seek(15); - // Now seek to the beginning of the stream. - Seek(0); + // Now seek position 5. + Seek(5); // Completely overlap the old seek point. - NewSegmentAppend(5, 15); + NewSegmentAppend(10, 15); - // The seek at time 0 should still be pending. + // The seek at position 5 should still be pending. CheckNoNextBuffer(); } @@ -1235,18 +1253,21 @@ TEST_F(SourceBufferStreamTest, OldSeekPoint_MiddleOverlap) { } TEST_F(SourceBufferStreamTest, OldSeekPoint_MiddleOverlap_Pending) { - // Append 15 buffers at positions 5 through 19 and seek to position 15. - NewSegmentAppend(5, 15); - Seek(15); + // Append 2 buffers at positions 0 through 1. + NewSegmentAppend(0, 2); - // Now seek to the beginning of the stream. - Seek(0); + // Append 15 buffers at positions 10 through 24 and seek to position 20. + NewSegmentAppend(10, 15); + Seek(20); - // Overlap the middle of the range such that there are now two ranges. - NewSegmentAppend(10, 3); - CheckExpectedRanges("{ [5,12) [15,19) }"); + // Now seek to position 5. + Seek(5); - // The seek at time 0 should still be pending. + // Overlap the middle of the range such that it is now split into two ranges. + NewSegmentAppend(15, 3); + CheckExpectedRanges("{ [0,1) [10,17) [20,24) }"); + + // The seek at position 5 should still be pending. CheckNoNextBuffer(); } @@ -1269,15 +1290,18 @@ TEST_F(SourceBufferStreamTest, OldSeekPoint_StartOverlap) { } TEST_F(SourceBufferStreamTest, OldSeekPoint_StartOverlap_Pending) { - // Append 15 buffers at positions 5 through 19 and seek to position 15. - NewSegmentAppend(5, 15); - Seek(15); + // Append 2 buffers at positions 0 through 1. + NewSegmentAppend(0, 2); - // Now seek to the beginning of the stream. - Seek(0); + // Append 15 buffers at positions 10 through 24 and seek to position 20. + NewSegmentAppend(10, 15); + Seek(20); + + // Now seek to position 5. + Seek(5); // Start overlap the old seek point. - NewSegmentAppend(10, 10); + NewSegmentAppend(15, 10); // The seek at time 0 should still be pending. CheckNoNextBuffer(); @@ -1302,15 +1326,18 @@ TEST_F(SourceBufferStreamTest, OldSeekPoint_EndOverlap) { } TEST_F(SourceBufferStreamTest, OldSeekPoint_EndOverlap_Pending) { - // Append 15 buffers at positions 10 through 24 and seek to start of range. - NewSegmentAppend(10, 15); - Seek(10); + // Append 2 buffers at positions 0 through 1. + NewSegmentAppend(0, 2); - // Now seek to the beginning of the stream. - Seek(0); + // Append 15 buffers at positions 15 through 29 and seek to start of range. + NewSegmentAppend(15, 15); + Seek(15); + + // Now seek to position 5 + Seek(5); // End overlap the old seek point. - NewSegmentAppend(5, 10); + NewSegmentAppend(10, 10); // The seek at time 0 should still be pending. CheckNoNextBuffer(); @@ -1361,6 +1388,7 @@ TEST_F(SourceBufferStreamTest, GetNextBuffer_ExhaustThenAppend) { TEST_F(SourceBufferStreamTest, GetNextBuffer_ExhaustThenStartOverlap) { // Append 10 buffers at positions 0 through 9 and exhaust the buffers. NewSegmentAppend(0, 10, &kDataA); + Seek(0); CheckExpectedBuffers(0, 9, &kDataA); // Next buffer is at position 10, so should not be able to fulfill request. @@ -1389,6 +1417,7 @@ TEST_F(SourceBufferStreamTest, GetNextBuffer_ExhaustThenStartOverlap) { TEST_F(SourceBufferStreamTest, GetNextBuffer_ExhaustThenCompleteOverlap) { // Append 5 buffers at positions 10 through 14 and exhaust the buffers. NewSegmentAppend(10, 5, &kDataA); + Seek(10); CheckExpectedBuffers(10, 14, &kDataA); // Next buffer is at position 15, so should not be able to fulfill request. @@ -1416,6 +1445,7 @@ TEST_F(SourceBufferStreamTest, GetNextBuffer_ExhaustThenCompleteOverlap) { TEST_F(SourceBufferStreamTest, GetNextBuffer_ExhaustThenEndOverlap) { // Append 5 buffers at positions 10 through 14 and exhaust the buffers. NewSegmentAppend(10, 5, &kDataA); + Seek(10); CheckExpectedBuffers(10, 14, &kDataA); CheckExpectedRanges("{ [10,14) }"); @@ -1467,17 +1497,10 @@ TEST_F(SourceBufferStreamTest, GetNextBuffer_Overlap_Selected_Complete) { CheckExpectedBuffers(10, 14, &kDataB); } -TEST_F(SourceBufferStreamTest, GetNextBuffer_NoSeek) { - // Append 5 buffers at position 5. - NewSegmentAppend(5, 5); - - // Should receive buffers from the start without needing to seek. - CheckExpectedBuffers(5, 5); -} - TEST_F(SourceBufferStreamTest, PresentationTimestampIndependence) { // Append 20 buffers at position 0. NewSegmentAppend(0, 20); + Seek(0); int last_keyframe_idx = -1; base::TimeDelta last_keyframe_presentation_timestamp; diff --git a/webkit/media/webmediaplayer_impl.cc b/webkit/media/webmediaplayer_impl.cc index 41bcf11..0281177 100644 --- a/webkit/media/webmediaplayer_impl.cc +++ b/webkit/media/webmediaplayer_impl.cc @@ -776,7 +776,9 @@ void WebMediaPlayerImpl::Repaint() { void WebMediaPlayerImpl::OnPipelineInitialize(PipelineStatus status) { DCHECK_EQ(main_loop_, MessageLoop::current()); if (status != media::PIPELINE_OK) { - OnPipelineError(status); + // Any error that occurs before the pipeline can initialize should be + // considered a format error. + SetNetworkState(WebMediaPlayer::NetworkStateFormatError); // Repaint to trigger UI update. Repaint(); return; @@ -840,6 +842,9 @@ void WebMediaPlayerImpl::OnPipelineError(PipelineStatus error) { SetNetworkState(WebMediaPlayer::NetworkStateNetworkError); break; + // TODO(vrk): Because OnPipelineInitialize() directly reports the + // NetworkStateFormatError instead of calling OnPipelineError(), I believe + // this block can be deleted. Should look into it! (crbug.com/126070) case media::PIPELINE_ERROR_INITIALIZATION_FAILED: case media::PIPELINE_ERROR_REQUIRED_FILTER_MISSING: case media::PIPELINE_ERROR_COULD_NOT_RENDER: |