// Copyright 2018 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 "content/browser/media/capture/lame_window_capturer_chromeos.h"

#include <algorithm>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/location.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "media/base/limits.h"
#include "media/base/video_util.h"
#include "mojo/public/cpp/base/shared_memory_utils.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "ui/gfx/geometry/rect.h"

using media::VideoFrame;
using media::VideoFrameMetadata;

namespace content {

namespace {
// Returns |raw_size| with width and height truncated to even-numbered values.
gfx::Size AdjustSizeForI420Format(const gfx::Size& raw_size) {
  return gfx::Size(raw_size.width() & ~1, raw_size.height() & ~1);
}
}  // namespace

// static
constexpr base::TimeDelta LameWindowCapturerChromeOS::kAbsoluteMinCapturePeriod;

LameWindowCapturerChromeOS::LameWindowCapturerChromeOS(aura::Window* target)
    : target_(target),
      copy_request_source_(base::UnguessableToken::Create()),
      weak_factory_(this) {
  if (target_) {
    target_->AddObserver(this);
  }
}

LameWindowCapturerChromeOS::~LameWindowCapturerChromeOS() {
  if (target_) {
    target_->RemoveObserver(this);
  }
}

void LameWindowCapturerChromeOS::SetFormat(media::VideoPixelFormat format,
                                           media::ColorSpace color_space) {
  if (format != media::PIXEL_FORMAT_I420) {
    LOG(DFATAL) << "Invalid pixel format: Only I420 is supported.";
  }

  if (color_space != media::COLOR_SPACE_UNSPECIFIED &&
      color_space != media::COLOR_SPACE_HD_REC709) {
    LOG(DFATAL) << "Unsupported color space: Only BT.709 is supported.";
  }
}

void LameWindowCapturerChromeOS::SetMinCapturePeriod(
    base::TimeDelta min_capture_period) {
  capture_period_ = std::max(min_capture_period, kAbsoluteMinCapturePeriod);

  // If the capture period is being changed while the timer is already running,
  // re-start with the new capture period.
  if (timer_.IsRunning()) {
    timer_.Start(FROM_HERE, capture_period_, this,
                 &LameWindowCapturerChromeOS::CaptureNextFrame);
  }
}

void LameWindowCapturerChromeOS::SetMinSizeChangePeriod(
    base::TimeDelta min_period) {}

void LameWindowCapturerChromeOS::SetResolutionConstraints(
    const gfx::Size& min_size,
    const gfx::Size& max_size,
    bool use_fixed_aspect_ratio) {
  if (max_size.width() <= 1 || max_size.height() <= 1 ||
      max_size.width() > media::limits::kMaxDimension ||
      max_size.height() > media::limits::kMaxDimension) {
    LOG(DFATAL) << "Invalid max_size (" << max_size.ToString()
                << "): It must be within media::limits.";
    return;
  }

  // Set the capture size to the max size, adjusted for the I420 format.
  capture_size_ = AdjustSizeForI420Format(max_size);
  DCHECK(!capture_size_.IsEmpty());

  // Cancel any in-flight captures that would be using the old size and clear
  // the buffer pool.
  weak_factory_.InvalidateWeakPtrs();
  buffer_pool_.clear();
  in_flight_count_ = 0;
}

void LameWindowCapturerChromeOS::SetAutoThrottlingEnabled(bool enabled) {
  NOTIMPLEMENTED();
}

void LameWindowCapturerChromeOS::ChangeTarget(
    const base::Optional<viz::FrameSinkId>& frame_sink_id) {
  // The LameWindowCapturerChromeOS does not capture from compositor frame
  // sinks.
}

void LameWindowCapturerChromeOS::Start(
    viz::mojom::FrameSinkVideoConsumerPtr consumer) {
  DCHECK(consumer);

  Stop();

  consumer_ = std::move(consumer);
  // In the future, if the connection to the consumer is lost before a call to
  // Stop(), make that call on its behalf.
  consumer_.set_connection_error_handler(base::BindOnce(
      &LameWindowCapturerChromeOS::Stop, base::Unretained(this)));

  timer_.Start(FROM_HERE, capture_period_, this,
               &LameWindowCapturerChromeOS::CaptureNextFrame);
}

void LameWindowCapturerChromeOS::Stop() {
  // Stop the timer, cancel any in-flight frames, and clear the buffer pool.
  timer_.Stop();
  weak_factory_.InvalidateWeakPtrs();
  buffer_pool_.clear();
  in_flight_count_ = 0;

  if (consumer_) {
    consumer_->OnStopped();
    consumer_.reset();
  }
}

void LameWindowCapturerChromeOS::RequestRefreshFrame() {
  // This is ignored because the LameWindowCapturerChromeOS captures frames
  // continuously.
}

void LameWindowCapturerChromeOS::CreateOverlay(
    int32_t stacking_index,
    viz::mojom::FrameSinkVideoCaptureOverlayRequest request) {
  // LameWindowCapturerChromeOS only supports one overlay at a time. If one
  // already exists, the following will cause it to be dropped.
  overlay_ =
      std::make_unique<LameCaptureOverlayChromeOS>(this, std::move(request));
}

class LameWindowCapturerChromeOS::InFlightFrame
    : public viz::mojom::FrameSinkVideoConsumerFrameCallbacks {
 public:
  InFlightFrame(base::WeakPtr<LameWindowCapturerChromeOS> capturer,
                base::MappedReadOnlyRegion buffer)
      : capturer_(std::move(capturer)), buffer_(std::move(buffer)) {}

  ~InFlightFrame() final { Done(); }

  base::ReadOnlySharedMemoryRegion CloneBufferHandle() {
    return buffer_.region.Duplicate();
  }

  VideoFrame* video_frame() const { return video_frame_.get(); }
  void set_video_frame(scoped_refptr<VideoFrame> frame) {
    video_frame_ = std::move(frame);
  }

  const gfx::Rect& content_rect() const { return content_rect_; }
  void set_content_rect(const gfx::Rect& rect) { content_rect_ = rect; }

  void set_overlay_renderer(LameCaptureOverlayChromeOS::OnceRenderer renderer) {
    overlay_renderer_ = std::move(renderer);
  }
  void RenderOptionalOverlay() {
    if (overlay_renderer_) {
      std::move(overlay_renderer_).Run(video_frame_.get());
    }
  }

  void Done() final {
    video_frame_ = nullptr;

    if (auto* capturer = capturer_.get()) {
      DCHECK_GT(capturer->in_flight_count_, 0);
      --capturer->in_flight_count_;
      // If the capture size hasn't changed, return the buffer to the pool.
      if (buffer_.mapping.size() ==
          VideoFrame::AllocationSize(media::PIXEL_FORMAT_I420,
                                     capturer->capture_size_)) {
        capturer->buffer_pool_.emplace_back(std::move(buffer_));
      }
      capturer_ = nullptr;
    }

    buffer_ = base::MappedReadOnlyRegion();
  }

  void ProvideFeedback(double utilization) final {}

 private:
  base::WeakPtr<LameWindowCapturerChromeOS> capturer_;
  base::MappedReadOnlyRegion buffer_;
  scoped_refptr<VideoFrame> video_frame_;
  gfx::Rect content_rect_;
  LameCaptureOverlayChromeOS::OnceRenderer overlay_renderer_;

  DISALLOW_COPY_AND_ASSIGN(InFlightFrame);
};

void LameWindowCapturerChromeOS::OnOverlayConnectionLost(
    LameCaptureOverlayChromeOS* overlay) {
  if (overlay_.get() == overlay) {
    overlay_.reset();
  }
}

void LameWindowCapturerChromeOS::CaptureNextFrame() {
  // If the maximum frame in-flight count has been reached, skip this frame.
  if (in_flight_count_ >= kMaxFramesInFlight) {
    return;
  }

  // Attempt to re-use a buffer from the pool. Otherwise, create a new one.
  const size_t allocation_size =
      VideoFrame::AllocationSize(media::PIXEL_FORMAT_I420, capture_size_);
  base::MappedReadOnlyRegion buffer;
  if (buffer_pool_.empty()) {
    buffer = mojo::CreateReadOnlySharedMemoryRegion(allocation_size);
    if (!buffer.IsValid()) {
      // If the shared memory region creation failed, just abort this frame,
      // hoping the issue is a transient one (e.g., lack of an available region
      // in the address space).
      return;
    }
  } else {
    buffer = std::move(buffer_pool_.back());
    buffer_pool_.pop_back();
    DCHECK(buffer.IsValid());
    DCHECK_EQ(buffer.mapping.size(), allocation_size);
  }
  void* const backing_memory = buffer.mapping.memory();

  // At this point, frame capture will proceed. Create an InFlightFrame to track
  // population and consumption of the frame, and to eventually return the
  // buffer to the pool and decrement |in_flight_count_|.
  ++in_flight_count_;
  auto in_flight_frame = std::make_unique<InFlightFrame>(
      weak_factory_.GetWeakPtr(), std::move(buffer));

  // Create a VideoFrame that wraps the mapped buffer.
  const base::TimeTicks begin_time = base::TimeTicks::Now();
  if (first_frame_reference_time_.is_null()) {
    first_frame_reference_time_ = begin_time;
  }
  in_flight_frame->set_video_frame(VideoFrame::WrapExternalData(
      media::PIXEL_FORMAT_I420, capture_size_, gfx::Rect(capture_size_),
      capture_size_, static_cast<uint8_t*>(backing_memory), allocation_size,
      begin_time - first_frame_reference_time_));
  auto* const frame = in_flight_frame->video_frame();
  DCHECK(frame);
  VideoFrameMetadata* const metadata = frame->metadata();
  metadata->SetTimeTicks(VideoFrameMetadata::CAPTURE_BEGIN_TIME, begin_time);
  metadata->SetInteger(VideoFrameMetadata::COLOR_SPACE,
                       media::COLOR_SPACE_HD_REC709);
  metadata->SetTimeDelta(VideoFrameMetadata::FRAME_DURATION, capture_period_);
  metadata->SetDouble(VideoFrameMetadata::FRAME_RATE,
                      1.0 / capture_period_.InSecondsF());
  metadata->SetTimeTicks(VideoFrameMetadata::REFERENCE_TIME, begin_time);

  // Compute the region of the VideoFrame that will contain the content. If
  // there is nothing to copy from/to (e.g., the target is gone, or is sized too
  // small), send a blank black frame immediately.
  const gfx::Size source_size =
      target_ ? target_->bounds().size() : gfx::Size();
  const gfx::Rect content_rect = source_size.IsEmpty()
                                     ? gfx::Rect()
                                     : media::ComputeLetterboxRegionForI420(
                                           frame->visible_rect(), source_size);
  in_flight_frame->set_content_rect(content_rect);
  if (content_rect.IsEmpty()) {
    media::LetterboxVideoFrame(frame, gfx::Rect());
    DeliverFrame(std::move(in_flight_frame));
    return;
  }
  DCHECK(target_);

  if (overlay_) {
    in_flight_frame->set_overlay_renderer(overlay_->MakeRenderer(content_rect));
  }

  // Request a copy of the Layer associated with the |target_| aura::Window.
  auto request = std::make_unique<viz::CopyOutputRequest>(
      // Note: As of this writing, I420_PLANES is not supported external to VIZ.
      viz::CopyOutputRequest::ResultFormat::RGBA_BITMAP,
      base::BindOnce(&LameWindowCapturerChromeOS::DidCopyFrame,
                     weak_factory_.GetWeakPtr(), std::move(in_flight_frame)));
  request->set_source(copy_request_source_);
  request->set_area(gfx::Rect(source_size));
  request->SetScaleRatio(
      gfx::Vector2d(source_size.width(), source_size.height()),
      gfx::Vector2d(content_rect.width(), content_rect.height()));
  request->set_result_selection(gfx::Rect(content_rect.size()));
  target_->layer()->RequestCopyOfOutput(std::move(request));
}

void LameWindowCapturerChromeOS::DidCopyFrame(
    std::unique_ptr<InFlightFrame> in_flight_frame,
    std::unique_ptr<viz::CopyOutputResult> result) {
  // Populate the VideoFrame from the CopyOutputResult.
  auto* const frame = in_flight_frame->video_frame();
  DCHECK(frame);
  const auto& content_rect = in_flight_frame->content_rect();
  const int y_stride = frame->stride(VideoFrame::kYPlane);
  uint8_t* const y = frame->visible_data(VideoFrame::kYPlane) +
                     content_rect.y() * y_stride + content_rect.x();
  const int u_stride = frame->stride(VideoFrame::kUPlane);
  uint8_t* const u = frame->visible_data(VideoFrame::kUPlane) +
                     (content_rect.y() / 2) * u_stride + (content_rect.x() / 2);
  const int v_stride = frame->stride(VideoFrame::kVPlane);
  uint8_t* const v = frame->visible_data(VideoFrame::kVPlane) +
                     (content_rect.y() / 2) * v_stride + (content_rect.x() / 2);
  if (!result->ReadI420Planes(y, y_stride, u, u_stride, v, v_stride)) {
    return;  // Copy request failed, punt.
  }

  in_flight_frame->RenderOptionalOverlay();

  // The result may be smaller than what was requested, if unforeseen clamping
  // to the source boundaries occurred by the executor of the copy request.
  // However, the result should never contain more than what was requested.
  DCHECK_LE(result->size().width(), content_rect.width());
  DCHECK_LE(result->size().height(), content_rect.height());
  media::LetterboxVideoFrame(
      frame, gfx::Rect(content_rect.origin(),
                       AdjustSizeForI420Format(result->size())));

  DeliverFrame(std::move(in_flight_frame));
}

void LameWindowCapturerChromeOS::DeliverFrame(
    std::unique_ptr<InFlightFrame> in_flight_frame) {
  auto* const frame = in_flight_frame->video_frame();
  DCHECK(frame);
  frame->metadata()->SetTimeTicks(VideoFrameMetadata::CAPTURE_END_TIME,
                                  base::TimeTicks::Now());

  // Clone the buffer handle for the consumer.
  base::ReadOnlySharedMemoryRegion handle =
      in_flight_frame->CloneBufferHandle();
  if (!handle.IsValid()) {
    return;  // This should only fail if the OS is exhausted of handles.
  }

  // Assemble frame layout, format, and metadata into a mojo struct to send to
  // the consumer.
  media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New();
  info->timestamp = frame->timestamp();
  info->metadata = frame->metadata()->GetInternalValues().Clone();
  info->pixel_format = frame->format();
  info->coded_size = frame->coded_size();
  info->visible_rect = frame->visible_rect();
  const gfx::Rect update_rect = frame->visible_rect();
  const gfx::Rect content_rect = in_flight_frame->content_rect();

  // Create a mojo message pipe and bind to the InFlightFrame to wait for the
  // Done() signal from the consumer. The mojo::StrongBinding takes ownership of
  // the InFlightFrame.
  viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks;
  mojo::MakeStrongBinding(std::move(in_flight_frame),
                          mojo::MakeRequest(&callbacks));

  // Send the frame to the consumer.
  consumer_->OnFrameCaptured(std::move(handle), std::move(info), update_rect,
                             content_rect, std::move(callbacks));
}

void LameWindowCapturerChromeOS::OnWindowDestroying(aura::Window* window) {
  if (window == target_) {
    target_->RemoveObserver(this);
    target_ = nullptr;
    // The capturer may continue running, but it will notice the target is gone
    // and produce blank black frames hereafter.
  }
}

}  // namespace content
