chromium/chromecast/media/base/video_plane_controller.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/media/base/video_plane_controller.h"

#include <stddef.h>
#include <stdint.h>

#include <vector>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chromecast/public/cast_media_shlib.h"
#include "media/base/video_transformation.h"
#include "ui/gfx/geometry/rect.h"

namespace chromecast {
namespace media {

namespace {

bool RectFEqual(const RectF& r1, const RectF& r2) {
  return r1.x == r2.x && r1.y == r2.y && r1.width == r2.width &&
         r1.height == r2.height;
}

bool SizeEqual(const Size& s1, const Size& s2) {
  return s1.width == s2.width && s1.height == s2.height;
}

bool DisplayRectFValid(const RectF& r) {
  return r.width >= 0 && r.height >= 0;
}

bool ResolutionSizeValid(const Size& s) {
  return s.width >= 0 && s.height >= 0;
}

// Translates a gfx::OverlayTransform into a VideoPlane::Transform.
// Could be just a lookup table once we have unit tests for this code
// to ensure it stays in sync with OverlayTransform.
chromecast::media::VideoPlane::Transform ConvertTransform(
    gfx::OverlayTransform transform) {
  switch (transform) {
    case gfx::OVERLAY_TRANSFORM_NONE:
      return chromecast::media::VideoPlane::TRANSFORM_NONE;
    case gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL:
      return chromecast::media::VideoPlane::FLIP_HORIZONTAL;
    case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL:
      return chromecast::media::VideoPlane::FLIP_VERTICAL;
    case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90:
      return chromecast::media::VideoPlane::ROTATE_90;
    case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_180:
      return chromecast::media::VideoPlane::ROTATE_180;
    case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270:
      return chromecast::media::VideoPlane::ROTATE_270;
    default:
      NOTREACHED_IN_MIGRATION();
      return chromecast::media::VideoPlane::TRANSFORM_NONE;
  }
}

chromecast::media::VideoPlane::Transform ConvertTransform(
    const ::media::VideoTransformation& transformation) {
  if (!transformation.mirrored) {
    switch (transformation.rotation) {
      case ::media::VIDEO_ROTATION_0:
        return chromecast::media::VideoPlane::TRANSFORM_NONE;
      case ::media::VIDEO_ROTATION_90:
        return chromecast::media::VideoPlane::ROTATE_90;
      case ::media::VIDEO_ROTATION_180:
        return chromecast::media::VideoPlane::ROTATE_180;
      case ::media::VIDEO_ROTATION_270:
        return chromecast::media::VideoPlane::ROTATE_270;
    }
  } else if (transformation.rotation == ::media::VIDEO_ROTATION_0) {
    return chromecast::media::VideoPlane::FLIP_HORIZONTAL;
  }

  NOTREACHED_IN_MIGRATION();
  return chromecast::media::VideoPlane::TRANSFORM_NONE;
}

}  // namespace

// Helper class for calling VideoPlane::SetGeometry with rate-limiting.
// SetGeometry can take on the order of 100ms to run in some implementations
// and can be called on the order of 20x / second (as fast as graphics frames
// are produced).  This creates an ever-growing backlog of tasks on the media
// thread.
// This class measures the time taken to run SetGeometry to determine a
// reasonable frequency at which to call it.  Excess calls are coalesced
// to just set the most recent geometry.
class VideoPlaneController::RateLimitedSetVideoPlaneGeometry
    : public base::RefCountedThreadSafe<RateLimitedSetVideoPlaneGeometry> {
 public:
  RateLimitedSetVideoPlaneGeometry(
      const scoped_refptr<base::SingleThreadTaskRunner>& task_runner)
      : pending_display_rect_(0, 0, 0, 0),
        pending_set_geometry_(false),
        min_calling_interval_ms_(0),
        sample_counter_(0),
        task_runner_(task_runner) {}

  RateLimitedSetVideoPlaneGeometry(const RateLimitedSetVideoPlaneGeometry&) =
      delete;
  RateLimitedSetVideoPlaneGeometry& operator=(
      const RateLimitedSetVideoPlaneGeometry&) = delete;

  void SetGeometry(const chromecast::RectF& display_rect,
                   VideoPlane::Transform transform) {
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK(DisplayRectFValid(display_rect));

    base::TimeTicks now = base::TimeTicks::Now();
    base::TimeDelta elapsed = now - last_set_geometry_time_;

    if (elapsed < base::Milliseconds(min_calling_interval_ms_)) {
      if (!pending_set_geometry_) {
        pending_set_geometry_ = true;

        task_runner_->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(
                &RateLimitedSetVideoPlaneGeometry::ApplyPendingSetGeometry,
                this),
            base::Milliseconds(2 * min_calling_interval_ms_));
      }

      pending_display_rect_ = display_rect;
      pending_transform_ = transform;
      return;
    }
    last_set_geometry_time_ = now;

    LOG(INFO) << __FUNCTION__ << " rect=" << display_rect.width << "x"
              << display_rect.height << " @" << display_rect.x << ","
              << display_rect.y << " transform " << transform;

    VideoPlane* video_plane = CastMediaShlib::GetVideoPlane();
    CHECK(video_plane);
    base::TimeTicks start = base::TimeTicks::Now();
    video_plane->SetGeometry(display_rect, transform);

    base::TimeDelta set_geometry_time = base::TimeTicks::Now() - start;
    UpdateAverageTime(set_geometry_time.InMilliseconds());
  }

 private:
  friend class base::RefCountedThreadSafe<RateLimitedSetVideoPlaneGeometry>;
  ~RateLimitedSetVideoPlaneGeometry() {}

  void UpdateAverageTime(int64_t sample) {
    const size_t kSampleCount = 5;
    if (samples_.size() < kSampleCount)
      samples_.push_back(sample);
    else
      samples_[sample_counter_++ % kSampleCount] = sample;
    int64_t total = 0;
    for (int64_t s : samples_)
      total += s;
    min_calling_interval_ms_ = 2 * total / samples_.size();
  }

  void ApplyPendingSetGeometry() {
    if (pending_set_geometry_) {
      pending_set_geometry_ = false;
      SetGeometry(pending_display_rect_, pending_transform_);
    }
  }

  RectF pending_display_rect_;
  VideoPlane::Transform pending_transform_;
  bool pending_set_geometry_;
  base::TimeTicks last_set_geometry_time_;

  // Don't call SetGeometry faster than this interval.
  int64_t min_calling_interval_ms_;

  // Min calling interval is computed as double average of last few time samples
  // (i.e. allow at least as much time between calls as the call itself takes).
  std::vector<int64_t> samples_;
  size_t sample_counter_;

  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
};

VideoPlaneController::VideoPlaneController(
    const Size& graphics_resolution,
    scoped_refptr<base::SingleThreadTaskRunner> media_task_runner)
    : is_paused_(false),
      have_screen_res_(false),
      screen_res_(0, 0),
      graphics_plane_res_(graphics_resolution),
      coordinates_(VideoPlane::GetCoordinates ? VideoPlane::GetCoordinates()
                                              : VideoPlane::kScreen),
      have_video_plane_geometry_(false),
      video_plane_display_rect_(0, 0),
      video_plane_transform_(VideoPlane::TRANSFORM_NONE),
      media_task_runner_(media_task_runner),
      video_plane_wrapper_(
          new RateLimitedSetVideoPlaneGeometry(media_task_runner_)) {}

VideoPlaneController::~VideoPlaneController() {}

void VideoPlaneController::SetGeometry(const gfx::RectF& gfx_display_rect,
                                       gfx::OverlayTransform gfx_transform) {
  SetGeometryInternal(gfx_display_rect, ConvertTransform(gfx_transform));
}

void VideoPlaneController::SetGeometryFromMediaType(
    const gfx::Rect& gfx_display_rect,
    const ::media::VideoTransformation& transform) {
  SetGeometryInternal(gfx::RectF(gfx_display_rect),
                      ConvertTransform(transform));
}

void VideoPlaneController::SetGeometryInternal(
    const gfx::RectF& gfx_display_rect,
    VideoPlane::Transform transform) {
  if (!thread_checker_.CalledOnValidThread()) {
    media_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&VideoPlaneController::SetGeometryInternal,
                                  weak_factory_.GetWeakPtr(), gfx_display_rect,
                                  transform));
    return;
  }
  const RectF display_rect(gfx_display_rect.x(), gfx_display_rect.y(),
                           gfx_display_rect.width(), gfx_display_rect.height());
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK(DisplayRectFValid(display_rect));
  if (have_video_plane_geometry_ &&
      RectFEqual(display_rect, video_plane_display_rect_) &&
      transform == video_plane_transform_) {
    DVLOG(2) << "No change found in geometry parameters.";
    return;
  }

  LOG(INFO) << "New geometry parameters "
            << " rect=" << display_rect.width << "x" << display_rect.height
            << " @" << display_rect.x << "," << display_rect.y << " transform "
            << transform;

  have_video_plane_geometry_ = true;
  video_plane_display_rect_ = display_rect;
  video_plane_transform_ = transform;

  MaybeRunSetGeometry();
}

void VideoPlaneController::SetScreenResolution(const Size& resolution) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK(ResolutionSizeValid(resolution));
  if (have_screen_res_ && SizeEqual(resolution, screen_res_)) {
    DVLOG(2) << "No change found in screen resolution.";
    return;
  }

  LOG(INFO) << "New screen resolution " << resolution.width << "x"
            << resolution.height;

  have_screen_res_ = true;
  screen_res_ = resolution;

  MaybeRunSetGeometry();
}

void VideoPlaneController::Pause() {
  DCHECK(thread_checker_.CalledOnValidThread());
  LOG(INFO) << "Pausing controller. No more VideoPlane SetGeometry calls.";
  is_paused_ = true;
}

void VideoPlaneController::Resume() {
  DCHECK(thread_checker_.CalledOnValidThread());
  LOG(INFO) << "Resuming controller. VideoPlane SetGeometry calls are active.";
  is_paused_ = false;
  ClearVideoPlaneGeometry();
}

bool VideoPlaneController::is_paused() const {
  DCHECK(thread_checker_.CalledOnValidThread());
  return is_paused_;
}

void VideoPlaneController::MaybeRunSetGeometry() {
  if (is_paused_) {
    DVLOG(2)
        << "All VideoPlane SetGeometry calls are paused. Ignoring request.";
    return;
  }

  if (!HaveDataForSetGeometry()) {
    DVLOG(2) << "Don't have all VideoPlane SetGeometry data. Ignoring request.";
    return;
  }

  DCHECK(graphics_plane_res_.width != 0 && graphics_plane_res_.height != 0);

  RectF scaled_rect = video_plane_display_rect_;
  // Note that graphics coordinates do not need to be scaled, since the received
  // values are already in those coordinates.
  if (coordinates_ != VideoPlane::Coordinates::kGraphics &&
      (graphics_plane_res_.width != screen_res_.width ||
       graphics_plane_res_.height != screen_res_.height)) {
    float sx =
        static_cast<float>(screen_res_.width) / graphics_plane_res_.width;
    float sy =
        static_cast<float>(screen_res_.height) / graphics_plane_res_.height;
    scaled_rect.x *= sx;
    scaled_rect.y *= sy;
    scaled_rect.width *= sx;
    scaled_rect.height *= sy;
  }

  media_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&RateLimitedSetVideoPlaneGeometry::SetGeometry,
                                video_plane_wrapper_, scaled_rect,
                                video_plane_transform_));
}

bool VideoPlaneController::HaveDataForSetGeometry() const {
  const bool screen_res_set_or_unnecessary =
      have_screen_res_ || (coordinates_ == VideoPlane::Coordinates::kGraphics);
  return screen_res_set_or_unnecessary && have_video_plane_geometry_;
}

void VideoPlaneController::ClearVideoPlaneGeometry() {
  have_video_plane_geometry_ = false;
  video_plane_display_rect_ = RectF(0, 0);
  video_plane_transform_ = VideoPlane::TRANSFORM_NONE;
}

}  // namespace media
}  // namespace chromecast