// Copyright (c) 2013 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/desktop_capture_device.h"

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

#include "base/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "content/browser/media/capture/desktop_capture_device_uma_types.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/browser/power_save_blocker.h"
#include "media/base/video_util.h"
#include "media/capture/content/capture_resolution_chooser.h"
#include "third_party/libyuv/include/libyuv/scale_argb.h"
#include "third_party/webrtc/modules/desktop_capture/cropping_window_capturer.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_and_cursor_composer.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
#include "third_party/webrtc/modules/desktop_capture/mouse_cursor_monitor.h"
#include "third_party/webrtc/modules/desktop_capture/screen_capturer.h"

namespace content {

namespace {

// Maximum CPU time percentage of a single core that can be consumed for desktop
// capturing. This means that on systems where screen scraping is slow we may
// need to capture at frame rate lower than requested. This is necessary to keep
// UI responsive.
const int kMaximumCpuConsumptionPercentage = 50;

webrtc::DesktopRect ComputeLetterboxRect(
    const webrtc::DesktopSize& max_size,
    const webrtc::DesktopSize& source_size) {
  gfx::Rect result = media::ComputeLetterboxRegion(
      gfx::Rect(0, 0, max_size.width(), max_size.height()),
      gfx::Size(source_size.width(), source_size.height()));
  return webrtc::DesktopRect::MakeLTRB(
      result.x(), result.y(), result.right(), result.bottom());
}

bool IsFrameUnpackedOrInverted(webrtc::DesktopFrame* frame) {
  return frame->stride() !=
      frame->size().width() * webrtc::DesktopFrame::kBytesPerPixel;
}

}  // namespace

class DesktopCaptureDevice::Core : public webrtc::DesktopCapturer::Callback {
 public:
  Core(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
       scoped_ptr<webrtc::DesktopCapturer> capturer,
       DesktopMediaID::Type type);
  ~Core() override;

  // Implementation of VideoCaptureDevice methods.
  void AllocateAndStart(const media::VideoCaptureParams& params,
                        scoped_ptr<Client> client);

  void SetNotificationWindowId(gfx::NativeViewId window_id);

 private:

  // webrtc::DesktopCapturer::Callback interface
  webrtc::SharedMemory* CreateSharedMemory(size_t size) override;
  void OnCaptureCompleted(webrtc::DesktopFrame* frame) override;

  // Method that is scheduled on |task_runner_| to be called on regular interval
  // to capture a frame.
  void OnCaptureTimer();

  // Captures a frame and schedules timer for the next one.
  void CaptureFrameAndScheduleNext();

  // Captures a single frame.
  void DoCapture();

  // Task runner used for capturing operations.
  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;

  // The underlying DesktopCapturer instance used to capture frames.
  scoped_ptr<webrtc::DesktopCapturer> desktop_capturer_;

  // The device client which proxies device events to the controller. Accessed
  // on the task_runner_ thread.
  scoped_ptr<Client> client_;

  // Requested video capture frame rate.
  float requested_frame_rate_;

  // Size of frame most recently captured from the source.
  webrtc::DesktopSize previous_frame_size_;

  // Determines the size of frames to deliver to the |client_|.
  scoped_ptr<media::CaptureResolutionChooser> resolution_chooser_;

  // DesktopFrame into which captured frames are down-scaled and/or letterboxed,
  // depending upon the caller's requested capture capabilities. If frames can
  // be returned to the caller directly then this is NULL.
  scoped_ptr<webrtc::DesktopFrame> output_frame_;

  // Timer used to capture the frame.
  base::OneShotTimer capture_timer_;

  // True when waiting for |desktop_capturer_| to capture current frame.
  bool capture_in_progress_;

  // True if the first capture call has returned. Used to log the first capture
  // result.
  bool first_capture_returned_;

  // The type of the capturer.
  DesktopMediaID::Type capturer_type_;

  scoped_ptr<webrtc::BasicDesktopFrame> black_frame_;

  // TODO(jiayl): Remove power_save_blocker_ when there is an API to keep the
  // screen from sleeping for the drive-by web.
  scoped_ptr<PowerSaveBlocker> power_save_blocker_;

  DISALLOW_COPY_AND_ASSIGN(Core);
};

DesktopCaptureDevice::Core::Core(
    scoped_refptr<base::SingleThreadTaskRunner> task_runner,
    scoped_ptr<webrtc::DesktopCapturer> capturer,
    DesktopMediaID::Type type)
    : task_runner_(task_runner),
      desktop_capturer_(std::move(capturer)),
      capture_in_progress_(false),
      first_capture_returned_(false),
      capturer_type_(type) {}

DesktopCaptureDevice::Core::~Core() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  client_.reset();
  output_frame_.reset();
  previous_frame_size_.set(0, 0);
  desktop_capturer_.reset();
}

void DesktopCaptureDevice::Core::AllocateAndStart(
    const media::VideoCaptureParams& params,
    scoped_ptr<Client> client) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_GT(params.requested_format.frame_size.GetArea(), 0);
  DCHECK_GT(params.requested_format.frame_rate, 0);
  DCHECK(desktop_capturer_);
  DCHECK(client.get());
  DCHECK(!client_.get());

  client_ = std::move(client);
  requested_frame_rate_ = params.requested_format.frame_rate;
  resolution_chooser_.reset(new media::CaptureResolutionChooser(
      params.requested_format.frame_size,
      params.resolution_change_policy));

  power_save_blocker_.reset(
      PowerSaveBlocker::Create(
          PowerSaveBlocker::kPowerSaveBlockPreventDisplaySleep,
          PowerSaveBlocker::kReasonOther,
          "DesktopCaptureDevice is running").release());

  desktop_capturer_->Start(this);

  CaptureFrameAndScheduleNext();
}

void DesktopCaptureDevice::Core::SetNotificationWindowId(
    gfx::NativeViewId window_id) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(window_id);
  desktop_capturer_->SetExcludedWindow(window_id);
}

webrtc::SharedMemory*
DesktopCaptureDevice::Core::CreateSharedMemory(size_t size) {
  return NULL;
}

void DesktopCaptureDevice::Core::OnCaptureCompleted(
    webrtc::DesktopFrame* frame) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(capture_in_progress_);

  if (!first_capture_returned_) {
    first_capture_returned_ = true;
    if (capturer_type_ == DesktopMediaID::TYPE_SCREEN) {
      IncrementDesktopCaptureCounter(frame ? FIRST_SCREEN_CAPTURE_SUCCEEDED
                                           : FIRST_SCREEN_CAPTURE_FAILED);
    } else {
      IncrementDesktopCaptureCounter(frame ? FIRST_WINDOW_CAPTURE_SUCCEEDED
                                           : FIRST_WINDOW_CAPTURE_FAILED);
    }
  }

  capture_in_progress_ = false;

  if (!frame) {
    client_->OnError(FROM_HERE, "Failed to capture a frame.");
    return;
  }

  if (!client_)
    return;

  base::TimeDelta capture_time(
      base::TimeDelta::FromMilliseconds(frame->capture_time_ms()));

  // The two UMA_ blocks must be put in its own scope since it creates a static
  // variable which expected constant histogram name.
  if (capturer_type_ == DesktopMediaID::TYPE_SCREEN) {
    UMA_HISTOGRAM_TIMES(kUmaScreenCaptureTime, capture_time);
  } else {
    UMA_HISTOGRAM_TIMES(kUmaWindowCaptureTime, capture_time);
  }

  scoped_ptr<webrtc::DesktopFrame> owned_frame(frame);

  // If the frame size has changed, drop the output frame (if any), and
  // determine the new output size.
  if (!previous_frame_size_.equals(frame->size())) {
    output_frame_.reset();
    resolution_chooser_->SetSourceSize(gfx::Size(frame->size().width(),
                                                 frame->size().height()));
    previous_frame_size_ = frame->size();
  }
  // Align to 2x2 pixel boundaries, as required by OnIncomingCapturedData() so
  // it can convert the frame to I420 format.
  const webrtc::DesktopSize output_size(
      resolution_chooser_->capture_size().width() & ~1,
      resolution_chooser_->capture_size().height() & ~1);
  if (output_size.is_empty())
    return;

  // On OSX We receive a 1x1 frame when the shared window is minimized. It
  // cannot be subsampled to I420 and will be dropped downstream. So we replace
  // it with a black frame to avoid the video appearing frozen at the last
  // frame.
  if (frame->size().width() == 1 || frame->size().height() == 1) {
    if (!black_frame_.get()) {
      black_frame_.reset(new webrtc::BasicDesktopFrame(output_size));
      memset(black_frame_->data(),
             0,
             black_frame_->stride() * black_frame_->size().height());
    }
    owned_frame.reset();
    frame = black_frame_.get();
  }

  size_t output_bytes = output_size.width() * output_size.height() *
      webrtc::DesktopFrame::kBytesPerPixel;
  const uint8_t* output_data = NULL;

  if (!frame->size().equals(output_size)) {
    // Down-scale and/or letterbox to the target format if the frame does not
    // match the output size.

    // Allocate a buffer of the correct size to scale the frame into.
    // |output_frame_| is cleared whenever the output size changes, so we don't
    // need to worry about clearing out stale pixel data in letterboxed areas.
    if (!output_frame_) {
      output_frame_.reset(new webrtc::BasicDesktopFrame(output_size));
      memset(output_frame_->data(), 0, output_bytes);
    }
    DCHECK(output_frame_->size().equals(output_size));

    // TODO(wez): Optimize this to scale only changed portions of the output,
    // using ARGBScaleClip().
    const webrtc::DesktopRect output_rect =
        ComputeLetterboxRect(output_size, frame->size());
    uint8_t* output_rect_data = output_frame_->data() +
        output_frame_->stride() * output_rect.top() +
        webrtc::DesktopFrame::kBytesPerPixel * output_rect.left();
    libyuv::ARGBScale(frame->data(), frame->stride(),
                      frame->size().width(), frame->size().height(),
                      output_rect_data, output_frame_->stride(),
                      output_rect.width(), output_rect.height(),
                      libyuv::kFilterBilinear);
    output_data = output_frame_->data();
  } else if (IsFrameUnpackedOrInverted(frame)) {
    // If |frame| is not packed top-to-bottom then create a packed top-to-bottom
    // copy.
    // This is required if the frame is inverted (see crbug.com/306876), or if
    // |frame| is cropped form a larger frame (see crbug.com/437740).
    if (!output_frame_) {
      output_frame_.reset(new webrtc::BasicDesktopFrame(output_size));
      memset(output_frame_->data(), 0, output_bytes);
    }

    output_frame_->CopyPixelsFrom(
        *frame,
        webrtc::DesktopVector(),
        webrtc::DesktopRect::MakeSize(frame->size()));
    output_data = output_frame_->data();
  } else {
    // If the captured frame matches the output size, we can return the pixel
    // data directly.
    output_data = frame->data();
  }

  client_->OnIncomingCapturedData(
      output_data, output_bytes,
      media::VideoCaptureFormat(
          gfx::Size(output_size.width(), output_size.height()),
          requested_frame_rate_, media::PIXEL_FORMAT_ARGB),
      0, base::TimeTicks::Now());
}

void DesktopCaptureDevice::Core::OnCaptureTimer() {
  DCHECK(task_runner_->BelongsToCurrentThread());

  if (!client_)
    return;

  CaptureFrameAndScheduleNext();
}

void DesktopCaptureDevice::Core::CaptureFrameAndScheduleNext() {
  DCHECK(task_runner_->BelongsToCurrentThread());

  base::TimeTicks started_time = base::TimeTicks::Now();
  DoCapture();
  base::TimeDelta last_capture_duration = base::TimeTicks::Now() - started_time;

  // Limit frame-rate to reduce CPU consumption.
  base::TimeDelta capture_period = std::max(
      (last_capture_duration * 100) / kMaximumCpuConsumptionPercentage,
      base::TimeDelta::FromMicroseconds(static_cast<int64_t>(
          1000000.0 / requested_frame_rate_ + 0.5 /* round to nearest int */)));

  // Schedule a task for the next frame.
  capture_timer_.Start(FROM_HERE, capture_period - last_capture_duration,
                       this, &Core::OnCaptureTimer);
}

void DesktopCaptureDevice::Core::DoCapture() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(!capture_in_progress_);

  capture_in_progress_ = true;
  desktop_capturer_->Capture(webrtc::DesktopRegion());

  // Currently only synchronous implementations of DesktopCapturer are
  // supported.
  DCHECK(!capture_in_progress_);
}

// static
scoped_ptr<media::VideoCaptureDevice> DesktopCaptureDevice::Create(
    const DesktopMediaID& source) {
  webrtc::DesktopCaptureOptions options =
      webrtc::DesktopCaptureOptions::CreateDefault();
  // Leave desktop effects enabled during WebRTC captures.
  options.set_disable_effects(false);

#if defined(OS_WIN)
      options.set_allow_use_magnification_api(true);
#endif

  scoped_ptr<webrtc::DesktopCapturer> capturer;

  switch (source.type) {
    case DesktopMediaID::TYPE_SCREEN: {
      scoped_ptr<webrtc::ScreenCapturer> screen_capturer(
          webrtc::ScreenCapturer::Create(options));
      if (screen_capturer && screen_capturer->SelectScreen(source.id)) {
        capturer.reset(new webrtc::DesktopAndCursorComposer(
            screen_capturer.release(),
            webrtc::MouseCursorMonitor::CreateForScreen(options, source.id)));
        IncrementDesktopCaptureCounter(SCREEN_CAPTURER_CREATED);
      }
      break;
    }

    case DesktopMediaID::TYPE_WINDOW: {
      scoped_ptr<webrtc::WindowCapturer> window_capturer(
          webrtc::CroppingWindowCapturer::Create(options));
      if (window_capturer && window_capturer->SelectWindow(source.id)) {
        window_capturer->BringSelectedWindowToFront();
        capturer.reset(new webrtc::DesktopAndCursorComposer(
            window_capturer.release(),
            webrtc::MouseCursorMonitor::CreateForWindow(options, source.id)));
        IncrementDesktopCaptureCounter(WINDOW_CAPTURER_CREATED);
      }
      break;
    }

    default: {
      NOTREACHED();
    }
  }

  scoped_ptr<media::VideoCaptureDevice> result;
  if (capturer)
    result.reset(new DesktopCaptureDevice(std::move(capturer), source.type));

  return result;
}

DesktopCaptureDevice::~DesktopCaptureDevice() {
  DCHECK(!core_);
}

void DesktopCaptureDevice::AllocateAndStart(
    const media::VideoCaptureParams& params,
    scoped_ptr<Client> client) {
  thread_.task_runner()->PostTask(
      FROM_HERE,
      base::Bind(&Core::AllocateAndStart, base::Unretained(core_.get()), params,
                 base::Passed(&client)));
}

void DesktopCaptureDevice::StopAndDeAllocate() {
  if (core_) {
    thread_.task_runner()->DeleteSoon(FROM_HERE, core_.release());
    thread_.Stop();
  }
}

void DesktopCaptureDevice::SetNotificationWindowId(
    gfx::NativeViewId window_id) {
  // This may be called after the capturer has been stopped.
  if (!core_)
    return;
  thread_.task_runner()->PostTask(
      FROM_HERE,
      base::Bind(&Core::SetNotificationWindowId,
                 base::Unretained(core_.get()),
                 window_id));
}

DesktopCaptureDevice::DesktopCaptureDevice(
    scoped_ptr<webrtc::DesktopCapturer> capturer,
    DesktopMediaID::Type type)
    : thread_("desktopCaptureThread") {
#if defined(OS_WIN)
  // On Windows the thread must be a UI thread.
  base::MessageLoop::Type thread_type = base::MessageLoop::TYPE_UI;
#else
  base::MessageLoop::Type thread_type = base::MessageLoop::TYPE_DEFAULT;
#endif

  thread_.StartWithOptions(base::Thread::Options(thread_type, 0));

  core_.reset(new Core(thread_.task_runner(), std::move(capturer), type));
}

}  // namespace content
