Chromium Code Reviews| Index: media/capture/video_capture_oracle.cc |
| diff --git a/media/capture/video_capture_oracle.cc b/media/capture/video_capture_oracle.cc |
| index 0d88397c2d5ff490a713944af34fa3d24932449b..bc6dd2dd384b7e7b58b963180ea9cd22384d2ebd 100644 |
| --- a/media/capture/video_capture_oracle.cc |
| +++ b/media/capture/video_capture_oracle.cc |
| @@ -7,6 +7,7 @@ |
| #include <algorithm> |
| #include "base/format_macros.h" |
| +#include "base/numerics/safe_conversions.h" |
| #include "base/strings/stringprintf.h" |
| namespace media { |
| @@ -24,6 +25,39 @@ namespace { |
| // further into the WebRTC encoding stack. |
| const int kNumRedundantCapturesOfStaticContent = 200; |
| +// The half-life of data points provided to the accumulator used when evaluating |
| +// the recent utilization of the buffer pool. This value is based on a |
| +// simulation, and reacts quickly to change to avoid depleting the buffer pool |
| +// (which would cause hard frame drops). |
| +const int kBufferUtilizationEvaluationMicros = 200000; |
| + |
| +// The half-life of data points provided to the accumulator used when evaluating |
| +// the recent resource utilization of the consumer. The trade-off made here is |
| +// reaction time versus over-reacting to outlier data points. |
| +const int kConsumerCapabilityEvaluationMicros = 1000000; |
| + |
| +// The minimum amount of time that must pass between changes to the capture |
| +// size. This throttles the rate of size changes, to avoid stressing consumers |
| +// and to allow the end-to-end system sufficient time to stabilize before |
| +// re-evaluating the capture size. |
| +const int kMinSizeChangePeriodMicros = 3000000; |
| + |
| +// The maximum amount of time that may elapse without a feedback update. Any |
| +// longer, and currently-accumulated feedback is not considered recent enough to |
| +// base decisions off of. This prevents changes to the capture size when there |
| +// is an unexpected pause in events. |
| +const int kMaxTimeSinceLastFeedbackUpdateMicros = 1000000; |
| + |
| +// The amount of additional time, since content animation was last detected, to |
| +// continue being extra-careful about increasing the capture size. This is used |
| +// to prevent breif periods of non-animating content from throwing off the |
| +// heuristics that decide whether to increase the capture size. |
| +const int kDebouncingPeriodForAnimatedContentMicros = 3000000; |
|
hubbe
2015/06/25 21:57:43
All of these constants are a bit hard to read (too
miu
2015/07/01 22:11:47
Done. Sort of. It all comes back down to all tho
|
| + |
| +// When content is animating, this is the length of time the system must be |
| +// contiguously under-utilized before increasing the capture size. |
| +const int kProvingPeriodForAnimatedContentMicros = 30000000; |
| + |
| // Given the amount of time between frames, compare to the expected amount of |
| // time between frames at |frame_rate| and return the fractional difference. |
| double FractionFromExpectedFrameRate(base::TimeDelta delta, int frame_rate) { |
| @@ -34,19 +68,51 @@ double FractionFromExpectedFrameRate(base::TimeDelta delta, int frame_rate) { |
| expected_delta.InMillisecondsF(); |
| } |
| +// Returns the next-higher TimeTicks value. |
| +// TODO(miu): Patch FeedbackSignalAccumulator reset behavior and remove this |
| +// hack. |
| +base::TimeTicks JustAfter(base::TimeTicks t) { |
| + return t + base::TimeDelta::FromMicroseconds(1); |
| +} |
| + |
| +// Returns true if updates have been accumulated by |accumulator| for a |
| +// sufficient amount of time and the latest update was fairly recent, relative |
| +// to |now|. |
| +bool HasSufficientRecentFeedback(const FeedbackSignalAccumulator& accumulator, |
| + base::TimeTicks now) { |
| + const base::TimeDelta amount_of_history = |
| + accumulator.update_time() - accumulator.reset_time(); |
| + return (amount_of_history.InMicroseconds() >= kMinSizeChangePeriodMicros) && |
| + ((now - accumulator.update_time()).InMicroseconds() <= |
| + kMaxTimeSinceLastFeedbackUpdateMicros); |
| +} |
| + |
| } // anonymous namespace |
| -VideoCaptureOracle::VideoCaptureOracle(base::TimeDelta min_capture_period) |
| +VideoCaptureOracle::VideoCaptureOracle( |
| + base::TimeDelta min_capture_period, |
| + const gfx::Size& max_frame_size, |
| + media::ResolutionChangePolicy resolution_change_policy) |
| : next_frame_number_(0), |
| last_successfully_delivered_frame_number_(-1), |
| num_frames_pending_(0), |
| smoothing_sampler_(min_capture_period, |
| kNumRedundantCapturesOfStaticContent), |
| - content_sampler_(min_capture_period) { |
| -} |
| + content_sampler_(min_capture_period), |
| + resolution_chooser_(max_frame_size, resolution_change_policy), |
| + buffer_pool_utilization_(base::TimeDelta::FromMicroseconds( |
| + kBufferUtilizationEvaluationMicros)), |
| + estimated_capable_area_(base::TimeDelta::FromMicroseconds( |
| + kConsumerCapabilityEvaluationMicros)) {} |
| VideoCaptureOracle::~VideoCaptureOracle() {} |
| +void VideoCaptureOracle::SetSourceSize(const gfx::Size& source_size) { |
| + resolution_chooser_.SetSourceSize(source_size); |
| + // If the |resolution_chooser_| computed a new capture size, that will become |
| + // visible via a future call to ObserveEventAndDecideCapture(). |
| +} |
| + |
| bool VideoCaptureOracle::ObserveEventAndDecideCapture( |
| Event event, |
| const gfx::Rect& damage_rect, |
| @@ -63,21 +129,27 @@ bool VideoCaptureOracle::ObserveEventAndDecideCapture( |
| bool should_sample = false; |
| duration_of_next_frame_ = base::TimeDelta(); |
| switch (event) { |
| - case kCompositorUpdate: |
| + case kCompositorUpdate: { |
| smoothing_sampler_.ConsiderPresentationEvent(event_time); |
| + const bool had_proposal = content_sampler_.HasProposal(); |
| content_sampler_.ConsiderPresentationEvent(damage_rect, event_time); |
| if (content_sampler_.HasProposal()) { |
| + VLOG_IF(1, !had_proposal) << "Content sampler now detects animation."; |
| should_sample = content_sampler_.ShouldSample(); |
| if (should_sample) { |
| event_time = content_sampler_.frame_timestamp(); |
| duration_of_next_frame_ = content_sampler_.sampling_period(); |
| } |
| + last_time_animation_was_detected_ = event_time; |
| } else { |
| + VLOG_IF(1, had_proposal) << "Content sampler detects animation ended."; |
| should_sample = smoothing_sampler_.ShouldSample(); |
| if (should_sample) |
| duration_of_next_frame_ = smoothing_sampler_.min_capture_period(); |
| } |
| break; |
| + } |
| + |
| case kTimerPoll: |
| // While the timer is firing, only allow a sampling if there are none |
| // currently in-progress. |
| @@ -87,26 +159,65 @@ bool VideoCaptureOracle::ObserveEventAndDecideCapture( |
| duration_of_next_frame_ = smoothing_sampler_.min_capture_period(); |
| } |
| break; |
| + |
| case kNumEvents: |
| NOTREACHED(); |
| break; |
| } |
| + if (!should_sample) |
| + return false; |
| + |
| + // Update |capture_size_| and reset all feedback signal accumulators if |
| + // either: 1) this is the first frame; or 2) |resolution_chooser_| has an |
| + // updated capture size and sufficient time has passed since the last size |
| + // change. |
| + if (next_frame_number_ == 0) { |
| + CommitCaptureSizeAndReset(event_time - duration_of_next_frame_); |
| + } else if (capture_size_ != resolution_chooser_.capture_size()) { |
| + const base::TimeDelta time_since_last_change = |
| + event_time - buffer_pool_utilization_.reset_time(); |
| + if (time_since_last_change.InMicroseconds() >= kMinSizeChangePeriodMicros) |
| + CommitCaptureSizeAndReset(GetFrameTimestamp(next_frame_number_ - 1)); |
| + } |
| + |
| SetFrameTimestamp(next_frame_number_, event_time); |
| - return should_sample; |
| + return true; |
| } |
| -int VideoCaptureOracle::RecordCapture() { |
| +int VideoCaptureOracle::RecordCapture(double pool_utilization) { |
| + DCHECK(std::isfinite(pool_utilization) && pool_utilization >= 0.0); |
| + |
| smoothing_sampler_.RecordSample(); |
| - content_sampler_.RecordSample(GetFrameTimestamp(next_frame_number_)); |
| + const base::TimeTicks timestamp = GetFrameTimestamp(next_frame_number_); |
| + content_sampler_.RecordSample(timestamp); |
| + |
| + buffer_pool_utilization_.Update(pool_utilization, timestamp); |
| + AnalyzeAndAdjust(timestamp); |
| + |
| num_frames_pending_++; |
| return next_frame_number_++; |
| } |
| +void VideoCaptureOracle::RecordWillNotCapture(double pool_utilization) { |
| + DCHECK(std::isfinite(pool_utilization) && pool_utilization >= 0.0); |
| + |
| + VLOG(1) << "Client rejects proposal to capture frame (at #" |
| + << next_frame_number_ << ")."; |
| + |
| + const base::TimeTicks timestamp = GetFrameTimestamp(next_frame_number_); |
| + buffer_pool_utilization_.Update(pool_utilization, timestamp); |
| + AnalyzeAndAdjust(timestamp); |
| + |
| + // Note: Do not advance |next_frame_number_| since it will be re-used for the |
| + // next capture proposal. |
| +} |
| + |
| bool VideoCaptureOracle::CompleteCapture(int frame_number, |
| bool capture_was_successful, |
| base::TimeTicks* frame_timestamp) { |
| num_frames_pending_--; |
| + DCHECK_GE(num_frames_pending_, 0); |
| // Drop frame if previously delivered frame number is higher. |
| if (last_successfully_delivered_frame_number_ > frame_number) { |
| @@ -117,6 +228,11 @@ bool VideoCaptureOracle::CompleteCapture(int frame_number, |
| return false; |
| } |
| + if (!IsFrameInRecentHistory(frame_number)) { |
| + LOG(WARNING) << "Very old capture being ignored: frame #" << frame_number; |
| + return false; |
| + } |
| + |
| if (!capture_was_successful) { |
| VLOG(2) << "Capture of frame #" << frame_number << " was not successful."; |
| return false; |
| @@ -129,7 +245,7 @@ bool VideoCaptureOracle::CompleteCapture(int frame_number, |
| // If enabled, log a measurement of how this frame timestamp has incremented |
| // in relation to an ideal increment. |
| - if (VLOG_IS_ON(2) && frame_number > 0) { |
| + if (VLOG_IS_ON(3) && frame_number > 0) { |
| const base::TimeDelta delta = |
| *frame_timestamp - GetFrameTimestamp(frame_number - 1); |
| if (content_sampler_.HasProposal()) { |
| @@ -137,7 +253,7 @@ bool VideoCaptureOracle::CompleteCapture(int frame_number, |
| 1000000.0 / content_sampler_.detected_period().InMicroseconds(); |
| const int rounded_frame_rate = |
| static_cast<int>(estimated_frame_rate + 0.5); |
| - VLOG(2) << base::StringPrintf( |
| + VLOG_STREAM(3) << base::StringPrintf( |
| "Captured #%d: delta=%" PRId64 " usec" |
| ", now locked into {%s}, %+0.1f%% slower than %d FPS", |
| frame_number, |
| @@ -146,7 +262,7 @@ bool VideoCaptureOracle::CompleteCapture(int frame_number, |
| 100.0 * FractionFromExpectedFrameRate(delta, rounded_frame_rate), |
| rounded_frame_rate); |
| } else { |
| - VLOG(2) << base::StringPrintf( |
| + VLOG_STREAM(3) << base::StringPrintf( |
| "Captured #%d: delta=%" PRId64 " usec" |
| ", d/30fps=%+0.1f%%, d/25fps=%+0.1f%%, d/24fps=%+0.1f%%", |
| frame_number, |
| @@ -157,18 +273,204 @@ bool VideoCaptureOracle::CompleteCapture(int frame_number, |
| } |
| } |
| - return !frame_timestamp->is_null(); |
| + return true; |
| +} |
| + |
| +void VideoCaptureOracle::RecordConsumerFeedback(int frame_number, |
| + double resource_utilization) { |
| + if (!std::isfinite(resource_utilization)) { |
| + LOG(DFATAL) << "Non-finite utilization provided by consumer for frame #" |
| + << frame_number << ": " << resource_utilization; |
| + return; |
| + } |
| + if (resource_utilization <= 0.0) |
| + return; // Non-positive values are normal, meaning N/A. |
| + |
| + if (!IsFrameInRecentHistory(frame_number)) { |
| + VLOG(1) << "Very old frame feedback being ignored: frame #" << frame_number; |
| + return; |
| + } |
| + const base::TimeTicks timestamp = GetFrameTimestamp(frame_number); |
| + |
| + // Translate the utilization metric to be in terms of the capable frame area |
| + // and update the feedback accumulators. Research suggests utilization is at |
| + // most linearly proportional to area, and typically is sublinear. Either |
| + // way, the end-to-end system should converge to the right place using the |
| + // more-conservative assumption (linear). |
| + const int area_at_full_utilization = |
| + base::saturated_cast<int>(capture_size_.GetArea() / resource_utilization); |
| + estimated_capable_area_.Update(area_at_full_utilization, timestamp); |
| } |
| base::TimeTicks VideoCaptureOracle::GetFrameTimestamp(int frame_number) const { |
| - DCHECK_LE(frame_number, next_frame_number_); |
| - DCHECK_LT(next_frame_number_ - frame_number, kMaxFrameTimestamps); |
| + DCHECK(IsFrameInRecentHistory(frame_number)); |
| return frame_timestamps_[frame_number % kMaxFrameTimestamps]; |
| } |
| void VideoCaptureOracle::SetFrameTimestamp(int frame_number, |
| base::TimeTicks timestamp) { |
| + DCHECK(IsFrameInRecentHistory(frame_number)); |
| frame_timestamps_[frame_number % kMaxFrameTimestamps] = timestamp; |
| } |
| +bool VideoCaptureOracle::IsFrameInRecentHistory(int frame_number) const { |
| + return ((next_frame_number_ - frame_number) < kMaxFrameTimestamps && |
| + frame_number <= next_frame_number_ && |
| + frame_number >= 0); |
| +} |
| + |
| +void VideoCaptureOracle::CommitCaptureSizeAndReset( |
| + base::TimeTicks last_frame_time) { |
| + capture_size_ = resolution_chooser_.capture_size(); |
| + VLOG(2) << "Now proposing a capture size of " << capture_size_.ToString(); |
| + |
| + // Reset each short-term feedback accumulator with a stable-state starting |
| + // value. |
| + const base::TimeTicks ignore_before_time = JustAfter(last_frame_time); |
| + buffer_pool_utilization_.Reset(1.0, ignore_before_time); |
| + estimated_capable_area_.Reset(capture_size_.GetArea(), ignore_before_time); |
| + |
| + // With the new capture size, erase any prior conclusion about the end-to-end |
| + // system being under-utilized. |
| + start_time_of_underutilization_ = base::TimeTicks(); |
| +} |
| + |
| +void VideoCaptureOracle::AnalyzeAndAdjust(const base::TimeTicks analyze_time) { |
| + const int decreased_area = AnalyzeForDecreasedArea(analyze_time); |
| + if (decreased_area > 0) { |
| + resolution_chooser_.SetTargetFrameArea(decreased_area); |
| + return; |
| + } |
| + |
| + const int increased_area = AnalyzeForIncreasedArea(analyze_time); |
| + if (increased_area > 0) { |
| + resolution_chooser_.SetTargetFrameArea(increased_area); |
| + return; |
| + } |
| + |
| + // Explicitly set the target frame area to the current capture area. This |
| + // cancels-out the results of a previous call to this method, where the |
| + // |resolution_chooser_| may have been instructed to increase or decrease the |
| + // capture size. Conditions may have changed since then which indicate no |
| + // change should be committed (via CommitCaptureSizeAndReset()). |
| + resolution_chooser_.SetTargetFrameArea(capture_size_.GetArea()); |
| +} |
| + |
| +int VideoCaptureOracle::AnalyzeForDecreasedArea(base::TimeTicks analyze_time) { |
| + const int current_area = capture_size_.GetArea(); |
| + DCHECK_GT(current_area, 0); |
| + |
| + // Translate the recent-average buffer pool utilization to be in terms of |
| + // "capable number of pixels per frame," for an apples-to-apples comparison |
| + // below. |
| + int buffer_capable_area; |
| + if (HasSufficientRecentFeedback(buffer_pool_utilization_, analyze_time) && |
| + buffer_pool_utilization_.current() > 1.0) { |
| + // This calculation is hand-wavy, but seems to work well in a variety of |
| + // situations. |
| + buffer_capable_area = static_cast<int>( |
| + current_area / buffer_pool_utilization_.current()); |
| + } else { |
| + buffer_capable_area = current_area; |
| + } |
| + |
| + int consumer_capable_area; |
| + if (HasSufficientRecentFeedback(estimated_capable_area_, analyze_time)) { |
| + consumer_capable_area = |
| + base::saturated_cast<int>(estimated_capable_area_.current()); |
| + } else { |
| + consumer_capable_area = current_area; |
| + } |
| + |
| + // If either of the "capable areas" is less than the current capture area, |
| + // decrease the capture area by AT LEAST one step. |
| + int decreased_area = -1; |
| + const int capable_area = std::min(buffer_capable_area, consumer_capable_area); |
| + if (capable_area < current_area) { |
| + decreased_area = std::min( |
| + capable_area, |
| + resolution_chooser_.FindSmallerFrameSize(current_area, 1).GetArea()); |
| + start_time_of_underutilization_ = base::TimeTicks(); |
| + VLOG(2) << "Proposing a " |
| + << (100.0 * (current_area - decreased_area) / current_area) |
| + << "% decrease in capture area. :-("; |
| + } |
| + |
| + // Always log the capability interpretations at verbose logging level 3. At |
| + // level 2, only log when when proposing a decreased area. |
| + VLOG(decreased_area == -1 ? 3 : 2) |
| + << "Capability of pool=" << (100.0 * buffer_capable_area / current_area) |
| + << "%, consumer=" << (100.0 * consumer_capable_area / current_area) |
| + << '%'; |
| + |
| + return decreased_area; |
| +} |
| + |
| +int VideoCaptureOracle::AnalyzeForIncreasedArea(base::TimeTicks analyze_time) { |
| + // Compute what one step up in capture size/area would be. If the current |
| + // area is already at the maximum, no further analysis is necessary. |
| + const int current_area = capture_size_.GetArea(); |
| + const int increased_area = |
| + resolution_chooser_.FindLargerFrameSize(current_area, 1).GetArea(); |
| + if (increased_area <= current_area) |
| + return -1; |
| + |
| + // Determine whether the buffer pool could handle an increase in area. |
| + if (!HasSufficientRecentFeedback(buffer_pool_utilization_, analyze_time)) |
| + return -1; |
| + if (buffer_pool_utilization_.current() > 0.0) { |
| + const int buffer_capable_area = base::saturated_cast<int>( |
| + current_area / buffer_pool_utilization_.current()); |
| + if (buffer_capable_area < increased_area) { |
| + start_time_of_underutilization_ = base::TimeTicks(); |
| + return -1; // Buffer pool is not under-utilized. |
| + } |
| + } |
| + |
| + // Determine whether the consumer could handle an increase in area. |
| + if (HasSufficientRecentFeedback(estimated_capable_area_, analyze_time)) { |
| + if (estimated_capable_area_.current() < increased_area) { |
| + start_time_of_underutilization_ = base::TimeTicks(); |
| + return -1; // Consumer is not under-utilized. |
| + } |
| + } else if (estimated_capable_area_.update_time() == |
| + estimated_capable_area_.reset_time()) { |
| + // The consumer does not provide any feedback. In this case, the consumer's |
| + // capability isn't a consideration. |
| + } else { |
| + // Consumer is providing feedback, but hasn't reported it recently. Just in |
| + // case it's stalled, don't make things worse by increasing the capture |
| + // area. |
| + start_time_of_underutilization_ = base::TimeTicks(); |
| + return -1; |
| + } |
| + |
| + // At this point, the system is currently under-utilized. Reset the start |
| + // time if the system was not under-utilized when the last analysis was made. |
| + if (start_time_of_underutilization_.is_null()) |
| + start_time_of_underutilization_ = analyze_time; |
| + |
| + // While content is animating, require a "proving period" of contiguous |
| + // under-utilization before increasing the capture area. This will mitigate |
| + // the risk of causing frames to be dropped when increasing the load. If |
| + // content is not animating, be aggressive about increasing the capture area, |
| + // to improve the quality of non-animating content (where frame drops are not |
| + // much of a concern). |
| + if ((analyze_time - last_time_animation_was_detected_).InMicroseconds() < |
| + kDebouncingPeriodForAnimatedContentMicros) { |
| + if ((analyze_time - start_time_of_underutilization_).InMicroseconds() < |
| + kProvingPeriodForAnimatedContentMicros) { |
| + // Content is animating and the system has not been contiguously |
| + // under-utilizated for long enough. |
| + return -1; |
| + } |
| + } |
| + |
| + VLOG(2) << "Proposing a " |
| + << (100.0 * (increased_area - current_area) / current_area) |
| + << "% increase in capture area. :-)"; |
| + |
| + return increased_area; |
| +} |
| + |
| } // namespace media |