// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/media/audio_output_stream_broker.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/browser_main_loop.h"
#include "content/browser/media/audio_stream_broker_helper.h"
#include "content/browser/media/media_internals.h"
#include "content/browser/renderer_host/media/media_stream_manager.h"
#include "content/browser/renderer_host/media/preferred_audio_output_device_manager.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/media_observer.h"
#include "content/public/common/content_client.h"
#include "media/audio/audio_device_description.h"
#include "media/audio/audio_logging.h"
#include "media/mojo/mojom/audio_data_pipe.mojom.h"
#include "media/mojo/mojom/audio_output_stream.mojom.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"

namespace content {

namespace {

enum class StreamBrokerDisconnectReason {
  kDefault = 0,
  kPlatformError,
  kTerminatedByClient,
  kTerminatedByClientAwaitingCreated,
  kStreamCreationFailed,
  kDocumentDestroyed,
  kDocumentDestroyedAwaitingCreated,
  kMaxValue = kDocumentDestroyedAwaitingCreated
};

using DisconnectReason =
    media::mojom::AudioOutputStreamObserver::DisconnectReason;

StreamBrokerDisconnectReason GetDisconnectReason(DisconnectReason reason,
                                                 bool awaiting_created) {
  switch (reason) {
    case DisconnectReason::kPlatformError:
      return StreamBrokerDisconnectReason::kPlatformError;
    case DisconnectReason::kTerminatedByClient:
      return awaiting_created
                 ? StreamBrokerDisconnectReason::
                       kTerminatedByClientAwaitingCreated
                 : StreamBrokerDisconnectReason::kTerminatedByClient;
    case DisconnectReason::kStreamCreationFailed:
      return StreamBrokerDisconnectReason::kStreamCreationFailed;
    case DisconnectReason::kDocumentDestroyed:
      return awaiting_created
                 ? StreamBrokerDisconnectReason::
                       kDocumentDestroyedAwaitingCreated
                 : StreamBrokerDisconnectReason::kDocumentDestroyed;
    case DisconnectReason::kDefault:
      return StreamBrokerDisconnectReason::kDefault;
  }
}

perfetto::NamedTrack GetTracingTrack(const AudioOutputStreamBroker* broker) {
  return perfetto::NamedTrack::FromPointer("AudioOutputStreamBroker", broker);
}

}  // namespace

AudioOutputStreamBroker::AudioOutputStreamBroker(
    int render_process_id,
    int render_frame_id,
    const GlobalRenderFrameHostToken& main_frame_token,
    int stream_id,
    const std::string& output_device_id,
    const media::AudioParameters& params,
    const base::UnguessableToken& group_id,
    DeleterCallback deleter,
    mojo::PendingRemote<media::mojom::AudioOutputStreamProviderClient> client)
    : AudioStreamBroker(render_process_id, render_frame_id),
      main_frame_token_(main_frame_token),
      output_device_id_(output_device_id),
      params_(params),
      group_id_(group_id),
      deleter_(std::move(deleter)),
      client_(std::move(client)),
      observer_(render_process_id, render_frame_id, stream_id),
      observer_receiver_(&observer_) {
  DCHECK(client_);
  DCHECK(deleter_);
  DCHECK(group_id_);
  TRACE_EVENT_BEGIN("audio", "AudioOutputStreamBroker", GetTracingTrack(this));

  MediaObserver* media_observer =
      GetContentClient()->browser()->GetMediaObserver();

  // May be null in unit tests.
  if (media_observer)
    media_observer->OnCreatingAudioStream(render_process_id, render_frame_id);

  // Unretained is safe because |this| owns |client_|
  client_.set_disconnect_handler(
      base::BindOnce(&AudioOutputStreamBroker::Cleanup, base::Unretained(this),
                     DisconnectReason::kTerminatedByClient));

  NotifyFrameHostOfAudioStreamStarted(render_process_id, render_frame_id,
                                      /*is_capturing=*/false);
}

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

  NotifyFrameHostOfAudioStreamStopped(render_process_id(), render_frame_id(),
                                      /*is_capturing=*/false);

  const StreamBrokerDisconnectReason reason =
      GetDisconnectReason(disconnect_reason_, AwaitingCreated());

  if (AwaitingCreated()) {
    // End "CreateStream" trace event.
    TRACE_EVENT_END("audio", GetTracingTrack(this), "success",
                    "failed or cancelled");
  }

  if (MediaStreamManager::GetPreferredOutputManagerInstance()) {
    MediaStreamManager::GetPreferredOutputManagerInstance()->RemoveSwitcher(
        main_frame_token_, this);
  }

  // End "AudioOutputStreamBroker" trace event.
  TRACE_EVENT_END("audio", GetTracingTrack(this), "disconnect reason",
                  static_cast<uint32_t>(reason));
}

void AudioOutputStreamBroker::CreateStream(
    media::mojom::AudioStreamFactory* factory) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  DCHECK(!observer_receiver_.is_bound());
  DCHECK(!device_switch_interface_.is_bound());
  TRACE_EVENT_BEGIN("audio", "CreateStream", GetTracingTrack(this), "device id",
                    output_device_id_);

  stream_creation_start_time_ = base::TimeTicks::Now();

  // Set up observer ptr. Unretained is safe because |this| owns
  // |observer_receiver_|.
  mojo::PendingAssociatedRemote<media::mojom::AudioOutputStreamObserver>
      observer;
  observer_receiver_.Bind(observer.InitWithNewEndpointAndPassReceiver());
  observer_receiver_.set_disconnect_with_reason_handler(base::BindOnce(
      &AudioOutputStreamBroker::ObserverBindingLost, base::Unretained(this)));

  mojo::PendingRemote<media::mojom::AudioOutputStream> stream;
  auto stream_receiver = stream.InitWithNewPipeAndPassReceiver();

  // Note that the component id for AudioLog is used to differentiate between
  // several users of the same audio log. Since this audio log is for a single
  // stream, the component id used doesn't matter.
  constexpr int log_component_id = 0;

  if (MediaStreamManager::GetPreferredOutputManagerInstance() &&
      media::AudioDeviceDescription::IsDefaultDevice(output_device_id_)) {
    // Register the device switcher with PreferredAudioOutputDeviceManager.
    // `output_device_id_` will be updated by the `SwitchAudioOutputDeviceId`,
    // which is called by the PreferredAudioOutputDeviceManager during
    // `AddSwitcher()`.
    MediaStreamManager::GetPreferredOutputManagerInstance()->AddSwitcher(
        main_frame_token_, this);

    factory->CreateSwitchableOutputStream(
        std::move(stream_receiver),
        device_switch_interface_.BindNewPipeAndPassReceiver(),
        std::move(observer),
        MediaInternals::GetInstance()->CreateMojoAudioLog(
            media::AudioLogFactory::AudioComponent::kAudioOuputController,
            log_component_id, render_process_id(), render_frame_id()),
        output_device_id_, params_, group_id_,
        base::BindOnce(&AudioOutputStreamBroker::StreamCreated,
                       weak_ptr_factory_.GetWeakPtr(), std::move(stream)));
  } else {
    factory->CreateOutputStream(
        std::move(stream_receiver), std::move(observer),
        MediaInternals::GetInstance()->CreateMojoAudioLog(
            media::AudioLogFactory::AudioComponent::kAudioOuputController,
            log_component_id, render_process_id(), render_frame_id()),
        output_device_id_, params_, group_id_,
        base::BindOnce(&AudioOutputStreamBroker::StreamCreated,
                       weak_ptr_factory_.GetWeakPtr(), std::move(stream)));
  }
}

void AudioOutputStreamBroker::StreamCreated(
    mojo::PendingRemote<media::mojom::AudioOutputStream> stream,
    media::mojom::ReadWriteAudioDataPipePtr data_pipe) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  // End "CreateStream" trace event.
  TRACE_EVENT_END("audio", GetTracingTrack(this), "success", !!data_pipe);
  stream_creation_start_time_ = base::TimeTicks();

  if (!data_pipe) {
    // Stream creation failed. Signal error.
    client_.ResetWithReason(
        static_cast<uint32_t>(DisconnectReason::kPlatformError), std::string());
    Cleanup(DisconnectReason::kStreamCreationFailed);
    return;
  }

  client_->Created(std::move(stream), std::move(data_pipe));
}

void AudioOutputStreamBroker::ObserverBindingLost(
    uint32_t reason,
    const std::string& description) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  TRACE_EVENT_INSTANT("audio", "ObserverBindingLost", GetTracingTrack(this),
                      "reset reason", reason);
  if (reason > static_cast<uint32_t>(DisconnectReason::kMaxValue)) {
    NOTREACHED() << "Invalid reason: " << reason;
  }

  DisconnectReason reason_enum = static_cast<DisconnectReason>(reason);

  // Errors are propagated to the client, which is responsible for retries. See
  // crbug.com/40551225 for context.
  client_.ResetWithReason(
      static_cast<uint32_t>(DisconnectReason::kPlatformError), std::string());
  Cleanup((reason_enum == DisconnectReason::kPlatformError && AwaitingCreated())
              ? DisconnectReason::kStreamCreationFailed
              : reason_enum);
}

void AudioOutputStreamBroker::Cleanup(DisconnectReason reason) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  DCHECK_EQ(DisconnectReason::kDocumentDestroyed, disconnect_reason_);
  disconnect_reason_ = reason;
  std::move(deleter_).Run(this);
}

bool AudioOutputStreamBroker::AwaitingCreated() const {
  return stream_creation_start_time_ != base::TimeTicks();
}

void AudioOutputStreamBroker::SwitchAudioOutputDeviceId(
    const std::string& device_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  output_device_id_ = device_id;
  if (device_switch_interface_.is_bound()) {
    device_switch_interface_->SwitchAudioOutputDeviceId(output_device_id_);
  }
}

}  // namespace content
