// 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 #include #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; // 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& 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( 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& 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(); camera_preview_widget_->Init(CreateWidgetParams(initial_temp_bounds)); auto* camera_preview_window = camera_preview_widget_->GetNativeWindow(); camera_preview_window->SetEventTargeter( std::make_unique(camera_preview_window)); mojo::Remote 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( 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 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