// Copyright (c) 2015 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 "media/capture/content/animated_content_sampler.h" #include #include #include namespace media { namespace { // These specify the minimum/maximum amount of recent event history to examine // to detect animated content. If the values are too low, there is a greater // risk of false-positive detections and low accuracy. If they are too high, // the the implementation will be slow to lock-in/out, and also will not react // well to mildly-variable frame rate content (e.g., 25 +/- 1 FPS). // // These values were established by experimenting with a wide variety of // scenarios, including 24/25/30 FPS videos, 60 FPS WebGL demos, and the // transitions between static and animated content. const int kMinObservationWindowMillis = 1000; const int kMaxObservationWindowMillis = 2000; // The maximum amount of time that can elapse before declaring two subsequent // events as "not animating." This is the same value found in // cc::FrameRateCounter. const int kNonAnimatingThresholdMillis = 250; // 4 FPS // The slowest that content can be animating in order for AnimatedContentSampler // to lock-in. This is the threshold at which the "smoothness" problem is no // longer relevant. const int kMaxLockInPeriodMicros = 83333; // 12 FPS // The amount of time over which to fully correct the drift of the rewritten // frame timestamps from the presentation event timestamps. The lower the // value, the higher the variance in frame timestamps. const int kDriftCorrectionMillis = 2000; } // anonymous namespace AnimatedContentSampler::AnimatedContentSampler( base::TimeDelta min_capture_period) : min_capture_period_(min_capture_period), sampling_state_(NOT_SAMPLING) { DCHECK_GT(min_capture_period_, base::TimeDelta()); } AnimatedContentSampler::~AnimatedContentSampler() { } void AnimatedContentSampler::SetTargetSamplingPeriod(base::TimeDelta period) { target_sampling_period_ = period; } void AnimatedContentSampler::ConsiderPresentationEvent( const gfx::Rect& damage_rect, base::TimeTicks event_time) { // Analyze the current event and recent history to determine whether animating // content is detected. AddObservation(damage_rect, event_time); if (!AnalyzeObservations(event_time, &detected_region_, &detected_period_) || detected_period_ <= base::TimeDelta() || detected_period_ > base::TimeDelta::FromMicroseconds(kMaxLockInPeriodMicros)) { // Animated content not detected. detected_region_ = gfx::Rect(); detected_period_ = base::TimeDelta(); sampling_state_ = NOT_SAMPLING; return; } // At this point, animation is being detected. Update the sampling period // since the client may call the accessor method even if the heuristics below // decide not to sample the current event. sampling_period_ = ComputeSamplingPeriod( detected_period_, target_sampling_period_, min_capture_period_); // If this is the first event causing animating content to be detected, // transition to the START_SAMPLING state. if (sampling_state_ == NOT_SAMPLING) sampling_state_ = START_SAMPLING; // If the current event does not represent a frame that is part of the // animation, do not sample. if (damage_rect != detected_region_) { if (sampling_state_ == SHOULD_SAMPLE) sampling_state_ = SHOULD_NOT_SAMPLE; return; } // When starting sampling, determine where to sync-up for sampling and frame // timestamp rewriting. Otherwise, just add one animation period's worth of // tokens to the token bucket. if (sampling_state_ == START_SAMPLING) { if (event_time - frame_timestamp_ > sampling_period_) { // The frame timestamp sequence should start with the current event // time. frame_timestamp_ = event_time - sampling_period_; token_bucket_ = sampling_period_; } else { // The frame timestamp sequence will continue from the last recorded // frame timestamp. token_bucket_ = event_time - frame_timestamp_; } // Provide a little extra in the initial token bucket so that minor error in // the detected period won't prevent a reasonably-timed event from being // sampled. token_bucket_ += detected_period_ / 2; } else { token_bucket_ += detected_period_; } // If the token bucket is full enough, take tokens from it and propose // sampling. Otherwise, do not sample. DCHECK_LE(detected_period_, sampling_period_); if (token_bucket_ >= sampling_period_) { token_bucket_ -= sampling_period_; frame_timestamp_ = ComputeNextFrameTimestamp(event_time); sampling_state_ = SHOULD_SAMPLE; } else { sampling_state_ = SHOULD_NOT_SAMPLE; } } bool AnimatedContentSampler::HasProposal() const { return sampling_state_ != NOT_SAMPLING; } bool AnimatedContentSampler::ShouldSample() const { return sampling_state_ == SHOULD_SAMPLE; } void AnimatedContentSampler::RecordSample(base::TimeTicks frame_timestamp) { if (sampling_state_ == NOT_SAMPLING) frame_timestamp_ = frame_timestamp; else if (sampling_state_ == SHOULD_SAMPLE) sampling_state_ = SHOULD_NOT_SAMPLE; } void AnimatedContentSampler::AddObservation(const gfx::Rect& damage_rect, base::TimeTicks event_time) { if (damage_rect.IsEmpty()) return; // Useless observation. // Add the observation to the FIFO queue. if (!observations_.empty() && observations_.back().event_time > event_time) return; // The implementation assumes chronological order. observations_.push_back(Observation(damage_rect, event_time)); // Prune-out old observations. const base::TimeDelta threshold = base::TimeDelta::FromMilliseconds(kMaxObservationWindowMillis); while ((event_time - observations_.front().event_time) > threshold) observations_.pop_front(); } gfx::Rect AnimatedContentSampler::ElectMajorityDamageRect() const { // This is an derivative of the Boyer-Moore Majority Vote Algorithm where each // pixel in a candidate gets one vote, as opposed to each candidate getting // one vote. const gfx::Rect* candidate = NULL; int64_t votes = 0; for (ObservationFifo::const_iterator i = observations_.begin(); i != observations_.end(); ++i) { DCHECK_GT(i->damage_rect.size().GetArea(), 0); if (votes == 0) { candidate = &(i->damage_rect); votes = candidate->size().GetArea(); } else if (i->damage_rect == *candidate) { votes += i->damage_rect.size().GetArea(); } else { votes -= i->damage_rect.size().GetArea(); if (votes < 0) { candidate = &(i->damage_rect); votes = -votes; } } } return (votes > 0) ? *candidate : gfx::Rect(); } bool AnimatedContentSampler::AnalyzeObservations( base::TimeTicks event_time, gfx::Rect* rect, base::TimeDelta* period) const { const gfx::Rect elected_rect = ElectMajorityDamageRect(); if (elected_rect.IsEmpty()) return false; // There is no regular animation present. // Scan |observations_|, gathering metrics about the ones having a damage Rect // equivalent to the |elected_rect|. Along the way, break early whenever the // event times reveal a non-animating period. int64_t num_pixels_damaged_in_all = 0; int64_t num_pixels_damaged_in_chosen = 0; base::TimeDelta sum_frame_durations; size_t count_frame_durations = 0; base::TimeTicks first_event_time; base::TimeTicks last_event_time; for (ObservationFifo::const_reverse_iterator i = observations_.rbegin(); i != observations_.rend(); ++i) { const int area = i->damage_rect.size().GetArea(); num_pixels_damaged_in_all += area; if (i->damage_rect != elected_rect) continue; num_pixels_damaged_in_chosen += area; if (last_event_time.is_null()) { last_event_time = i->event_time; if ((event_time - last_event_time) >= base::TimeDelta::FromMilliseconds(kNonAnimatingThresholdMillis)) { return false; // Content animation has recently ended. } } else { const base::TimeDelta frame_duration = first_event_time - i->event_time; if (frame_duration >= base::TimeDelta::FromMilliseconds(kNonAnimatingThresholdMillis)) { break; // Content not animating before this point. } sum_frame_durations += frame_duration; ++count_frame_durations; } first_event_time = i->event_time; } if ((last_event_time - first_event_time) < base::TimeDelta::FromMilliseconds(kMinObservationWindowMillis)) { return false; // Content has not animated for long enough for accuracy. } if (num_pixels_damaged_in_chosen <= (num_pixels_damaged_in_all * 2 / 3)) return false; // Animation is not damaging a supermajority of pixels. *rect = elected_rect; DCHECK_GT(count_frame_durations, 0u); *period = sum_frame_durations / count_frame_durations; return true; } base::TimeTicks AnimatedContentSampler::ComputeNextFrameTimestamp( base::TimeTicks event_time) const { // The ideal next frame timestamp one sampling period since the last one. const base::TimeTicks ideal_timestamp = frame_timestamp_ + sampling_period_; // Account for two main sources of drift: 1) The clock drift of the system // clock relative to the video hardware, which affects the event times; and // 2) The small error introduced by this frame timestamp rewriting, as it is // based on averaging over recent events. // // TODO(miu): This is similar to the ClockSmoother in // media/base/audio_shifter.cc. Consider refactor-and-reuse here. const base::TimeDelta drift = ideal_timestamp - event_time; const int64_t correct_over_num_frames = base::TimeDelta::FromMilliseconds(kDriftCorrectionMillis) / sampling_period_; DCHECK_GT(correct_over_num_frames, 0); return ideal_timestamp - drift / correct_over_num_frames; } // static base::TimeDelta AnimatedContentSampler::ComputeSamplingPeriod( base::TimeDelta animation_period, base::TimeDelta target_sampling_period, base::TimeDelta min_capture_period) { // If the animation rate is unknown, return the ideal sampling period. if (animation_period == base::TimeDelta()) { return std::max(target_sampling_period, min_capture_period); } // Determine whether subsampling is needed. If so, compute the sampling // period corresponding to the sampling rate is the closest integer division // of the animation frame rate to the target sampling rate. // // For example, consider a target sampling rate of 30 FPS and an animation // rate of 42 FPS. Possible sampling rates would be 42/1 = 42, 42/2 = 21, // 42/3 = 14, and so on. Of these candidates, 21 FPS is closest to 30. base::TimeDelta sampling_period; if (animation_period < target_sampling_period) { const int64_t ratio = target_sampling_period / animation_period; const double target_fps = 1.0 / target_sampling_period.InSecondsF(); const double animation_fps = 1.0 / animation_period.InSecondsF(); if (std::abs(animation_fps / ratio - target_fps) < std::abs(animation_fps / (ratio + 1) - target_fps)) { sampling_period = ratio * animation_period; } else { sampling_period = (ratio + 1) * animation_period; } } else { sampling_period = animation_period; } return std::max(sampling_period, min_capture_period); } } // namespace media