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

#include "components/viz/service/frame_sinks/video_detector.h"

#include <array>
#include <memory>
#include <utility>
#include <vector>

#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "components/viz/common/features.h"
#include "components/viz/common/quads/compositor_frame.h"
#include "components/viz/common/quads/draw_quad.h"
#include "components/viz/common/quads/texture_draw_quad.h"
#include "components/viz/service/surfaces/surface.h"
#include "components/viz/service/surfaces/surface_manager.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/latency/latency_info.h"

namespace viz {

constexpr base::TimeDelta VideoDetector::kMaxVideoTimeout;
constexpr base::TimeDelta VideoDetector::kMinVideoTimeout;
constexpr base::TimeDelta VideoDetector::kMinVideoDuration;

// Stores information about updates to a client and determines whether it's
// likely that a video is playing in it.
class VideoDetector::ClientInfo {
 public:
  ClientInfo() = default;

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

  // Called when a Surface belonging to this client is drawn. Returns true if we
  // determine that video is playing in this client.
  bool ReportDrawnAndCheckForVideo(Surface* surface, base::TimeTicks now) {
    uint32_t frame_index = surface->GetActiveFrameIndex();

    // If |frame_index| hasn't increased, then no new frame was submitted since
    // the last draw.
    if (frame_index <= last_drawn_frame_index_)
      return false;

    last_drawn_frame_index_ = frame_index;

    const CompositorFrame& frame = surface->GetActiveFrame();

    if (!frame.metadata.may_contain_video) {
      return false;
    }

    gfx::Rect damage = frame.render_pass_list.back()->damage_rect;
    if (frame.render_pass_list.back()->has_per_quad_damage) {
      for (auto* quad : frame.render_pass_list.back()->quad_list) {
        if (quad->material != DrawQuad::Material::kTextureContent)
          continue;

        auto* texture_quad = TextureDrawQuad::MaterialCast(quad);
        if (!texture_quad->damage_rect)
          continue;

        damage.Union(*texture_quad->damage_rect);
      }
    }

    damage =
        gfx::ScaleToEnclosingRect(damage, 1.f / frame.device_scale_factor());

    if (damage.width() < kMinDamageWidth || damage.height() < kMinDamageHeight)
      return false;

    // If the buffer is full, drop the first timestamp.
    if (buffer_size_ == kMinFramesPerSecond) {
      buffer_start_ = (buffer_start_ + 1) % kMinFramesPerSecond;
      buffer_size_--;
    }

    update_times_[(buffer_start_ + buffer_size_) % kMinFramesPerSecond] = now;
    buffer_size_++;

    const bool in_video =
        (buffer_size_ == kMinFramesPerSecond) &&
        (now - update_times_[buffer_start_] <= base::Seconds(1));

    if (in_video && video_start_time_.is_null())
      video_start_time_ = update_times_[buffer_start_];
    else if (!in_video && !video_start_time_.is_null())
      video_start_time_ = base::TimeTicks();

    const base::TimeDelta elapsed = now - video_start_time_;
    return in_video && elapsed >= kMinVideoDuration;
  }

 private:
  // Circular buffer containing update times of the last (up to
  // |kMinFramesPerSecond|) video-sized updates to this client.
  std::array<base::TimeTicks, kMinFramesPerSecond> update_times_;

  // Time at which the current sequence of updates that looks like video
  // started. Empty if video isn't currently playing.
  base::TimeTicks video_start_time_;

  // Index into |update_times_| of the oldest update.
  uint32_t buffer_start_ = 0;

  // Number of updates stored in |update_times_|.
  uint32_t buffer_size_ = 0;

  // Frame index of the last drawn Surface. We use this number to determine
  // whether a new frame was submitted since the last time the Surface was
  // drawn.
  uint32_t last_drawn_frame_index_ = 0;
};

VideoDetector::VideoDetector(
    const std::vector<FrameSinkId>& registered_frame_sink_ids,
    SurfaceManager* surface_manager,
    const base::TickClock* tick_clock,
    scoped_refptr<base::SequencedTaskRunner> task_runner)
    : tick_clock_(tick_clock),
      video_inactive_timer_(tick_clock),
      surface_manager_(surface_manager) {
  surface_manager_->AddObserver(this);
  for (auto& frame_sink_id : registered_frame_sink_ids)
    client_infos_[frame_sink_id] = std::make_unique<ClientInfo>();
  if (task_runner)
    video_inactive_timer_.SetTaskRunner(task_runner);
}

VideoDetector::~VideoDetector() {
  surface_manager_->RemoveObserver(this);
}

void VideoDetector::OnVideoActivityEnded() {
  CHECK(video_is_playing_);
  video_is_playing_ = false;
  for (auto& observer : observers_) {
    observer->OnVideoActivityEnded();
  }
}

void VideoDetector::AddObserver(
    mojo::PendingRemote<mojom::VideoDetectorObserver> pending_observer) {
  mojo::Remote<mojom::VideoDetectorObserver> observer(
      std::move(pending_observer));
  if (video_is_playing_)
    observer->OnVideoActivityStarted();
  observers_.Add(std::move(observer));
}

void VideoDetector::OnFrameSinkIdRegistered(const FrameSinkId& frame_sink_id) {
  CHECK(!client_infos_.count(frame_sink_id));
  client_infos_[frame_sink_id] = std::make_unique<ClientInfo>();
}

void VideoDetector::OnFrameSinkIdInvalidated(const FrameSinkId& frame_sink_id) {
  client_infos_.erase(frame_sink_id);
}

bool VideoDetector::OnSurfaceDamaged(
    const SurfaceId& surface_id,
    const BeginFrameAck& ack,
    HandleInteraction handle_interaction,
    const std::vector<ui::LatencyInfo>& latency_info) {
  return false;
}

// |surface| is scheduled to be drawn. See if it has a new frame since the
// last time it was drawn and record the damage.
void VideoDetector::OnSurfaceWillBeDrawn(Surface* surface) {
  // If there is no observer, don't waste cycles detecting video activity.
  if (observers_.empty())
    return;

  const FrameSinkId& frame_sink_id = surface->surface_id().frame_sink_id();

  auto it = client_infos_.find(frame_sink_id);

  // If the corresponding entry in |client_infos_| does not exist, it means the
  // FrameSinkId has been invalidated and the client's CompositorFrameSink is
  // destroyed so it cannot send new frames and video activity is impossible.
  if (it == client_infos_.end())
    return;

  base::TimeTicks now = tick_clock_->NowTicks();

  if (it->second->ReportDrawnAndCheckForVideo(surface, now)) {
    // Avoid (re)starting the timer every frame since it has considerable
    // overhead.
    if (!video_inactive_timer_.IsRunning() ||
        (video_inactive_timer_.desired_run_time() - now) < kMinVideoTimeout) {
      video_inactive_timer_.Start(FROM_HERE, kMaxVideoTimeout, this,
                                  &VideoDetector::OnVideoActivityEnded);
    }
    if (!video_is_playing_) {
      video_is_playing_ = true;
      for (auto& observer : observers_) {
        observer->OnVideoActivityStarted();
      }
    }
  }
}

}  // namespace viz
