// 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 "services/audio/output_stream.h"

#include <utility>

#include "base/bind.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "services/audio/group_coordinator.h"

namespace audio {

const float kSilenceThresholdDBFS = -72.24719896f;

// Desired polling frequency.  Note: If this is set too low, short-duration
// "blip" sounds won't be detected.  http://crbug.com/339133#c4
const int kPowerMeasurementsPerSecond = 15;

OutputStream::OutputStream(
    CreatedCallback created_callback,
    DeleteCallback delete_callback,
    media::mojom::AudioOutputStreamRequest stream_request,
    media::mojom::AudioOutputStreamObserverAssociatedPtr observer,
    media::mojom::AudioLogPtr log,
    media::AudioManager* audio_manager,
    const std::string& output_device_id,
    const media::AudioParameters& params,
    GroupCoordinator* coordinator,
    const base::UnguessableToken& group_id)
    : foreign_socket_(),
      delete_callback_(std::move(delete_callback)),
      binding_(this, std::move(stream_request)),
      observer_(std::move(observer)),
      log_(log ? media::mojom::ThreadSafeAudioLogPtr::Create(std::move(log))
               : nullptr),
      coordinator_(coordinator),
      // Unretained is safe since we own |reader_|
      reader_(log_ ? base::BindRepeating(&media::mojom::AudioLog::OnLogMessage,
                                         base::Unretained(log_->get()))
                   : base::DoNothing(),
              params,
              &foreign_socket_),
      controller_(audio_manager,
                  this,
                  params,
                  output_device_id,
                  group_id,
                  &reader_),
      weak_factory_(this) {
  DCHECK(binding_.is_bound());
  DCHECK(created_callback);
  DCHECK(delete_callback_);
  DCHECK(coordinator_);
  TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("audio", "audio::OutputStream", this);
  TRACE_EVENT_NESTABLE_ASYNC_BEGIN2("audio", "OutputStream", this, "device id",
                                    output_device_id, "params",
                                    params.AsHumanReadableString());

  // |this| owns these objects, so unretained is safe.
  base::RepeatingClosure error_handler =
      base::BindRepeating(&OutputStream::OnError, base::Unretained(this));
  binding_.set_connection_error_handler(error_handler);

  // We allow the observer to terminate the stream by closing the message pipe.
  if (observer_)
    observer_.set_connection_error_handler(std::move(error_handler));

  if (log_)
    log_->get()->OnCreated(params, output_device_id);

  coordinator_->RegisterGroupMember(&controller_);
  if (!reader_.IsValid() || !controller_.Create(false)) {
    // Either SyncReader initialization failed or the controller failed to
    // create the stream. In the latter case, the controller will have called
    // OnControllerError().
    std::move(created_callback).Run(nullptr);
    return;
  }

  CreateAudioPipe(std::move(created_callback));
}

OutputStream::~OutputStream() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  if (log_)
    log_->get()->OnClosed();

  if (observer_)
    observer_.ResetWithReason(
        static_cast<uint32_t>(media::mojom::AudioOutputStreamObserver::
                                  DisconnectReason::kTerminatedByClient),
        std::string());

  controller_.Close();
  coordinator_->UnregisterGroupMember(&controller_);

  if (is_audible_)
    TRACE_EVENT_NESTABLE_ASYNC_END0("audio", "Audible", this);

  if (playing_)
    TRACE_EVENT_NESTABLE_ASYNC_END0("audio", "Playing", this);

  TRACE_EVENT_NESTABLE_ASYNC_END0("audio", "OutputStream", this);
  TRACE_EVENT_NESTABLE_ASYNC_END0("audio", "audio::OutputStream", this);
}

void OutputStream::Play() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  controller_.Play();
  if (log_)
    log_->get()->OnStarted();
}

void OutputStream::Pause() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  controller_.Pause();
  if (log_)
    log_->get()->OnStopped();
}

void OutputStream::SetVolume(double volume) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  TRACE_EVENT_NESTABLE_ASYNC_INSTANT1("audio", "SetVolume", this, "volume",
                                      volume);

  if (volume < 0 || volume > 1) {
    mojo::ReportBadMessage("Invalid volume");
    OnControllerError();
    return;
  }

  controller_.SetVolume(volume);
  if (log_)
    log_->get()->OnSetVolume(volume);
}

void OutputStream::CreateAudioPipe(CreatedCallback created_callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  DCHECK(reader_.IsValid());
  TRACE_EVENT_NESTABLE_ASYNC_INSTANT0("audio", "CreateAudioPipe", this);

  base::UnsafeSharedMemoryRegion shared_memory_region =
      reader_.TakeSharedMemoryRegion();
  mojo::ScopedHandle socket_handle =
      mojo::WrapPlatformFile(foreign_socket_.Release());
  if (!shared_memory_region.IsValid() || !socket_handle.is_valid()) {
    std::move(created_callback).Run(nullptr);
    OnError();
    return;
  }

  std::move(created_callback)
      .Run({base::in_place, std::move(shared_memory_region),
            std::move(socket_handle)});
}

void OutputStream::OnControllerPlaying() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  if (playing_)
    return;

  TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("audio", "Playing", this);
  playing_ = true;
  if (observer_)
    observer_->DidStartPlaying();
  if (OutputController::will_monitor_audio_levels()) {
    DCHECK(!poll_timer_.IsRunning());
    // base::Unretained is safe because |this| owns |poll_timer_|.
    poll_timer_.Start(
        FROM_HERE,
        base::TimeDelta::FromSeconds(1) / kPowerMeasurementsPerSecond,
        base::BindRepeating(&OutputStream::PollAudioLevel,
                            base::Unretained(this)));
    return;
  }

  // In case we don't monitor audio levels, we assume a stream is audible when
  // it's playing.
  if (observer_)
    observer_->DidChangeAudibleState(true);
}

void OutputStream::OnControllerPaused() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  if (!playing_)
    return;

  playing_ = false;
  if (OutputController::will_monitor_audio_levels()) {
    DCHECK(poll_timer_.IsRunning());
    poll_timer_.Stop();
  }
  if (observer_)
    observer_->DidStopPlaying();
  TRACE_EVENT_NESTABLE_ASYNC_END0("audio", "Playing", this);
}

void OutputStream::OnControllerError() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  TRACE_EVENT_NESTABLE_ASYNC_INSTANT0("audio", "OnControllerError", this);

  // Stop checking the audio level to avoid using this object while it's being
  // torn down.
  poll_timer_.Stop();

  if (log_)
    log_->get()->OnError();

  if (observer_) {
    observer_.ResetWithReason(
        static_cast<uint32_t>(media::mojom::AudioOutputStreamObserver::
                                  DisconnectReason::kPlatformError),
        std::string());
  }

  OnError();
}

void OutputStream::OnLog(base::StringPiece message) {
  // No sequence check: |log_| is thread-safe.
  if (log_)
    log_->get()->OnLogMessage(message.as_string());
}

void OutputStream::OnError() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  TRACE_EVENT_NESTABLE_ASYNC_INSTANT0("audio", "OnError", this);

  // Defer callback so we're not destructed while in the constructor.
  base::SequencedTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::BindOnce(&OutputStream::CallDeleter, weak_factory_.GetWeakPtr()));

  // Ignore any incoming calls.
  binding_.Close();
}

void OutputStream::CallDeleter() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  std::move(delete_callback_).Run(this);
}

void OutputStream::PollAudioLevel() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  bool was_audible = is_audible_;
  is_audible_ = IsAudible();

  if (is_audible_ && !was_audible) {
    TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("audio", "Audible", this);
    if (observer_)
      observer_->DidChangeAudibleState(is_audible_);
  } else if (!is_audible_ && was_audible) {
    TRACE_EVENT_NESTABLE_ASYNC_END0("audio", "Audible", this);
    if (observer_)
      observer_->DidChangeAudibleState(is_audible_);
  }
}

bool OutputStream::IsAudible() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  float power_dbfs = controller_.ReadCurrentPowerAndClip().first;
  return power_dbfs >= kSilenceThresholdDBFS;
}

}  // namespace audio
