// Copyright 2015 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 "media/muxers/webm_muxer.h"

#include <algorithm>
#include <memory>

#include "base/bind.h"
#include "media/base/audio_parameters.h"
#include "media/base/limits.h"
#include "media/base/video_frame.h"
#include "media/filters/opus_constants.h"

namespace media {

namespace {

void WriteOpusHeader(const media::AudioParameters& params, uint8_t* header) {
  // See https://wiki.xiph.org/OggOpus#ID_Header.
  // Set magic signature.
  std::string label = "OpusHead";
  memcpy(header + OPUS_EXTRADATA_LABEL_OFFSET, label.c_str(), label.size());
  // Set Opus version.
  header[OPUS_EXTRADATA_VERSION_OFFSET] = 1;
  // Set channel count.
  header[OPUS_EXTRADATA_CHANNELS_OFFSET] = params.channels();
  // Set pre-skip
  uint16_t skip = 0;
  memcpy(header + OPUS_EXTRADATA_SKIP_SAMPLES_OFFSET, &skip, sizeof(uint16_t));
  // Set original input sample rate in Hz.
  uint32_t sample_rate = params.sample_rate();
  memcpy(header + OPUS_EXTRADATA_SAMPLE_RATE_OFFSET, &sample_rate,
         sizeof(uint32_t));
  // Set output gain in dB.
  uint16_t gain = 0;
  memcpy(header + OPUS_EXTRADATA_GAIN_OFFSET, &gain, 2);

  // Set channel mapping.
  if (params.channels() > 2) {
    // Also possible to have a multistream, not supported for now.
    DCHECK_LE(params.channels(), OPUS_MAX_VORBIS_CHANNELS);
    header[OPUS_EXTRADATA_CHANNEL_MAPPING_OFFSET] = 1;
    // Assuming no coupled streams. This should actually be
    // channels() - |coupled_streams|.
    header[OPUS_EXTRADATA_NUM_STREAMS_OFFSET] = params.channels();
    header[OPUS_EXTRADATA_NUM_COUPLED_OFFSET] = 0;
    // Set the actual stream map.
    for (int i = 0; i < params.channels(); ++i) {
      header[OPUS_EXTRADATA_STREAM_MAP_OFFSET + i] =
          kOpusVorbisChannelMap[params.channels() - 1][i];
    }
  } else {
    header[OPUS_EXTRADATA_CHANNEL_MAPPING_OFFSET] = 0;
  }
}

static double GetFrameRate(const WebmMuxer::VideoParameters& params) {
  const double kZeroFrameRate = 0.0;
  const double kDefaultFrameRate = 30.0;

  double frame_rate = params.frame_rate;
  if (frame_rate <= kZeroFrameRate ||
      frame_rate > media::limits::kMaxFramesPerSecond) {
    frame_rate = kDefaultFrameRate;
  }
  return frame_rate;
}

static const char kH264CodecId[] = "V_MPEG4/ISO/AVC";
static const char kPcmCodecId[] = "A_PCM/FLOAT/IEEE";

static const char* MkvCodeIcForMediaVideoCodecId(VideoCodec video_codec) {
  switch (video_codec) {
    case kCodecVP8:
      return mkvmuxer::Tracks::kVp8CodecId;
    case kCodecVP9:
      return mkvmuxer::Tracks::kVp9CodecId;
    case kCodecH264:
      return kH264CodecId;
    default:
      NOTREACHED() << "Unsupported codec " << GetCodecName(video_codec);
      return "";
  }
}

}  // anonymous namespace

WebmMuxer::VideoParameters::VideoParameters(
    scoped_refptr<media::VideoFrame> frame) {
  visible_rect_size = frame->visible_rect().size();
  frame_rate = 0.0;
  ignore_result(frame->metadata()->GetDouble(VideoFrameMetadata::FRAME_RATE,
                                             &frame_rate));
}

WebmMuxer::VideoParameters::~VideoParameters() = default;

WebmMuxer::WebmMuxer(VideoCodec video_codec,
                     AudioCodec audio_codec,
                     bool has_video,
                     bool has_audio,
                     const WriteDataCB& write_data_callback)
    : video_codec_(video_codec),
      audio_codec_(audio_codec),
      video_track_index_(0),
      audio_track_index_(0),
      has_video_(has_video),
      has_audio_(has_audio),
      write_data_callback_(write_data_callback),
      position_(0),
      force_one_libwebm_error_(false) {
  DCHECK(has_video_ || has_audio_);
  DCHECK(!write_data_callback_.is_null());
  DCHECK(video_codec == kCodecVP8 || video_codec == kCodecVP9 ||
         video_codec == kCodecH264)
      << " Unsupported video codec: " << GetCodecName(video_codec);
  DCHECK(audio_codec == kCodecOpus || audio_codec == kCodecPCM)
      << " Unsupported audio codec: " << GetCodecName(audio_codec);

  segment_.Init(this);
  segment_.set_mode(mkvmuxer::Segment::kLive);
  segment_.OutputCues(false);

  mkvmuxer::SegmentInfo* const info = segment_.GetSegmentInfo();
  info->set_writing_app("Chrome");
  info->set_muxing_app("Chrome");

  // Creation is done on a different thread than main activities.
  thread_checker_.DetachFromThread();
}

WebmMuxer::~WebmMuxer() {
  // No need to segment_.Finalize() since is not Seekable(), i.e. a live
  // stream, but is a good practice.
  DCHECK(thread_checker_.CalledOnValidThread());
  segment_.Finalize();
}

bool WebmMuxer::OnEncodedVideo(const VideoParameters& params,
                               std::unique_ptr<std::string> encoded_data,
                               std::unique_ptr<std::string> encoded_alpha,
                               base::TimeTicks timestamp,
                               bool is_key_frame) {
  DVLOG(1) << __func__ << " - " << encoded_data->size() << "B";
  DCHECK(thread_checker_.CalledOnValidThread());

  if (encoded_data->size() == 0u) {
    DLOG(WARNING) << __func__ << ": zero size encoded frame, skipping";
    // Some encoders give sporadic zero-size data, see https://crbug.com/716451.
    return true;
  }

  if (!video_track_index_) {
    // |track_index_|, cannot be zero (!), initialize WebmMuxer in that case.
    // http://www.matroska.org/technical/specs/index.html#Tracks
    AddVideoTrack(params.visible_rect_size, GetFrameRate(params));
    if (first_frame_timestamp_video_.is_null())
      first_frame_timestamp_video_ = timestamp;
  }

  // TODO(ajose): Support multiple tracks: http://crbug.com/528523
  if (has_audio_ && !audio_track_index_) {
    DVLOG(1) << __func__ << ": delaying until audio track ready.";
    if (is_key_frame)  // Upon Key frame reception, empty the encoded queue.
      encoded_frames_queue_.clear();

    encoded_frames_queue_.push_back(std::make_unique<EncodedVideoFrame>(
        std::move(encoded_data), std::move(encoded_alpha), timestamp,
        is_key_frame));
    return true;
  }

  // Any saved encoded video frames must have been dumped in OnEncodedAudio();
  DCHECK(encoded_frames_queue_.empty());

  return AddFrame(std::move(encoded_data), std::move(encoded_alpha),
                  video_track_index_, timestamp - first_frame_timestamp_video_,
                  is_key_frame);
}

bool WebmMuxer::OnEncodedAudio(const media::AudioParameters& params,
                               std::unique_ptr<std::string> encoded_data,
                               base::TimeTicks timestamp) {
  DVLOG(2) << __func__ << " - " << encoded_data->size() << "B";
  DCHECK(thread_checker_.CalledOnValidThread());

  if (!audio_track_index_) {
    AddAudioTrack(params);
    if (first_frame_timestamp_audio_.is_null())
      first_frame_timestamp_audio_ = timestamp;
  }

  // TODO(ajose): Don't drop audio data: http://crbug.com/547948
  // TODO(ajose): Support multiple tracks: http://crbug.com/528523
  if (has_video_ && !video_track_index_) {
    DVLOG(1) << __func__ << ": delaying until video track ready.";
    return true;
  }

  // Dump all saved encoded video frames if any.
  while (!encoded_frames_queue_.empty()) {
    const bool res = AddFrame(
        std::make_unique<std::string>(*encoded_frames_queue_.front()->data),
        encoded_frames_queue_.front()->alpha_data
            ? std::make_unique<std::string>(
                  *encoded_frames_queue_.front()->alpha_data)
            : nullptr,
        video_track_index_,
        encoded_frames_queue_.front()->timestamp - first_frame_timestamp_video_,
        encoded_frames_queue_.front()->is_keyframe);
    if (!res)
      return false;
    encoded_frames_queue_.pop_front();
  }
  return AddFrame(std::move(encoded_data), nullptr, audio_track_index_,
                  timestamp - first_frame_timestamp_audio_,
                  true /* is_key_frame -- always true for audio */);
}

void WebmMuxer::Pause() {
  DVLOG(1) << __func__;
  DCHECK(thread_checker_.CalledOnValidThread());
  if (!elapsed_time_in_pause_)
    elapsed_time_in_pause_.reset(new base::ElapsedTimer());
}

void WebmMuxer::Resume() {
  DVLOG(1) << __func__;
  DCHECK(thread_checker_.CalledOnValidThread());
  if (elapsed_time_in_pause_) {
    total_time_in_pause_ += elapsed_time_in_pause_->Elapsed();
    elapsed_time_in_pause_.reset();
  }
}

void WebmMuxer::AddVideoTrack(const gfx::Size& frame_size, double frame_rate) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK_EQ(0u, video_track_index_)
      << "WebmMuxer can only be initialized once.";

  video_track_index_ =
      segment_.AddVideoTrack(frame_size.width(), frame_size.height(), 0);
  if (video_track_index_ <= 0) {  // See https://crbug.com/616391.
    NOTREACHED() << "Error adding video track";
    return;
  }

  mkvmuxer::VideoTrack* const video_track =
      reinterpret_cast<mkvmuxer::VideoTrack*>(
          segment_.GetTrackByNumber(video_track_index_));
  DCHECK(video_track);
  video_track->set_codec_id(MkvCodeIcForMediaVideoCodecId(video_codec_));
  DCHECK_EQ(0ull, video_track->crop_right());
  DCHECK_EQ(0ull, video_track->crop_left());
  DCHECK_EQ(0ull, video_track->crop_top());
  DCHECK_EQ(0ull, video_track->crop_bottom());
  DCHECK_EQ(0.0f, video_track->frame_rate());

  // Segment's timestamps should be in milliseconds, DCHECK it. See
  // http://www.webmproject.org/docs/container/#muxer-guidelines
  DCHECK_EQ(1000000ull, segment_.GetSegmentInfo()->timecode_scale());

  // Set alpha channel parameters for only VPX (crbug.com/711825).
  if (video_codec_ == kCodecH264)
    return;
  video_track->SetAlphaMode(mkvmuxer::VideoTrack::kAlpha);
  // Alpha channel, if present, is stored in a BlockAdditional next to the
  // associated opaque Block, see
  // https://matroska.org/technical/specs/index.html#BlockAdditional.
  // This follows Method 1 for VP8 encoding of A-channel described on
  // http://wiki.webmproject.org/alpha-channel.
  video_track->set_max_block_additional_id(1);
}

void WebmMuxer::AddAudioTrack(const media::AudioParameters& params) {
  DVLOG(1) << __func__ << " " << params.AsHumanReadableString();
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK_EQ(0u, audio_track_index_)
      << "WebmMuxer audio can only be initialised once.";

  audio_track_index_ =
      segment_.AddAudioTrack(params.sample_rate(), params.channels(), 0);
  if (audio_track_index_ <= 0) {  // See https://crbug.com/616391.
    NOTREACHED() << "Error adding audio track";
    return;
  }

  mkvmuxer::AudioTrack* const audio_track =
      reinterpret_cast<mkvmuxer::AudioTrack*>(
          segment_.GetTrackByNumber(audio_track_index_));
  DCHECK(audio_track);
  DCHECK_EQ(params.sample_rate(), audio_track->sample_rate());
  DCHECK_EQ(params.channels(), static_cast<int>(audio_track->channels()));

  // Audio data is always pcm_f32le.
  audio_track->set_bit_depth(32u);

  if (audio_codec_ == kCodecOpus) {
    audio_track->set_codec_id(mkvmuxer::Tracks::kOpusCodecId);

    uint8_t opus_header[OPUS_EXTRADATA_SIZE];
    WriteOpusHeader(params, opus_header);

    if (!audio_track->SetCodecPrivate(opus_header, OPUS_EXTRADATA_SIZE))
      LOG(ERROR) << __func__ << ": failed to set opus header.";

    // Segment's timestamps should be in milliseconds, DCHECK it. See
    // http://www.webmproject.org/docs/container/#muxer-guidelines
    DCHECK_EQ(1000000ull, segment_.GetSegmentInfo()->timecode_scale());
  } else if (audio_codec_ == kCodecPCM) {
    audio_track->set_codec_id(kPcmCodecId);
  }
}

mkvmuxer::int32 WebmMuxer::Write(const void* buf, mkvmuxer::uint32 len) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK(buf);
  write_data_callback_.Run(
      base::StringPiece(reinterpret_cast<const char*>(buf), len));
  position_ += len;
  return 0;
}

mkvmuxer::int64 WebmMuxer::Position() const {
  return position_.ValueOrDie();
}

mkvmuxer::int32 WebmMuxer::Position(mkvmuxer::int64 position) {
  // The stream is not Seekable() so indicate we cannot set the position.
  return -1;
}

bool WebmMuxer::Seekable() const {
  return false;
}

void WebmMuxer::ElementStartNotify(mkvmuxer::uint64 element_id,
                                   mkvmuxer::int64 position) {
  // This method gets pinged before items are sent to |write_data_callback_|.
  DCHECK_GE(position, position_.ValueOrDefault(0))
      << "Can't go back in a live WebM stream.";
}

bool WebmMuxer::AddFrame(std::unique_ptr<std::string> encoded_data,
                         std::unique_ptr<std::string> encoded_alpha,
                         uint8_t track_index,
                         base::TimeDelta timestamp,
                         bool is_key_frame) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK(!has_video_ || video_track_index_);
  DCHECK(!has_audio_ || audio_track_index_);

  most_recent_timestamp_ =
      std::max(most_recent_timestamp_, timestamp - total_time_in_pause_);

  if (force_one_libwebm_error_) {
    DVLOG(1) << "Forcing a libwebm error";
    force_one_libwebm_error_ = false;
    return false;
  }

  DCHECK(encoded_data->data());
  if (!encoded_alpha || encoded_alpha->empty()) {
    return segment_.AddFrame(
        reinterpret_cast<const uint8_t*>(encoded_data->data()),
        encoded_data->size(), track_index,
        most_recent_timestamp_.InMicroseconds() *
            base::Time::kNanosecondsPerMicrosecond,
        is_key_frame);
  }

  DCHECK(encoded_alpha->data());
  return segment_.AddFrameWithAdditional(
      reinterpret_cast<const uint8_t*>(encoded_data->data()),
      encoded_data->size(),
      reinterpret_cast<const uint8_t*>(encoded_alpha->data()),
      encoded_alpha->size(), 1 /* add_id */, track_index,
      most_recent_timestamp_.InMicroseconds() *
          base::Time::kNanosecondsPerMicrosecond,
      is_key_frame);
}

WebmMuxer::EncodedVideoFrame::EncodedVideoFrame(
    std::unique_ptr<std::string> data,
    std::unique_ptr<std::string> alpha_data,
    base::TimeTicks timestamp,
    bool is_keyframe)
    : data(std::move(data)),
      alpha_data(std::move(alpha_data)),
      timestamp(timestamp),
      is_keyframe(is_keyframe) {}

WebmMuxer::EncodedVideoFrame::~EncodedVideoFrame() = default;

}  // namespace media
