| // Copyright 2022 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 "ash/capture_mode/capture_mode_camera_controller.h" |
| |
| #include <algorithm> |
| #include <cstring> |
| |
| #include "ash/accessibility/accessibility_controller_impl.h" |
| #include "ash/capture_mode/capture_mode_button.h" |
| #include "ash/capture_mode/capture_mode_camera_preview_view.h" |
| #include "ash/capture_mode/capture_mode_constants.h" |
| #include "ash/capture_mode/capture_mode_controller.h" |
| #include "ash/capture_mode/capture_mode_metrics.h" |
| #include "ash/capture_mode/capture_mode_session.h" |
| #include "ash/capture_mode/capture_mode_util.h" |
| #include "ash/public/cpp/capture_mode/capture_mode_delegate.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/message_center/unified_message_center_bubble.h" |
| #include "ash/system/status_area_widget.h" |
| #include "ash/system/tray/system_tray_notifier.h" |
| #include "ash/system/unified/unified_system_tray.h" |
| #include "ash/system/unified/unified_system_tray_controller.h" |
| #include "ash/wm/pip/pip_positioner.h" |
| #include "ash/wm/window_state.h" |
| #include "ash/wm/wm_event.h" |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/check.h" |
| #include "base/containers/contains.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "media/capture/video/video_capture_device_descriptor.h" |
| #include "ui/aura/window_targeter.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/animation/tween.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/point_conversions.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/geometry/transform.h" |
| #include "ui/views/animation/animation_builder.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| #include "ui/wm/core/window_properties.h" |
| #include "ui/wm/core/window_util.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // The maximum amount of time we allow a `selected_camera_` to remain |
| // disconnected before we consider it gone forever, and we clear its ID from |
| // `selected_camera_`. |
| constexpr base::TimeDelta kDisconnectionGracePeriod = base::Seconds(10); |
| |
| // The animation duration for the bounds change operation on the camera preview. |
| constexpr base::TimeDelta kCameraBoundsChangeAnimationDuration = |
| base::Milliseconds(150); |
| |
| // The duration for the camera preview fading out process. |
| constexpr base::TimeDelta kCameraPreviewFadeOutDuration = |
| base::Milliseconds(50); |
| // The duration for the camera preview fading in process. |
| constexpr base::TimeDelta kCameraPreviewFadeInDuration = |
| base::Milliseconds(150); |
| |
| // Defines a map type to map a camera model ID (or display name) to the number |
| // of cameras of that model that are currently connected. |
| using ModelIdToCountMap = std::map<std::string, int>; |
| |
| // Using the given `cam_models_map` which tracks the number of cameras connected |
| // of each model, returns the next `CameraId::number` for the given |
| // `model_id_or_display_name`. |
| int GetNextCameraNumber(const std::string& model_id_or_display_name, |
| ModelIdToCountMap* cam_models_map) { |
| return ++(*cam_models_map)[model_id_or_display_name]; |
| } |
| |
| // Returns a reference to either the model ID (if available) or the display name |
| // from the given `descriptor`. |
| const std::string& PickModelIdOrDisplayName( |
| const media::VideoCaptureDeviceDescriptor& descriptor) { |
| return descriptor.model_id.empty() ? descriptor.display_name() |
| : descriptor.model_id; |
| } |
| |
| // Returns true if the `incoming_list` (supplied by the video source provider) |
| // contains different items than the ones in `current_list` (which is the |
| // currently `available_cameras_` maintained by `CaptureModeCameraController`). |
| bool DidDevicesChange( |
| const std::vector<media::VideoCaptureDeviceInfo>& incoming_list, |
| const CameraInfoList& current_list) { |
| if (incoming_list.size() != current_list.size()) |
| return true; |
| |
| ModelIdToCountMap cam_models_map; |
| for (const auto& incoming_camera : incoming_list) { |
| const auto& device_id = incoming_camera.descriptor.device_id; |
| const auto iter = std::find_if(current_list.begin(), current_list.end(), |
| [device_id](const CameraInfo& info) { |
| return info.device_id == device_id; |
| }); |
| if (iter == current_list.end()) |
| return true; |
| |
| const auto& model_id_or_display_name = |
| PickModelIdOrDisplayName(incoming_camera.descriptor); |
| const int cam_number = |
| GetNextCameraNumber(model_id_or_display_name, &cam_models_map); |
| |
| const CameraInfo& found_info = *iter; |
| if (found_info.display_name != incoming_camera.descriptor.display_name() || |
| found_info.camera_id.model_id_or_display_name() != |
| model_id_or_display_name || |
| found_info.camera_id.number() != cam_number) { |
| // It is unexpected that the supported formats of the same camera device |
| // change, so we ignore comparing them here. |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| // Picks and returns the most suitable supported camera format from the given |
| // list of `supported_formats` for the given camera `preview_widget_size`. |
| // Note that this assumes that `supported_formats` is sorted as described in the |
| // documentation of `CameraInfo::supported_formats`. |
| media::VideoCaptureFormat PickSuitableCaptureFormat( |
| const gfx::Size& preview_widget_size, |
| const media::VideoCaptureFormats& supported_formats) { |
| DCHECK(!supported_formats.empty()); |
| DCHECK_EQ(preview_widget_size.height(), preview_widget_size.width()) |
| << "The preview widget is always assumed to be a square."; |
| |
| size_t result_index = 0; |
| float current_frame_rate = 0.f; |
| for (size_t i = 0; i < supported_formats.size(); ++i) { |
| const auto& format = supported_formats[i]; |
| // Once we find a format with a larger height than the preview's, we stop |
| // and return what we found so far. |
| if (format.frame_size.height() > preview_widget_size.height()) |
| break; |
| |
| if (format.frame_rate >= current_frame_rate && format.frame_rate <= 30.f) { |
| current_frame_rate = format.frame_rate; |
| result_index = i; |
| } |
| } |
| |
| return supported_formats[result_index]; |
| } |
| |
| // Only rear device cameras marked as facing the environment should not act as |
| // a mirror (those cameras are typically not used as selfie cams). |
| bool ShouldCameraActLikeAMirror(const CameraInfo& camera_info) { |
| return camera_info.camera_facing_mode != |
| media::MEDIA_VIDEO_FACING_ENVIRONMENT; |
| } |
| |
| // Returns the CameraInfo item in `list` whose ID is equal to the given `id`, or |
| // nullptr if no such item exists. |
| const CameraInfo* GetCameraInfoById(const CameraId& id, |
| const CameraInfoList& list) { |
| const auto iter = std::find_if( |
| list.begin(), list.end(), |
| [&id](const CameraInfo& info) { return info.camera_id == id; }); |
| return iter == list.end() ? nullptr : &(*iter); |
| } |
| |
| // Returns the widget init params needed to create the camera preview widget. |
| views::Widget::InitParams CreateWidgetParams(const gfx::Rect& bounds) { |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); |
| params.parent = CaptureModeController::Get()->GetCameraPreviewParentWindow(); |
| params.bounds = bounds; |
| params.name = "CameraPreviewWidget"; |
| return params; |
| } |
| |
| // Called by `ContinueDraggingPreview` to make sure camera preview is not |
| // dragged outside of the capture surface. |
| void AdjustBoundsWithinConfinedBounds(const gfx::Rect& confined_bounds, |
| gfx::Rect& preview_bounds) { |
| const int x = preview_bounds.x(); |
| if (int offset = x - confined_bounds.x(); offset < 0) { |
| preview_bounds.set_x(x - offset); |
| } else if (int offset = confined_bounds.right() - preview_bounds.right(); |
| offset < 0) { |
| preview_bounds.set_x(x + offset); |
| } |
| |
| const int y = preview_bounds.y(); |
| if (int offset = y - confined_bounds.y(); offset < 0) { |
| preview_bounds.set_y(y - offset); |
| } else if (int offset = confined_bounds.bottom() - preview_bounds.bottom(); |
| offset < 0) { |
| preview_bounds.set_y(y + offset); |
| } |
| } |
| |
| // Returns the bounds that should be used in the bounds animation of the given |
| // `camera_preview_window`. If this window is parented to a window that uses |
| // screen coordinates, then the given `target_bounds` are in screen coordinates, |
| // and cannot be used for bounds animation (bounds animates relative to the |
| // window's parent). In this case, the bounds returned are in parent |
| // coordinates. |
| gfx::Rect GetTargetBoundsForBoundsAnimation( |
| const gfx::Rect& target_bounds, |
| aura::Window* camera_preview_window) { |
| gfx::Rect result = target_bounds; |
| auto* parent = camera_preview_window->parent(); |
| if (parent->GetProperty(wm::kUsesScreenCoordinatesKey)) |
| wm::ConvertRectFromScreen(parent, &result); |
| return result; |
| } |
| |
| gfx::Rect GetCollisionAvoidanceRect(aura::Window* root_window) { |
| DCHECK(root_window); |
| |
| UnifiedSystemTray* tray = RootWindowController::ForWindow(root_window) |
| ->GetStatusAreaWidget() |
| ->unified_system_tray(); |
| |
| if (!tray->IsBubbleShown()) |
| return gfx::Rect(); |
| |
| gfx::Rect collision_avoidance_rect = tray->GetBubbleBoundsInScreen(); |
| auto* message_center_bubble = tray->message_center_bubble(); |
| |
| if (message_center_bubble->IsMessageCenterVisible()) |
| collision_avoidance_rect.Union(message_center_bubble->GetBoundsInScreen()); |
| |
| // TODO(conniekxu): Return a vector of collision avoidance rects including |
| // other system UIs, like launcher. |
| return collision_avoidance_rect; |
| } |
| |
| // Called to avoid overlap between camera preview and system UIs when camera |
| // preview's bounds are changed, camera preview is created or destroyed. |
| void UpdateFloatingPanelBoundsIfNeeded(aura::Window* root_window) { |
| DCHECK(root_window); |
| |
| Shell::Get()->accessibility_controller()->UpdateFloatingPanelBoundsIfNeeded(); |
| |
| auto* pip_window_container = |
| root_window->GetChildById(kShellWindowId_PipContainer); |
| |
| for (aura::Window* pip_window : pip_window_container->children()) { |
| auto* pip_window_state = WindowState::Get(pip_window); |
| if (pip_window_state->IsPip()) |
| pip_window_state->UpdatePipBounds(); |
| } |
| } |
| |
| // Calculates a temporary suitable size to use for creating the preview widget. |
| // This size will be used to pick a suitable camera capture format, hence it |
| // will be based on the maximum size the preview can ever get to on the device |
| // with the current configuration. |
| gfx::Size CalculatePreviewInitialSize() { |
| int max_shorter_side = 0; |
| for (auto* root_window : Shell::GetAllRootWindows()) { |
| const auto work_area = display::Screen::GetScreen() |
| ->GetDisplayNearestWindow(root_window) |
| .work_area(); |
| const int shorter_side = std::min(work_area.width(), work_area.height()); |
| max_shorter_side = std::max(max_shorter_side, shorter_side); |
| } |
| const int preview_diameter = |
| max_shorter_side / capture_mode::kCaptureSurfaceShortSideDivider; |
| return gfx::Size(preview_diameter, preview_diameter); |
| } |
| |
| // Returns the appropriate `message_id` for ChromeVox alert on setting camera |
| // preview snap position. `for_collision_avoidance` indicates whether the |
| // preview snap position updating happens because of its bounds overlap with |
| // other system surfaces. |
| int GetMessageIdForSnapPosition(CameraPreviewSnapPosition snap_position, |
| bool for_collision_avoidance) { |
| switch (snap_position) { |
| case CameraPreviewSnapPosition::kTopRight: |
| return for_collision_avoidance |
| ? IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_UPPER_RIGHT_ON_CONFLICT |
| : IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_UPPER_RIGHT; |
| case CameraPreviewSnapPosition::kTopLeft: |
| return for_collision_avoidance |
| ? IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_UPPER_LEFT_ON_CONFLICT |
| : IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_UPPER_LEFT; |
| case CameraPreviewSnapPosition::kBottomRight: |
| return for_collision_avoidance |
| ? IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_LOWER_RIGHT_ON_CONFLICT |
| : IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_LOWER_RIGHT; |
| case CameraPreviewSnapPosition::kBottomLeft: |
| return for_collision_avoidance |
| ? IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_LOWER_LEFT_ON_CONFLICT |
| : IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_SNAPPED_TO_LOWER_LEFT; |
| } |
| } |
| |
| // Defines a window targeter that will be installed on the camera preview |
| // widget's window so that we can allow located events outside of the camera |
| // preview circle to go through and not be consumed by the camera preview. This |
| // enables the user to interact with other UI components below camera preview. |
| class CameraPreviewTargeter : public aura::WindowTargeter { |
| public: |
| explicit CameraPreviewTargeter(aura::Window* camera_preview_window) |
| : camera_preview_window_(camera_preview_window) {} |
| CameraPreviewTargeter(const CameraPreviewTargeter&) = delete; |
| CameraPreviewTargeter& operator=(const CameraPreviewTargeter&) = delete; |
| ~CameraPreviewTargeter() override = default; |
| |
| // aura::WindowTargeter: |
| ui::EventTarget* FindTargetForEvent(ui::EventTarget* root, |
| ui::Event* event) override { |
| if (event->IsLocatedEvent()) { |
| auto screen_location = event->AsLocatedEvent()->root_location(); |
| wm::ConvertPointToScreen(camera_preview_window_->GetRootWindow(), |
| &screen_location); |
| const gfx::Rect camera_preview_bounds = |
| camera_preview_window_->GetBoundsInScreen(); |
| const gfx::Point camera_preview_center_point = |
| camera_preview_bounds.CenterPoint(); |
| const int camera_preview_radius = camera_preview_bounds.width() / 2 - |
| capture_mode::kCameraPreviewBorderSize; |
| |
| // Check if events are outside of the camera preview circle by comparing |
| // if the distance between screen location and center of camera preview is |
| // larger than camera preview circle's radius. If it's larger, allow the |
| // events to go through so that they can be used by other UI components |
| // below camera preview. |
| if ((screen_location - camera_preview_center_point).LengthSquared() > |
| camera_preview_radius * camera_preview_radius) { |
| return nullptr; |
| } |
| } |
| |
| return aura::WindowTargeter::FindTargetForEvent(root, event); |
| } |
| |
| private: |
| aura::Window* const camera_preview_window_; |
| }; |
| |
| capture_mode_util::AnimationParams BuildCameraVisibilityAnimationParams( |
| bool target_visibility, |
| bool apply_scale_up_animation) { |
| return {target_visibility ? kCameraPreviewFadeInDuration |
| : kCameraPreviewFadeOutDuration, |
| gfx::Tween::LINEAR, apply_scale_up_animation}; |
| } |
| |
| } // namespace |
| |
| // ----------------------------------------------------------------------------- |
| // CameraId: |
| |
| CameraId::CameraId(std::string model_id_or_display_name, int number) |
| : model_id_or_display_name_(std::move(model_id_or_display_name)), |
| number_(number) { |
| DCHECK(!model_id_or_display_name_.empty()); |
| DCHECK_GE(number, 1); |
| } |
| |
| bool CameraId::operator<(const CameraId& rhs) const { |
| const int result = std::strcmp(model_id_or_display_name_.c_str(), |
| rhs.model_id_or_display_name_.c_str()); |
| return result != 0 ? result : (number_ < rhs.number_); |
| } |
| |
| std::string CameraId::ToString() const { |
| return base::StringPrintf("%s:%0d", model_id_or_display_name_.c_str(), |
| number_); |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // CameraInfo: |
| |
| CameraInfo::CameraInfo(CameraId camera_id, |
| std::string device_id, |
| std::string display_name, |
| const media::VideoCaptureFormats& supported_formats, |
| media::VideoFacingMode camera_facing_mode) |
| : camera_id(std::move(camera_id)), |
| device_id(std::move(device_id)), |
| display_name(std::move(display_name)), |
| supported_formats(supported_formats), |
| camera_facing_mode(camera_facing_mode) {} |
| |
| CameraInfo::CameraInfo(CameraInfo&&) = default; |
| |
| CameraInfo& CameraInfo::operator=(CameraInfo&&) = default; |
| |
| CameraInfo::~CameraInfo() = default; |
| |
| // ----------------------------------------------------------------------------- |
| // CaptureModeCameraController: |
| |
| CaptureModeCameraController::CaptureModeCameraController( |
| CaptureModeDelegate* delegate) |
| : delegate_(delegate) { |
| DCHECK(delegate_); |
| DCHECK(base::SystemMonitor::Get()) |
| << "No instance of SystemMonitor exists. If this is a unit test, please " |
| "create one."; |
| |
| base::SystemMonitor::Get()->AddDevicesChangedObserver(this); |
| ReconnectToVideoSourceProvider(); |
| Shell::Get()->system_tray_notifier()->AddSystemTrayObserver(this); |
| } |
| |
| CaptureModeCameraController::~CaptureModeCameraController() { |
| base::SystemMonitor::Get()->RemoveDevicesChangedObserver(this); |
| Shell::Get()->system_tray_notifier()->RemoveSystemTrayObserver(this); |
| } |
| |
| void CaptureModeCameraController::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void CaptureModeCameraController::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void CaptureModeCameraController::MaybeSelectFirstCamera() { |
| if (!selected_camera_.is_valid() && !available_cameras_.empty()) |
| SetSelectedCamera(available_cameras_[0].camera_id); |
| } |
| |
| bool CaptureModeCameraController::IsCameraDisabledByPolicy() const { |
| return delegate_->IsCameraDisabledByPolicy(); |
| } |
| |
| std::string CaptureModeCameraController::GetDisplayNameOfSelectedCamera() |
| const { |
| if (selected_camera_.is_valid()) { |
| const CameraInfo* camera_info = |
| GetCameraInfoById(selected_camera_, available_cameras_); |
| // `camera_info` might not exist in the test even though the |
| // `selected_camera_` is valid. |
| if (camera_info) |
| return camera_info->display_name; |
| } |
| return std::string(); |
| } |
| |
| void CaptureModeCameraController::SetSelectedCamera(CameraId camera_id) { |
| // When cameras are disabled by policy, we don't allow any camera selection. |
| if (IsCameraDisabledByPolicy()) { |
| LOG(WARNING) << "Camera is disabled by policy. Selecting camera: " |
| << camera_id.ToString() << " will be ignored."; |
| camera_id = CameraId{}; |
| } |
| |
| if (selected_camera_ == camera_id) |
| return; |
| |
| selected_camera_ = std::move(camera_id); |
| camera_reconnect_timer_.Stop(); |
| |
| for (auto& observer : observers_) |
| observer.OnSelectedCameraChanged(selected_camera_); |
| |
| const std::string camera_display_name = GetDisplayNameOfSelectedCamera(); |
| if (!camera_display_name.empty()) { |
| capture_mode_util::TriggerAccessibilityAlert(l10n_util::GetStringFUTF8( |
| IDS_ASH_SCREEN_CAPTURE_SELECTED_CAMERA_CHANGED, |
| base::UTF8ToUTF16(camera_display_name))); |
| } |
| |
| RefreshCameraPreview(); |
| } |
| |
| void CaptureModeCameraController::SetShouldShowPreview(bool value) { |
| should_show_preview_ = value; |
| RefreshCameraPreview(); |
| } |
| |
| void CaptureModeCameraController::MaybeReparentPreviewWidget() { |
| if (!camera_preview_widget_) |
| return; |
| |
| // Remove the camera preview from its transient parent if it has one. And |
| // reparenting it to the corresponding parent again. This is done to keep |
| // the camera preview in a correct state that is not a transient child of |
| // its parent. |
| auto* native_window = camera_preview_widget_->GetNativeWindow(); |
| if (auto* transient_parent = wm::GetTransientParent(native_window)) |
| wm::RemoveTransientChild(transient_parent, native_window); |
| |
| const bool was_visible_before = camera_preview_widget_->IsVisible(); |
| auto* controller = CaptureModeController::Get(); |
| auto* parent = controller->GetCameraPreviewParentWindow(); |
| DCHECK(parent); |
| if (parent != native_window->parent()) |
| views::Widget::ReparentNativeView(native_window, parent); |
| DCHECK(!wm::GetTransientParent(native_window)); |
| |
| MaybeUpdatePreviewWidget(); |
| if (was_visible_before != camera_preview_widget_->IsVisible()) { |
| capture_mode_util::TriggerAccessibilityAlertSoon( |
| was_visible_before ? IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_HIDDEN |
| : IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_ON); |
| } |
| } |
| |
| void CaptureModeCameraController::SetCameraPreviewSnapPosition( |
| CameraPreviewSnapPosition value, |
| bool animate) { |
| // Trigger a11y alert on setting camera preview snap position even though the |
| // snap position may actually not change. |
| capture_mode_util::TriggerAccessibilityAlert( |
| GetMessageIdForSnapPosition(value, /*for_collision_avoidance=*/false)); |
| |
| camera_preview_snap_position_ = value; |
| MaybeUpdatePreviewWidget(animate); |
| } |
| |
| void CaptureModeCameraController::MaybeUpdatePreviewWidget(bool animate) { |
| if (!camera_preview_widget_) |
| return; |
| |
| auto* controller = CaptureModeController::Get(); |
| DCHECK(controller->IsActive() || controller->is_recording_in_progress()); |
| const gfx::Rect confine_bounds = controller->GetCameraPreviewConfineBounds(); |
| |
| const capture_mode_util::CameraPreviewSizeSpecs size_specs = |
| capture_mode_util::CalculateCameraPreviewSizeSpecs( |
| confine_bounds.size(), is_camera_preview_collapsed_); |
| |
| camera_preview_view_->SetIsCollapsible(size_specs.is_collapsible); |
| |
| // If the surface within which the preview is confined becomes too small, the |
| // preview should hide immediately to avoid seeing it hide with animation |
| // outside the bounds of the new confine bounds. https://crbug.com/1320087. |
| const bool should_animate_visibility = |
| !size_specs.is_surface_too_small && !confine_bounds.IsEmpty() && |
| (camera_preview_widget_->GetNativeWindow()->parent()->GetId() != |
| kShellWindowId_UnparentedContainer); |
| |
| // We don't need to update the bounds if the widget is hidden. |
| // Also notice we should update the bounds before updating the visibility |
| // since when `is_first_bounds_update_` is true, we should apply the scale up |
| // animation which requires the bounds are set first to the camera preview. |
| const bool did_bounds_change = |
| size_specs.should_be_visible && |
| SetCameraPreviewBounds( |
| CalculatePreviewWidgetTargetBounds(confine_bounds, size_specs.size), |
| animate); |
| |
| const bool did_visibility_change = capture_mode_util::SetWidgetVisibility( |
| camera_preview_widget_.get(), size_specs.should_be_visible, |
| !should_animate_visibility |
| ? absl::nullopt |
| : absl::make_optional<capture_mode_util::AnimationParams>( |
| BuildCameraVisibilityAnimationParams( |
| /*target_visibility=*/size_specs.should_be_visible, |
| /*apply_scale_up_animation=*/is_first_bounds_update_))); |
| |
| if (controller->IsActive() && !controller->is_recording_in_progress()) { |
| controller->capture_mode_session() |
| ->OnCameraPreviewBoundsOrVisibilityChanged( |
| /*capture_surface_became_too_small=*/size_specs |
| .is_surface_too_small, |
| /*did_bounds_or_visibility_change=*/did_visibility_change || |
| did_bounds_change); |
| } |
| |
| if (did_bounds_change) { |
| UpdateFloatingPanelBoundsIfNeeded( |
| camera_preview_widget_->GetNativeWindow()->GetRootWindow()); |
| } |
| } |
| |
| void CaptureModeCameraController::StartDraggingPreview( |
| const gfx::PointF& screen_location) { |
| is_drag_in_progress_ = true; |
| previous_location_in_screen_ = screen_location; |
| |
| camera_preview_view_->RefreshResizeButtonVisibility(); |
| |
| auto* controller = CaptureModeController::Get(); |
| if (controller->IsActive()) |
| controller->capture_mode_session()->OnCameraPreviewDragStarted(); |
| |
| // Use cursor compositing instead of the platform cursor when dragging to |
| // ensure the cursor is aligned with the camera preview. |
| Shell::Get()->UpdateCursorCompositingEnabled(); |
| } |
| |
| void CaptureModeCameraController::ContinueDraggingPreview( |
| const gfx::PointF& screen_location) { |
| gfx::Rect current_bounds = GetCurrentBoundsMatchingConfineBoundsCoordinates(); |
| |
| current_bounds.Offset( |
| gfx::ToRoundedVector2d(screen_location - previous_location_in_screen_)); |
| AdjustBoundsWithinConfinedBounds( |
| CaptureModeController::Get()->GetCameraPreviewConfineBounds(), |
| current_bounds); |
| camera_preview_widget_->SetBounds(current_bounds); |
| previous_location_in_screen_ = screen_location; |
| } |
| |
| void CaptureModeCameraController::EndDraggingPreview( |
| const gfx::PointF& screen_location, |
| bool is_touch) { |
| ContinueDraggingPreview(screen_location); |
| SetCameraPreviewSnapPosition(CalculateSnapPositionOnDragEnded(), |
| /*animate=*/true); |
| |
| is_drag_in_progress_ = false; |
| camera_preview_view_->RefreshResizeButtonVisibility(); |
| |
| // Disable cursor compositing at the end of the drag. |
| Shell::Get()->UpdateCursorCompositingEnabled(); |
| |
| auto* controller = CaptureModeController::Get(); |
| if (controller->IsActive()) { |
| controller->capture_mode_session()->OnCameraPreviewDragEnded( |
| gfx::ToRoundedPoint(screen_location), is_touch); |
| } |
| } |
| |
| void CaptureModeCameraController::ToggleCameraPreviewSize() { |
| DCHECK(camera_preview_view_); |
| is_camera_preview_collapsed_ = !is_camera_preview_collapsed_; |
| MaybeUpdatePreviewWidget(/*animate=*/true); |
| } |
| |
| void CaptureModeCameraController::OnCaptureSessionStarted() { |
| GetCameraDevices(); |
| } |
| |
| void CaptureModeCameraController::OnRecordingStarted( |
| bool is_in_projector_mode) { |
| // Check if there's a camera disconnection that happened before recording |
| // starts. In this case, we don't want the camera preview to show, even if the |
| // camera reconnects within the allowed grace period. |
| if (selected_camera_.is_valid() && !camera_preview_widget_) |
| SetShouldShowPreview(false); |
| |
| in_recording_camera_disconnections_ = 0; |
| |
| const bool starts_with_camera = camera_preview_widget(); |
| RecordRecordingStartsWithCamera(starts_with_camera, is_in_projector_mode); |
| RecordCameraSizeOnStart(is_camera_preview_collapsed_ |
| ? CaptureModeCameraSize::kCollapsed |
| : CaptureModeCameraSize::kExpanded); |
| RecordCameraPositionOnStart(camera_preview_snap_position_); |
| } |
| |
| void CaptureModeCameraController::OnRecordingEnded() { |
| DCHECK(in_recording_camera_disconnections_); |
| SetShouldShowPreview(false); |
| RecordCameraDisconnectionsDuringRecordings( |
| *in_recording_camera_disconnections_); |
| in_recording_camera_disconnections_.reset(); |
| } |
| |
| void CaptureModeCameraController::OnFrameHandlerFatalError() { |
| if (!camera_preview_widget_) |
| return; |
| |
| DCHECK(camera_preview_view_); |
| DCHECK_EQ(selected_camera_, camera_preview_view_->camera_id()); |
| |
| base::EraseIf(available_cameras_, [&](const CameraInfo& info) { |
| return selected_camera_ == info.camera_id; |
| }); |
| |
| for (auto& observer : observers_) |
| observer.OnAvailableCamerasChanged(available_cameras_); |
| |
| RefreshCameraPreview(); |
| GetCameraDevices(); |
| } |
| |
| void CaptureModeCameraController::OnShuttingDown() { |
| // This should destroy any camera preview if present. |
| SetShouldShowPreview(false); |
| DCHECK(!should_show_preview_); |
| DCHECK(!camera_preview_widget_); |
| |
| is_shutting_down_ = true; |
| } |
| |
| void CaptureModeCameraController::PseudoFocusCameraPreview() { |
| DCHECK(camera_preview_view_); |
| DCHECK(camera_preview_view_->GetVisible()); |
| |
| auto* controller = CaptureModeController::Get(); |
| DCHECK(!controller->IsActive()); |
| DCHECK(controller->is_recording_in_progress()); |
| |
| camera_preview_view_->PseudoFocus(); |
| camera_preview_view_->UpdateA11yOverrideWindow(); |
| } |
| |
| void CaptureModeCameraController::OnActiveUserSessionChanged() { |
| if (!did_first_user_login_) { |
| did_first_user_login_ = true; |
| GetCameraDevices(); |
| } |
| } |
| |
| void CaptureModeCameraController::OnDevicesChanged( |
| base::SystemMonitor::DeviceType device_type) { |
| if (device_type == base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE) |
| GetCameraDevices(); |
| } |
| |
| void CaptureModeCameraController::OnSystemTrayBubbleShown() { |
| MaybeUpdatePreviewWidget(/*animate=*/true); |
| } |
| |
| void CaptureModeCameraController::ReconnectToVideoSourceProvider() { |
| if (is_shutting_down_) |
| return; |
| |
| video_source_provider_remote_.reset(); |
| most_recent_request_id_ = 0; |
| delegate_->ConnectToVideoSourceProvider( |
| video_source_provider_remote_.BindNewPipeAndPassReceiver()); |
| video_source_provider_remote_.set_disconnect_handler(base::BindOnce( |
| &CaptureModeCameraController::ReconnectToVideoSourceProvider, |
| base::Unretained(this))); |
| GetCameraDevices(); |
| } |
| |
| void CaptureModeCameraController::GetCameraDevices() { |
| if (is_shutting_down_ || !did_first_user_login_) |
| return; |
| |
| DCHECK(video_source_provider_remote_); |
| |
| video_source_provider_remote_->GetSourceInfos(base::BindOnce( |
| &CaptureModeCameraController::OnCameraDevicesReceived, |
| weak_ptr_factory_.GetWeakPtr(), ++most_recent_request_id_)); |
| } |
| |
| void CaptureModeCameraController::OnCameraDevicesReceived( |
| RequestId request_id, |
| const std::vector<media::VideoCaptureDeviceInfo>& devices) { |
| if (request_id < most_recent_request_id_) { |
| // Ignore any out-dated requests replies, since a reply from a more recent |
| // request is pending. |
| return; |
| } |
| |
| DCHECK_EQ(request_id, most_recent_request_id_); |
| |
| // Run the optional for-test closure at the exit of this function's scope. |
| base::ScopedClosureRunner deferred_runner; |
| if (on_camera_list_received_for_test_) { |
| deferred_runner.ReplaceClosure( |
| std::move(on_camera_list_received_for_test_)); |
| } |
| |
| const bool should_report_cameras_number = |
| !did_report_number_of_cameras_before_ || |
| (devices.size() != available_cameras_.size()); |
| if (should_report_cameras_number) { |
| did_report_number_of_cameras_before_ = true; |
| RecordNumberOfConnectedCameras(devices.size()); |
| } |
| |
| if (!DidDevicesChange(devices, available_cameras_)) |
| return; |
| |
| available_cameras_.clear(); |
| ModelIdToCountMap cam_models_map; |
| for (const auto& device : devices) { |
| const auto& descriptor = device.descriptor; |
| const auto& model_id_or_display_name = PickModelIdOrDisplayName(descriptor); |
| const int cam_number = |
| GetNextCameraNumber(model_id_or_display_name, &cam_models_map); |
| available_cameras_.emplace_back( |
| CameraId(model_id_or_display_name, cam_number), descriptor.device_id, |
| descriptor.display_name(), device.supported_formats, descriptor.facing); |
| } |
| |
| for (auto& observer : observers_) |
| observer.OnAvailableCamerasChanged(available_cameras_); |
| |
| RefreshCameraPreview(); |
| } |
| |
| void CaptureModeCameraController::RefreshCameraPreview() { |
| if (is_shutting_down_) |
| return; |
| |
| bool was_visible_before = false; |
| aura::Window* old_root = nullptr; |
| if (camera_preview_widget_) { |
| was_visible_before = camera_preview_widget_->IsVisible(); |
| old_root = camera_preview_widget_->GetNativeWindow()->GetRootWindow(); |
| } |
| // Trigger a11y alert and update floating windows bounds when camera preview |
| // is created or destroyed. The reason to trigger |
| // `RunPostRefreshCameraPreview` at the exit of this function is we should |
| // wait for camera preview's creation or destruction to be finished. |
| base::ScopedClosureRunner deferred_runner( |
| base::BindOnce(&CaptureModeCameraController::RunPostRefreshCameraPreview, |
| weak_ptr_factory_.GetWeakPtr(), was_visible_before)); |
| |
| const CameraInfo* camera_info = nullptr; |
| if (selected_camera_.is_valid()) { |
| if (camera_info = GetCameraInfoById(selected_camera_, available_cameras_); |
| camera_info) { |
| if (camera_reconnect_timer_.IsRunning()) { |
| const base::TimeDelta remaining_time = |
| camera_reconnect_timer_.desired_run_time() - base::TimeTicks::Now(); |
| const int reconnect_duration_in_seconds = |
| (kDisconnectionGracePeriod - remaining_time).InSeconds(); |
| RecordCameraReconnectDuration(reconnect_duration_in_seconds, |
| kDisconnectionGracePeriod.InSeconds()); |
| capture_mode_util::TriggerAccessibilityAlert( |
| IDS_ASH_SCREEN_CAPTURE_CAMERA_RECONNECTED); |
| } |
| // When a selected camera becomes available, we stop any grace period |
| // timer (if any), and decide whether to show or hide the preview widget |
| // based on the current value of `should_show_preview_`. |
| camera_reconnect_timer_.Stop(); |
| if (!should_show_preview_) |
| camera_info = nullptr; |
| } else { |
| // Here the selected camera is disconnected, we'll give it a grace period |
| // just in case it may reconnect again (this helps in the case of flaky |
| // camera connections). |
| camera_reconnect_timer_.Start( |
| FROM_HERE, kDisconnectionGracePeriod, this, |
| &CaptureModeCameraController::OnSelectedCameraDisconnected); |
| |
| if (in_recording_camera_disconnections_) |
| ++(*in_recording_camera_disconnections_); |
| |
| capture_mode_util::TriggerAccessibilityAlert( |
| IDS_ASH_SCREEN_CAPTURE_CAMERA_DISCONNECTED); |
| } |
| } |
| |
| if (!camera_info) { |
| camera_preview_widget_.reset(); |
| camera_preview_view_ = nullptr; |
| if (old_root) |
| UpdateFloatingPanelBoundsIfNeeded(old_root); |
| return; |
| } |
| |
| // Destroying the existing camera preview widget before recreating a new one |
| // when a different camera was selected. |
| if (camera_preview_view_ && |
| camera_preview_view_->camera_id() != selected_camera_) { |
| camera_preview_widget_.reset(); |
| camera_preview_view_ = nullptr; |
| } |
| |
| DCHECK(!IsCameraDisabledByPolicy()); |
| |
| if (!camera_preview_widget_) { |
| const auto initial_temp_bounds = gfx::Rect(CalculatePreviewInitialSize()); |
| camera_preview_widget_ = std::make_unique<views::Widget>(); |
| camera_preview_widget_->Init(CreateWidgetParams(initial_temp_bounds)); |
| auto* camera_preview_window = camera_preview_widget_->GetNativeWindow(); |
| camera_preview_window->SetEventTargeter( |
| std::make_unique<CameraPreviewTargeter>(camera_preview_window)); |
| mojo::Remote<video_capture::mojom::VideoSource> camera_video_source; |
| video_source_provider_remote_->GetVideoSource( |
| camera_info->device_id, |
| camera_video_source.BindNewPipeAndPassReceiver()); |
| camera_preview_view_ = camera_preview_widget_->SetContentsView( |
| std::make_unique<CameraPreviewView>( |
| this, selected_camera_, std::move(camera_video_source), |
| PickSuitableCaptureFormat(initial_temp_bounds.size(), |
| camera_info->supported_formats), |
| ShouldCameraActLikeAMirror(*camera_info))); |
| ui::Layer* layer = camera_preview_widget_->GetLayer(); |
| layer->SetFillsBoundsOpaquely(false); |
| layer->SetMasksToBounds(true); |
| |
| // Set `is_first_bounds_update_` to true here right after it's recreated. |
| // And set it back to false after camera preview is parented and updated to |
| // the correct bounds and with correct visibility. The value of |
| // `is_first_bounds_update_` will be used in `FadeInCameraPreview`, if it's |
| // true, scale up animation will be applied to show camera preview. |
| is_first_bounds_update_ = true; |
| MaybeReparentPreviewWidget(); |
| is_first_bounds_update_ = false; |
| } |
| |
| DCHECK(camera_preview_view_); |
| DCHECK_EQ(selected_camera_, camera_preview_view_->camera_id()); |
| } |
| |
| void CaptureModeCameraController::OnSelectedCameraDisconnected() { |
| DCHECK(selected_camera_.is_valid()); |
| |
| LOG(WARNING) |
| << "Selected camera: " << selected_camera_.ToString() |
| << " remained disconnected for longer than the grace period. Clearing."; |
| SetSelectedCamera(CameraId()); |
| } |
| |
| gfx::Rect CaptureModeCameraController::CalculatePreviewWidgetTargetBounds( |
| const gfx::Rect& confine_bounds, |
| const gfx::Size& preview_size) { |
| auto* controller = CaptureModeController::Get(); |
| aura::Window* parent = |
| camera_preview_widget_ |
| ? camera_preview_widget_->GetNativeWindow()->parent() |
| : controller->GetCameraPreviewParentWindow(); |
| DCHECK(parent); |
| const gfx::Rect collision_rect_screen = |
| GetCollisionAvoidanceRect(parent->GetRootWindow()); |
| |
| std::vector<CameraPreviewSnapPosition> snap_positions = { |
| CameraPreviewSnapPosition::kBottomRight, |
| CameraPreviewSnapPosition::kTopRight, CameraPreviewSnapPosition::kTopLeft, |
| CameraPreviewSnapPosition::kBottomLeft}; |
| |
| // Move `camera_preview_snap_position_` to the beginning of `snap_positions` |
| // vector, since we should always try the current snap position first. |
| base::EraseIf(snap_positions, |
| [this](CameraPreviewSnapPosition snap_position) { |
| return snap_position == camera_preview_snap_position_; |
| }); |
| snap_positions.insert(snap_positions.begin(), camera_preview_snap_position_); |
| |
| // Cache the current preview bounds and return it directly when we find no |
| // other snap position with no collisions. |
| gfx::Rect current_preview_bounds; |
| for (CameraPreviewSnapPosition snap_position : snap_positions) { |
| gfx::Rect preview_bounds = GetPreviewWidgetBoundsForSnapPosition( |
| confine_bounds, preview_size, snap_position); |
| if (!current_preview_bounds.IsEmpty()) |
| current_preview_bounds = preview_bounds; |
| |
| gfx::Rect preview_bounds_in_screen = preview_bounds; |
| |
| // Need to convert preview bounds to screen bounds if it's not since we |
| // need to check whether it intersects with system tray bounds in screen. |
| // Make sure we use the same coordinate system before we make the |
| // comparison. |
| if (!parent->GetProperty(wm::kUsesScreenCoordinatesKey)) |
| wm::ConvertRectToScreen(parent, &preview_bounds_in_screen); |
| |
| if (!preview_bounds_in_screen.Intersects(collision_rect_screen)) { |
| if (snap_position != camera_preview_snap_position_) { |
| camera_preview_snap_position_ = snap_position; |
| capture_mode_util::TriggerAccessibilityAlert( |
| GetMessageIdForSnapPosition(snap_position, |
| /*for_collision_avoidance=*/true)); |
| } |
| // Notice return `preview_bounds` instead of `preview_bounds_in_screen`, |
| // since it's the target bounds for camera preview in its parent's |
| // coordinate system. |
| return preview_bounds; |
| } |
| } |
| return current_preview_bounds; |
| } |
| |
| gfx::Rect CaptureModeCameraController::GetPreviewWidgetBoundsForSnapPosition( |
| const gfx::Rect& confine_bounds, |
| const gfx::Size& preview_size, |
| CameraPreviewSnapPosition snap_position) const { |
| auto* controller = CaptureModeController::Get(); |
| DCHECK(controller->IsActive() || controller->is_recording_in_progress()); |
| |
| if (confine_bounds.IsEmpty()) |
| return gfx::Rect(preview_size); |
| |
| gfx::Point origin; |
| switch (snap_position) { |
| case CameraPreviewSnapPosition::kTopLeft: |
| origin = confine_bounds.origin(); |
| origin.Offset(capture_mode::kSpaceBetweenCameraPreviewAndEdges, |
| capture_mode::kSpaceBetweenCameraPreviewAndEdges); |
| break; |
| case CameraPreviewSnapPosition::kBottomLeft: |
| origin = gfx::Point( |
| confine_bounds.x() + capture_mode::kSpaceBetweenCameraPreviewAndEdges, |
| confine_bounds.bottom() - preview_size.height() - |
| capture_mode::kSpaceBetweenCameraPreviewAndEdges); |
| break; |
| case CameraPreviewSnapPosition::kBottomRight: |
| origin = gfx::Point(confine_bounds.right() - preview_size.width() - |
| capture_mode::kSpaceBetweenCameraPreviewAndEdges, |
| confine_bounds.bottom() - preview_size.height() - |
| capture_mode::kSpaceBetweenCameraPreviewAndEdges); |
| break; |
| case CameraPreviewSnapPosition::kTopRight: |
| origin = gfx::Point(confine_bounds.right() - preview_size.width() - |
| capture_mode::kSpaceBetweenCameraPreviewAndEdges, |
| confine_bounds.y() + |
| capture_mode::kSpaceBetweenCameraPreviewAndEdges); |
| break; |
| } |
| return gfx::Rect(origin, preview_size); |
| } |
| |
| CameraPreviewSnapPosition |
| CaptureModeCameraController::CalculateSnapPositionOnDragEnded() const { |
| const gfx::Point center_point_of_preview_widget = |
| GetCurrentBoundsMatchingConfineBoundsCoordinates().CenterPoint(); |
| const gfx::Point center_point_of_confine_bounds = |
| CaptureModeController::Get() |
| ->GetCameraPreviewConfineBounds() |
| .CenterPoint(); |
| |
| if (center_point_of_preview_widget.x() < center_point_of_confine_bounds.x()) { |
| return center_point_of_preview_widget.y() < |
| center_point_of_confine_bounds.y() |
| ? CameraPreviewSnapPosition::kTopLeft |
| : CameraPreviewSnapPosition::kBottomLeft; |
| } |
| |
| return center_point_of_preview_widget.y() < center_point_of_confine_bounds.y() |
| ? CameraPreviewSnapPosition::kTopRight |
| : CameraPreviewSnapPosition::kBottomRight; |
| } |
| |
| gfx::Rect |
| CaptureModeCameraController::GetCurrentBoundsMatchingConfineBoundsCoordinates() |
| const { |
| aura::Window* preview_window = camera_preview_widget_->GetNativeWindow(); |
| aura::Window* parent = preview_window->parent(); |
| if (parent->GetProperty(wm::kUsesScreenCoordinatesKey)) |
| return preview_window->GetBoundsInScreen(); |
| return preview_window->bounds(); |
| } |
| |
| void CaptureModeCameraController::RunPostRefreshCameraPreview( |
| bool was_preview_visible_before) { |
| const bool is_preview_visible_now = |
| camera_preview_widget_ && camera_preview_widget_->IsVisible(); |
| if (was_preview_visible_before != is_preview_visible_now) { |
| capture_mode_util::TriggerAccessibilityAlertSoon( |
| was_preview_visible_before |
| ? IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_HIDDEN |
| : IDS_ASH_SCREEN_CAPTURE_CAMERA_PREVIEW_ON); |
| } |
| |
| if (camera_preview_widget_) { |
| UpdateFloatingPanelBoundsIfNeeded( |
| camera_preview_widget_->GetNativeWindow()->GetRootWindow()); |
| } |
| } |
| |
| bool CaptureModeCameraController::SetCameraPreviewBounds( |
| const gfx::Rect& target_bounds, |
| bool animate) { |
| DCHECK(camera_preview_widget_); |
| |
| const auto current_bounds = |
| GetCurrentBoundsMatchingConfineBoundsCoordinates(); |
| if (target_bounds == current_bounds) |
| return false; |
| |
| auto* preview_window = camera_preview_widget_->GetNativeWindow(); |
| if (animate) { |
| views::AnimationBuilder builder; |
| builder.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); |
| |
| // If the size changes, that means the preview view has to re-layout to keep |
| // the resize button at the same size, and at the same offset from the |
| // bottom of the preview. In this case we have to use bounds animation to |
| // achieve this. Otherwise, transform animation will cause the button to |
| // shrink or expand during the animation. |
| if (target_bounds.size() == current_bounds.size()) { |
| // Use transform animation. |
| camera_preview_widget_->SetBounds(target_bounds); |
| gfx::Transform transform; |
| transform.Translate(current_bounds.CenterPoint() - |
| target_bounds.CenterPoint()); |
| ui::Layer* layer = preview_window->layer(); |
| layer->SetTransform(transform); |
| builder.Once() |
| .SetDuration(kCameraBoundsChangeAnimationDuration) |
| .SetTransform(layer, gfx::Transform(), |
| gfx::Tween::ACCEL_5_70_DECEL_90); |
| } else { |
| // Use bounds animation. |
| const auto target_bounds_in_parent = |
| GetTargetBoundsForBoundsAnimation(target_bounds, preview_window); |
| builder.Once() |
| .SetDuration(kCameraBoundsChangeAnimationDuration) |
| .SetBounds(preview_window, target_bounds_in_parent, |
| gfx::Tween::ACCEL_20_DECEL_100); |
| } |
| } else { |
| camera_preview_widget_->SetBounds(target_bounds); |
| } |
| |
| return true; |
| } |
| |
| } // namespace ash |