diff options
author | scherkus@chromium.org <scherkus@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-28 20:36:37 +0000 |
---|---|---|
committer | scherkus@chromium.org <scherkus@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-28 20:36:37 +0000 |
commit | 46564213965397d01fb87975f3bddd597985fcbf (patch) | |
tree | 346f9cf4005fcb9865196622f13462065f52a83f /media | |
parent | c2972193bd0d0c2354d0862444b7ce4662d52d77 (diff) | |
download | chromium_src-46564213965397d01fb87975f3bddd597985fcbf.zip chromium_src-46564213965397d01fb87975f3bddd597985fcbf.tar.gz chromium_src-46564213965397d01fb87975f3bddd597985fcbf.tar.bz2 |
Implemented a proper clock for audio/video synchronization.
More or less a change to pull out time management from PipelineImpl into a new class ClockImpl. Biggest difference is ClockImpl will use the system clock + linear interpolation to provide a more "precise" representation of the current playback position.
BUG=16508
TEST=a/v sync should remain the same, currentTime should report more precise values
Review URL: http://codereview.chromium.org/159517
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@21882 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media')
-rw-r--r-- | media/base/clock.h | 47 | ||||
-rw-r--r-- | media/base/clock_impl.cc | 65 | ||||
-rw-r--r-- | media/base/clock_impl.h | 60 | ||||
-rw-r--r-- | media/base/clock_impl_unittest.cc | 217 | ||||
-rw-r--r-- | media/base/pipeline_impl.cc | 23 | ||||
-rw-r--r-- | media/base/pipeline_impl.h | 7 | ||||
-rw-r--r-- | media/filters/video_renderer_base.cc | 40 | ||||
-rw-r--r-- | media/media.gyp | 4 |
8 files changed, 437 insertions, 26 deletions
diff --git a/media/base/clock.h b/media/base/clock.h new file mode 100644 index 0000000..93af4a9 --- /dev/null +++ b/media/base/clock.h @@ -0,0 +1,47 @@ +// Copyright (c) 2009 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. +// +// A clock represent a single source of time to allow audio and video streams +// to synchronize with each other. Clocks essentially track the media time with +// respect to some other source of time, whether that may be the system clock, +// audio hardware or some other OS-level API. +// +// Clocks start off paused with a playback rate of 1.0f and a media time of 0. +// +// TODO(scherkus): Clocks will some day be responsible for executing callbacks +// given a media time. This will be used primarily by video renderers. For now +// we'll keep using a poll-and-sleep solution. + +#ifndef MEDIA_BASE_CLOCK_H_ +#define MEDIA_BASE_CLOCK_H_ + +#include "base/time.h" + +namespace media { + +class Clock { + public: + // Starts the clock and returns the current media time, which will increase + // with respect to the current playback rate. + virtual base::TimeDelta Play() = 0; + + // Stops the clock and returns the current media time, which will remain + // constant until Play() is called. + virtual base::TimeDelta Pause() = 0; + + // Sets a new playback rate. The rate at which the media time will increase + // will now change. + virtual void SetPlaybackRate(float playback_rate) = 0; + + // Forcefully sets the media time to the given time. This should only be used + // where a discontinuity in the media is found (i.e., seeking). + virtual void SetTime(const base::TimeDelta& time) = 0; + + // Returns the current elapsed media time. + virtual base::TimeDelta Elapsed() const = 0; +}; + +} // namespace media + +#endif // MEDIA_BASE_CLOCK_H_ diff --git a/media/base/clock_impl.cc b/media/base/clock_impl.cc new file mode 100644 index 0000000..cc46568 --- /dev/null +++ b/media/base/clock_impl.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2009 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/logging.h" +#include "media/base/clock_impl.h" + +namespace media { + +ClockImpl::ClockImpl(TimeProvider* time_provider) + : time_provider_(time_provider), + playing_(false), + playback_rate_(1.0f) { +} + +ClockImpl::~ClockImpl() { +} + +base::TimeDelta ClockImpl::Play() { + DCHECK(!playing_); + reference_ = time_provider_(); + playing_ = true; + return media_time_; +} + +base::TimeDelta ClockImpl::Pause() { + DCHECK(playing_); + // Save our new accumulated amount of media time. + media_time_ = Elapsed(); + playing_ = false; + return media_time_; +} + +void ClockImpl::SetTime(const base::TimeDelta& time) { + if (playing_) { + reference_ = time_provider_(); + } + media_time_ = time; +} + +void ClockImpl::SetPlaybackRate(float playback_rate) { + if (playing_) { + base::Time time = time_provider_(); + media_time_ = ElapsedViaProvidedTime(time); + reference_ = time; + } + playback_rate_ = playback_rate; +} + +base::TimeDelta ClockImpl::Elapsed() const { + if (!playing_) { + return media_time_; + } + return ElapsedViaProvidedTime(time_provider_()); +} + +base::TimeDelta ClockImpl::ElapsedViaProvidedTime( + const base::Time& time) const { + // TODO(scherkus): floating point badness scaling time by playback rate. + int64 now_us = (time - reference_).InMicroseconds(); + now_us = static_cast<int64>(now_us * playback_rate_); + return media_time_ + base::TimeDelta::FromMicroseconds(now_us); +} + +} // namespace media diff --git a/media/base/clock_impl.h b/media/base/clock_impl.h new file mode 100644 index 0000000..689d010 --- /dev/null +++ b/media/base/clock_impl.h @@ -0,0 +1,60 @@ +// Copyright (c) 2009 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. +// +// An implementation of Clock based on the system clock. ClockImpl uses linear +// interpolation to calculate the current media time since the last time +// SetTime() was called. +// +// ClockImpl is not thread-safe and must be externally locked. + +#ifndef MEDIA_BASE_CLOCK_IMPL_H_ +#define MEDIA_BASE_CLOCK_IMPL_H_ + +#include "media/base/clock.h" + +namespace media { + +// Type for a static function pointer that acts as a time source. +typedef base::Time(TimeProvider)(); + +class ClockImpl : public Clock { + public: + ClockImpl(TimeProvider* time_provider); + virtual ~ClockImpl(); + + // Clock implementation. + virtual base::TimeDelta Play(); + virtual base::TimeDelta Pause(); + virtual void SetPlaybackRate(float playback_rate); + virtual void SetTime(const base::TimeDelta& time); + virtual base::TimeDelta Elapsed() const; + + private: + // Returns the current media time treating the given time as the latest + // value as returned by |time_provider_|. + base::TimeDelta ElapsedViaProvidedTime(const base::Time& time) const; + + // Function returning current time in base::Time units. + TimeProvider* time_provider_; + + // Whether the clock is running. + bool playing_; + + // The system clock time when this clock last starting playing or had its + // time set via SetTime(). + base::Time reference_; + + // Current accumulated amount of media time. The remaining portion must be + // calculated by comparing the system time to the reference time. + base::TimeDelta media_time_; + + // Current playback rate. + float playback_rate_; + + DISALLOW_COPY_AND_ASSIGN(ClockImpl); +}; + +} // namespace media + +#endif // MEDIA_BASE_CLOCK_IMPL_H_ diff --git a/media/base/clock_impl_unittest.cc b/media/base/clock_impl_unittest.cc new file mode 100644 index 0000000..c23c18ec3 --- /dev/null +++ b/media/base/clock_impl_unittest.cc @@ -0,0 +1,217 @@ +// Copyright (c) 2009 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/logging.h" +#include "media/base/clock_impl.h" +#include "testing/gmock/include/gmock/gmock.h" + +using ::testing::DefaultValue; +using ::testing::InSequence; +using ::testing::Return; +using ::testing::StrictMock; + +namespace { + +// Provide a stream output operator so we can use EXPECT_EQ(...) with TimeDelta. +// +// TODO(scherkus): move this into the testing package. +std::ostream& operator<<(std::ostream& stream, const base::TimeDelta& time) { + return (stream << time.ToInternalValue()); +} + +} // namespace + +namespace media { + +class MockTimeProvider { + public: + MockTimeProvider() { + DCHECK(!instance_) << "Only one instance of MockTimeProvider can exist"; + DCHECK(!DefaultValue<base::Time>::IsSet()); + instance_ = this; + DefaultValue<base::Time>::Set(base::Time::FromInternalValue(0)); + } + + ~MockTimeProvider() { + instance_ = NULL; + DefaultValue<base::Time>::Clear(); + } + + MOCK_METHOD0(Now, base::Time()); + + static base::Time StaticNow() { + return instance_->Now(); + } + + private: + static MockTimeProvider* instance_; + DISALLOW_COPY_AND_ASSIGN(MockTimeProvider); +}; + +MockTimeProvider* MockTimeProvider::instance_ = NULL; + +TEST(ClockTest, Created) { + StrictMock<MockTimeProvider> mock_time; + const base::TimeDelta kExpected = base::TimeDelta::FromSeconds(0); + + ClockImpl clock(&MockTimeProvider::StaticNow); + EXPECT_EQ(kExpected, clock.Elapsed()); +} + +TEST(ClockTest, Play_NormalSpeed) { + InSequence s; + StrictMock<MockTimeProvider> mock_time; + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(4))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(6))); + const base::TimeDelta kZero; + const base::TimeDelta kExpected = base::TimeDelta::FromSeconds(2); + + ClockImpl clock(&MockTimeProvider::StaticNow); + EXPECT_EQ(kZero, clock.Play()); + EXPECT_EQ(kExpected, clock.Elapsed()); +} + +TEST(ClockTest, Play_DoubleSpeed) { + InSequence s; + StrictMock<MockTimeProvider> mock_time; + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(4))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(9))); + const base::TimeDelta kZero; + const base::TimeDelta kExpected = base::TimeDelta::FromSeconds(10); + + ClockImpl clock(&MockTimeProvider::StaticNow); + clock.SetPlaybackRate(2.0f); + EXPECT_EQ(kZero, clock.Play()); + EXPECT_EQ(kExpected, clock.Elapsed()); +} + +TEST(ClockTest, Play_HalfSpeed) { + InSequence s; + StrictMock<MockTimeProvider> mock_time; + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(4))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(8))); + const base::TimeDelta kZero; + const base::TimeDelta kExpected = base::TimeDelta::FromSeconds(2); + + ClockImpl clock(&MockTimeProvider::StaticNow); + clock.SetPlaybackRate(0.5f); + EXPECT_EQ(kZero, clock.Play()); + EXPECT_EQ(kExpected, clock.Elapsed()); +} + +TEST(ClockTest, Play_ZeroSpeed) { + // We'll play for 2 seconds at normal speed, 4 seconds at zero speed, and 8 + // seconds at normal speed: + // (1.0 x 2) + (0.0 x 4) + (1.0 x 8) = 10 + InSequence s; + StrictMock<MockTimeProvider> mock_time; + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(4))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(6))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(10))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(18))); + const base::TimeDelta kZero; + const base::TimeDelta kExpected = base::TimeDelta::FromSeconds(10); + + ClockImpl clock(&MockTimeProvider::StaticNow); + EXPECT_EQ(kZero, clock.Play()); + clock.SetPlaybackRate(0.0f); + clock.SetPlaybackRate(1.0f); + EXPECT_EQ(kExpected, clock.Elapsed()); +} + +TEST(ClockTest, Play_MultiSpeed) { + // We'll play for 2 seconds at half speed, 4 seconds at normal speed, and 8 + // seconds at double speed: + // (0.5 x 2) + (1.0 x 4) + (2.0 x 8) = 21 + InSequence s; + StrictMock<MockTimeProvider> mock_time; + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(4))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(6))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(10))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(18))); + const base::TimeDelta kZero; + const base::TimeDelta kExpected = base::TimeDelta::FromSeconds(21); + + ClockImpl clock(&MockTimeProvider::StaticNow); + clock.SetPlaybackRate(0.5f); + EXPECT_EQ(kZero, clock.Play()); + clock.SetPlaybackRate(1.0f); + clock.SetPlaybackRate(2.0f); + EXPECT_EQ(kExpected, clock.Elapsed()); +} + +TEST(ClockTest, Pause) { + InSequence s; + StrictMock<MockTimeProvider> mock_time; + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(4))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(8))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(12))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(16))); + const base::TimeDelta kZero; + const base::TimeDelta kFirstPause = base::TimeDelta::FromSeconds(4); + const base::TimeDelta kSecondPause = base::TimeDelta::FromSeconds(8); + + ClockImpl clock(&MockTimeProvider::StaticNow); + EXPECT_EQ(kZero, clock.Play()); + EXPECT_EQ(kFirstPause, clock.Pause()); + EXPECT_EQ(kFirstPause, clock.Elapsed()); + EXPECT_EQ(kFirstPause, clock.Play()); + EXPECT_EQ(kSecondPause, clock.Pause()); + EXPECT_EQ(kSecondPause, clock.Elapsed()); +} + +TEST(ClockTest, SetTime_Paused) { + // We'll remain paused while we set the time. The time should be simply + // updated without accessing the time provider. + InSequence s; + StrictMock<MockTimeProvider> mock_time; + const base::TimeDelta kFirstTime = base::TimeDelta::FromSeconds(4); + const base::TimeDelta kSecondTime = base::TimeDelta::FromSeconds(16); + + ClockImpl clock(&MockTimeProvider::StaticNow); + clock.SetTime(kFirstTime); + EXPECT_EQ(kFirstTime, clock.Elapsed()); + clock.SetTime(kSecondTime); + EXPECT_EQ(kSecondTime, clock.Elapsed()); +} + +TEST(ClockTest, SetTime_Playing) { + // We'll play for 4 seconds, then set the time to 12, then play for 4 more + // seconds. We'll expect a media time of 16. + InSequence s; + StrictMock<MockTimeProvider> mock_time; + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(4))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(8))); + EXPECT_CALL(mock_time, Now()) + .WillOnce(Return(base::Time::FromDoubleT(12))); + const base::TimeDelta kZero; + const base::TimeDelta kExepected = base::TimeDelta::FromSeconds(16); + + ClockImpl clock(&MockTimeProvider::StaticNow); + EXPECT_EQ(kZero, clock.Play()); + clock.SetTime(base::TimeDelta::FromSeconds(12)); + EXPECT_EQ(kExepected, clock.Elapsed()); +} + +} // namespace media diff --git a/media/base/pipeline_impl.cc b/media/base/pipeline_impl.cc index 20bd4cb..4ad6ff8 100644 --- a/media/base/pipeline_impl.cc +++ b/media/base/pipeline_impl.cc @@ -73,6 +73,7 @@ void DecrementCounter(Lock* lock, ConditionVariable* cond_var, int* count) { PipelineImpl::PipelineImpl(MessageLoop* message_loop) : message_loop_(message_loop), + clock_(&base::Time::Now), state_(kCreated), remaining_transitions_(0) { ResetState(); @@ -201,7 +202,7 @@ void PipelineImpl::SetVolume(float volume) { base::TimeDelta PipelineImpl::GetCurrentTime() const { AutoLock auto_lock(lock_); - return time_; + return clock_.Elapsed(); } base::TimeDelta PipelineImpl::GetBufferedTime() const { @@ -243,9 +244,10 @@ void PipelineImpl::SetPipelineErrorCallback(PipelineCallback* error_callback) { void PipelineImpl::ResetState() { AutoLock auto_lock(lock_); + const base::TimeDelta kZero; running_ = false; - duration_ = base::TimeDelta(); - buffered_time_ = base::TimeDelta(); + duration_ = kZero; + buffered_time_ = kZero; buffered_bytes_ = 0; total_bytes_ = 0; video_width_ = 0; @@ -253,7 +255,7 @@ void PipelineImpl::ResetState() { volume_ = 1.0f; playback_rate_ = 0.0f; error_ = PIPELINE_OK; - time_ = base::TimeDelta(); + clock_.SetTime(kZero); rendered_mime_types_.clear(); } @@ -307,7 +309,7 @@ base::TimeDelta PipelineImpl::GetTime() const { void PipelineImpl::SetTime(base::TimeDelta time) { DCHECK(IsRunning()); AutoLock auto_lock(lock_); - time_ = time; + clock_.SetTime(time); } void PipelineImpl::SetDuration(base::TimeDelta duration) { @@ -547,6 +549,7 @@ void PipelineImpl::ErrorChangedTask(PipelineError error) { void PipelineImpl::PlaybackRateChangedTask(float playback_rate) { DCHECK_EQ(MessageLoop::current(), message_loop_); + clock_.SetPlaybackRate(playback_rate); for (FilterVector::iterator iter = filters_.begin(); iter != filters_.end(); ++iter) { @@ -591,6 +594,7 @@ void PipelineImpl::SeekTask(base::TimeDelta time, remaining_transitions_ = filters_.size(); // Kick off seeking! + clock_.Pause(); filters_.front()->Pause( NewCallback(this, &PipelineImpl::OnFilterStateTransition)); } @@ -610,6 +614,12 @@ void PipelineImpl::FilterStateTransitionTask() { CHECK(remaining_transitions_ > 0u); if (--remaining_transitions_ == 0) { state_ = FindNextState(state_); + if (state_ == kSeeking) { + clock_.SetTime(seek_timestamp_); + } else if (state_ == kStarting) { + clock_.Play(); + } + if (StateTransitionsToStarted(state_)) { remaining_transitions_ = filters_.size(); } @@ -629,9 +639,6 @@ void PipelineImpl::FilterStateTransitionTask() { NOTREACHED(); } } else if (state_ == kStarted) { - // We've completed the seek, update the time. - SetTime(seek_timestamp_); - // Execute the seek callback, if present. Note that this might be the // initial callback passed into Start(). if (seek_callback_.get()) { diff --git a/media/base/pipeline_impl.h b/media/base/pipeline_impl.h index be3ad28..5c84e26 100644 --- a/media/base/pipeline_impl.h +++ b/media/base/pipeline_impl.h @@ -16,6 +16,7 @@ #include "base/ref_counted.h" #include "base/thread.h" #include "base/time.h" +#include "media/base/clock_impl.h" #include "media/base/filter_host.h" #include "media/base/pipeline.h" @@ -282,8 +283,10 @@ class PipelineImpl : public Pipeline, public FilterHost { // the filters. float playback_rate_; - // Current playback time. Set by filters. - base::TimeDelta time_; + // Reference clock. Keeps track of current playback time. Uses system + // clock and linear interpolation, but can have its time manually set + // by filters. + ClockImpl clock_; // Status of the pipeline. Initialized to PIPELINE_OK which indicates that // the pipeline is operating correctly. Any other value indicates that the diff --git a/media/filters/video_renderer_base.cc b/media/filters/video_renderer_base.cc index 6b4f570..6161852 100644 --- a/media/filters/video_renderer_base.cc +++ b/media/filters/video_renderer_base.cc @@ -21,12 +21,15 @@ namespace media { // difficult sections in the movie and decoding slows down. static const size_t kMaxFrames = 3; -// Sleeping for negative amounts actually hangs your thread on Windows! -static const int64 kMinSleepMilliseconds = 0; - -// This equates to ~13.33 fps, which is just under the typical 15 fps that -// lower quality cameras or shooting modes usually use for video encoding. -static const int64 kMaxSleepMilliseconds = 75; +// This equates to ~16.67 fps, which is just slow enough to be tolerable when +// our video renderer is ahead of the audio playback. +// +// A higher value will be a slower frame rate, which looks worse but allows the +// audio renderer to catch up faster. A lower value will be a smoother frame +// rate, but results in the video being out of sync for longer. +// +// TODO(scherkus): what if the native frame rate is 15 or 10 fps? +static const int64 kMaxSleepMilliseconds = 60; VideoRendererBase::VideoRendererBase() : width_(0), @@ -236,22 +239,27 @@ void VideoRendererBase::ThreadMain() { } } - // Notify subclass that |current_frame_| has been updated. - OnFrameAvailable(); - // Calculate our sleep duration. base::TimeDelta sleep = CalculateSleepDuration(next_frame, playback_rate); + int sleep_ms = static_cast<int>(sleep.InMilliseconds()); + + // If we're too far behind to catch up, simply drop the frame. + // + // This has the effect of potentially dropping a few frames when playback + // resumes after being paused. The alternative (sleeping for 0 milliseconds + // and trying to catch up) looks worse. + if (sleep_ms < 0) + continue; // To be safe, limit our sleep duration. - // TODO(scherkus): handle seeking gracefully.. right now a seek backwards - // will hit kMinSleepMilliseconds whereas a seek forward will hit - // kMaxSleepMilliseconds. - int sleep_ms = static_cast<int>(sleep.InMilliseconds()); - if (sleep_ms < kMinSleepMilliseconds) - sleep_ms = kMinSleepMilliseconds; - else if (sleep_ms > kMaxSleepMilliseconds) + // TODO(scherkus): handle seeking gracefully.. right now we tend to hit + // kMaxSleepMilliseconds a lot when we seek backwards. + if (sleep_ms > kMaxSleepMilliseconds) sleep_ms = kMaxSleepMilliseconds; + // Notify subclass that |current_frame_| has been updated. + OnFrameAvailable(); + PlatformThread::Sleep(sleep_ms); } } diff --git a/media/media.gyp b/media/media.gyp index 2c8e963..f725862 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -51,6 +51,9 @@ 'base/buffer_queue.h', 'base/buffer_queue.cc', 'base/buffers.h', + 'base/clock.h', + 'base/clock_impl.cc', + 'base/clock_impl.h', 'base/data_buffer.cc', 'base/data_buffer.h', 'base/factory.h', @@ -154,6 +157,7 @@ 'audio/simple_sources_unittest.cc', 'audio/win/audio_output_win_unittest.cc', 'base/buffer_queue_unittest.cc', + 'base/clock_impl_unittest.cc', 'base/data_buffer_unittest.cc', 'base/mock_ffmpeg.cc', 'base/mock_ffmpeg.h', |