Skip to content

Commit

Permalink
M116: Add fullscreen presentation detector to ScreenCaptureKit capturer
Browse files Browse the repository at this point in the history
This CL adds a module that tracks fullscreen presentations when
ScreenCaptureKit is used on macOS.

The user selects the editor window of the slideshow. Once the
slideshow starts, the module will automatically change what window
is captured so that the fullscreen slideshow is captured.

This feature is already implemented for the current macOS
window capturer. This CL is needed in order to not break this
functionality once we change to use ScreenCaptureKit on macOS.

Internal design doc:
go/macos-fullscreen-feature

The CL is tested on macOS 13.4 with the following applications:
- Microsoft PowerPoint for Mac 16.73
- Apache OpenOffice 4.1.14
- Keynote 13.1

(cherry picked from commit f6eb524)

Bug: chromium:1348011
Change-Id: I0ec0311bcf758eb6987f6e30e1d6fee15bcd9302
Low-Coverage-Reason: The reported test coverage is wrong. The newly added module has a high test coverage. The existing code that interacts with the ScreenCaptureKit API is hard to test, I will look into adding tests as a follow up.
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4295824
Commit-Queue: Johannes Kron <kron@chromium.org>
Reviewed-by: Mike West <mkwst@chromium.org>
Reviewed-by: Mark Foltz <mfoltz@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#1160326}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4644171
Cr-Commit-Position: refs/branch-heads/5845@{#101}
Cr-Branched-From: 5a5dff6-refs/heads/main@{#1160321}
  • Loading branch information
Johannes Kron authored and Chromium LUCI CQ committed Jun 26, 2023
1 parent 01735cb commit 8fe371f
Show file tree
Hide file tree
Showing 11 changed files with 1,002 additions and 4 deletions.
2 changes: 2 additions & 0 deletions content/browser/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,8 @@ source_set("browser") {
"media/capture/mouse_cursor_overlay_controller_mac.mm",
"media/capture/screen_capture_kit_device_mac.h",
"media/capture/screen_capture_kit_device_mac.mm",
"media/capture/screen_capture_kit_fullscreen_module.h",
"media/capture/screen_capture_kit_fullscreen_module.mm",
"media/capture/views_widget_video_capture_device_mac.cc",
"media/capture/views_widget_video_capture_device_mac.h",
]
Expand Down
61 changes: 58 additions & 3 deletions content/browser/media/capture/screen_capture_kit_device_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "base/threading/thread_checker.h"
#include "base/timer/timer.h"
#include "content/browser/media/capture/io_surface_capture_device_base_mac.h"
#include "content/browser/media/capture/screen_capture_kit_fullscreen_module.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
#include "ui/gfx/native_widget_types.h"
Expand Down Expand Up @@ -132,7 +133,8 @@ - (void)stream:(SCStream*)stream didStopWithError:(NSError*)error {
namespace {

class API_AVAILABLE(macos(12.3)) ScreenCaptureKitDeviceMac
: public IOSurfaceCaptureDeviceBase {
: public IOSurfaceCaptureDeviceBase,
public ScreenCaptureKitResetStreamInterface {
public:
ScreenCaptureKitDeviceMac(const DesktopMediaID& source)
: source_(source),
Expand Down Expand Up @@ -185,6 +187,10 @@ void OnShareableContentCreated(
initWithDesktopIndependentWindow:window]);
CGRect frame = [window frame];
stream_config_content_size_ = gfx::Size(frame.size);
if (!fullscreen_module_) {
fullscreen_module_ = MaybeCreateScreenCaptureKitFullscreenModule(
device_task_runner_, *this, window);
}
break;
}
}
Expand Down Expand Up @@ -249,6 +255,10 @@ void OnStreamStarted(bool error) {
return;
}
client()->OnStarted();

if (fullscreen_module_) {
fullscreen_module_->Start();
}
}
void OnStreamStopped(bool error) {
if (error) {
Expand Down Expand Up @@ -332,8 +342,21 @@ void OnStreamSample(gfx::ScopedInUseIOSurface io_surface,
visible_rect.value_or(gfx::Rect(actual_capture_format_.frame_size)));
}
void OnStreamError() {
client()->OnError(media::VideoCaptureError::kScreenCaptureKitStreamError,
FROM_HERE, "Stream delegate called didStopWithError");
if (is_resetting_ || (fullscreen_module_ &&
fullscreen_module_->is_fullscreen_window_active())) {
// Clear `is_resetting_` because the completion handler in ResetStreamTo()
// may not be called if there's an error.
is_resetting_ = false;

// The stream_ is no longer valid. Restart the stream from scratch.
if (fullscreen_module_) {
fullscreen_module_->Reset();
}
OnStart();
} else {
client()->OnError(media::VideoCaptureError::kScreenCaptureKitStreamError,
FROM_HERE, "Stream delegate called didStopWithError");
}
}

// IOSurfaceCaptureDeviceBase:
Expand Down Expand Up @@ -373,6 +396,32 @@ void OnStop() override {
stream_.reset();
}

// ScreenCaptureKitResetStreamInterface.
void ResetStreamTo(SCWindow* window) override {
if (!window || is_resetting_) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitResetStreamError,
FROM_HERE, "Error on ResetStreamTo.");
return;
}

is_resetting_ = true;
base::scoped_nsobject<SCContentFilter> filter;
filter.reset(
[[SCContentFilter alloc] initWithDesktopIndependentWindow:window]);

[stream_ updateContentFilter:filter
completionHandler:^(NSError* _Nullable error) {
is_resetting_ = false;
if (error) {
client()->OnError(
media::VideoCaptureError::kScreenCaptureKitStreamError,
FROM_HERE,
"Error on updateContentFilter (fullscreen window).");
}
}];
}

private:
const DesktopMediaID source_;
const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
Expand All @@ -390,6 +439,12 @@ void OnStop() override {
// Helper class that acts as output and delegate for `stream_`.
base::scoped_nsobject<ScreenCaptureKitDeviceHelper> helper_;

// This is used to detect when a captured presentation enters fullscreen mode.
// If this happens, the module will call the ResetStreamTo function.
std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module_;

bool is_resetting_ = false;

// The stream that does the capturing.
base::scoped_nsobject<SCStream> stream_;

Expand Down
117 changes: 117 additions & 0 deletions content/browser/media/capture/screen_capture_kit_fullscreen_module.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CONTENT_BROWSER_MEDIA_CAPTURE_SCREEN_CAPTURE_KIT_FULLSCREEN_MODULE_H_
#define CONTENT_BROWSER_MEDIA_CAPTURE_SCREEN_CAPTURE_KIT_FULLSCREEN_MODULE_H_

#include <CoreGraphics/CGWindow.h>
#import <ScreenCaptureKit/ScreenCaptureKit.h>
#include "base/mac/scoped_nsobject.h"
#import "base/task/single_thread_task_runner.h"
#include "base/timer/timer.h"
#include "content/common/content_export.h"

namespace content {

class API_AVAILABLE(macos(12.3))
CONTENT_EXPORT ScreenCaptureKitResetStreamInterface {
public:
// This function is called if the ScreenCaptureKitFullScreenModule detects a
// fullscreen presentation that corresponds to the originally captured window.
// The parameter is the fullscreen window. It is also called with the original
// window as parameter if the fullscreen window is no longer on screen.
virtual void ResetStreamTo(SCWindow* window) = 0;
};

class API_AVAILABLE(macos(12.3))
CONTENT_EXPORT ScreenCaptureKitFullscreenModule {
public:
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class Mode {
kUnsupported = 0,
kPowerPoint = 1,
kOpenOffice = 2,
kKeynote = 3,
kLibreOffice = 4,
kMaxValue = kLibreOffice,
};

using ContentHandler =
base::OnceCallback<void(base::scoped_nsobject<SCShareableContent>)>;
using GetShareableContentCallback =
base::RepeatingCallback<void(ContentHandler)>;

ScreenCaptureKitFullscreenModule(
scoped_refptr<base::SingleThreadTaskRunner> device_task_runner,
ScreenCaptureKitResetStreamInterface& reset_stream_interface,
CGWindowID original_window_id,
pid_t original_window_pid,
Mode mode);
~ScreenCaptureKitFullscreenModule();

void Start();
void Reset();

bool is_fullscreen_window_active() const { return fullscreen_mode_active_; }
Mode get_mode() const { return mode_; }

// Sets a callback to be used whenever the ScreenCaptureKit OS API
// GetShareableContent is called. Used in tests to enable mocking of this API.
void set_get_sharable_content_for_test(
GetShareableContentCallback get_shareable_content) {
get_shareable_content_for_test_ = get_shareable_content;
}

private:
void CheckForFullscreenPresentation();
void OnFullscreenShareableContentCreated(
base::scoped_nsobject<SCShareableContent> content);
void OnExitFullscreenShareableContentCreated(
base::scoped_nsobject<SCShareableContent> content);
SCWindow* GetFullscreenWindow(
base::scoped_nsobject<SCShareableContent> content,
SCWindow* editor_window,
int number_of_impress_editor_windows) const;

const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;

// Interface to the owner of the SCK stream.
ScreenCaptureKitResetStreamInterface& reset_stream_interface_;

// Identifier of the original window that is captured.
const CGWindowID original_window_id_;
const pid_t original_window_pid_;

// The mode, corresponding to what slideshow application we're tracking.
const Mode mode_;
// True, if we've detected a fullscreen presentation and requested the stream
// to be reset to the new window.
bool fullscreen_mode_active_ = false;
// Identified of the fullscreen window.
CGWindowID fullscreen_window_id_ = 0;
// Callback to mock function that is used in tests to mock the SCK OS API
// GetShareableContent.
GetShareableContentCallback get_shareable_content_for_test_;

base::RepeatingTimer timer_;

base::WeakPtrFactory<ScreenCaptureKitFullscreenModule> weak_factory_{this};
};

// Creates and returns a ScreenCaptureKitFullscreenModule if `original_window`
// corresponds to a supported slideshow application. `reset_stream_interface` is
// a reference to the object that owns the SCK stream. Upon detection of a
// fullscreen slideshow, the reset function will be called.
// `reset_stream_interface` must outlive the returned fullscreen module.
// `device_task_runner` specifies the task runner where all operations are run.
std::unique_ptr<ScreenCaptureKitFullscreenModule> CONTENT_EXPORT
MaybeCreateScreenCaptureKitFullscreenModule(
scoped_refptr<base::SingleThreadTaskRunner> device_task_runner,
ScreenCaptureKitResetStreamInterface& reset_stream_interface,
SCWindow* original_window) API_AVAILABLE(macos(12.3));

} // namespace content

#endif // CONTENT_BROWSER_MEDIA_CAPTURE_SCREEN_CAPTURE_KIT_FULLSCREEN_MODULE_H_

0 comments on commit 8fe371f

Please sign in to comment.