// 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 "third_party/blink/renderer/modules/mediarecorder/media_recorder.h"

#include <algorithm>
#include <limits>

#include "base/time/time.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/dictionary.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_error_event_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_recording_state.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/events/error_event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/timing/dom_window_performance.h"
#include "third_party/blink/renderer/core/timing/window_performance.h"
#include "third_party/blink/renderer/modules/event_target_modules.h"
#include "third_party/blink/renderer/modules/mediarecorder/blob_event.h"
#include "third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h"
#include "third_party/blink/renderer/modules/mediarecorder/video_track_recorder.h"
#include "third_party/blink/renderer/platform/blob/blob_data.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_descriptor.h"
#include "third_party/blink/renderer/platform/network/mime/content_type.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"

namespace blink {

namespace {

struct MediaRecorderBitrates {
  const std::optional<uint32_t> audio_bps;
  const std::optional<uint32_t> video_bps;
  const std::optional<uint32_t> overall_bps;
};

// Boundaries of Opus SILK bitrate from https://www.opus-codec.org/.
const int kSmallestPossibleOpusBitRate = 5000;
const int kLargestPossibleOpusBitRate = 510000;

// Smallest Vpx bitrate that can be requested.
// 75kbps is the min bitrate recommended by VP9 VOD settings for 320x240 videos.
const int kSmallestPossibleVpxBitRate = 75000;

// Both values come from YouTube recommended upload encoding settings and are
// used by other browser vendors. See
// https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
const int kDefaultVideoBitRate = 2500e3;  // 2.5Mbps
const int kDefaultAudioBitRate = 128e3;   // 128kbps

V8RecordingState::Enum StateToV8Enum(MediaRecorder::State state) {
  switch (state) {
    case MediaRecorder::State::kInactive:
      return V8RecordingState::Enum::kInactive;
    case MediaRecorder::State::kRecording:
      return V8RecordingState::Enum::kRecording;
    case MediaRecorder::State::kPaused:
      return V8RecordingState::Enum::kPaused;
  }
  NOTREACHED();
}

V8BitrateMode::Enum BitrateModeToV8Enum(
    AudioTrackRecorder::BitrateMode bitrateMode) {
  switch (bitrateMode) {
    case AudioTrackRecorder::BitrateMode::kConstant:
      return V8BitrateMode::Enum::kConstant;
    case AudioTrackRecorder::BitrateMode::kVariable:
      return V8BitrateMode::Enum::kVariable;
  }
  NOTREACHED();
}

AudioTrackRecorder::BitrateMode GetBitrateModeFromOptions(
    const MediaRecorderOptions* const options) {
  if (options->hasAudioBitrateMode()) {
    if (options->audioBitrateMode() == V8BitrateMode::Enum::kConstant) {
      return AudioTrackRecorder::BitrateMode::kConstant;
    }
  }

  return AudioTrackRecorder::BitrateMode::kVariable;
}

void LogConsoleMessage(ExecutionContext* context, const String& message) {
  context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
      mojom::blink::ConsoleMessageSource::kJavaScript,
      mojom::blink::ConsoleMessageLevel::kWarning, message));
}

uint32_t ClampAudioBitRate(ExecutionContext* context, uint32_t audio_bps) {
  if (audio_bps > kLargestPossibleOpusBitRate) {
    LogConsoleMessage(
        context,
        String::Format(
            "Clamping calculated audio bitrate (%dbps) to the maximum (%dbps)",
            audio_bps, kLargestPossibleOpusBitRate));
    return kLargestPossibleOpusBitRate;
  }
  if (audio_bps < kSmallestPossibleOpusBitRate) {
    LogConsoleMessage(
        context,
        String::Format(
            "Clamping calculated audio bitrate (%dbps) to the minimum (%dbps)",
            audio_bps, kSmallestPossibleOpusBitRate));
    return kSmallestPossibleOpusBitRate;
  }
  return audio_bps;
}

uint32_t ClampVideoBitRate(ExecutionContext* context, uint32_t video_bps) {
  if (video_bps < kSmallestPossibleVpxBitRate) {
    LogConsoleMessage(
        context,
        String::Format(
            "Clamping calculated video bitrate (%dbps) to the minimum (%dbps)",
            video_bps, kSmallestPossibleVpxBitRate));
    return kSmallestPossibleVpxBitRate;
  }
  return video_bps;
}

// Allocates the requested bit rates from |options| into the respective
// |{audio,video}_bps| (where a value of zero indicates Platform to use
// whatever it sees fit). If |options.bitsPerSecond()| is specified, it
// overrides any specific bitrate, and the UA is free to allocate as desired:
// here a 90%/10% video/audio is used. In all cases where a value is explicited
// or calculated, values are clamped in sane ranges.
// This method throws NotSupportedError.
MediaRecorderBitrates GetBitratesFromOptions(
    ExceptionState& exception_state,
    ExecutionContext* context,
    const MediaRecorderOptions* options) {
  // Clamp incoming values into a signed integer's range.
  // TODO(mcasas): This section would no be needed if the bit rates are signed
  // or double, see https://github.com/w3c/mediacapture-record/issues/48.
  constexpr uint32_t kMaxIntAsUnsigned = std::numeric_limits<int>::max();

  std::optional<uint32_t> audio_bps;
  if (options->hasAudioBitsPerSecond()) {
    audio_bps = std::min(options->audioBitsPerSecond(), kMaxIntAsUnsigned);
  }
  std::optional<uint32_t> video_bps;
  if (options->hasVideoBitsPerSecond()) {
    video_bps = std::min(options->videoBitsPerSecond(), kMaxIntAsUnsigned);
  }
  std::optional<uint32_t> overall_bps;
  if (options->hasBitsPerSecond()) {
    overall_bps = std::min(options->bitsPerSecond(), kMaxIntAsUnsigned);
    audio_bps = ClampAudioBitRate(context, overall_bps.value() / 10);
    video_bps = overall_bps.value() >= audio_bps.value()
                    ? overall_bps.value() - audio_bps.value()
                    : 0u;
    video_bps = ClampVideoBitRate(context, video_bps.value());
  }

  return {audio_bps, video_bps, overall_bps};
}

}  // namespace

MediaRecorder* MediaRecorder::Create(ExecutionContext* context,
                                     MediaStream* stream,
                                     ExceptionState& exception_state) {
  return MakeGarbageCollected<MediaRecorder>(
      context, stream, MediaRecorderOptions::Create(), exception_state);
}

MediaRecorder* MediaRecorder::Create(ExecutionContext* context,
                                     MediaStream* stream,
                                     const MediaRecorderOptions* options,
                                     ExceptionState& exception_state) {
  return MakeGarbageCollected<MediaRecorder>(context, stream, options,
                                             exception_state);
}

MediaRecorder::MediaRecorder(ExecutionContext* context,
                             MediaStream* stream,
                             const MediaRecorderOptions* options,
                             ExceptionState& exception_state)
    : ActiveScriptWrappable<MediaRecorder>({}),
      ExecutionContextLifecycleObserver(context),
      stream_(stream),
      mime_type_(options->mimeType()) {
  if (context->IsContextDestroyed()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Execution context is detached.");
    return;
  }
  if (options->hasVideoKeyFrameIntervalDuration() &&
      options->hasVideoKeyFrameIntervalCount()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        "Both videoKeyFrameIntervalDuration and videoKeyFrameIntervalCount "
        "can't be specified.");
    return;
  }
  KeyFrameRequestProcessor::Configuration key_frame_config;
  if (options->hasVideoKeyFrameIntervalDuration()) {
    key_frame_config =
        base::Milliseconds(options->videoKeyFrameIntervalDuration());
  } else if (options->hasVideoKeyFrameIntervalCount()) {
    key_frame_config = options->videoKeyFrameIntervalCount();
  }
  recorder_handler_ = MakeGarbageCollected<MediaRecorderHandler>(
      context->GetTaskRunner(TaskType::kInternalMediaRealTime),
      key_frame_config);
  if (!recorder_handler_) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        "No MediaRecorder handler can be created.");
    return;
  }

  const MediaRecorderBitrates bitrates =
      GetBitratesFromOptions(exception_state, context, options);
  const ContentType content_type(mime_type_);
  if (!recorder_handler_->Initialize(this, stream->Descriptor(),
                                     content_type.GetType(),
                                     content_type.Parameter("codecs"),
                                     GetBitrateModeFromOptions(options))) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        StrCat({"Failed to initialize native MediaRecorder the type provided (",
                mime_type_, ") is not supported."}));
  }

  audio_bits_per_second_ = bitrates.audio_bps.value_or(kDefaultAudioBitRate);
  video_bits_per_second_ = bitrates.video_bps.value_or(kDefaultVideoBitRate);
  overall_bits_per_second_ = bitrates.overall_bps;
}

MediaRecorder::~MediaRecorder() = default;

V8RecordingState MediaRecorder::state() const {
  return V8RecordingState(StateToV8Enum(state_));
}

V8BitrateMode MediaRecorder::audioBitrateMode() const {
  if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
    // Return a valid enum value; variable is the default.
    return V8BitrateMode(V8BitrateMode::Enum::kVariable);
  }
  DCHECK(recorder_handler_);
  return V8BitrateMode(
      BitrateModeToV8Enum(recorder_handler_->AudioBitrateMode()));
}

void MediaRecorder::start(ExceptionState& exception_state) {
  start(std::numeric_limits<int>::max() /* timeSlice */, exception_state);
}

void MediaRecorder::start(int time_slice, ExceptionState& exception_state) {
  if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Execution context is detached.");
    return;
  }
  if (state_ != State::kInactive) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      StrCat({"The MediaRecorder's state is '",
                                              state().AsStringView(), "'."}));
    return;
  }

  if (stream_->getTracks().size() == 0) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "The MediaRecorder cannot start because"
                                      "there are no audio or video tracks "
                                      "available.");
    return;
  }

  state_ = State::kRecording;

  if (stream_->getAudioTracks().size() == 0) {
    audio_bits_per_second_ = 0;
    if (overall_bits_per_second_.has_value()) {
      video_bits_per_second_ = ClampVideoBitRate(
          GetExecutionContext(), overall_bits_per_second_.value());
    }
  }

  if (stream_->getVideoTracks().size() == 0) {
    video_bits_per_second_ = 0;
    if (overall_bits_per_second_.has_value()) {
      audio_bits_per_second_ = ClampAudioBitRate(
          GetExecutionContext(), overall_bits_per_second_.value());
    }
  }

  const ContentType content_type(mime_type_);
  if (!recorder_handler_->Start(time_slice, content_type.GetType(),
                                audio_bits_per_second_,
                                video_bits_per_second_)) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        "There was an error starting the MediaRecorder.");
  }
}

void MediaRecorder::stop(ExceptionState& exception_state) {
  if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Execution context is detached.");
    return;
  }
  if (state_ == State::kInactive) {
    return;
  }

  StopRecording(/*error_event=*/nullptr);
}

void MediaRecorder::pause(ExceptionState& exception_state) {
  if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Execution context is detached.");
    return;
  }
  if (state_ == State::kInactive) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "The MediaRecorder's state is 'inactive'.");
    return;
  }
  if (state_ == State::kPaused)
    return;

  state_ = State::kPaused;

  recorder_handler_->Pause();

  ScheduleDispatchEvent(Event::Create(event_type_names::kPause));
}

void MediaRecorder::resume(ExceptionState& exception_state) {
  if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Execution context is detached.");
    return;
  }
  if (state_ == State::kInactive) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "The MediaRecorder's state is 'inactive'.");
    return;
  }
  if (state_ == State::kRecording)
    return;

  state_ = State::kRecording;

  recorder_handler_->Resume();
  ScheduleDispatchEvent(Event::Create(event_type_names::kResume));
}

void MediaRecorder::requestData(ExceptionState& exception_state) {
  if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Execution context is detached.");
    return;
  }
  if (state_ == State::kInactive) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "The MediaRecorder's state is 'inactive'.");
    return;
  }

  if (recorder_handler_) {
    recorder_handler_->MaybeFlush();
  }

  WriteData(/*data=*/{}, /*last_in_slice=*/true, /*error_event=*/nullptr);
}

bool MediaRecorder::isTypeSupported(ExecutionContext* context,
                                    const String& type) {
  MediaRecorderHandler* handler = MakeGarbageCollected<MediaRecorderHandler>(
      context->GetTaskRunner(TaskType::kInternalMediaRealTime),
      KeyFrameRequestProcessor::Configuration());
  if (!handler)
    return false;

  // If true is returned from this method, it only indicates that the
  // MediaRecorder implementation is capable of recording Blob objects for the
  // specified MIME type. Recording may still fail if sufficient resources are
  // not available to support the concrete media encoding.
  // https://w3c.github.io/mediacapture-record/#dom-mediarecorder-istypesupported
  ContentType content_type(type);
  bool result = handler->CanSupportMimeType(
      content_type.GetType(), content_type.Parameter("codecs"),
      MediaRecorderHandler::CanSupportMimeTypeCaller::kIsTypeSupported);

  return result;
}

const AtomicString& MediaRecorder::InterfaceName() const {
  return event_target_names::kMediaRecorder;
}

ExecutionContext* MediaRecorder::GetExecutionContext() const {
  return ExecutionContextLifecycleObserver::GetExecutionContext();
}

void MediaRecorder::ContextDestroyed() {
  if (blob_data_) {
    // Cache |blob_data_->length()| because of std::move in argument list.
    const uint64_t blob_data_length = blob_data_->length();
    CreateBlobEvent(MakeGarbageCollected<Blob>(
        BlobDataHandle::Create(std::move(blob_data_), blob_data_length)));
  }

  state_ = State::kInactive;
  stream_.Clear();
  if (recorder_handler_) {
    recorder_handler_->Stop();
    recorder_handler_ = nullptr;
  }
}

void MediaRecorder::WriteData(base::span<const uint8_t> data,
                              bool last_in_slice,
                              ErrorEvent* error_event) {
  MaybeEmitStartEvent();

  if (error_event) {
    ScheduleDispatchEvent(error_event);
  }

  if (!blob_data_) {
    blob_data_ = std::make_unique<BlobData>();
    blob_data_->SetContentType(mime_type_);
  }
  if (!data.empty()) {
    blob_data_->AppendBytes(data);
  }

  if (!last_in_slice)
    return;

  // Cache |blob_data_->length()| because of std::move in argument list.
  const uint64_t blob_data_length = blob_data_->length();
  CreateBlobEvent(MakeGarbageCollected<Blob>(
      BlobDataHandle::Create(std::move(blob_data_), blob_data_length)));
}

void MediaRecorder::OnError(DOMExceptionCode code, const String& message) {
  DVLOG(1) << __func__ << " message=" << message.Ascii();

  ScriptState* script_state =
      ToScriptStateForMainWorld(DomWindow()->GetFrame());
  ScriptState::Scope scope(script_state);
  ScriptValue error_value = ScriptValue::From(
      script_state, MakeGarbageCollected<DOMException>(code, message));
  ErrorEventInit* event_init = ErrorEventInit::Create();
  event_init->setError(error_value);
  StopRecording(
      ErrorEvent::Create(script_state, event_type_names::kError, event_init));
}

void MediaRecorder::MaybeEmitStartEvent() {
  if (emitted_start_event_) {
    return;
  }
  mime_type_ = recorder_handler_->ActualMimeType();
  ScheduleDispatchEvent(Event::Create(event_type_names::kStart));
  emitted_start_event_ = true;
}

void MediaRecorder::OnAllTracksEnded() {
  DVLOG(1) << __func__;
  StopRecording(/*error_event=*/nullptr);
}

void MediaRecorder::OnStreamChanged(const String& message) {
  DVLOG(1) << __func__ << " message=" << message.Ascii()
           << " state_=" << static_cast<int>(state_);
  if (state_ != State::kInactive) {
    OnError(DOMExceptionCode::kInvalidModificationError, message);
  }
}

void MediaRecorder::CreateBlobEvent(Blob* blob) {
  const base::TimeTicks now = base::TimeTicks::Now();
  double timecode = 0;
  if (!blob_event_first_chunk_timecode_.has_value()) {
    blob_event_first_chunk_timecode_ = now;
  } else {
    if (LocalDOMWindow* window =
            DynamicTo<LocalDOMWindow>(GetExecutionContext())) {
      if (WindowPerformance* performance =
              DOMWindowPerformance::performance(*window)) {
        timecode = performance->MonotonicTimeToDOMHighResTimeStamp(now) -
                   performance->MonotonicTimeToDOMHighResTimeStamp(
                       blob_event_first_chunk_timecode_.value());
      }
    }
  }

  ScheduleDispatchEvent(MakeGarbageCollected<BlobEvent>(
      event_type_names::kDataavailable, blob, timecode));
}

void MediaRecorder::StopRecording(ErrorEvent* error_event) {
  if (state_ == State::kInactive) {
    // This may happen if all tracks have ended and recording has stopped or
    // never started.
    return;
  }
  if (!recorder_handler_) {
    // This may happen when ContextDestroyed has executed, but the
    // MediaRecorderHandler still exists and all tracks
    // have ended leading to a call to OnAllTracksEnded.
    return;
  }
  // Make sure that starting the recorder again yields an onstart event.
  state_ = State::kInactive;

  recorder_handler_->Stop();
  WriteData(/*data=*/{}, /*last_in_slice=*/true, error_event);
  ScheduleDispatchEvent(Event::Create(event_type_names::kStop));
  emitted_start_event_ = false;
}

void MediaRecorder::ScheduleDispatchEvent(Event* event) {
  scheduled_events_.push_back(event);
  // Only schedule a post if we are placing the first item in the queue.
  if (scheduled_events_.size() == 1) {
    if (auto* context = GetExecutionContext()) {
      // MediaStream recording should use DOM manipulation task source.
      // https://www.w3.org/TR/mediastream-recording/
      context->GetTaskRunner(TaskType::kDOMManipulation)
          ->PostTask(FROM_HERE, BindOnce(&MediaRecorder::DispatchScheduledEvent,
                                         WrapPersistent(this)));
    }
  }
}

void MediaRecorder::DispatchScheduledEvent() {
  HeapVector<Member<Event>> events;
  events.swap(scheduled_events_);

  for (const auto& event : events)
    DispatchEvent(*event);
}

void MediaRecorder::Trace(Visitor* visitor) const {
  visitor->Trace(stream_);
  visitor->Trace(recorder_handler_);
  visitor->Trace(scheduled_events_);
  EventTarget::Trace(visitor);
  ExecutionContextLifecycleObserver::Trace(visitor);
}

void MediaRecorder::UpdateAudioBitrate(uint32_t bits_per_second) {
  audio_bits_per_second_ = bits_per_second;
}

}  // namespace blink
