From 13cf4cb065ad180ad1e71373dc5ed3f7a41847ec Mon Sep 17 00:00:00 2001 From: Ahmed Fakhry Date: Mon, 15 Mar 2021 18:58:19 +0000 Subject: [PATCH] capture_mode: Show an image thumbnail of the video in the notification With CL, the recording service will be able to provide an RGB image to the client, which can be used as a thumbnail representation of the video. This CL also has the necessary changes to show a "play" icon overlay on top of the video thumbnail in the notification image. BUG=1176327 TEST=Manual. Added unit test. Change-Id: Ifc080d055da36cc8b73f11b21de99c7f94fb25dd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2753760 Commit-Queue: Ahmed Fakhry Reviewed-by: Daniel Cheng Reviewed-by: James Cook Cr-Commit-Position: refs/heads/master@{#862899} --- ash/capture_mode/capture_mode_controller.cc | 60 ++++++++----- ash/capture_mode/capture_mode_controller.h | 8 +- .../capture_mode_notification_view.cc | 85 ++++++++++++++----- .../capture_mode_notification_view.h | 34 +++++--- ash/capture_mode/capture_mode_unittests.cc | 31 +++++++ .../test_capture_mode_delegate.cc | 12 ++- ash/capture_mode/test_capture_mode_delegate.h | 5 ++ ash/resources/vector_icons/BUILD.gn | 1 + .../vector_icons/capture_mode_play.icon | 10 +++ ash/services/recording/public/mojom/BUILD.gn | 1 + .../public/mojom/recording_service.mojom | 6 +- ash/services/recording/recording_service.cc | 41 ++++++++- ash/services/recording/recording_service.h | 6 ++ 13 files changed, 244 insertions(+), 56 deletions(-) create mode 100644 ash/resources/vector_icons/capture_mode_play.icon diff --git a/ash/capture_mode/capture_mode_controller.cc b/ash/capture_mode/capture_mode_controller.cc index f150f499e27578..5b692e2c77895f 100644 --- a/ash/capture_mode/capture_mode_controller.cc +++ b/ash/capture_mode/capture_mode_controller.cc @@ -68,8 +68,9 @@ constexpr char kScreenCaptureNotificationId[] = "capture_mode_notification"; constexpr char kScreenCaptureStoppedNotificationId[] = "capture_mode_stopped_notification"; constexpr char kScreenCaptureNotifierId[] = "ash.capture_mode_controller"; -constexpr char kScreenCaptureNotificationType[] = - "capture_mode_notification_type"; +constexpr char kScreenShotNotificationType[] = "screen_shot_notification_type"; +constexpr char kScreenRecordingNotificationType[] = + "screen_recording_notification_type"; // The format strings of the file names of captured images. // TODO(afakhry): Discuss with UX localizing "Screenshot" and "Screen @@ -162,6 +163,8 @@ void DeleteFileAsync(scoped_refptr task_runner, } // Shows a Capture Mode related notification with the given parameters. +// |for_video_thumbnail| will be considered only if |optional_fields| contain +// an image to show in the notification as a thumbnail for what was captured. void ShowNotification( const std::string& notification_id, int title_id, @@ -170,7 +173,8 @@ void ShowNotification( scoped_refptr delegate, message_center::SystemNotificationWarningLevel warning_level = message_center::SystemNotificationWarningLevel::NORMAL, - const gfx::VectorIcon& notification_icon = kCaptureModeIcon) { + const gfx::VectorIcon& notification_icon = kCaptureModeIcon, + bool for_video_thumbnail = false) { const auto type = optional_fields.image.IsEmpty() ? message_center::NOTIFICATION_TYPE_SIMPLE : message_center::NOTIFICATION_TYPE_CUSTOM; @@ -184,8 +188,11 @@ void ShowNotification( message_center::NotifierType::SYSTEM_COMPONENT, kScreenCaptureNotifierId), optional_fields, delegate, notification_icon, warning_level); - if (type == message_center::NOTIFICATION_TYPE_CUSTOM) - notification->set_custom_view_type(kScreenCaptureNotificationType); + if (type == message_center::NOTIFICATION_TYPE_CUSTOM) { + notification->set_custom_view_type(for_video_thumbnail + ? kScreenRecordingNotificationType + : kScreenShotNotificationType); + } // Remove the previous notification before showing the new one if there is // any. @@ -304,10 +311,15 @@ CaptureModeController::CaptureModeController( weak_ptr_factory_.GetWeakPtr())); DCHECK(!message_center::MessageViewFactory::HasCustomNotificationViewFactory( - kScreenCaptureNotificationType)); + kScreenShotNotificationType)); + DCHECK(!message_center::MessageViewFactory::HasCustomNotificationViewFactory( + kScreenRecordingNotificationType)); + message_center::MessageViewFactory::SetCustomNotificationViewFactory( + kScreenShotNotificationType, + base::BindRepeating(&CaptureModeNotificationView::CreateForImage)); message_center::MessageViewFactory::SetCustomNotificationViewFactory( - kScreenCaptureNotificationType, - base::BindRepeating(&CaptureModeNotificationView::Create)); + kScreenRecordingNotificationType, + base::BindRepeating(&CaptureModeNotificationView::CreateForVideo)); Shell::Get()->session_controller()->AddObserver(this); chromeos::PowerManagerClient::Get()->AddObserver(this); @@ -316,9 +328,11 @@ CaptureModeController::CaptureModeController( CaptureModeController::~CaptureModeController() { chromeos::PowerManagerClient::Get()->RemoveObserver(this); Shell::Get()->session_controller()->RemoveObserver(this); - // Remove the custom notification view factory. + // Remove the custom notification view factories. message_center::MessageViewFactory::ClearCustomNotificationViewFactory( - kScreenCaptureNotificationType); + kScreenShotNotificationType); + message_center::MessageViewFactory::ClearCustomNotificationViewFactory( + kScreenRecordingNotificationType); DCHECK_EQ(g_instance, this); g_instance = nullptr; @@ -504,7 +518,7 @@ void CaptureModeController::RefreshContentProtection() { // HDCP violation is also considered a failure, and we're not going to wait // for any buffered frames in the recording service. RecordEndRecordingReason(EndRecordingReason::kHdcpInterruption); - OnRecordingEnded(/*success=*/false); + OnRecordingEnded(/*success=*/false, gfx::ImageSkia()); ShowVideoRecordingStoppedNotification(/*for_hdcp=*/true); } } @@ -516,7 +530,8 @@ void CaptureModeController::OnMuxerOutput(const std::string& chunk) { .Then(on_video_file_status_); } -void CaptureModeController::OnRecordingEnded(bool success) { +void CaptureModeController::OnRecordingEnded(bool success, + const gfx::ImageSkia& thumbnail) { delegate_->StopObservingRestrictedContent(); // If |success| is false, then recording has been force-terminated due to a @@ -534,7 +549,7 @@ void CaptureModeController::OnRecordingEnded(bool success) { DCHECK(video_file_handler_); video_file_handler_.AsyncCall(&VideoFileHandler::FlushBufferedChunks) .Then(base::BindOnce(&CaptureModeController::OnVideoFileSaved, - weak_ptr_factory_.GetWeakPtr())); + weak_ptr_factory_.GetWeakPtr(), thumbnail)); } void CaptureModeController::OnActiveUserSessionChanged( @@ -644,7 +659,7 @@ void CaptureModeController::EndSessionOrRecording(EndRecordingReason reason) { // block the suspend until all chunks have been received, and then we can // resume it. RecordEndRecordingReason(EndRecordingReason::kImminentSuspend); - OnRecordingEnded(/*success=*/false); + OnRecordingEnded(/*success=*/false, gfx::ImageSkia()); return; } @@ -786,7 +801,7 @@ void CaptureModeController::OnRecordingServiceDisconnected() { // StopRecording(), and it calling us back with OnRecordingEnded(), so we call // OnRecordingEnded() in all cases. RecordEndRecordingReason(EndRecordingReason::kRecordingServiceDisconnected); - OnRecordingEnded(/*success=*/false); + OnRecordingEnded(/*success=*/false, gfx::ImageSkia()); } CaptureAllowance CaptureModeController::IsCaptureAllowedByEnterprisePolicies( @@ -819,7 +834,9 @@ void CaptureModeController::TerminateRecordingUiElements() { void CaptureModeController::CaptureImage(const CaptureParams& capture_params, const base::FilePath& path) { - DCHECK_EQ(CaptureModeType::kImage, type_); + // Note that |type_| may not necessarily be |kImage| here, since this may be + // called to take an instant fullscreen screenshot for the keyboard shortcut, + // which doesn't go through the capture mode UI, and doesn't change |type_|. DCHECK_EQ(CaptureAllowance::kAllowed, IsCaptureAllowedByEnterprisePolicies(capture_params)); @@ -909,14 +926,17 @@ void CaptureModeController::OnVideoFileStatus(bool success) { EndVideoRecording(EndRecordingReason::kFileIoError); } -void CaptureModeController::OnVideoFileSaved(bool success) { +void CaptureModeController::OnVideoFileSaved( + const gfx::ImageSkia& video_thumbnail, + bool success) { DCHECK(base::CurrentUIThread::IsSet()); DCHECK(video_file_handler_); if (!success) { ShowFailureNotification(); } else { - ShowPreviewNotification(current_video_file_path_, gfx::Image(), + ShowPreviewNotification(current_video_file_path_, + gfx::Image(video_thumbnail), CaptureModeType::kVideo); DCHECK(!recording_start_time_.is_null()); RecordCaptureModeRecordTime( @@ -965,7 +985,9 @@ void CaptureModeController::ShowPreviewNotification( base::MakeRefCounted( base::BindRepeating(&CaptureModeController::HandleNotificationClicked, weak_ptr_factory_.GetWeakPtr(), - screen_capture_path, type))); + screen_capture_path, type)), + message_center::SystemNotificationWarningLevel::NORMAL, kCaptureModeIcon, + for_video); } void CaptureModeController::HandleNotificationClicked( diff --git a/ash/capture_mode/capture_mode_controller.h b/ash/capture_mode/capture_mode_controller.h index 77cfcf80a6ca62..5d523aec766193 100644 --- a/ash/capture_mode/capture_mode_controller.h +++ b/ash/capture_mode/capture_mode_controller.h @@ -116,7 +116,7 @@ class ASH_EXPORT CaptureModeController // recording::mojom::RecordingServiceClient: void OnMuxerOutput(const std::string& chunk) override; - void OnRecordingEnded(bool success) override; + void OnRecordingEnded(bool success, const gfx::ImageSkia& thumbnail) override; // SessionObserver: void OnActiveUserSessionChanged(const AccountId& account_id) override; @@ -237,8 +237,10 @@ class ASH_EXPORT CaptureModeController void OnVideoFileStatus(bool success); // Called back when the |video_file_handler_| flushes the remaining cached - // video chunks in its buffer. Called on the UI thread. - void OnVideoFileSaved(bool success); + // video chunks in its buffer. Called on the UI thread. |video_thumbnail| is + // an RGB image provided by the recording service that can be used as a + // thumbnail of the video in the notification. + void OnVideoFileSaved(const gfx::ImageSkia& video_thumbnail, bool success); // Shows a preview notification of the newly taken screenshot or screen // recording. diff --git a/ash/capture_mode/capture_mode_notification_view.cc b/ash/capture_mode/capture_mode_notification_view.cc index c60190c19af1d6..07f5a6abcdb0b4 100644 --- a/ash/capture_mode/capture_mode_notification_view.cc +++ b/ash/capture_mode/capture_mode_notification_view.cc @@ -9,6 +9,7 @@ #include "ash/style/ash_color_provider.h" #include "ash/style/scoped_light_mode_as_default.h" #include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/paint_vector_icon.h" #include "ui/views/background.h" #include "ui/views/layout/box_layout.h" #include "ui/views/view.h" @@ -17,15 +18,20 @@ namespace ash { namespace { -// Constants related to the banner view on the image capture notification. +// Constants related to the banner view on the image capture notifications. constexpr int kBannerHeightDip = 36; constexpr int kBannerHorizontalInsetDip = 12; constexpr int kBannerVerticalInsetDip = 8; constexpr int kBannerIconTextSpacingDip = 8; constexpr int kBannerIconSizeDip = 20; +// Constants related to the play icon view for video capture notifications. +constexpr int kPlayIconSizeDip = 24; +constexpr int kPlayIconBackgroundCornerRadiusDip = 20; +constexpr gfx::Size kPlayIconViewSize{40, 40}; + // Creates the banner view that will show on top of the notification image. -std::unique_ptr CreateBannerViewImpl() { +std::unique_ptr CreateBannerView() { std::unique_ptr banner_view = std::make_unique(); // Use the light mode as default as notification is still using light @@ -61,15 +67,34 @@ std::unique_ptr CreateBannerViewImpl() { return banner_view; } +// Creates the play icon view which shows on top of the video thumbnail in the +// notification. +std::unique_ptr CreatePlayIconView() { + auto play_view = std::make_unique(); + auto* color_provider = AshColorProvider::Get(); + const SkColor icon_color = color_provider->GetContentLayerColor( + AshColorProvider::ContentLayerType::kIconColorPrimary); + play_view->SetImage(gfx::CreateVectorIcon(kCaptureModePlayIcon, + kPlayIconSizeDip, icon_color)); + play_view->SetHorizontalAlignment(views::ImageView::Alignment::kCenter); + play_view->SetVerticalAlignment(views::ImageView::Alignment::kCenter); + const SkColor background_color = color_provider->GetBaseLayerColor( + AshColorProvider::BaseLayerType::kTransparent80); + play_view->SetBackground(views::CreateRoundedRectBackground( + background_color, kPlayIconBackgroundCornerRadiusDip)); + return play_view; +} + } // namespace CaptureModeNotificationView::CaptureModeNotificationView( - const message_center::Notification& notification) - : message_center::NotificationViewMD(notification) { - // Create the banner view if notification image is not empty. The banner - // will show on top of the notification image. + const message_center::Notification& notification, + CaptureModeType capture_type) + : message_center::NotificationViewMD(notification), + capture_type_(capture_type) { + // Creates the extra view which will depend on the type of the notification. if (!notification.image().IsEmpty()) - CreateBannerView(); + CreateExtraView(); // We need to observe this view as |this| view will be re-used for // notifications for with/without image scenarios if |this| is not destroyed @@ -81,33 +106,51 @@ CaptureModeNotificationView::~CaptureModeNotificationView() = default; // static std::unique_ptr -CaptureModeNotificationView::Create( +CaptureModeNotificationView::CreateForImage( const message_center::Notification& notification) { - return std::make_unique(notification); + return std::make_unique(notification, + CaptureModeType::kImage); +} + +// static +std::unique_ptr +CaptureModeNotificationView::CreateForVideo( + const message_center::Notification& notification) { + return std::make_unique(notification, + CaptureModeType::kVideo); } void CaptureModeNotificationView::Layout() { message_center::NotificationViewMD::Layout(); - if (!banner_view_) + if (!extra_view_) return; - // Calculate the banner view's desired bounds. - gfx::Rect banner_bounds = image_container_view()->GetContentsBounds(); - banner_bounds.set_y(banner_bounds.bottom() - kBannerHeightDip); - banner_bounds.set_height(kBannerHeightDip); - banner_view_->SetBoundsRect(banner_bounds); + gfx::Rect extra_view_bounds = image_container_view()->GetContentsBounds(); + + if (capture_type_ == CaptureModeType::kImage) { + // The extra view in this case is a banner laid out at the bottom of the + // image container. + extra_view_bounds.set_y(extra_view_bounds.bottom() - kBannerHeightDip); + extra_view_bounds.set_height(kBannerHeightDip); + } else { + DCHECK_EQ(capture_type_, CaptureModeType::kVideo); + // The extra view in this case is a play icon centered in the view. + extra_view_bounds.ClampToCenteredSize(kPlayIconViewSize); + } + + extra_view_->SetBoundsRect(extra_view_bounds); } void CaptureModeNotificationView::OnChildViewAdded(views::View* observed_view, views::View* child) { if (observed_view == this && child == image_container_view()) - CreateBannerView(); + CreateExtraView(); } void CaptureModeNotificationView::OnChildViewRemoved(views::View* observed_view, views::View* child) { if (observed_view == this && child == image_container_view()) - banner_view_ = nullptr; + extra_view_ = nullptr; } void CaptureModeNotificationView::OnViewIsDeleting(View* observed_view) { @@ -115,11 +158,13 @@ void CaptureModeNotificationView::OnViewIsDeleting(View* observed_view) { views::View::RemoveObserver(this); } -void CaptureModeNotificationView::CreateBannerView() { +void CaptureModeNotificationView::CreateExtraView() { DCHECK(image_container_view()); DCHECK(!image_container_view()->children().empty()); - DCHECK(!banner_view_); - banner_view_ = image_container_view()->AddChildView(CreateBannerViewImpl()); + DCHECK(!extra_view_); + extra_view_ = image_container_view()->AddChildView( + capture_type_ == CaptureModeType::kImage ? CreateBannerView() + : CreatePlayIconView()); } } // namespace ash diff --git a/ash/capture_mode/capture_mode_notification_view.h b/ash/capture_mode/capture_mode_notification_view.h index f8b185621d3da4..81fa2e322848ca 100644 --- a/ash/capture_mode/capture_mode_notification_view.h +++ b/ash/capture_mode/capture_mode_notification_view.h @@ -6,28 +6,36 @@ #define ASH_CAPTURE_MODE_CAPTURE_MODE_NOTIFICATION_VIEW_H_ #include "ash/ash_export.h" +#include "ash/capture_mode/capture_mode_types.h" #include "ui/message_center/views/notification_view_md.h" #include "ui/views/view_observer.h" namespace ash { -// A customized notification view for capture mode that can show a notification -// with a banner on top of the notification image. +// A customized notification view for capture mode that adjusts the capture +// notification by either showing a banner on top of the notification image for +// image captures, or a play icon on top of the video thumbnail. class ASH_EXPORT CaptureModeNotificationView : public message_center::NotificationViewMD, public views::ViewObserver { public: - explicit CaptureModeNotificationView( - const message_center::Notification& notification); + CaptureModeNotificationView(const message_center::Notification& notification, + CaptureModeType capture_type); CaptureModeNotificationView(const CaptureModeNotificationView&) = delete; CaptureModeNotificationView& operator=(const CaptureModeNotificationView&) = delete; ~CaptureModeNotificationView() override; // Creates the custom capture mode notification for image capture - // notification. There is a banner on top of the image area of the + // notifications. There is a banner on top of the image area of the // notification to indicate the image has been copied to clipboard. - static std::unique_ptr Create( + static std::unique_ptr CreateForImage( + const message_center::Notification& notification); + + // Creates the custom capture mode notification for video capture + // notifications. There is a superimposed "play" icon on top of the video + // thumbnail image. + static std::unique_ptr CreateForVideo( const message_center::Notification& notification); // message_center::NotificationViewMD: @@ -41,11 +49,17 @@ class ASH_EXPORT CaptureModeNotificationView void OnViewIsDeleting(View* observed_view) override; private: - void CreateBannerView(); + void CreateExtraView(); + + // The type of capture this notification was created for. + const CaptureModeType capture_type_; - // The banner view that shows a banner string on top of the captured image. - // Owned by view hierarchy. - views::View* banner_view_ = nullptr; + // The extra view created on top of the notification image. This will be a + // banner clarifying that the image was copied to the clipboard in case of + // image capture, or a superimposed "play" icon on top of the video thumbnail + // image. + // Owned by the view hierarchy. + views::View* extra_view_ = nullptr; }; } // namespace ash diff --git a/ash/capture_mode/capture_mode_unittests.cc b/ash/capture_mode/capture_mode_unittests.cc index c4b3dd5262e01a..58647333c539f3 100644 --- a/ash/capture_mode/capture_mode_unittests.cc +++ b/ash/capture_mode/capture_mode_unittests.cc @@ -64,6 +64,7 @@ #include "ui/gfx/geometry/insets.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/vector2d.h" +#include "ui/gfx/image/image_unittest_util.h" #include "ui/message_center/message_center.h" #include "ui/message_center/message_center_observer.h" #include "ui/message_center/public/cpp/notification.h" @@ -1663,6 +1664,36 @@ TEST_F(CaptureModeTest, CaptureModeEntryPointHistograms) { kTabletHistogram, CaptureModeEntryType::kAccelTakePartialScreenshot, 2); } +// Verifies that the video notification will show the same thumbnail image as +// sent by recording service. +TEST_F(CaptureModeTest, VideoNotificationThumbnail) { + auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, + CaptureModeType::kVideo); + controller->StartVideoRecordingImmediatelyForTesting(); + EXPECT_TRUE(controller->is_recording_in_progress()); + + auto* test_delegate = + static_cast(controller->delegate_for_testing()); + + // Use a random bitmap as the fake thumbnail. + SkBitmap thumbnail; + thumbnail.allocN32Pixels(400, 300); + EXPECT_FALSE(thumbnail.drawsNothing()); + test_delegate->SetVideoThumbnail( + gfx::ImageSkia::CreateFrom1xBitmap(thumbnail)); + + CaptureNotificationWaiter waiter; + controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); + EXPECT_FALSE(controller->is_recording_in_progress()); + waiter.Wait(); + + const message_center::Notification* notification = GetPreviewNotification(); + EXPECT_TRUE(notification); + EXPECT_FALSE(notification->image().IsEmpty()); + const SkBitmap actual_thumbnail = notification->image().AsBitmap(); + EXPECT_TRUE(gfx::test::AreBitmapsEqual(actual_thumbnail, thumbnail)); +} + TEST_F(CaptureModeTest, WindowRecordingCaptureId) { auto window = CreateTestWindow(gfx::Rect(200, 200)); StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); diff --git a/ash/capture_mode/test_capture_mode_delegate.cc b/ash/capture_mode/test_capture_mode_delegate.cc index 40de09a7b00190..1082ad55ec4d7a 100644 --- a/ash/capture_mode/test_capture_mode_delegate.cc +++ b/ash/capture_mode/test_capture_mode_delegate.cc @@ -26,6 +26,9 @@ class FakeRecordingService : public recording::mojom::RecordingService { } gfx::Size frame_sink_size() const { return frame_sink_size_; } const gfx::Size& video_size() const { return video_size_; } + void set_thumbnail(const gfx::ImageSkia& thumbnail) { + thumbnail_ = thumbnail; + } void Bind( mojo::PendingReceiver receiver) { @@ -76,7 +79,7 @@ class FakeRecordingService : public recording::mojom::RecordingService { video_size_ = crop_region.size(); } void StopRecording() override { - remote_client_->OnRecordingEnded(/*success=*/true); + remote_client_->OnRecordingEnded(/*success=*/true, thumbnail_); remote_client_.FlushForTesting(); } void OnRecordedWindowChangingRoot( @@ -102,6 +105,7 @@ class FakeRecordingService : public recording::mojom::RecordingService { CaptureModeSource current_capture_source_ = CaptureModeSource::kFullscreen; gfx::Size frame_sink_size_; gfx::Size video_size_; + gfx::ImageSkia thumbnail_; }; // ----------------------------------------------------------------------------- @@ -129,6 +133,12 @@ gfx::Size TestCaptureModeDelegate::GetCurrentVideoSize() const { return fake_service_ ? fake_service_->video_size() : gfx::Size(); } +void TestCaptureModeDelegate::SetVideoThumbnail( + const gfx::ImageSkia& thumbnail) { + if (fake_service_) + fake_service_->set_thumbnail(thumbnail); +} + base::FilePath TestCaptureModeDelegate::GetScreenCaptureDir() const { return fake_downloads_dir_; } diff --git a/ash/capture_mode/test_capture_mode_delegate.h b/ash/capture_mode/test_capture_mode_delegate.h index b012f6b3fa27d5..04f4e4742409fe 100644 --- a/ash/capture_mode/test_capture_mode_delegate.h +++ b/ash/capture_mode/test_capture_mode_delegate.h @@ -12,6 +12,7 @@ #include "base/files/file_path.h" #include "components/viz/common/surfaces/frame_sink_id.h" #include "ui/gfx/geometry/size.h" +#include "ui/gfx/image/image_skia.h" namespace ash { @@ -33,6 +34,10 @@ class TestCaptureModeDelegate : public CaptureModeDelegate { // Gets the current video size being captured by the fake service. gfx::Size GetCurrentVideoSize() const; + // Sets the thumbnail image that will be used by the fake service to provide + // it to the client. + void SetVideoThumbnail(const gfx::ImageSkia& thumbnail); + // CaptureModeDelegate: base::FilePath GetScreenCaptureDir() const override; void ShowScreenCaptureItemInFolder(const base::FilePath& file_path) override; diff --git a/ash/resources/vector_icons/BUILD.gn b/ash/resources/vector_icons/BUILD.gn index e6bc524af90f0c..726c8c4f2d2180 100644 --- a/ash/resources/vector_icons/BUILD.gn +++ b/ash/resources/vector_icons/BUILD.gn @@ -36,6 +36,7 @@ aggregate_vector_icons("ash_vector_icons") { "capture_mode_image.icon", "capture_mode_mic.icon", "capture_mode_mic_off.icon", + "capture_mode_play.icon", "capture_mode_region.icon", "capture_mode_settings.icon", "capture_mode_video.icon", diff --git a/ash/resources/vector_icons/capture_mode_play.icon b/ash/resources/vector_icons/capture_mode_play.icon new file mode 100644 index 00000000000000..a5a79474d01a9d --- /dev/null +++ b/ash/resources/vector_icons/capture_mode_play.icon @@ -0,0 +1,10 @@ +// Copyright 2021 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. + +CANVAS_DIMENSIONS, 20, +MOVE_TO, 6, 16, +R_LINE_TO, 10, -6, +LINE_TO, 6, 4, +R_V_LINE_TO, 12, +CLOSE diff --git a/ash/services/recording/public/mojom/BUILD.gn b/ash/services/recording/public/mojom/BUILD.gn index f1c259dd6c8893..99626923a21ec1 100644 --- a/ash/services/recording/public/mojom/BUILD.gn +++ b/ash/services/recording/public/mojom/BUILD.gn @@ -10,5 +10,6 @@ mojom("mojom") { deps = [ "//media/mojo/mojom", "//services/viz/privileged/mojom/compositing", + "//ui/gfx/image/mojom", ] } diff --git a/ash/services/recording/public/mojom/recording_service.mojom b/ash/services/recording/public/mojom/recording_service.mojom index e4f33d192adff2..6744cde67edb97 100644 --- a/ash/services/recording/public/mojom/recording_service.mojom +++ b/ash/services/recording/public/mojom/recording_service.mojom @@ -10,6 +10,7 @@ import "services/viz/privileged/mojom/compositing/frame_sink_video_capture.mojom import "services/viz/public/mojom/compositing/frame_sink_id.mojom"; import "services/viz/public/mojom/compositing/subtree_capture_id.mojom"; import "ui/gfx/geometry/mojom/geometry.mojom"; +import "ui/gfx/image/mojom/image.mojom"; // Defines the interface for the recording service client (e.g. ash), which // lives in a remote process other than that of the recording service itself. @@ -25,8 +26,9 @@ interface RecordingServiceClient { // Called by the service to inform the client that an in-progress video // recording ended, and no further frames will be provided. If |success| is // false, then recording is being terminated by the service itself due to an - // internal failure. - OnRecordingEnded(bool success); + // internal failure. The service can provide an RGB image |thumbnail| + // representing the recorded video. It can be empty in case of a failure. + OnRecordingEnded(bool success, gfx.mojom.ImageSkia? thumbnail); }; // Defines the interface of the recording service which is implemented by diff --git a/ash/services/recording/recording_service.cc b/ash/services/recording/recording_service.cc index f91b55345df457..495f29dde9f3ce 100644 --- a/ash/services/recording/recording_service.cc +++ b/ash/services/recording/recording_service.cc @@ -18,8 +18,11 @@ #include "media/base/audio_codecs.h" #include "media/base/status.h" #include "media/base/video_frame.h" +#include "media/base/video_util.h" #include "media/capture/mojom/video_capture_types.mojom.h" +#include "media/renderers/paint_canvas_video_renderer.h" #include "services/audio/public/cpp/device_factory.h" +#include "ui/gfx/image/image_skia_operations.h" namespace recording { @@ -42,6 +45,11 @@ constexpr float kBitsPerSecondPerSquarePixel = // IPC call to the client. constexpr int kMaxBufferedChunks = 238; +// The size within which we will try to fit a thumbnail image extracted from the +// first valid video frame. The value was chosen to be suitable with the image +// container in the notification UI. +constexpr gfx::Size kThumbnailSize{328, 184}; + // Calculates the bitrate used to initialize the video encoder based on the // given |capture_size|. uint64_t CalculateVpxEncoderBitrate(const gfx::Size& capture_size) { @@ -72,6 +80,34 @@ media::AudioParameters GetAudioParameters() { kAudioSampleRate / 100); } +// Extracts a potentially scaled-down RGB image from the given video |frame|, +// which is suitable to use as a thumbnail for the video. +gfx::ImageSkia ExtractImageFromVideoFrame(const media::VideoFrame& frame) { + const gfx::Size visible_size = frame.visible_rect().size(); + media::PaintCanvasVideoRenderer renderer; + SkBitmap bitmap; + bitmap.allocN32Pixels(visible_size.width(), visible_size.height()); + renderer.ConvertVideoFrameToRGBPixels(&frame, bitmap.getPixels(), + bitmap.rowBytes()); + + // Since this image will be used as a thumbnail, we can scale it down to save + // on memory if needed. For example, if recording a FHD display, that will be + // (for 12 bits/pixel): + // 1920 * 1080 * 12 / 8, which is approx. = 3 MB, which is a lot to keep + // around for a thumbnail. + const gfx::ImageSkia thumbnail = gfx::ImageSkia::CreateFrom1xBitmap(bitmap); + if (visible_size.width() <= kThumbnailSize.width() && + visible_size.height() <= kThumbnailSize.height()) { + return thumbnail; + } + + const gfx::Size scaled_size = + media::ScaleSizeToFitWithinTarget(visible_size, kThumbnailSize); + return gfx::ImageSkiaOperations::CreateResizedImage( + thumbnail, skia::ImageOperations::ResizeMethod::RESIZE_BETTER, + scaled_size); +} + } // namespace RecordingService::RecordingService( @@ -256,6 +292,9 @@ void RecordingService::OnFrameCaptured( frame->set_metadata(info->metadata); frame->set_color_space(info->color_space.value()); + if (video_thumbnail_.isNull()) + video_thumbnail_ = ExtractImageFromVideoFrame(*frame); + encoder_muxer_.AsyncCall(&RecordingEncoderMuxer::EncodeVideo).WithArgs(frame); } @@ -453,7 +492,7 @@ void RecordingService::SignalRecordingEndedToClient(bool success) { DCHECK(encoder_muxer_); encoder_muxer_.Reset(); - client_remote_->OnRecordingEnded(success); + client_remote_->OnRecordingEnded(success, video_thumbnail_); } void RecordingService::OnMuxerWrite(base::StringPiece data) { diff --git a/ash/services/recording/recording_service.h b/ash/services/recording/recording_service.h index b47c8b315ef62f..fa6ad2a2c6a67a 100644 --- a/ash/services/recording/recording_service.h +++ b/ash/services/recording/recording_service.h @@ -25,6 +25,7 @@ #include "mojo/public/cpp/bindings/receiver.h" #include "mojo/public/cpp/bindings/remote.h" #include "services/viz/privileged/mojom/compositing/frame_sink_video_capture.mojom-forward.h" +#include "ui/gfx/image/image_skia.h" namespace recording { @@ -183,6 +184,11 @@ class RecordingService : public mojom::RecordingService, mojo::Remote client_remote_ GUARDED_BY_CONTEXT(main_thread_checker_); + // A cached scaled down rgb image of the first valid video frame which will be + // used to provide the client with an image thumbnail representing the + // recorded video. + gfx::ImageSkia video_thumbnail_ GUARDED_BY_CONTEXT(main_thread_checker_); + // True if a failure has been propagated from |encoder_muxer_| that we will // end recording abruptly and ignore any incoming audio/video frames. bool did_failure_occur_ GUARDED_BY_CONTEXT(main_thread_checker_) = false;