// 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/picture_in_picture/video_picture_in_picture_window_controller_impl.h"

#include <set>
#include <utility>

#include "components/viz/common/surfaces/surface_id.h"
#include "content/browser/media/media_web_contents_observer.h"
#include "content/browser/media/session/media_session_impl.h"
#include "content/browser/picture_in_picture/document_picture_in_picture_window_controller_impl.h"
#include "content/browser/picture_in_picture/picture_in_picture_session.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/immersive_playback_options.h"
#include "content/public/browser/overlay_window.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"  // for PictureInPictureResult
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_client.h"
#include "media/base/media_switches.h"

namespace content {

// static
VideoPictureInPictureWindowController*
PictureInPictureWindowController::GetOrCreateVideoPictureInPictureController(
    WebContents* web_contents) {
  return VideoPictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
      web_contents);
}

VideoPictureInPictureWindowControllerImpl::
    ~VideoPictureInPictureWindowControllerImpl() = default;

VideoPictureInPictureWindowControllerImpl::
    VideoPictureInPictureWindowControllerImpl(WebContents* web_contents)
    : WebContentsUserData<VideoPictureInPictureWindowControllerImpl>(
          *web_contents),
      WebContentsObserver(web_contents) {
  MediaSessionImpl* media_session =
      MediaSessionImpl::FromWebContents(web_contents);
  if (media_session) {
    media_session->UpdateVideoPictureInPictureWindowController(this);
  }
}

void VideoPictureInPictureWindowControllerImpl::Show() {
  DCHECK(window_);
  DCHECK(surface_id_.is_valid());

  MediaSessionImpl* media_session = MediaSessionImpl::Get(web_contents());
  media_session_action_play_handled_ = media_session->ShouldRouteAction(
      media_session::mojom::MediaSessionAction::kPlay);
  media_session_action_pause_handled_ = media_session->ShouldRouteAction(
      media_session::mojom::MediaSessionAction::kPause);
  media_session_action_skip_ad_handled_ = media_session->ShouldRouteAction(
      media_session::mojom::MediaSessionAction::kSkipAd);
  media_session_action_next_track_handled_ = media_session->ShouldRouteAction(
      media_session::mojom::MediaSessionAction::kNextTrack);
  media_session_action_previous_track_handled_ =
      media_session->ShouldRouteAction(
          media_session::mojom::MediaSessionAction::kPreviousTrack);
  media_session_action_toggle_microphone_handled_ =
      media_session->ShouldRouteAction(
          media_session::mojom::MediaSessionAction::kToggleMicrophone);
  media_session_action_toggle_camera_handled_ =
      media_session->ShouldRouteAction(
          media_session::mojom::MediaSessionAction::kToggleCamera);
  media_session_action_hang_up_handled_ = media_session->ShouldRouteAction(
      media_session::mojom::MediaSessionAction::kHangUp);
  media_session_action_previous_slide_handled_ =
      media_session->ShouldRouteAction(
          media_session::mojom::MediaSessionAction::kPreviousSlide);
  media_session_action_next_slide_handled_ = media_session->ShouldRouteAction(
      media_session::mojom::MediaSessionAction::kNextSlide);

  UpdatePlayPauseButtonVisibility();
  window_->SetSkipAdButtonVisibility(media_session_action_skip_ad_handled_);
  window_->SetNextTrackButtonVisibility(
      media_session_action_next_track_handled_);
  window_->SetPreviousTrackButtonVisibility(
      media_session_action_previous_track_handled_);
  window_->SetMicrophoneMuted(microphone_muted_);
  window_->SetToggleMicrophoneButtonVisibility(
      media_session_action_toggle_microphone_handled_);
  window_->SetCameraState(camera_turned_on_);
  window_->SetToggleCameraButtonVisibility(
      media_session_action_toggle_camera_handled_);
  window_->SetHangUpButtonVisibility(media_session_action_hang_up_handled_);
  window_->SetNextSlideButtonVisibility(
      media_session_action_next_slide_handled_);
  window_->SetPreviousSlideButtonVisibility(
      media_session_action_previous_slide_handled_);
  window_->SetFaviconImages(favicon_images_);
  window_->SetSourceTitle(source_title_);
  window_->ShowInactive();
  GetWebContentsImpl()->SetHasPictureInPictureVideo(true);
}

void VideoPictureInPictureWindowControllerImpl::FocusInitiator() {
  GetWebContentsImpl()->Activate();
}

void VideoPictureInPictureWindowControllerImpl::Close(bool should_pause_video) {
  if (!window_ || !window_->IsVisible())
    return;

  window_->Hide();
  // The call to `Hide()` may cause `window_` to be cleared.
  CloseInternal(should_pause_video);
}

void VideoPictureInPictureWindowControllerImpl::CloseAndFocusInitiator() {
  Close(false /* should_pause_video */);
  FocusInitiator();
}

void VideoPictureInPictureWindowControllerImpl::OnWindowDestroyed(
    bool should_pause_video) {
  window_ = nullptr;
  CloseInternal(should_pause_video);
}

void VideoPictureInPictureWindowControllerImpl::EmbedSurface(
    const viz::SurfaceId& surface_id,
    const gfx::Size& natural_size) {
  // If there is no window, then it's already been closed. A new call to
  // StartSession is required, which will replace `surface_id_` anyway. For
  // example, if the user closes the pip window, we will end up clearing
  // `window_` as part of that. If the renderer is in the process up updating
  // the SurfaceId, then we can get here without a window.
  if (!window_)
    return;

  DCHECK(active_session_);
  DCHECK(surface_id.is_valid());

  surface_id_ = surface_id;

  // Update the playback state in step with the video surface id. If the surface
  // id was updated for the same video, this is a no-op. This could be updated
  // for a different video if another media player on the same WebContents
  // enters Picture-in-Picture mode.
  UpdatePlaybackState();

  window_->UpdateNaturalSize(natural_size);
  window_->SetSurfaceId(surface_id_);
}

VideoOverlayWindow*
VideoPictureInPictureWindowControllerImpl::GetWindowForTesting() {
  return window_.get();
}

void VideoPictureInPictureWindowControllerImpl::UpdateLayerBounds() {
  if (active_session_ && window_ && window_->IsVisible())
    active_session_->NotifyWindowResized(window_->GetBounds().size());
}

bool VideoPictureInPictureWindowControllerImpl::IsPlayerActive() {
  if (!active_session_ || !active_session_->player_id().has_value())
    return false;

  return GetWebContentsImpl()->media_web_contents_observer()->IsPlayerActive(
      active_session_->player_id().value());
}

bool VideoPictureInPictureWindowControllerImpl::IsImmersive() const {
  return is_immersive_;
}

WebContents* VideoPictureInPictureWindowControllerImpl::GetWebContents() {
  return web_contents();
}

WebContents* VideoPictureInPictureWindowControllerImpl::GetChildWebContents() {
  return nullptr;
}

std::optional<url::Origin>
VideoPictureInPictureWindowControllerImpl::GetOrigin() {
  if (!active_session_ || !active_session_->player_id().has_value()) {
    return std::nullopt;
  }
  RenderFrameHost* rfh =
      RenderFrameHost::FromID(active_session_->player_id()->frame_routing_id);
  if (!rfh) {
    return std::nullopt;
  }
  return rfh->GetLastCommittedOrigin();
}

void VideoPictureInPictureWindowControllerImpl::UpdatePlaybackState() {
  if (!window_)
    return;

  auto playback_state = VideoOverlayWindow::PlaybackState::kPaused;
  const std::optional<media_session::MediaPosition>& effective_media_position =
      GetEffectiveMediaPosition();
  if (IsPlayerActive()) {
    playback_state = VideoOverlayWindow::PlaybackState::kPlaying;
  } else if (effective_media_position.has_value() &&
             effective_media_position->end_of_media()) {
    playback_state = VideoOverlayWindow::PlaybackState::kEndOfVideo;
  }

  window_->SetPlaybackState(playback_state);
}

void VideoPictureInPictureWindowControllerImpl::UpdateMediaPosition() {
  const std::optional<media_session::MediaPosition>& effective_position =
      GetEffectiveMediaPosition();
  if (window_ && effective_position.has_value()) {
    window_->SetMediaPosition(*effective_position);
    window_received_media_position_ = true;
  } else {
    window_received_media_position_ = false;
  }
}

bool VideoPictureInPictureWindowControllerImpl::TogglePlayPause() {
  // This comes from the window, rather than the renderer, so we must actually
  // have a window at this point.
  DCHECK(window_);
  DCHECK(active_session_);

  if (IsPlayerActive()) {
    return PauseInternal();
  }
  return PlayInternal();
}

void VideoPictureInPictureWindowControllerImpl::Play() {
  // This comes from the window, rather than the renderer, so we must actually
  // have a window at this point.
  DCHECK(window_);
  DCHECK(active_session_);

  PlayInternal();
}

void VideoPictureInPictureWindowControllerImpl::Pause() {
  // This comes from the window, rather than the renderer, so we must actually
  // have a window at this point.
  DCHECK(window_);
  DCHECK(active_session_);

  PauseInternal();
}

bool VideoPictureInPictureWindowControllerImpl::PlayInternal() {
  if (media_session_action_play_handled_) {
    MediaSessionImpl::Get(web_contents())
        ->Resume(MediaSession::SuspendType::kUI);
    return false /* still paused */;
  }

  active_session_->GetMediaPlayerRemote()->RequestPlay(
      /*triggered_by_user=*/true);
  return true /* playing */;
}

bool VideoPictureInPictureWindowControllerImpl::PauseInternal() {
  if (media_session_action_pause_handled_) {
    MediaSessionImpl::Get(web_contents())
        ->Suspend(MediaSession::SuspendType::kUI);
    return true /* still playing */;
  }

  active_session_->GetMediaPlayerRemote()->RequestPause(
      /*triggered_by_user=*/false);
  return false /* paused */;
}

const std::optional<media_session::MediaPosition>&
VideoPictureInPictureWindowControllerImpl::GetEffectiveMediaPosition() const {
  if (media_session_media_position_.has_value()) {
    return media_session_media_position_;
  }
  return pip_session_media_position_;
}

PictureInPictureResult VideoPictureInPictureWindowControllerImpl::StartSession(
    PictureInPictureServiceImpl* service,
    const MediaPlayerId& player_id,
    mojo::PendingAssociatedRemote<media::mojom::MediaPlayer> player_remote,
    const viz::SurfaceId& surface_id,
    const gfx::Size& natural_size,
    bool show_play_pause_button,
    mojo::PendingRemote<blink::mojom::PictureInPictureSessionObserver> observer,
    const gfx::Rect& source_bounds,
    std::optional<content::ImmersiveOptions> immersive_options,
    mojo::PendingRemote<blink::mojom::PictureInPictureSession>* session_remote,
    gfx::Size* window_size) {
  if (window_) {
    // We shouldn't be switching between standard and immersive
    // Picture-in-Picture modes if a window already exists. Immersive mode is
    // currently only supported on Android XR, where Picture-in-Picture is
    // disabled. Furthermore, the window types for these modes are different and
    // cannot be reused.
    if (IsImmersive() != immersive_options.has_value()) {
      mojo::ReportBadMessage("Inconsistent immersive state in StartSession");
      return PictureInPictureResult::kNotSupported;
    }
  }

  is_immersive_ = immersive_options.has_value();

  PictureInPictureResult result = PictureInPictureResult::kNotSupported;
  WebContentsDelegate* delegate = web_contents()->GetDelegate();
  if (delegate && (IsImmersive() ? delegate->IsImmersivePlaybackEnabled()
                                 : delegate->IsPictureInPictureEnabled())) {
    result = GetWebContentsImpl()->EnterPictureInPicture();
  }

  // Picture-in-Picture may not be supported by all embedders, so we should only
  // create the session if the EnterPictureInPicture request was successful.
  if (result != PictureInPictureResult::kSuccess) {
    is_immersive_ = false;
    return result;
  }

  if (active_session_) {
    active_session_->Disconnect();
    pip_session_media_position_ = std::nullopt;
  }

  source_bounds_ = source_bounds;

  active_session_ = std::make_unique<PictureInPictureSession>(
      service, player_id, std::move(player_remote),
      session_remote->InitWithNewPipeAndPassReceiver(), std::move(observer));

  // There can be a window already if this session is replacing an old one,
  // without the old one being closed first.
  if (!window_) {
    window_ =
        GetContentClient()->browser()->CreateWindowForVideoPictureInPicture(
            this);
  }
  DCHECK(window_) << "Picture in Picture requires a valid window.";

  // If this is an immersive Picture-in-Picture session, set the immersive
  // options on the window.
  if (immersive_options) {
    window_->SetImmersiveVideoOptions(immersive_options.value());
  }

  // If the window is closed by the system, then the picture in picture session
  // will end. The renderer must call `StartSession()` again.
  EmbedSurface(surface_id, natural_size);
  SetShowPlayPauseButton(show_play_pause_button);
  Show();

  if (on_window_created_notify_observers_callback_) {
    std::move(on_window_created_notify_observers_callback_).Run();
  }

  // TODO(crbug.com/40227464): Rather than set this synchronously, we should
  // call back with the bounds once the window provides them.
  *window_size = GetSize();
  return result;
}

void VideoPictureInPictureWindowControllerImpl::
    RequestImmersivePlaybackConfirmation(
        const content::ImmersiveOptions& default_options,
        RequestImmersivePlaybackConfirmationCallback callback) {
  WebContentsDelegate* delegate = web_contents()->GetDelegate();
  if (!delegate || !delegate->IsImmersivePlaybackEnabled()) {
    content::ImmersivePlaybackConfirmationResult result;
    result.status = content::ImmersivePlaybackConfirmationStatus::kFailed;
    std::move(callback).Run(std::move(result));
    return;
  }

  delegate->RequestImmersivePlaybackConfirmation(default_options,
                                                 std::move(callback));
}

void VideoPictureInPictureWindowControllerImpl::OnServiceDeleted(
    PictureInPictureServiceImpl* service) {
  if (!active_session_ || active_session_->service() != service)
    return;

  active_session_->Shutdown();
  active_session_ = nullptr;
  pip_session_media_position_ = std::nullopt;
  is_immersive_ = false;
}

void VideoPictureInPictureWindowControllerImpl::SetShowPlayPauseButton(
    bool show_play_pause_button) {
  always_show_play_pause_button_ = show_play_pause_button;
  UpdatePlayPauseButtonVisibility();
}

void VideoPictureInPictureWindowControllerImpl::SetMediaPosition(
    const media_session::MediaPosition& media_position) {
  if (media_position == pip_session_media_position_ &&
      window_received_media_position_) {
    return;
  }
  pip_session_media_position_ = media_position;
  UpdatePlaybackState();
  UpdateMediaPosition();
}

void VideoPictureInPictureWindowControllerImpl::SetPlaybackControlsVisibility(
    bool is_visible) {
  always_show_play_pause_button_ = false;

  UpdatePlayPauseButtonVisibility();

  if (window_) {
    window_->SetPlaybackControlsVisibility(is_visible);
  }
}

void VideoPictureInPictureWindowControllerImpl::SkipAd() {
  if (media_session_action_skip_ad_handled_)
    MediaSession::Get(web_contents())->SkipAd();
}

void VideoPictureInPictureWindowControllerImpl::PreviousSlide() {
  if (media_session_action_previous_slide_handled_)
    MediaSession::Get(web_contents())->PreviousSlide();
}

void VideoPictureInPictureWindowControllerImpl::NextSlide() {
  if (media_session_action_next_slide_handled_)
    MediaSession::Get(web_contents())->NextSlide();
}

void VideoPictureInPictureWindowControllerImpl::NextTrack() {
  if (media_session_action_next_track_handled_)
    MediaSession::Get(web_contents())->NextTrack();
}

void VideoPictureInPictureWindowControllerImpl::PreviousTrack() {
  if (media_session_action_previous_track_handled_)
    MediaSession::Get(web_contents())->PreviousTrack();
}

void VideoPictureInPictureWindowControllerImpl::ToggleMicrophone() {
  if (!media_session_action_toggle_microphone_handled_)
    return;

  MediaSession::Get(web_contents())->ToggleMicrophone();
}

void VideoPictureInPictureWindowControllerImpl::ToggleCamera() {
  if (!media_session_action_toggle_camera_handled_)
    return;

  MediaSession::Get(web_contents())->ToggleCamera();
}

void VideoPictureInPictureWindowControllerImpl::HangUp() {
  if (media_session_action_hang_up_handled_)
    MediaSession::Get(web_contents())->HangUp();
}

void VideoPictureInPictureWindowControllerImpl::RequestMute(bool mute) {
  DCHECK(active_session_);
  active_session_->GetMediaPlayerRemote()->RequestMute(mute);
}

bool VideoPictureInPictureWindowControllerImpl::GetMuteStatus() {
  return MediaSessionImpl::Get(web_contents())->GetMuteStatus();
}

void VideoPictureInPictureWindowControllerImpl::SeekTo(base::TimeDelta time) {
  // Default to the Media Session handler if it's available.
  if (media_session_action_seek_to_handled_) {
    MediaSession::Get(web_contents())->SeekTo(time);
    return;
  }
  // Otherwise, directly seek the video player.
  active_session_->GetMediaPlayerRemote()->RequestSeekTo(time);
}

void VideoPictureInPictureWindowControllerImpl::MediaSessionInfoChanged(
    const media_session::mojom::MediaSessionInfoPtr& info) {
  if (!info)
    return;

  microphone_muted_ =
      info->microphone_state == media_session::mojom::MicrophoneState::kMuted;
  camera_turned_on_ =
      info->camera_state == media_session::mojom::CameraState::kTurnedOn;

  if (!window_)
    return;

  window_->SetMicrophoneMuted(microphone_muted_);
  window_->SetCameraState(camera_turned_on_);
}

void VideoPictureInPictureWindowControllerImpl::MediaSessionActionsChanged(
    const std::set<media_session::mojom::MediaSessionAction>& actions) {
  // TODO(crbug.com/40608570): Currently, the first Media Session to be created
  // (independently of the frame) will be used. This means, we could show a
  // Skip Ad button for a PiP video from another frame. Ideally, we should have
  // a Media Session per frame, not per tab. This is not implemented yet.

  media_session_action_pause_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kPause) !=
      actions.end();
  media_session_action_play_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kPlay) !=
      actions.end();
  media_session_action_skip_ad_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kSkipAd) !=
      actions.end();
  media_session_action_next_track_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kNextTrack) !=
      actions.end();
  media_session_action_previous_track_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kPreviousTrack) !=
      actions.end();
  media_session_action_toggle_microphone_handled_ =
      actions.find(
          media_session::mojom::MediaSessionAction::kToggleMicrophone) !=
      actions.end();
  media_session_action_toggle_camera_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kToggleCamera) !=
      actions.end();
  media_session_action_hang_up_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kHangUp) !=
      actions.end();
  media_session_action_previous_slide_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kPreviousSlide) !=
      actions.end();
  media_session_action_next_slide_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kNextSlide) !=
      actions.end();
  media_session_action_seek_to_handled_ =
      actions.find(media_session::mojom::MediaSessionAction::kSeekTo) !=
      actions.end();

  if (!window_)
    return;

  UpdatePlayPauseButtonVisibility();
  UpdateHidePictureInPictureButtonVisibility();
  window_->SetSkipAdButtonVisibility(media_session_action_skip_ad_handled_);
  window_->SetNextTrackButtonVisibility(
      media_session_action_next_track_handled_);
  window_->SetPreviousTrackButtonVisibility(
      media_session_action_previous_track_handled_);
  window_->SetToggleMicrophoneButtonVisibility(
      media_session_action_toggle_microphone_handled_);
  window_->SetToggleCameraButtonVisibility(
      media_session_action_toggle_camera_handled_);
  window_->SetHangUpButtonVisibility(media_session_action_hang_up_handled_);
  window_->SetNextSlideButtonVisibility(
      media_session_action_next_slide_handled_);
  window_->SetPreviousSlideButtonVisibility(
      media_session_action_previous_slide_handled_);
}

void VideoPictureInPictureWindowControllerImpl::MediaSessionPositionChanged(
    const std::optional<media_session::MediaPosition>& media_position) {
  // If we've already sent this position to |window|, then no need to update
  // again.
  if (media_position == media_session_media_position_ &&
      window_received_media_position_) {
    return;
  }

  media_session_media_position_ = media_position;
  UpdatePlaybackState();
  UpdateMediaPosition();
}

void VideoPictureInPictureWindowControllerImpl::MediaSessionImagesChanged(
    const base::flat_map<media_session::mojom::MediaSessionImageType,
                         std::vector<media_session::MediaImage>>& images) {
  auto it =
      images.find(media_session::mojom::MediaSessionImageType::kSourceIcon);
  if (it == images.end()) {
    if (favicon_images_.empty()) {
      return;
    }
    favicon_images_.clear();
  } else {
    if (it->second == favicon_images_) {
      return;
    }
    favicon_images_ = it->second;
  }

  if (window_) {
    window_->SetFaviconImages(favicon_images_);
  }
}

void VideoPictureInPictureWindowControllerImpl::MediaSessionMetadataChanged(
    const std::optional<media_session::MediaMetadata>& metadata) {
  if (metadata) {
    source_title_ = metadata->source_title;
  } else {
    source_title_.clear();
  }

  if (window_) {
    window_->SetSourceTitle(source_title_);
  }
}

gfx::Size VideoPictureInPictureWindowControllerImpl::GetSize() {
  return window_->GetBounds().size();
}

void VideoPictureInPictureWindowControllerImpl::MediaStartedPlaying(
    const MediaPlayerInfo&,
    const MediaPlayerId& media_player_id) {
  if (web_contents()->IsBeingDestroyed())
    return;

  if (!active_session_ || active_session_->player_id() != media_player_id)
    return;

  UpdatePlaybackState();
}

void VideoPictureInPictureWindowControllerImpl::MediaStoppedPlaying(
    const MediaPlayerInfo&,
    const MediaPlayerId& media_player_id,
    WebContentsObserver::MediaStoppedReason) {
  if (web_contents()->IsBeingDestroyed())
    return;

  if (!active_session_ || active_session_->player_id() != media_player_id)
    return;

  UpdatePlaybackState();
}

void VideoPictureInPictureWindowControllerImpl::MediaMutedStatusChanged(
    const MediaPlayerId& id,
    bool muted) {
  if (!base::FeatureList::IsEnabled(media::kPictureInPictureMuteControl)) {
    return;
  }
  if (!active_session_ || active_session_->player_id() != id ||
      web_contents()->IsBeingDestroyed()) {
    return;
  }
  window_->SetMediaMuted(muted);
}

void VideoPictureInPictureWindowControllerImpl::WebContentsDestroyed() {
  if (window_) {
    window_->Close();
  }

  // The web contents are being destroyed. Stop observing.
  Observe(nullptr);
}

void VideoPictureInPictureWindowControllerImpl::OnLeavingPictureInPicture(
    bool should_pause_video) {
  DCHECK(active_session_);

  if (IsPlayerActive() && should_pause_video) {
    // Pause the current video so there is only one video playing at a time.
    active_session_->GetMediaPlayerRemote()->RequestPause(
        /*triggered_by_user=*/false);
  }

  active_session_->Shutdown();
  active_session_ = nullptr;
  pip_session_media_position_ = std::nullopt;
  is_immersive_ = false;
}

void VideoPictureInPictureWindowControllerImpl::CloseInternal(
    bool should_pause_video) {
  // `web_contents()` can be null during `WebContents` destruction. If this
  // controller is notified of destruction first, it clears its pointer via
  // `Observe(nullptr)`. If `PictureInPictureWindowManager` tries to close the
  // window later in the teardown sequence, it can crash when accessing
  // `web_contents()`.
  //
  // We shouldn't have an empty active_session_ in this case but (at least for
  // there tests), extensions seem to be closing the window before the
  // WebContents is marked as being destroyed. It leads to `CloseInternal()`
  // being called twice. This early check avoids the rest of the code having to
  // be aware of this oddity.
  if (!web_contents() || web_contents()->IsBeingDestroyed() ||
      !active_session_) {
    return;
  }

  GetWebContentsImpl()->SetHasPictureInPictureVideo(false);
  OnLeavingPictureInPicture(should_pause_video);
  surface_id_ = viz::SurfaceId();
}

const gfx::Rect& VideoPictureInPictureWindowControllerImpl::GetSourceBounds()
    const {
  return source_bounds_;
}

void VideoPictureInPictureWindowControllerImpl::GetMediaImage(
    const media_session::MediaImage& image,
    int minimum_size_px,
    int desired_size_px,
    MediaSession::GetMediaImageBitmapCallback callback) {
  MediaSessionImpl* media_session = MediaSessionImpl::Get(web_contents());
  CHECK(media_session);
  media_session->GetMediaImageBitmap(image, minimum_size_px, desired_size_px,
                                     std::move(callback));
}

std::optional<gfx::Rect>
VideoPictureInPictureWindowControllerImpl::GetWindowBoundsInScreen() {
  if (!window_)
    return std::nullopt;
  return window_->GetBounds();
}

void VideoPictureInPictureWindowControllerImpl::
    UpdatePlayPauseButtonVisibility() {
  if (!window_)
    return;

  window_->SetPlayPauseButtonVisibility(ShouldShowPlayPauseButton());
}

bool VideoPictureInPictureWindowControllerImpl::ShouldShowPlayPauseButton() {
  return (media_session_action_pause_handled_ &&
          media_session_action_play_handled_) ||
         always_show_play_pause_button_;
}

void VideoPictureInPictureWindowControllerImpl::
    UpdateHidePictureInPictureButtonVisibility() {
  if (!window_) {
    return;
  }

  // The button's visibility is tied to the play/pause button. The final
  // decision to show the button is delegated to the platform-specific window
  // implementation.
  window_->SetHidePictureInPictureButtonVisibility(ShouldShowPlayPauseButton());
}

void VideoPictureInPictureWindowControllerImpl::
    SetOnWindowCreatedNotifyObserversCallback(
        base::OnceClosure on_window_created_notify_observers_callback) {
  on_window_created_notify_observers_callback_ =
      std::move(on_window_created_notify_observers_callback);
}

WebContentsImpl*
VideoPictureInPictureWindowControllerImpl::GetWebContentsImpl() {
  return static_cast<WebContentsImpl*>(web_contents());
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(VideoPictureInPictureWindowControllerImpl);

}  // namespace content
