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 |