// Copyright 2014 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/renderer/media/media_stream_video_capturer_source.h"

#include <utility>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "content/public/common/media_stream_request.h"
#include "content/renderer/media/media_stream_constraints_util.h"
#include "content/renderer/media/video_capture_impl_manager.h"
#include "content/renderer/render_thread_impl.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/limits.h"
#include "media/base/video_capturer_source.h"
#include "media/base/video_frame.h"

namespace content {

namespace {

// Resolutions used if the source doesn't support capability enumeration.
struct {
  int width;
  int height;
} const kVideoResolutions[] = {{1920, 1080},
                               {1280, 720},
                               {960, 720},
                               {640, 480},
                               {640, 360},
                               {320, 240},
                               {320, 180}};

// Frame rates for sources with no support for capability enumeration.
const int kVideoFrameRates[] = {30, 60};

// Hard upper-bound frame rate for tab/desktop capture.
const double kMaxScreenCastFrameRate = 120.0;

// Allows the user to Override default power line frequency.
const char kPowerLineFrequency[] = "googPowerLineFrequency";

// Returns true if the value for width or height is reasonable.
bool DimensionValueIsValid(int x) {
  return x > 0 && x <= media::limits::kMaxDimension;
}

// Returns true if the value for frame rate is reasonable.
bool FrameRateValueIsValid(double frame_rate) {
  return (frame_rate > (1.0 / 60.0)) &&  // Lower-bound: One frame per minute.
      (frame_rate <= media::limits::kMaxFramesPerSecond);
}

// Returns true if the aspect ratio of |a| and |b| are equivalent to two
// significant digits.
bool AreNearlyEquivalentInAspectRatio(const gfx::Size& a, const gfx::Size& b) {
  DCHECK(!a.IsEmpty());
  DCHECK(!b.IsEmpty());
  const int aspect_ratio_a = (100 * a.width()) / a.height();
  const int aspect_ratio_b = (100 * b.width()) / b.height();
  return aspect_ratio_a == aspect_ratio_b;
}

// Checks if |device_info|s type is a generated content, e.g. Tab or Desktop.
bool IsContentVideoCaptureDevice(const StreamDeviceInfo& device_info) {
  return device_info.device.type == MEDIA_TAB_VIDEO_CAPTURE ||
         device_info.device.type == MEDIA_DESKTOP_VIDEO_CAPTURE;
}

// Interprets the properties in |constraints| to override values in |params| and
// determine the resolution change policy.
void SetContentCaptureParamsFromConstraints(
    const blink::WebMediaConstraints& constraints,
    MediaStreamType type,
    media::VideoCaptureParams* params) {
  // The default resolution change policies for tab versus desktop capture are
  // the way they are for legacy reasons.
  if (type == MEDIA_TAB_VIDEO_CAPTURE) {
    params->resolution_change_policy =
        media::RESOLUTION_POLICY_FIXED_RESOLUTION;
  } else if (type == MEDIA_DESKTOP_VIDEO_CAPTURE) {
    params->resolution_change_policy =
        media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT;
  } else {
    NOTREACHED();
  }

  // If the maximum frame resolution was provided in the constraints, use it if
  // either: 1) none has been set yet; or 2) the maximum specificed is smaller
  // than the current setting.
  int width = 0;
  int height = 0;
  gfx::Size desired_max_frame_size;
  if (GetConstraintValueAsInteger(constraints,
                                  MediaStreamVideoSource::kMaxWidth,
                                  &width) &&
      GetConstraintValueAsInteger(constraints,
                                  MediaStreamVideoSource::kMaxHeight,
                                  &height) &&
      DimensionValueIsValid(width) &&
      DimensionValueIsValid(height)) {
    desired_max_frame_size.SetSize(width, height);
    if (params->requested_format.frame_size.IsEmpty() ||
        desired_max_frame_size.width() <
            params->requested_format.frame_size.width() ||
        desired_max_frame_size.height() <
            params->requested_format.frame_size.height()) {
      params->requested_format.frame_size = desired_max_frame_size;
    }
  }

  // Set the default frame resolution if none was provided.
  if (params->requested_format.frame_size.IsEmpty()) {
    params->requested_format.frame_size.SetSize(
        MediaStreamVideoSource::kDefaultWidth,
        MediaStreamVideoSource::kDefaultHeight);
  }

  // If the maximum frame rate was provided, use it if either: 1) none has been
  // set yet; or 2) the maximum specificed is smaller than the current setting.
  double frame_rate = 0.0;
  if (GetConstraintValueAsDouble(constraints,
                                 MediaStreamVideoSource::kMaxFrameRate,
                                 &frame_rate) &&
      FrameRateValueIsValid(frame_rate)) {
    if (params->requested_format.frame_rate <= 0.0f ||
        frame_rate < params->requested_format.frame_rate) {
      params->requested_format.frame_rate = frame_rate;
    }
  }

  // Set the default frame rate if none was provided.
  if (params->requested_format.frame_rate <= 0.0f) {
    params->requested_format.frame_rate =
        MediaStreamVideoSource::kDefaultFrameRate;
  }

  // If the minimum frame resolution was provided, compare it to the maximum
  // frame resolution to determine the intended resolution change policy.
  if (!desired_max_frame_size.IsEmpty() &&
      GetConstraintValueAsInteger(constraints,
                                  MediaStreamVideoSource::kMinWidth,
                                  &width) &&
      GetConstraintValueAsInteger(constraints,
                                  MediaStreamVideoSource::kMinHeight,
                                  &height) &&
      width <= desired_max_frame_size.width() &&
      height <= desired_max_frame_size.height()) {
    if (width == desired_max_frame_size.width() &&
        height == desired_max_frame_size.height()) {
      // Constraints explicitly require a single frame resolution.
      params->resolution_change_policy =
          media::RESOLUTION_POLICY_FIXED_RESOLUTION;
    } else if (DimensionValueIsValid(width) &&
               DimensionValueIsValid(height) &&
               AreNearlyEquivalentInAspectRatio(gfx::Size(width, height),
                                                desired_max_frame_size)) {
      // Constraints only mention a single aspect ratio.
      params->resolution_change_policy =
          media::RESOLUTION_POLICY_FIXED_ASPECT_RATIO;
    } else {
      // Constraints specify a minimum resolution that is smaller than the
      // maximum resolution and has a different aspect ratio (possibly even
      // 0x0). This indicates any frame resolution and aspect ratio is
      // acceptable.
      params->resolution_change_policy =
          media::RESOLUTION_POLICY_ANY_WITHIN_LIMIT;
    }
  }

  DVLOG(1) << __FUNCTION__ << " "
           << media::VideoCaptureFormat::ToString(params->requested_format)
           << " with resolution change policy "
           << params->resolution_change_policy;
}

// Interprets the properties in |constraints| to override values in |params| and
// determine the power line frequency.
void SetPowerLineFrequencyParamFromConstraints(
    const blink::WebMediaConstraints& constraints,
    media::VideoCaptureParams* params) {
  int freq;
  params->power_line_frequency = media::PowerLineFrequency::FREQUENCY_DEFAULT;
  if (!GetOptionalConstraintValueAsInteger(constraints, kPowerLineFrequency,
                                           &freq)) {
    return;
  }
  if (freq == static_cast<int>(media::PowerLineFrequency::FREQUENCY_50HZ))
    params->power_line_frequency = media::PowerLineFrequency::FREQUENCY_50HZ;
  else if (freq == static_cast<int>(media::PowerLineFrequency::FREQUENCY_60HZ))
    params->power_line_frequency = media::PowerLineFrequency::FREQUENCY_60HZ;
}

// LocalVideoCapturerSource is a delegate used by MediaStreamVideoCapturerSource
// for local video capture. It uses the Render singleton VideoCaptureImplManager
// to start / stop and receive I420 frames from Chrome's video capture
// implementation. This is a main Render thread only object.
class LocalVideoCapturerSource final : public media::VideoCapturerSource {
 public:
  explicit LocalVideoCapturerSource(const StreamDeviceInfo& device_info);
  ~LocalVideoCapturerSource() override;

  // VideoCaptureDelegate Implementation.
  void GetCurrentSupportedFormats(
      int max_requested_width,
      int max_requested_height,
      double max_requested_frame_rate,
      const VideoCaptureDeviceFormatsCB& callback) override;
  void StartCapture(const media::VideoCaptureParams& params,
                    const VideoCaptureDeliverFrameCB& new_frame_callback,
                    const RunningCallback& running_callback) override;
  void StopCapture() override;

 private:
  void OnStateUpdate(VideoCaptureState state);
  void OnDeviceFormatsInUseReceived(const media::VideoCaptureFormats& formats);
  void OnDeviceSupportedFormatsEnumerated(
      const media::VideoCaptureFormats& formats);

  // |session_id_| identifies the capture device used for this capture session.
  const media::VideoCaptureSessionId session_id_;

  VideoCaptureImplManager* const manager_;

  const base::Closure release_device_cb_;

  // Indicates if we are capturing generated content, e.g. Tab or Desktop.
  const bool is_content_capture_;

  // These two are valid between StartCapture() and StopCapture().
  base::Closure stop_capture_cb_;
  RunningCallback running_callback_;

  // Placeholder keeping the callback between asynchronous device enumeration
  // calls.
  VideoCaptureDeviceFormatsCB formats_enumerated_callback_;

  // Bound to the main render thread.
  base::ThreadChecker thread_checker_;

  base::WeakPtrFactory<LocalVideoCapturerSource> weak_factory_;

  DISALLOW_COPY_AND_ASSIGN(LocalVideoCapturerSource);
};

}  // namespace

LocalVideoCapturerSource::LocalVideoCapturerSource(
    const StreamDeviceInfo& device_info)
    : session_id_(device_info.session_id),
      manager_(RenderThreadImpl::current()->video_capture_impl_manager()),
      release_device_cb_(manager_->UseDevice(session_id_)),
      is_content_capture_(IsContentVideoCaptureDevice(device_info)),
      weak_factory_(this) {
  DCHECK(RenderThreadImpl::current());
}

LocalVideoCapturerSource::~LocalVideoCapturerSource() {
  DCHECK(thread_checker_.CalledOnValidThread());
  release_device_cb_.Run();
}

void LocalVideoCapturerSource::GetCurrentSupportedFormats(
    int max_requested_width,
    int max_requested_height,
    double max_requested_frame_rate,
    const VideoCaptureDeviceFormatsCB& callback) {
  DVLOG(3) << "GetCurrentSupportedFormats({ max_requested_height = "
           << max_requested_height << "}) { max_requested_width = "
           << max_requested_width << "}) { max_requested_frame_rate = "
           << max_requested_frame_rate << "})";
  DCHECK(thread_checker_.CalledOnValidThread());

  if (is_content_capture_) {
    const int width = max_requested_width ?
        max_requested_width : MediaStreamVideoSource::kDefaultWidth;
    const int height = max_requested_height ?
        max_requested_height : MediaStreamVideoSource::kDefaultHeight;
    callback.Run(media::VideoCaptureFormats(
        1, media::VideoCaptureFormat(
               gfx::Size(width, height),
               static_cast<float>(
                   std::min(kMaxScreenCastFrameRate, max_requested_frame_rate)),
               media::PIXEL_FORMAT_I420)));
    return;
  }

  DCHECK(formats_enumerated_callback_.is_null());
  formats_enumerated_callback_ = callback;
  manager_->GetDeviceFormatsInUse(
      session_id_, media::BindToCurrentLoop(base::Bind(
                       &LocalVideoCapturerSource::OnDeviceFormatsInUseReceived,
                       weak_factory_.GetWeakPtr())));
}

void LocalVideoCapturerSource::StartCapture(
    const media::VideoCaptureParams& params,
    const VideoCaptureDeliverFrameCB& new_frame_callback,
    const RunningCallback& running_callback) {
  DCHECK(params.requested_format.IsValid());
  DCHECK(thread_checker_.CalledOnValidThread());
  running_callback_ = running_callback;

  stop_capture_cb_ = manager_->StartCapture(
      session_id_, params, media::BindToCurrentLoop(base::Bind(
                               &LocalVideoCapturerSource::OnStateUpdate,
                               weak_factory_.GetWeakPtr())),
      new_frame_callback);
}

void LocalVideoCapturerSource::StopCapture() {
  DVLOG(3) << __FUNCTION__;
  DCHECK(thread_checker_.CalledOnValidThread());
  // Immediately make sure we don't provide more frames.
  if (!stop_capture_cb_.is_null())
    base::ResetAndReturn(&stop_capture_cb_).Run();
  running_callback_.Reset();
  // Invalidate any potential format enumerations going on.
  formats_enumerated_callback_.Reset();
}

void LocalVideoCapturerSource::OnStateUpdate(VideoCaptureState state) {
  DVLOG(3) << __FUNCTION__ << " state = " << state;
  DCHECK(thread_checker_.CalledOnValidThread());
  if (running_callback_.is_null())
    return;
  const bool is_started_ok = state == VIDEO_CAPTURE_STATE_STARTED;
  running_callback_.Run(is_started_ok);
  if (!is_started_ok)
    running_callback_.Reset();
}

void LocalVideoCapturerSource::OnDeviceFormatsInUseReceived(
    const media::VideoCaptureFormats& formats_in_use) {
  DVLOG(3) << __FUNCTION__ << ", #formats received: " << formats_in_use.size();
  DCHECK(thread_checker_.CalledOnValidThread());
  // StopCapture() might have destroyed |formats_enumerated_callback_| before
  // arriving here.
  if (formats_enumerated_callback_.is_null())
    return;
  if (formats_in_use.size()) {
    base::ResetAndReturn(&formats_enumerated_callback_).Run(formats_in_use);
    return;
  }

  // The device doesn't seem to have formats in use so try and retrieve the
  // whole list of supported ones.
  manager_->GetDeviceSupportedFormats(
      session_id_,
      media::BindToCurrentLoop(
          base::Bind(
              &LocalVideoCapturerSource::OnDeviceSupportedFormatsEnumerated,
              weak_factory_.GetWeakPtr())));
}

void LocalVideoCapturerSource::OnDeviceSupportedFormatsEnumerated(
    const media::VideoCaptureFormats& formats) {
  DVLOG(3) << __FUNCTION__ << ", #formats received: " << formats.size();
  DCHECK(thread_checker_.CalledOnValidThread());
  // StopCapture() might have destroyed |formats_enumerated_callback_| before
  // arriving here.
  if (formats_enumerated_callback_.is_null())
    return;
  if (formats.size()) {
    base::ResetAndReturn(&formats_enumerated_callback_).Run(formats);
    return;
  }

  // The capture device doesn't seem to support capability enumeration, compose
  // a fallback list of capabilities.
  media::VideoCaptureFormats default_formats;
  for (const auto& resolution : kVideoResolutions) {
    for (const auto frame_rate : kVideoFrameRates) {
      default_formats.push_back(media::VideoCaptureFormat(
          gfx::Size(resolution.width, resolution.height), frame_rate,
          media::PIXEL_FORMAT_I420));
    }
  }
  base::ResetAndReturn(&formats_enumerated_callback_).Run(default_formats);
}

MediaStreamVideoCapturerSource::MediaStreamVideoCapturerSource(
    const SourceStoppedCallback& stop_callback,
    scoped_ptr<media::VideoCapturerSource> source)
    : source_(std::move(source)) {
  SetStopCallback(stop_callback);
}

MediaStreamVideoCapturerSource::MediaStreamVideoCapturerSource(
    const SourceStoppedCallback& stop_callback,
    const StreamDeviceInfo& device_info)
    : source_(new LocalVideoCapturerSource(device_info)) {
  SetStopCallback(stop_callback);
  SetDeviceInfo(device_info);
}

MediaStreamVideoCapturerSource::~MediaStreamVideoCapturerSource() {
}

void MediaStreamVideoCapturerSource::GetCurrentSupportedFormats(
    int max_requested_width,
    int max_requested_height,
    double max_requested_frame_rate,
    const VideoCaptureDeviceFormatsCB& callback) {
  source_->GetCurrentSupportedFormats(
      max_requested_width,
      max_requested_height,
      max_requested_frame_rate,
      callback);
}

void MediaStreamVideoCapturerSource::StartSourceImpl(
    const media::VideoCaptureFormat& format,
    const blink::WebMediaConstraints& constraints,
    const VideoCaptureDeliverFrameCB& frame_callback) {
  media::VideoCaptureParams new_params;
  new_params.requested_format = format;
  if (IsContentVideoCaptureDevice(device_info())) {
    SetContentCaptureParamsFromConstraints(
        constraints, device_info().device.type, &new_params);
  } else if (device_info().device.type == MEDIA_DEVICE_VIDEO_CAPTURE) {
    SetPowerLineFrequencyParamFromConstraints(constraints, &new_params);
  }

  source_->StartCapture(new_params,
                          frame_callback,
                          base::Bind(&MediaStreamVideoCapturerSource::OnStarted,
                                     base::Unretained(this)));
}

void MediaStreamVideoCapturerSource::StopSourceImpl() {
  source_->StopCapture();
}

void MediaStreamVideoCapturerSource::OnStarted(bool result) {
  OnStartDone(result ? MEDIA_DEVICE_OK : MEDIA_DEVICE_TRACK_START_FAILURE);
}

const char*
MediaStreamVideoCapturerSource::GetPowerLineFrequencyForTesting() const {
  return kPowerLineFrequency;
}

}  // namespace content
