diff options
author | dalecurtis <dalecurtis@chromium.org> | 2015-06-09 20:17:55 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-06-10 03:18:25 +0000 |
commit | 8a047ffa05b477e148428c787e86601d21d73ce2 (patch) | |
tree | 1a6fc9a4fec1d964ec6c5f48eec2f7dcf0772ce9 /media | |
parent | 938e803e0a762fde0935ad7005e8f84d84f7e36e (diff) | |
download | chromium_src-8a047ffa05b477e148428c787e86601d21d73ce2.zip chromium_src-8a047ffa05b477e148428c787e86601d21d73ce2.tar.gz chromium_src-8a047ffa05b477e148428c787e86601d21d73ce2.tar.bz2 |
Improve audio/video sync during underflow, reduce underflow frequency.
The current approach doesn't account for expired frames when resuming
after underflow, which means we may resume and then immediately fail
again; these frames are also out of sync with the audio playing out.
Fixing this requires several changes to the pipeline:
- TimeSource::ConvertMediaTimestamps() will now always convert timestamps,
even when time is stopped. When time is stopped the converted values are
based on the last known media wall clock time.
- TimeSource::ConvertMediaTimestamps() will now return the current media
wall clock time when an empty timestamps vector is given; this value is
used to figure out which video frames have been played out already.
- Introduce AudioClock::CompensateForSuspendedWrites() to ensure the audio
delay value is correctly used when return the current wall clock time.
- Introduce |was_time_moving_| to VideoRendererAlgorithm to ensure the
|last_deadline_max_| value, used by RemoveExpiredFrames, is only changed
when time is ticking; otherwise the value given to RemoveExpiredFrames is
overwritten by Render() calls and EffectiveFramesQueued() will vary.
- Modifies VideoRendererImpl::FrameReady() to remove expired frames based
on the last known media wall clock time during underflow states.
Combined these changes lead to a noticeable improvement in audio/video sync
during underflow as well as a greatly reduced number of underflow events.
BUG=498525
TEST=lots of new tests.
Review URL: https://codereview.chromium.org/1160853006
Cr-Commit-Position: refs/heads/master@{#333672}
Diffstat (limited to 'media')
-rw-r--r-- | media/base/time_source.h | 9 | ||||
-rw-r--r-- | media/base/wall_clock_time_source.cc | 43 | ||||
-rw-r--r-- | media/base/wall_clock_time_source.h | 4 | ||||
-rw-r--r-- | media/base/wall_clock_time_source_unittest.cc | 66 | ||||
-rw-r--r-- | media/filters/audio_clock.cc | 21 | ||||
-rw-r--r-- | media/filters/audio_clock.h | 12 | ||||
-rw-r--r-- | media/filters/audio_clock_unittest.cc | 26 | ||||
-rw-r--r-- | media/filters/video_renderer_algorithm.cc | 38 | ||||
-rw-r--r-- | media/filters/video_renderer_algorithm.h | 16 | ||||
-rw-r--r-- | media/filters/video_renderer_algorithm_unittest.cc | 38 | ||||
-rw-r--r-- | media/renderers/audio_renderer_impl.cc | 70 | ||||
-rw-r--r-- | media/renderers/audio_renderer_impl.h | 7 | ||||
-rw-r--r-- | media/renderers/audio_renderer_impl_unittest.cc | 144 | ||||
-rw-r--r-- | media/renderers/video_renderer_impl.cc | 47 |
14 files changed, 438 insertions, 103 deletions
diff --git a/media/base/time_source.h b/media/base/time_source.h index 08cf0f0..496eced 100644 --- a/media/base/time_source.h +++ b/media/base/time_source.h @@ -49,9 +49,12 @@ class MEDIA_EXPORT TimeSource { // will never go backwards, the frequency at which they update may be low. virtual base::TimeDelta CurrentMediaTime() = 0; - // Converts a vector of media timestamps into a vector of wall clock times. If - // the media time is stopped, returns false and does not modify the output - // vector. Returns true and converts all timestamps otherwise. + // Converts a vector of media timestamps into a vector of wall clock times; if + // the media time is stopped, returns false, otherwise returns true. Even if + // time is stopped, timestamps will be converted. + // + // Passing an empty |media_timestamps| vector will return the last known media + // time as a wall clock time. // // Within a single call to GetWallClockTimes() the returned wall clock times // are a strictly increasing function of the given media times. There is no diff --git a/media/base/wall_clock_time_source.cc b/media/base/wall_clock_time_source.cc index 1c59be9..33ec372 100644 --- a/media/base/wall_clock_time_source.cc +++ b/media/base/wall_clock_time_source.cc @@ -20,16 +20,16 @@ void WallClockTimeSource::StartTicking() { base::AutoLock auto_lock(lock_); DCHECK(!ticking_); ticking_ = true; - reference_wall_ticks_ = tick_clock_->NowTicks(); + reference_time_ = tick_clock_->NowTicks(); } void WallClockTimeSource::StopTicking() { DVLOG(1) << __FUNCTION__; base::AutoLock auto_lock(lock_); DCHECK(ticking_); - base_time_ = CurrentMediaTime_Locked(); + base_timestamp_ = CurrentMediaTime_Locked(); ticking_ = false; - reference_wall_ticks_ = tick_clock_->NowTicks(); + reference_time_ = tick_clock_->NowTicks(); } void WallClockTimeSource::SetPlaybackRate(double playback_rate) { @@ -38,8 +38,8 @@ void WallClockTimeSource::SetPlaybackRate(double playback_rate) { // Estimate current media time using old rate to use as a new base time for // the new rate. if (ticking_) { - base_time_ = CurrentMediaTime_Locked(); - reference_wall_ticks_ = tick_clock_->NowTicks(); + base_timestamp_ = CurrentMediaTime_Locked(); + reference_time_ = tick_clock_->NowTicks(); } playback_rate_ = playback_rate; @@ -49,7 +49,7 @@ void WallClockTimeSource::SetMediaTime(base::TimeDelta time) { DVLOG(1) << __FUNCTION__ << "(" << time.InMicroseconds() << ")"; base::AutoLock auto_lock(lock_); CHECK(!ticking_); - base_time_ = time; + base_timestamp_ = time; } base::TimeDelta WallClockTimeSource::CurrentMediaTime() { @@ -61,29 +61,34 @@ bool WallClockTimeSource::GetWallClockTimes( const std::vector<base::TimeDelta>& media_timestamps, std::vector<base::TimeTicks>* wall_clock_times) { base::AutoLock auto_lock(lock_); - if (!ticking_ || !playback_rate_) - return false; - DCHECK(wall_clock_times->empty()); - wall_clock_times->reserve(media_timestamps.size()); - for (const auto& media_timestamp : media_timestamps) { - wall_clock_times->push_back( - reference_wall_ticks_ + - base::TimeDelta::FromMicroseconds( - (media_timestamp - base_time_).InMicroseconds() / playback_rate_)); + + if (media_timestamps.empty()) { + wall_clock_times->push_back(reference_time_); + } else { + // When playback is paused (rate is zero), assume a rate of 1.0. + const double playback_rate = playback_rate_ ? playback_rate_ : 1.0; + + wall_clock_times->reserve(media_timestamps.size()); + for (const auto& media_timestamp : media_timestamps) { + wall_clock_times->push_back(reference_time_ + + (media_timestamp - base_timestamp_) / + playback_rate); + } } - return true; + + return playback_rate_ && ticking_; } base::TimeDelta WallClockTimeSource::CurrentMediaTime_Locked() { lock_.AssertAcquired(); if (!ticking_ || !playback_rate_) - return base_time_; + return base_timestamp_; base::TimeTicks now = tick_clock_->NowTicks(); - return base_time_ + + return base_timestamp_ + base::TimeDelta::FromMicroseconds( - (now - reference_wall_ticks_).InMicroseconds() * playback_rate_); + (now - reference_time_).InMicroseconds() * playback_rate_); } } // namespace media diff --git a/media/base/wall_clock_time_source.h b/media/base/wall_clock_time_source.h index 2228319..4046e8e 100644 --- a/media/base/wall_clock_time_source.h +++ b/media/base/wall_clock_time_source.h @@ -48,8 +48,8 @@ class MEDIA_EXPORT WallClockTimeSource : public TimeSource { // delta between our reference ticks and the current system ticks and scaling // that time by the playback rate. double playback_rate_; - base::TimeDelta base_time_; - base::TimeTicks reference_wall_ticks_; + base::TimeDelta base_timestamp_; + base::TimeTicks reference_time_; // TODO(scherkus): Remove internal locking from this class after access to // Renderer::CurrentMediaTime() is single threaded http://crbug.com/370634 diff --git a/media/base/wall_clock_time_source_unittest.cc b/media/base/wall_clock_time_source_unittest.cc index d9b632f..f5f0bb3 100644 --- a/media/base/wall_clock_time_source_unittest.cc +++ b/media/base/wall_clock_time_source_unittest.cc @@ -28,22 +28,26 @@ class WallClockTimeSourceTest : public testing::Test { return time_source_.SetMediaTime(base::TimeDelta::FromSeconds(seconds)); } - bool IsWallClockNowForMediaTimeInSeconds(int seconds) { + base::TimeTicks ConvertMediaTime(base::TimeDelta timestamp, + bool* is_time_moving) { std::vector<base::TimeTicks> wall_clock_times; - EXPECT_TRUE(time_source_.GetWallClockTimes( - std::vector<base::TimeDelta>(1, base::TimeDelta::FromSeconds(seconds)), - &wall_clock_times)); - return tick_clock_->NowTicks() == wall_clock_times.front(); + *is_time_moving = time_source_.GetWallClockTimes( + std::vector<base::TimeDelta>(1, timestamp), &wall_clock_times); + return wall_clock_times[0]; + } + + bool IsWallClockNowForMediaTimeInSeconds(int seconds) { + bool is_time_moving = false; + return tick_clock_->NowTicks() == + ConvertMediaTime(base::TimeDelta::FromSeconds(seconds), + &is_time_moving); } bool IsTimeStopped() { - std::vector<base::TimeTicks> wall_clock_times; + bool is_time_moving = false; // Convert any random value, it shouldn't matter for this call. - const bool time_stopped = !time_source_.GetWallClockTimes( - std::vector<base::TimeDelta>(1, base::TimeDelta::FromSeconds(1)), - &wall_clock_times); - EXPECT_EQ(time_stopped, wall_clock_times.empty()); - return time_stopped; + ConvertMediaTime(base::TimeDelta::FromSeconds(1), &is_time_moving); + return !is_time_moving; } protected: @@ -118,4 +122,44 @@ TEST_F(WallClockTimeSourceTest, StopTicking) { EXPECT_TRUE(IsTimeStopped()); } +TEST_F(WallClockTimeSourceTest, ConvertsTimestampsWhenStopped) { + const base::TimeDelta kOneSecond = base::TimeDelta::FromSeconds(1); + bool is_time_moving = false; + EXPECT_EQ(base::TimeTicks(), + ConvertMediaTime(base::TimeDelta(), &is_time_moving)); + EXPECT_FALSE(is_time_moving); + EXPECT_NE(base::TimeTicks(), ConvertMediaTime(kOneSecond, &is_time_moving)); + EXPECT_FALSE(is_time_moving); + time_source_.StartTicking(); + time_source_.StopTicking(); + EXPECT_EQ(tick_clock_->NowTicks(), + ConvertMediaTime(base::TimeDelta(), &is_time_moving)); + EXPECT_FALSE(is_time_moving); + EXPECT_EQ(tick_clock_->NowTicks() + kOneSecond, + ConvertMediaTime(kOneSecond, &is_time_moving)); + EXPECT_FALSE(is_time_moving); +} + +TEST_F(WallClockTimeSourceTest, EmptyMediaTimestampsReturnMediaWallClockTime) { + std::vector<base::TimeTicks> wall_clock_times; + bool is_time_moving = time_source_.GetWallClockTimes( + std::vector<base::TimeDelta>(), &wall_clock_times); + EXPECT_FALSE(is_time_moving); + EXPECT_EQ(base::TimeTicks(), wall_clock_times[0]); + + wall_clock_times.clear(); + time_source_.StartTicking(); + is_time_moving = time_source_.GetWallClockTimes( + std::vector<base::TimeDelta>(), &wall_clock_times); + EXPECT_TRUE(is_time_moving); + EXPECT_EQ(tick_clock_->NowTicks(), wall_clock_times[0]); + + wall_clock_times.clear(); + time_source_.StopTicking(); + is_time_moving = time_source_.GetWallClockTimes( + std::vector<base::TimeDelta>(), &wall_clock_times); + EXPECT_FALSE(is_time_moving); + EXPECT_EQ(tick_clock_->NowTicks(), wall_clock_times[0]); +} + } // namespace media diff --git a/media/filters/audio_clock.cc b/media/filters/audio_clock.cc index aebc5e5..d93db1d 100644 --- a/media/filters/audio_clock.cc +++ b/media/filters/audio_clock.cc @@ -36,7 +36,7 @@ void AudioClock::WroteAudio(int frames_written, // First write: initialize buffer with silence. if (start_timestamp_ == front_timestamp_ && buffered_.empty()) - PushBufferedAudioData(delay_frames, 0.0f); + PushBufferedAudioData(delay_frames, 0.0); // Move frames from |buffered_| into the computed timestamp based on // |delay_frames|. @@ -47,7 +47,7 @@ void AudioClock::WroteAudio(int frames_written, std::max(INT64_C(0), total_buffered_frames_ - delay_frames); front_timestamp_ += ComputeBufferedMediaTime(frames_played); PushBufferedAudioData(frames_written, playback_rate); - PushBufferedAudioData(frames_requested - frames_written, 0.0f); + PushBufferedAudioData(frames_requested - frames_written, 0.0); PopBufferedAudioData(frames_played); back_timestamp_ += base::TimeDelta::FromMicroseconds( @@ -80,6 +80,23 @@ void AudioClock::WroteAudio(int frames_written, microseconds_per_frame_); } +void AudioClock::CompensateForSuspendedWrites(base::TimeDelta elapsed, + int delay_frames) { + const int64_t frames_elapsed = + elapsed.InMicroseconds() / microseconds_per_frame_ + 0.5; + + // No need to do anything if we're within the limits of our played out audio + // or there are no delay frames, the next WroteAudio() call will expire + // everything correctly. + if (frames_elapsed < total_buffered_frames_ || !delay_frames) + return; + + // Otherwise, flush everything and prime with the delay frames. + WroteAudio(0, 0, 0, 0); + DCHECK(buffered_.empty()); + PushBufferedAudioData(delay_frames, 0.0); +} + base::TimeDelta AudioClock::TimeUntilPlayback(base::TimeDelta timestamp) const { DCHECK_GE(timestamp, front_timestamp_); DCHECK_LE(timestamp, back_timestamp_); diff --git a/media/filters/audio_clock.h b/media/filters/audio_clock.h index fe462aba..f7711f6 100644 --- a/media/filters/audio_clock.h +++ b/media/filters/audio_clock.h @@ -59,6 +59,18 @@ class MEDIA_EXPORT AudioClock { int delay_frames, double playback_rate); + // If WroteAudio() calls are suspended (i.e. due to playback being paused) the + // AudioClock will not properly advance time (even though all data up until + // back_timestamp() will playout on the physical device). + // + // To compensate for this, when calls resume, before the next WroteAudio(), + // callers should call CompensateForSuspendedWrites() to advance the clock for + // audio which continued playing out while WroteAudio() calls were suspended. + // + // |delay_frames| must be provided to properly prime the clock to compensate + // for a new initial delay. + void CompensateForSuspendedWrites(base::TimeDelta elapsed, int delay_frames); + // Returns the bounds of media data currently buffered by the audio hardware, // taking silence and changes in playback rate into account. Buffered audio // structure and timestamps are updated with every call to WroteAudio(). diff --git a/media/filters/audio_clock_unittest.cc b/media/filters/audio_clock_unittest.cc index 3fe437e..2144bb0 100644 --- a/media/filters/audio_clock_unittest.cc +++ b/media/filters/audio_clock_unittest.cc @@ -330,4 +330,30 @@ TEST_F(AudioClockTest, SupportsYearsWorthOfAudioData) { EXPECT_EQ((huge * 2).InDays(), ContiguousAudioDataBufferedInDays()); } +TEST_F(AudioClockTest, CompensateForSuspendedWrites) { + // Buffer 6 seconds of delay and 1 second of audio data. + WroteAudio(10, 10, 60, 1.0); + + // Media timestamp zero has to wait for silence to pass. + const int kBaseTimeMs = 6000; + EXPECT_EQ(kBaseTimeMs, TimeUntilPlaybackInMilliseconds(0)); + + // Elapsing frames less than we have buffered should do nothing. + const int kDelayFrames = 2; + for (int i = 1000; i <= kBaseTimeMs; i += 1000) { + clock_.CompensateForSuspendedWrites(base::TimeDelta::FromMilliseconds(i), + kDelayFrames); + EXPECT_EQ(kBaseTimeMs - (i - 1000), TimeUntilPlaybackInMilliseconds(0)); + + // Write silence to simulate maintaining a 7s output buffer. + WroteAudio(0, 10, 60, 1.0); + } + + // Exhausting all frames should advance timestamps and prime the buffer with + // our delay frames value. + clock_.CompensateForSuspendedWrites(base::TimeDelta::FromMilliseconds(7000), + kDelayFrames); + EXPECT_EQ(kDelayFrames * 100, TimeUntilPlaybackInMilliseconds(1000)); +} + } // namespace media diff --git a/media/filters/video_renderer_algorithm.cc b/media/filters/video_renderer_algorithm.cc index d29cad8..2670823 100644 --- a/media/filters/video_renderer_algorithm.cc +++ b/media/filters/video_renderer_algorithm.cc @@ -69,28 +69,24 @@ scoped_refptr<VideoFrame> VideoRendererAlgorithm::Render( // to Render(). If so, we assume the last frame provided was rendered during // those intervals and adjust its render count appropriately. AccountForMissedIntervals(deadline_min, deadline_max); - last_deadline_max_ = deadline_max; // Step 3: Update the wall clock timestamps and frame duration estimates for // all frames currently in the |frame_queue_|. - if (!UpdateFrameStatistics()) { - DVLOG(2) << "Failed to update frame statistics."; - + UpdateFrameStatistics(); + const bool have_known_duration = average_frame_duration_ > base::TimeDelta(); + if (!(was_time_moving_ && have_known_duration)) { ReadyFrame& ready_frame = frame_queue_[last_frame_index_]; DCHECK(ready_frame.frame); // If duration is unknown, we don't have enough frames to make a good guess // about which frame to use, so always choose the first. - if (average_frame_duration_ == base::TimeDelta() && - !ready_frame.start_time.is_null()) { + if (was_time_moving_ && !have_known_duration) ++ready_frame.render_count; - } return ready_frame.frame; } - DCHECK_GT(average_frame_duration_, base::TimeDelta()); - + last_deadline_max_ = deadline_max; base::TimeDelta selected_frame_drift; // Step 4: Attempt to find the best frame by cadence. @@ -250,7 +246,8 @@ size_t VideoRendererAlgorithm::RemoveExpiredFrames(base::TimeTicks deadline) { if (deadline > last_deadline_max_) last_deadline_max_ = deadline; - if (!UpdateFrameStatistics() || frame_queue_.size() < 2) + UpdateFrameStatistics(); + if (frame_queue_.size() < 2) return 0; DCHECK_GT(average_frame_duration_, base::TimeDelta()); @@ -308,6 +305,7 @@ void VideoRendererAlgorithm::Reset() { first_frame_ = true; cadence_frame_counter_ = 0; last_render_ignored_cadence_frame_ = false; + was_time_moving_ = false; // Default to ATSC IS/191 recommendations for maximum acceptable drift before // we have enough frames to base the maximum on frame duration. @@ -414,7 +412,7 @@ void VideoRendererAlgorithm::AccountForMissedIntervals( base::TimeTicks deadline_min, base::TimeTicks deadline_max) { if (last_deadline_max_.is_null() || deadline_min <= last_deadline_max_ || - !have_rendered_frames_) { + !have_rendered_frames_ || !was_time_moving_) { return; } @@ -445,7 +443,7 @@ void VideoRendererAlgorithm::AccountForMissedIntervals( ready_frame.render_count += render_cycle_count; } -bool VideoRendererAlgorithm::UpdateFrameStatistics() { +void VideoRendererAlgorithm::UpdateFrameStatistics() { DCHECK(!frame_queue_.empty()); // Figure out all current ready frame times at once. @@ -454,10 +452,9 @@ bool VideoRendererAlgorithm::UpdateFrameStatistics() { for (const auto& ready_frame : frame_queue_) media_timestamps.push_back(ready_frame.frame->timestamp()); - // If time has stopped, we can bail out early. std::vector<base::TimeTicks> wall_clock_times; - if (!wall_clock_time_cb_.Run(media_timestamps, &wall_clock_times)) - return false; + was_time_moving_ = + wall_clock_time_cb_.Run(media_timestamps, &wall_clock_times); // Transfer the converted wall clock times into our frame queue. DCHECK_EQ(wall_clock_times.size(), frame_queue_.size()); @@ -473,7 +470,7 @@ bool VideoRendererAlgorithm::UpdateFrameStatistics() { frame_queue_.back().start_time = wall_clock_times.back(); if (!frame_duration_calculator_.count()) - return false; + return; // Compute |average_frame_duration_|, a moving average of the last few frames; // see kMovingAverageSamples for the exact number. @@ -496,7 +493,7 @@ bool VideoRendererAlgorithm::UpdateFrameStatistics() { // If we were called via RemoveExpiredFrames() and Render() was never called, // we may not have a render interval yet. if (render_interval_ == base::TimeDelta()) - return true; + return; const bool cadence_changed = cadence_estimator_.UpdateCadenceEstimate( render_interval_, average_frame_duration_, max_acceptable_drift_); @@ -504,15 +501,10 @@ bool VideoRendererAlgorithm::UpdateFrameStatistics() { // No need to update cadence if there's been no change; cadence will be set // as frames are added to the queue. if (!cadence_changed) - return true; + return; cadence_frame_counter_ = 0; UpdateCadenceForFrames(); - - // Thus far there appears to be no need for special 3:2 considerations, the - // smoothness scores seem to naturally fit that pattern based on maximizing - // frame coverage. - return true; } void VideoRendererAlgorithm::UpdateCadenceForFrames() { diff --git a/media/filters/video_renderer_algorithm.h b/media/filters/video_renderer_algorithm.h index 4a3da25..71d251f 100644 --- a/media/filters/video_renderer_algorithm.h +++ b/media/filters/video_renderer_algorithm.h @@ -117,6 +117,10 @@ class MEDIA_EXPORT VideoRendererAlgorithm { // be counted as effective. size_t EffectiveFramesQueued() const; + // Tells the algorithm that Render() callbacks have been suspended for a known + // reason and such stoppage shouldn't be counted against future frames. + void set_time_stopped() { was_time_moving_ = false; } + size_t frames_queued() const { return frame_queue_.size(); } // Returns the average of the duration of all frames in |frame_queue_| @@ -175,9 +179,8 @@ class MEDIA_EXPORT VideoRendererAlgorithm { base::TimeTicks deadline_max); // Updates the render count and wall clock timestamps for all frames in - // |frame_queue_|. Returns false if statistics can't be updated at this time; - // which occurs if media time has stopped or there are not enough frames to - // calculate an average frame duration. Updates |cadence_estimator_|. + // |frame_queue_|. Updates |was_time_stopped_|, |cadence_estimator_| and + // |frame_duration_calculator_|. // // Note: Wall clock time is recomputed each Render() call because it's // expected that the TimeSource powering TimeSource::WallClockTimeCB will skew @@ -186,7 +189,7 @@ class MEDIA_EXPORT VideoRendererAlgorithm { // TODO(dalecurtis): Investigate how accurate we need the wall clock times to // be, so we can avoid recomputing every time (we would need to recompute when // playback rate changes occur though). - bool UpdateFrameStatistics(); + void UpdateFrameStatistics(); // Updates the ideal render count for all frames in |frame_queue_| based on // the cadence returned by |cadence_estimator_|. Cadence is assigned based @@ -304,6 +307,11 @@ class MEDIA_EXPORT VideoRendererAlgorithm { // by cadence in favor of one by drift or coverage. bool last_render_ignored_cadence_frame_; + // Indicates if time was moving, set to the return value from + // UpdateFrameStatistics() during Render() or externally by + // set_time_stopped(). + bool was_time_moving_; + DISALLOW_COPY_AND_ASSIGN(VideoRendererAlgorithm); }; diff --git a/media/filters/video_renderer_algorithm_unittest.cc b/media/filters/video_renderer_algorithm_unittest.cc index d8b2eb0..fcdedd6 100644 --- a/media/filters/video_renderer_algorithm_unittest.cc +++ b/media/filters/video_renderer_algorithm_unittest.cc @@ -385,6 +385,40 @@ TEST_F(VideoRendererAlgorithmTest, AccountForMissingIntervals) { ASSERT_TRUE(frame); EXPECT_EQ(tg.interval(3), frame->timestamp()); EXPECT_EQ(0u, frames_dropped); + EXPECT_EQ(1, GetCurrentFrameDisplayCount()); + + // Stop the time source and verify AccountForMissedIntervals() doesn't try to + // account for intervals from pause behavior. + time_source_.StopTicking(); + frame = RenderAndStep(&tg, &frames_dropped); + ASSERT_TRUE(frame); + EXPECT_EQ(tg.interval(3), frame->timestamp()); + EXPECT_EQ(0u, frames_dropped); + EXPECT_EQ(1, GetCurrentFrameDisplayCount()); + + tg.step(100); + frame = RenderAndStep(&tg, &frames_dropped); + ASSERT_TRUE(frame); + EXPECT_EQ(tg.interval(3), frame->timestamp()); + EXPECT_EQ(0u, frames_dropped); + EXPECT_EQ(1, GetCurrentFrameDisplayCount()); + + time_source_.StartTicking(); + + // Now run the same test using set_time_stopped(); + frame = RenderAndStep(&tg, &frames_dropped); + ASSERT_TRUE(frame); + EXPECT_EQ(tg.interval(3), frame->timestamp()); + EXPECT_EQ(0u, frames_dropped); + EXPECT_EQ(2, GetCurrentFrameDisplayCount()); + + algorithm_.set_time_stopped(); + tg.step(100); + frame = RenderAndStep(&tg, &frames_dropped); + ASSERT_TRUE(frame); + EXPECT_EQ(tg.interval(3), frame->timestamp()); + EXPECT_EQ(0u, frames_dropped); + EXPECT_EQ(3, GetCurrentFrameDisplayCount()); } TEST_F(VideoRendererAlgorithmTest, OnLastFrameDropped) { @@ -1201,6 +1235,7 @@ TEST_F(VideoRendererAlgorithmTest, EnqueueFrames) { RenderAndStep(&tg, &frames_dropped); EXPECT_EQ(1u, frames_queued()); EXPECT_EQ(frame_1, rendered_frame); + EXPECT_EQ(1, GetCurrentFrameDisplayCount()); // The replaced frame should count as dropped. EXPECT_EQ(1u, frames_dropped); @@ -1213,6 +1248,7 @@ TEST_F(VideoRendererAlgorithmTest, EnqueueFrames) { EXPECT_EQ(1u, frames_queued()); EXPECT_EQ(frame_1, rendered_frame); EXPECT_EQ(1u, frames_dropped); + EXPECT_EQ(2, GetCurrentFrameDisplayCount()); // Trying to add a frame < 1 ms after the last frame should drop the frame. algorithm_.EnqueueFrame(CreateFrame(base::TimeDelta::FromMicroseconds(999))); @@ -1220,6 +1256,7 @@ TEST_F(VideoRendererAlgorithmTest, EnqueueFrames) { EXPECT_EQ(1u, frames_queued()); EXPECT_EQ(frame_1, rendered_frame); EXPECT_EQ(1u, frames_dropped); + EXPECT_EQ(3, GetCurrentFrameDisplayCount()); scoped_refptr<VideoFrame> frame_3 = CreateFrame(tg.interval(1)); algorithm_.EnqueueFrame(frame_3); @@ -1232,6 +1269,7 @@ TEST_F(VideoRendererAlgorithmTest, EnqueueFrames) { EXPECT_EQ(1u, frames_queued()); EXPECT_EQ(frame_3, rendered_frame); EXPECT_EQ(1u, frames_dropped); + EXPECT_EQ(1, GetCurrentFrameDisplayCount()); } TEST_F(VideoRendererAlgorithmTest, CadenceForFutureFrames) { diff --git a/media/renderers/audio_renderer_impl.cc b/media/renderers/audio_renderer_impl.cc index 59f05b3..7a2c029 100644 --- a/media/renderers/audio_renderer_impl.cc +++ b/media/renderers/audio_renderer_impl.cc @@ -14,6 +14,7 @@ #include "base/logging.h" #include "base/metrics/histogram.h" #include "base/single_thread_task_runner.h" +#include "base/time/default_tick_clock.h" #include "media/base/audio_buffer.h" #include "media/base/audio_buffer_converter.h" #include "media/base/audio_hardware_config.h" @@ -52,6 +53,7 @@ AudioRendererImpl::AudioRendererImpl( audio_buffer_stream_( new AudioBufferStream(task_runner, decoders.Pass(), media_log)), hardware_config_(hardware_config), + tick_clock_(new base::DefaultTickClock()), playback_rate_(0.0), state_(kUninitialized), buffering_state_(BUFFERING_HAVE_NOTHING), @@ -171,38 +173,59 @@ bool AudioRendererImpl::GetWallClockTimes( const std::vector<base::TimeDelta>& media_timestamps, std::vector<base::TimeTicks>* wall_clock_times) { base::AutoLock auto_lock(lock_); - if (last_render_time_.is_null() || !stop_rendering_time_.is_null() || - !playback_rate_ || buffering_state_ != BUFFERING_HAVE_ENOUGH || - !sink_playing_) { - return false; + DCHECK(wall_clock_times->empty()); + + // When playback is paused (rate is zero), assume a rate of 1.0. + const double playback_rate = playback_rate_ ? playback_rate_ : 1.0; + const bool is_time_moving = sink_playing_ && playback_rate_ && + !last_render_time_.is_null() && + stop_rendering_time_.is_null(); + + // Pre-compute the time until playback of the audio buffer extents, since + // these values are frequently used below. + const base::TimeDelta time_until_front = + audio_clock_->TimeUntilPlayback(audio_clock_->front_timestamp()); + const base::TimeDelta time_until_back = + audio_clock_->TimeUntilPlayback(audio_clock_->back_timestamp()); + + if (media_timestamps.empty()) { + // Return the current media time as a wall clock time while accounting for + // frames which may be in the process of play out. + wall_clock_times->push_back(std::min( + std::max(tick_clock_->NowTicks(), last_render_time_ + time_until_front), + last_render_time_ + time_until_back)); + return is_time_moving; } - DCHECK(wall_clock_times->empty()); wall_clock_times->reserve(media_timestamps.size()); for (const auto& media_timestamp : media_timestamps) { - base::TimeDelta base_time; - if (media_timestamp < audio_clock_->front_timestamp()) { - // See notes about |media_time| values less than |base_time| in TimeSource - // header. - base_time = audio_clock_->front_timestamp(); - } else if (media_timestamp > audio_clock_->back_timestamp()) { - base_time = audio_clock_->back_timestamp(); - } else { - // No need to estimate time, so return the actual wallclock time. + // When time was or is moving and the requested media timestamp is within + // range of played out audio, we can provide an exact conversion. + if (!last_render_time_.is_null() && + media_timestamp >= audio_clock_->front_timestamp() && + media_timestamp <= audio_clock_->back_timestamp()) { wall_clock_times->push_back( - last_render_time_ + - audio_clock_->TimeUntilPlayback(media_timestamp)); + last_render_time_ + audio_clock_->TimeUntilPlayback(media_timestamp)); continue; } + base::TimeDelta base_timestamp, time_until_playback; + if (media_timestamp < audio_clock_->front_timestamp()) { + base_timestamp = audio_clock_->front_timestamp(); + time_until_playback = time_until_front; + } else { + base_timestamp = audio_clock_->back_timestamp(); + time_until_playback = time_until_back; + } + // In practice, most calls will be estimates given the relatively small // window in which clients can get the actual time. - wall_clock_times->push_back( - last_render_time_ + audio_clock_->TimeUntilPlayback(base_time) + - base::TimeDelta::FromMicroseconds( - (media_timestamp - base_time).InMicroseconds() / playback_rate_)); + wall_clock_times->push_back(last_render_time_ + time_until_playback + + (media_timestamp - base_timestamp) / + playback_rate); } - return true; + + return is_time_moving; } TimeSource* AudioRendererImpl::GetTimeSource() { @@ -596,10 +619,11 @@ int AudioRendererImpl::Render(AudioBus* audio_bus, int frames_written = 0; { base::AutoLock auto_lock(lock_); - last_render_time_ = base::TimeTicks::Now(); + last_render_time_ = tick_clock_->NowTicks(); if (!stop_rendering_time_.is_null()) { - // TODO(dalecurtis): Use |stop_rendering_time_| to advance the AudioClock. + audio_clock_->CompensateForSuspendedWrites( + last_render_time_ - stop_rendering_time_, delay_frames); stop_rendering_time_ = base::TimeTicks(); } diff --git a/media/renderers/audio_renderer_impl.h b/media/renderers/audio_renderer_impl.h index 43d12ea..944d8ed 100644 --- a/media/renderers/audio_renderer_impl.h +++ b/media/renderers/audio_renderer_impl.h @@ -36,6 +36,7 @@ namespace base { class SingleThreadTaskRunner; +class TickClock; } namespace media { @@ -217,6 +218,9 @@ class MEDIA_EXPORT AudioRendererImpl // Callback provided to Flush(). base::Closure flush_cb_; + // Overridable tick clock for testing. + scoped_ptr<base::TickClock> tick_clock_; + // After Initialize() has completed, all variables below must be accessed // under |lock_|. ------------------------------------------------------------ base::Lock lock_; @@ -257,7 +261,8 @@ class MEDIA_EXPORT AudioRendererImpl base::TimeTicks last_render_time_; // Set to the value of |last_render_time_| when StopRendering_Locked() is - // called for any reason. Cleared by the next successful Render() call. + // called for any reason. Cleared by the next successful Render() call after + // being used to adjust for lost time between the last call. base::TimeTicks stop_rendering_time_; // Set upon receipt of the first decoded buffer after a StartPlayingFrom(). diff --git a/media/renderers/audio_renderer_impl_unittest.cc b/media/renderers/audio_renderer_impl_unittest.cc index e65dd4b..6a76435 100644 --- a/media/renderers/audio_renderer_impl_unittest.cc +++ b/media/renderers/audio_renderer_impl_unittest.cc @@ -7,6 +7,7 @@ #include "base/format_macros.h" #include "base/run_loop.h" #include "base/strings/stringprintf.h" +#include "base/test/simple_test_tick_clock.h" #include "media/base/audio_buffer_converter.h" #include "media/base/audio_hardware_config.h" #include "media/base/audio_splicer.h" @@ -60,6 +61,7 @@ class AudioRendererImplTest : public ::testing::Test { // Give the decoder some non-garbage media properties. AudioRendererImplTest() : hardware_config_(AudioParameters(), AudioParameters()), + tick_clock_(new base::SimpleTestTickClock()), demuxer_stream_(DemuxerStream::AUDIO), decoder_(new MockAudioDecoder()), ended_(false) { @@ -98,6 +100,8 @@ class AudioRendererImplTest : public ::testing::Test { decoders.Pass(), hardware_config_, new MediaLog())); + renderer_->tick_clock_.reset(tick_clock_); + tick_clock_->Advance(base::TimeDelta::FromSeconds(1)); } virtual ~AudioRendererImplTest() { @@ -285,14 +289,34 @@ class AudioRendererImplTest : public ::testing::Test { // Attempts to consume |requested_frames| frames from |renderer_|'s internal // buffer. Returns true if and only if all of |requested_frames| were able // to be consumed. - bool ConsumeBufferedData(OutputFrames requested_frames) { + bool ConsumeBufferedData(OutputFrames requested_frames, + base::TimeDelta delay) { scoped_ptr<AudioBus> bus = AudioBus::Create(kChannels, requested_frames.value); int frames_read = 0; - EXPECT_TRUE(sink_->Render(bus.get(), 0, &frames_read)); + EXPECT_TRUE(sink_->Render(bus.get(), delay.InMilliseconds(), &frames_read)); return frames_read == requested_frames.value; } + bool ConsumeBufferedData(OutputFrames requested_frames) { + return ConsumeBufferedData(requested_frames, base::TimeDelta()); + } + + base::TimeTicks ConvertMediaTime(base::TimeDelta timestamp, + bool* is_time_moving) { + std::vector<base::TimeTicks> wall_clock_times; + *is_time_moving = renderer_->GetWallClockTimes( + std::vector<base::TimeDelta>(1, timestamp), &wall_clock_times); + return wall_clock_times[0]; + } + + base::TimeTicks CurrentMediaWallClockTime(bool* is_time_moving) { + std::vector<base::TimeTicks> wall_clock_times; + *is_time_moving = renderer_->GetWallClockTimes( + std::vector<base::TimeDelta>(), &wall_clock_times); + return wall_clock_times[0]; + } + OutputFrames frames_buffered() { return OutputFrames(renderer_->algorithm_->frames_buffered()); } @@ -334,6 +358,7 @@ class AudioRendererImplTest : public ::testing::Test { scoped_ptr<AudioRendererImpl> renderer_; scoped_refptr<FakeAudioRendererSink> sink_; AudioHardwareConfig hardware_config_; + base::SimpleTestTickClock* tick_clock_; private: void DecodeDecoder(const scoped_refptr<DecoderBuffer>& buffer, @@ -516,10 +541,6 @@ TEST_F(AudioRendererImplTest, Underflow_Flush) { WaitForPendingRead(); StopTicking(); - // After time stops ticking wall clock times should not be returned. - EXPECT_FALSE( - renderer_->GetWallClockTimes(std::vector<base::TimeDelta>(1), nullptr)); - // We shouldn't expect another buffering state change when flushing. FlushDuringPendingRead(); } @@ -749,4 +770,115 @@ TEST_F(AudioRendererImplTest, SetPlaybackRate) { EXPECT_EQ(FakeAudioRendererSink::kPlaying, sink_->state()); } +TEST_F(AudioRendererImplTest, TimeSourceBehavior) { + Initialize(); + Preroll(); + + AudioTimestampHelper timestamp_helper(kOutputSamplesPerSecond); + timestamp_helper.SetBaseTimestamp(base::TimeDelta()); + + // Prior to start, time should be shown as not moving. + bool is_time_moving = false; + EXPECT_EQ(base::TimeTicks(), + ConvertMediaTime(base::TimeDelta(), &is_time_moving)); + EXPECT_FALSE(is_time_moving); + + EXPECT_EQ(base::TimeTicks(), CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_FALSE(is_time_moving); + + // Start ticking, but use a zero playback rate, time should still be stopped + // until a positive playback rate is set and the first Render() is called. + renderer_->SetPlaybackRate(0.0); + StartTicking(); + EXPECT_EQ(base::TimeTicks(), CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_FALSE(is_time_moving); + renderer_->SetPlaybackRate(1.0); + EXPECT_EQ(base::TimeTicks(), CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_FALSE(is_time_moving); + renderer_->SetPlaybackRate(1.0); + + // Issue the first render call to start time moving. + OutputFrames frames_to_consume(frames_buffered().value / 2); + EXPECT_TRUE(ConsumeBufferedData(frames_to_consume)); + WaitForPendingRead(); + + // Time shouldn't change just yet because we've only sent the initial audio + // data to the hardware. + EXPECT_EQ(tick_clock_->NowTicks(), + ConvertMediaTime(base::TimeDelta(), &is_time_moving)); + EXPECT_TRUE(is_time_moving); + + // Consume some more audio data. + frames_to_consume = frames_buffered(); + tick_clock_->Advance( + base::TimeDelta::FromSecondsD(1.0 / kOutputSamplesPerSecond)); + EXPECT_TRUE(ConsumeBufferedData(frames_to_consume)); + + // Time should change now that the audio hardware has called back. + const base::TimeTicks wall_clock_time_zero = + tick_clock_->NowTicks() - + timestamp_helper.GetFrameDuration(frames_to_consume.value); + EXPECT_EQ(wall_clock_time_zero, + ConvertMediaTime(base::TimeDelta(), &is_time_moving)); + EXPECT_TRUE(is_time_moving); + + // The current wall clock time should change as our tick clock advances, up + // until we've reached the end of played out frames. + const int kSteps = 4; + const base::TimeDelta kAdvanceDelta = + timestamp_helper.GetFrameDuration(frames_to_consume.value) / kSteps; + + for (int i = 0; i < kSteps; ++i) { + tick_clock_->Advance(kAdvanceDelta); + EXPECT_EQ(tick_clock_->NowTicks(), + CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_TRUE(is_time_moving); + } + + // Converting the current media time should be relative to wall clock zero. + EXPECT_EQ(wall_clock_time_zero + kSteps * kAdvanceDelta, + ConvertMediaTime(renderer_->CurrentMediaTime(), &is_time_moving)); + EXPECT_TRUE(is_time_moving); + + // Advancing once more will exceed the amount of played out frames finally. + base::TimeTicks current_time = tick_clock_->NowTicks(); + tick_clock_->Advance( + base::TimeDelta::FromSecondsD(1.0 / kOutputSamplesPerSecond)); + EXPECT_EQ(current_time, CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_TRUE(is_time_moving); + + StopTicking(); + DeliverRemainingAudio(); + + // Elapse a lot of time between StopTicking() and the next Render() call. + const base::TimeDelta kOneSecond = base::TimeDelta::FromSeconds(1); + tick_clock_->Advance(kOneSecond); + StartTicking(); + + // Time should be stopped until the next render call. + EXPECT_EQ(current_time, CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_FALSE(is_time_moving); + + // Consume some buffered data with a small delay. + base::TimeDelta delay_time = base::TimeDelta::FromMilliseconds(50); + frames_to_consume.value = frames_buffered().value / 16; + EXPECT_TRUE(ConsumeBufferedData(frames_to_consume, delay_time)); + + // Verify time is adjusted for the current delay. + current_time = tick_clock_->NowTicks() + delay_time; + EXPECT_EQ(current_time, CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_TRUE(is_time_moving); + EXPECT_EQ(current_time, + ConvertMediaTime(renderer_->CurrentMediaTime(), &is_time_moving)); + EXPECT_TRUE(is_time_moving); + + // Advance far enough that we shouldn't be clamped to current time (tested + // already above). + tick_clock_->Advance(kOneSecond); + EXPECT_EQ( + current_time + timestamp_helper.GetFrameDuration(frames_to_consume.value), + CurrentMediaWallClockTime(&is_time_moving)); + EXPECT_TRUE(is_time_moving); +} + } // namespace media diff --git a/media/renderers/video_renderer_impl.cc b/media/renderers/video_renderer_impl.cc index 2bb4547..c89aa1d 100644 --- a/media/renderers/video_renderer_impl.cc +++ b/media/renderers/video_renderer_impl.cc @@ -504,9 +504,45 @@ void VideoRendererImpl::FrameReady(VideoFrameStream::Status status, AddReadyFrame_Locked(frame); } + // Background rendering updates may not be ticking fast enough by itself to + // remove expired frames, so give it a boost here by ensuring we don't exit + // the decoding cycle too early. + // + // Similarly, if we've paused for underflow, remove all frames which are + // before the current media time. + const bool have_nothing = buffering_state_ != BUFFERING_HAVE_ENOUGH; + const bool have_nothing_and_paused = have_nothing && !sink_started_; + if (was_background_rendering_ || + (use_new_video_renderering_path_ && have_nothing_and_paused && + drop_frames_)) { + base::TimeTicks expiry_time; + if (have_nothing_and_paused) { + // Use the current media wall clock time plus the frame duration since + // RemoveExpiredFrames() is expecting the end point of an interval (it + // will subtract from the given value). + std::vector<base::TimeTicks> current_time; + wall_clock_time_cb_.Run(std::vector<base::TimeDelta>(), ¤t_time); + expiry_time = current_time[0] + algorithm_->average_frame_duration(); + } else { + expiry_time = tick_clock_->NowTicks(); + } + + // Prior to rendering the first frame, |have_nothing_and_paused| will be + // true, correspondingly the |expiry_time| will be null; in this case + // there's no reason to try and remove any frames. + if (!expiry_time.is_null()) { + const size_t removed_frames = + algorithm_->RemoveExpiredFrames(expiry_time); + + // Frames removed during underflow should be counted as dropped. + if (have_nothing_and_paused && removed_frames) + frames_dropped_ += removed_frames; + } + } + // Signal buffering state if we've met our conditions for having enough // data. - if (buffering_state_ != BUFFERING_HAVE_ENOUGH && HaveEnoughData_Locked()) { + if (have_nothing && HaveEnoughData_Locked()) { TransitionToHaveEnough_Locked(); if (use_new_video_renderering_path_ && !sink_started_ && !rendered_end_of_stream_) { @@ -516,14 +552,6 @@ void VideoRendererImpl::FrameReady(VideoFrameStream::Status status, } } - // Background rendering updates may not be ticking fast enough by itself to - // remove expired frames, so give it a boost here by ensuring we don't exit - // the decoding cycle too early. - if (was_background_rendering_) { - DCHECK(use_new_video_renderering_path_); - algorithm_->RemoveExpiredFrames(tick_clock_->NowTicks()); - } - // Always request more decoded video if we have capacity. This serves two // purposes: // 1) Prerolling while paused @@ -711,6 +739,7 @@ void VideoRendererImpl::StartSink() { void VideoRendererImpl::StopSink() { DCHECK(task_runner_->BelongsToCurrentThread()); sink_->Stop(); + algorithm_->set_time_stopped(); sink_started_ = false; was_background_rendering_ = false; } |