blob: 5530851cd39f84660e2b3ad94171b386c10d35c5 [file] [log] [blame]
/*
* Copyright (C) 2021-2022 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ScreenCaptureKitCaptureSource.h"
#if HAVE(SCREEN_CAPTURE_KIT)
#import "DisplayCaptureManager.h"
#import "Logging.h"
#import "PlatformMediaSessionManager.h"
#import "PlatformScreen.h"
#import "RealtimeMediaSourceCenter.h"
#import "RealtimeVideoUtilities.h"
#import <ScreenCaptureKit/ScreenCaptureKit.h>
#import <wtf/BlockObjCExceptions.h>
#import <wtf/BlockPtr.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/UUID.h>
#import <wtf/text/StringToIntegerConversion.h>
#import <pal/mac/ScreenCaptureKitSoftLink.h>
typedef NS_ENUM(NSInteger, WKSCFrameStatus) {
WKSCFrameStatusFrameComplete,
WKSCFrameStatusFrameIdle,
WKSCFrameStatusFrameBlank,
WKSCFrameStatusFrameSuspended,
WKSCFrameStatusFrameStarted,
WKSCFrameStatusFrameStopped
};
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
@interface SCStream (SCStream_Deprecated)
- (void)startCaptureWithCFrameHandler:(SCStreamBufferFrameAvailableHandler)frameHandler completionHandler:(void (^)(NSError *error))completionHandler;
@end
using namespace WebCore;
@interface WebCoreScreenCaptureKitHelper : NSObject<SCStreamDelegate> {
WeakPtr<ScreenCaptureKitCaptureSource> _callback;
}
- (instancetype)initWithCallback:(WeakPtr<ScreenCaptureKitCaptureSource>&&)callback;
- (void)disconnect;
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error;
@end
@implementation WebCoreScreenCaptureKitHelper
- (instancetype)initWithCallback:(WeakPtr<ScreenCaptureKitCaptureSource>&&)callback
{
self = [super init];
if (!self)
return self;
return self;
}
- (void)disconnect
{
_callback = nullptr;
}
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error
{
callOnMainRunLoop([strongSelf = RetainPtr { self }, error = RetainPtr { error }]() mutable {
if (!strongSelf->_callback)
return;
strongSelf->_callback->streamFailedWithError(WTFMove(error), "-[SCStreamDelegate stream:didStopWithError:] called"_s);
});
}
@end
#pragma clang diagnostic pop
namespace WebCore {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
static void forEachNSWindow(const Function<bool(NSDictionary *, unsigned, const String&)>&);
bool ScreenCaptureKitCaptureSource::m_enabled;
void ScreenCaptureKitCaptureSource::setEnabled(bool enabled)
{
m_enabled = enabled;
}
bool ScreenCaptureKitCaptureSource::isAvailable()
{
return m_enabled && PAL::isScreenCaptureKitFrameworkAvailable();
}
Expected<UniqueRef<DisplayCaptureSourceCocoa::Capturer>, String> ScreenCaptureKitCaptureSource::create(const CaptureDevice& device, const MediaConstraints*)
{
ASSERT(device.type() == CaptureDevice::DeviceType::Screen || device.type() == CaptureDevice::DeviceType::Window);
auto deviceID = parseInteger<uint32_t>(device.persistentId());
if (!deviceID)
return makeUnexpected("Invalid display device ID"_s);
return UniqueRef<DisplayCaptureSourceCocoa::Capturer>(makeUniqueRef<ScreenCaptureKitCaptureSource>(device, deviceID.value()));
}
ScreenCaptureKitCaptureSource::ScreenCaptureKitCaptureSource(const CaptureDevice& device, uint32_t deviceID)
: DisplayCaptureSourceCocoa::Capturer()
, m_captureDevice(device)
, m_deviceID(deviceID)
{
}
ScreenCaptureKitCaptureSource::~ScreenCaptureKitCaptureSource()
{
}
bool ScreenCaptureKitCaptureSource::start()
{
ASSERT(isAvailable());
ALWAYS_LOG_IF(loggerPtr(), LOGIDENTIFIER);
if (m_isRunning)
return true;
m_isRunning = true;
startContentStream();
return m_isRunning;
}
void ScreenCaptureKitCaptureSource::stop()
{
if (!m_isRunning)
return;
ALWAYS_LOG_IF(loggerPtr(), LOGIDENTIFIER);
m_isRunning = false;
if (m_contentStream) {
[m_contentStream stopWithCompletionHandler:makeBlockPtr([weakThis = WeakPtr { *this }] (NSError *error) mutable {
if (!error)
return;
callOnMainRunLoop([weakThis = WTFMove(weakThis), error = RetainPtr { error }]() mutable {
if (weakThis)
weakThis->streamFailedWithError(WTFMove(error), "-[SCStream stopWithCompletionHandler:] failed"_s);
});
}).get()];
}
}
void ScreenCaptureKitCaptureSource::streamFailedWithError(RetainPtr<NSError>&& error, const String& message)
{
ASSERT(isMainThread());
ERROR_LOG_IF(loggerPtr() && error, LOGIDENTIFIER, message, " with error '", [[error localizedDescription] UTF8String], "'");
ERROR_LOG_IF(loggerPtr() && !error, LOGIDENTIFIER, message);
captureFailed();
}
DisplayCaptureSourceCocoa::DisplayFrameType ScreenCaptureKitCaptureSource::generateFrame()
{
return m_currentFrame;
}
void ScreenCaptureKitCaptureSource::processSharableContent(RetainPtr<SCShareableContent>&& shareableContent, RetainPtr<NSError>&& error)
{
ASSERT(isMainRunLoop());
if (error) {
streamFailedWithError(WTFMove(error), "-[SCStream getShareableContentWithCompletionHandler:] failed"_s);
return;
}
if (m_captureDevice.type() == CaptureDevice::DeviceType::Screen) {
[[shareableContent displays] enumerateObjectsUsingBlock:makeBlockPtr([&] (SCDisplay *display, NSUInteger, BOOL *stop) {
if (display.displayID == m_deviceID) {
m_content = display;
*stop = YES;
}
}).get()];
} else if (m_captureDevice.type() == CaptureDevice::DeviceType::Window) {
[[shareableContent windows] enumerateObjectsUsingBlock:makeBlockPtr([&] (SCWindow *window, NSUInteger, BOOL *stop) {
if (window.windowID == m_deviceID) {
m_content = window;
*stop = YES;
}
}).get()];
} else {
ASSERT_NOT_REACHED();
return;
}
if (!m_content) {
streamFailedWithError(nil, "capture device not found"_s);
return;
}
startContentStream();
}
void ScreenCaptureKitCaptureSource::findShareableContent()
{
ASSERT(!m_content);
[PAL::getSCShareableContentClass() getShareableContentWithCompletionHandler:makeBlockPtr([weakThis = WeakPtr { *this }] (SCShareableContent *shareableContent, NSError *error) mutable {
callOnMainRunLoop([weakThis = WTFMove(weakThis), shareableContent = RetainPtr { shareableContent }, error = RetainPtr { error }]() mutable {
if (weakThis)
weakThis->processSharableContent(WTFMove(shareableContent), WTFMove(error));
});
}).get()];
}
RetainPtr<SCStreamConfiguration> ScreenCaptureKitCaptureSource::streamConfiguration()
{
if (m_streamConfiguration)
return m_streamConfiguration;
m_streamConfiguration = adoptNS([PAL::allocSCStreamConfigurationInstance() init]);
[m_streamConfiguration setPixelFormat:preferedPixelBufferFormat()];
[m_streamConfiguration setShowsCursor:YES];
[m_streamConfiguration setQueueDepth:6];
[m_streamConfiguration setColorSpaceName:kCGColorSpaceLinearSRGB];
[m_streamConfiguration setColorMatrix:kCGDisplayStreamYCbCrMatrix_SMPTE_240M_1995];
if (m_frameRate)
[m_streamConfiguration setMinimumFrameTime:1 / m_frameRate];
if (m_width && m_height) {
[m_streamConfiguration setWidth:m_width];
[m_streamConfiguration setHeight:m_height];
}
return m_streamConfiguration;
}
void ScreenCaptureKitCaptureSource::startContentStream()
{
if (m_contentStream)
return;
if (!m_content) {
findShareableContent();
return;
}
m_contentFilter = switchOn(m_content.value(),
[] (const RetainPtr<SCDisplay> display) -> RetainPtr<SCContentFilter> {
return adoptNS([PAL::allocSCContentFilterInstance() initWithDisplay:display.get() excludingWindows:nil]);
},
[] (const RetainPtr<SCWindow> window) -> RetainPtr<SCContentFilter> {
return adoptNS([PAL::allocSCContentFilterInstance() initWithDesktopIndependentWindow:window.get()]);
}
);
if (!m_contentFilter) {
streamFailedWithError(nil, "Failed to allocate SCContentFilter"_s);
return;
}
if (!m_captureHelper)
m_captureHelper = ([[WebCoreScreenCaptureKitHelper alloc] initWithCallback:this]);
m_contentStream = adoptNS([PAL::allocSCStreamInstance() initWithFilter:m_contentFilter.get() captureOutputProperties:streamConfiguration().get() delegate:m_captureHelper.get()]);
if (!m_contentStream) {
streamFailedWithError(nil, "Failed to allocate SLContentStream"_s);
return;
}
auto completionHandler = makeBlockPtr([weakThis = WeakPtr { *this }] (NSError *error) mutable {
if (!error)
return;
callOnMainRunLoop([weakThis = WTFMove(weakThis), error = RetainPtr { error }]() mutable {
if (weakThis)
weakThis->streamFailedWithError(WTFMove(error), "-[SCStream startCaptureWithFrameHandler:completionHandler:] failed"_s);
});
});
[m_contentStream startCaptureWithFrameHandler:frameAvailableHandler() completionHandler:completionHandler.get()];
m_isRunning = true;
}
IntSize ScreenCaptureKitCaptureSource::intrinsicSize() const
{
if (m_intrinsicSize)
return m_intrinsicSize.value();
if (m_captureDevice.type() == CaptureDevice::DeviceType::Screen) {
auto displayMode = adoptCF(CGDisplayCopyDisplayMode(m_deviceID));
auto screenWidth = CGDisplayModeGetPixelsWide(displayMode.get());
auto screenHeight = CGDisplayModeGetPixelsHigh(displayMode.get());
return { Checked<int>(screenWidth), Checked<int>(screenHeight) };
}
CGRect bounds = CGRectZero;
forEachNSWindow([&] (NSDictionary *windowInfo, unsigned windowID, const String&) mutable {
if (windowID != m_deviceID)
return false;
NSDictionary *boundsDict = windowInfo[(__bridge NSString *)kCGWindowBounds];
if (![boundsDict isKindOfClass:NSDictionary.class])
return false;
CGRectMakeWithDictionaryRepresentation((CFDictionaryRef)boundsDict, &bounds);
return true;
});
return { static_cast<int>(bounds.size.width), static_cast<int>(bounds.size.height) };
}
void ScreenCaptureKitCaptureSource::updateStreamConfiguration()
{
ASSERT(m_contentStream);
[m_contentStream updateStreamConfiguration:streamConfiguration().get() completionHandler:makeBlockPtr([weakThis = WeakPtr { *this }] (NSError *error) mutable {
if (!error)
return;
callOnMainRunLoop([weakThis = WTFMove(weakThis), error = RetainPtr { error }]() mutable {
if (weakThis)
weakThis->streamFailedWithError(WTFMove(error), "-[SCStream updateStreamConfiguration:] failed"_s);
});
}).get()];
}
void ScreenCaptureKitCaptureSource::commitConfiguration(const RealtimeMediaSourceSettings& settings)
{
if (m_width == settings.width() && m_height == settings.height() && m_frameRate == settings.frameRate())
return;
m_width = settings.width();
m_height = settings.height();
m_frameRate = settings.frameRate();
if (m_contentStream) {
m_streamConfiguration = nullptr;
updateStreamConfiguration();
}
}
ScreenCaptureKitCaptureSource::SCContentStreamUpdateCallback ScreenCaptureKitCaptureSource::frameAvailableHandler()
{
if (m_frameAvailableHandler)
return m_frameAvailableHandler.get();
m_frameAvailableHandler = makeBlockPtr([weakThis = WeakPtr { *this }] (SCStream *, CMSampleBufferRef sampleBuffer) mutable {
if (!weakThis)
return;
if (!sampleBuffer) {
RunLoop::main().dispatch([weakThis = WTFMove(weakThis), sampleBuffer = retainPtr(sampleBuffer)]() mutable {
if (weakThis)
RELEASE_LOG_ERROR(WebRTC, "ScreenCaptureKitCaptureSource::frameAvailableHandler: NULL sample buffer!");
});
return;
}
auto attachments = (__bridge NSArray *)PAL::CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
WKSCFrameStatus status = WKSCFrameStatusFrameStopped;
[attachments enumerateObjectsUsingBlock:makeBlockPtr([&] (NSDictionary *attachment, NSUInteger, BOOL *stop) {
auto statusNumber = (NSNumber *)attachment[PAL::SCStreamFrameInfoStatusKey];
if (!statusNumber)
return;
status = (WKSCFrameStatus)[statusNumber integerValue];
*stop = YES;
}).get()];
switch (status) {
case WKSCFrameStatusFrameStarted:
case WKSCFrameStatusFrameComplete:
break;
case WKSCFrameStatusFrameIdle:
case WKSCFrameStatusFrameBlank:
case WKSCFrameStatusFrameSuspended:
case WKSCFrameStatusFrameStopped:
return;
}
RunLoop::main().dispatch([weakThis = WTFMove(weakThis), sampleBuffer = retainPtr(sampleBuffer)]() mutable {
if (!weakThis)
return;
weakThis->m_intrinsicSize = IntSize(PAL::CMVideoFormatDescriptionGetPresentationDimensions(PAL::CMSampleBufferGetFormatDescription(sampleBuffer.get()), true, true));
weakThis->m_currentFrame = WTFMove(sampleBuffer);
});
});
return m_frameAvailableHandler.get();
}
CaptureDevice::DeviceType ScreenCaptureKitCaptureSource::deviceType() const
{
return m_captureDevice.type();
}
RealtimeMediaSourceSettings::DisplaySurfaceType ScreenCaptureKitCaptureSource::surfaceType() const
{
return m_captureDevice.type() == CaptureDevice::DeviceType::Screen ? RealtimeMediaSourceSettings::DisplaySurfaceType::Monitor : RealtimeMediaSourceSettings::DisplaySurfaceType::Window;
}
std::optional<CaptureDevice> ScreenCaptureKitCaptureSource::screenCaptureDeviceWithPersistentID(const String& displayIDString)
{
if (!isAvailable()) {
RELEASE_LOG_ERROR(WebRTC, "ScreenCaptureKitCaptureSource::screenCaptureDeviceWithPersistentID: screen capture unavailable");
return std::nullopt;
}
auto displayID = parseInteger<uint32_t>(displayIDString);
if (!displayID) {
RELEASE_LOG_ERROR(WebRTC, "ScreenCaptureKitCaptureSource::screenCaptureDeviceWithPersistentID: invalid display ID");
return std::nullopt;
}
return CaptureDevice(String::number(displayID.value()), CaptureDevice::DeviceType::Screen, "ScreenCaptureDevice"_s, emptyString(), true);
}
void ScreenCaptureKitCaptureSource::screenCaptureDevices(Vector<CaptureDevice>& displays)
{
if (!isAvailable())
return;
uint32_t displayCount = 0;
auto err = CGGetActiveDisplayList(0, nullptr, &displayCount);
if (err) {
RELEASE_LOG_ERROR(WebRTC, "ScreenCaptureKitCaptureSource::screenCaptureDevices - CGGetActiveDisplayList() returned error %d when trying to get display count", (int)err);
return;
}
if (!displayCount) {
RELEASE_LOG_ERROR(WebRTC, "CGGetActiveDisplayList() returned a display count of 0");
return;
}
Vector<CGDirectDisplayID> activeDisplays(displayCount);
err = CGGetActiveDisplayList(displayCount, activeDisplays.data(), &displayCount);
if (err) {
RELEASE_LOG_ERROR(WebRTC, "ScreenCaptureKitCaptureSource::screenCaptureDevices - CGGetActiveDisplayList() returned error %d when trying to get the active display list", (int)err);
return;
}
int count = 0;
for (auto displayID : activeDisplays) {
CaptureDevice displayDevice(String::number(displayID), CaptureDevice::DeviceType::Screen, makeString("Screen ", String::number(count++)));
displayDevice.setEnabled(CGDisplayIDToOpenGLDisplayMask(displayID));
displays.append(WTFMove(displayDevice));
}
}
std::optional<CaptureDevice> ScreenCaptureKitCaptureSource::windowCaptureDeviceWithPersistentID(const String& windowIDString)
{
auto windowID = parseInteger<uint32_t>(windowIDString);
if (!windowID) {
RELEASE_LOG_ERROR(WebRTC, "ScreenCaptureKitCaptureSource::windowCaptureDeviceWithPersistentID: invalid window ID");
return std::nullopt;
}
std::optional<CaptureDevice> device;
forEachNSWindow([&] (NSDictionary *, unsigned id, const String& windowTitle) mutable {
if (id != windowID.value())
return false;
device = CaptureDevice(String::number(windowID.value()), CaptureDevice::DeviceType::Window, windowTitle, emptyString(), true);
return true;
});
return device;
}
void ScreenCaptureKitCaptureSource::windowCaptureDevices(Vector<CaptureDevice>& windows)
{
if (!isAvailable())
return;
forEachNSWindow([&] (NSDictionary *, unsigned windowID, const String& windowTitle) mutable {
windows.append({ String::number(windowID), CaptureDevice::DeviceType::Window, windowTitle, emptyString(), true });
return false;
});
}
void ScreenCaptureKitCaptureSource::windowDevices(Vector<DisplayCaptureManager::WindowCaptureDevice>& devices)
{
if (!isAvailable())
return;
forEachNSWindow([&] (NSDictionary *windowInfo, unsigned windowID, const String& windowTitle) mutable {
auto *applicationName = (__bridge NSString *)(windowInfo[(__bridge NSString *)kCGWindowOwnerName]);
devices.append({ { String::number(windowID), CaptureDevice::DeviceType::Window, windowTitle, emptyString(), true }, applicationName });
return false;
});
}
void forEachNSWindow(const Function<bool(NSDictionary *info, unsigned windowID, const String& title)>& predicate)
{
RetainPtr<NSArray> windowList = adoptNS((__bridge NSArray *)CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID));
if (!windowList)
return;
[windowList enumerateObjectsUsingBlock:makeBlockPtr([&] (NSDictionary *windowInfo, NSUInteger, BOOL *stop) {
*stop = NO;
// Menus, the dock, etc have layers greater than 0, skip them.
int windowLayer = [(NSNumber *)windowInfo[(__bridge NSString *)kCGWindowLayer] integerValue];
if (windowLayer)
return;
// Skip windows that aren't on screen
if (![(NSNumber *)windowInfo[(__bridge NSString *)kCGWindowIsOnscreen] integerValue])
return;
auto *windowTitle = (__bridge NSString *)(windowInfo[(__bridge NSString *)kCGWindowName]);
auto windowID = (CGWindowID)[(NSNumber *)windowInfo[(__bridge NSString *)kCGWindowNumber] integerValue];
if (predicate(windowInfo, windowID, windowTitle))
*stop = YES;
}).get()];
}
#pragma clang diagnostic pop
} // namespace WebCore
#endif // HAVE(SCREEN_CAPTURE_KIT)