blob: 802458635b0259ac1d70022996e5129f713764c2 [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 "ReplayKitCaptureSource.h"
#if ENABLE(MEDIA_STREAM) && PLATFORM(IOS)
#import "Logging.h"
#import "RealtimeVideoUtilities.h"
#import <wtf/BlockPtr.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/UUID.h>
#import <wtf/text/StringToIntegerConversion.h>
#import <pal/cf/CoreMediaSoftLink.h>
#import <pal/ios/ReplayKitSoftLink.h>
using namespace WebCore;
@interface WebCoreReplayKitScreenRecorderHelper : NSObject {
WeakPtr<ReplayKitCaptureSource> _callback;
}
- (instancetype)initWithCallback:(WeakPtr<ReplayKitCaptureSource>&&)callback;
- (void)disconnect;
- (void)observeValueForKeyPath:keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context;
@end
@implementation WebCoreReplayKitScreenRecorderHelper
- (instancetype)initWithCallback:(WeakPtr<ReplayKitCaptureSource>&&)callback
{
self = [super init];
if (!self)
return self;
_callback = WTFMove(callback);
[[PAL::getRPScreenRecorderClass() sharedRecorder] addObserver:self forKeyPath:@"recording" options:NSKeyValueObservingOptionNew context:(void *)nil];
return self;
}
- (void)disconnect
{
_callback = nullptr;
[[PAL::getRPScreenRecorderClass() sharedRecorder] removeObserver:self forKeyPath:@"recording"];
}
- (void)observeValueForKeyPath:keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
UNUSED_PARAM(object);
UNUSED_PARAM(context);
if (!_callback)
return;
id newValue = [change valueForKey:NSKeyValueChangeNewKey];
bool willChange = [[change valueForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
#if !RELEASE_LOG_DISABLED
if (_callback->loggerPtr()) {
auto identifier = Logger::LogSiteIdentifier("ReplayKitCaptureSource", "observeValueForKeyPath", _callback->logIdentifier());
RetainPtr<NSString> valueString = adoptNS([[NSString alloc] initWithFormat:@"%@", newValue]);
_callback->logger().logAlways(_callback->logChannel(), identifier, willChange ? "will" : "did", " change '", [keyPath UTF8String], "' to ", [valueString.get() UTF8String]);
}
#endif
if (willChange)
return;
if ([keyPath isEqualToString:@"recording"])
_callback->captureStateDidChange();
}
@end
namespace WebCore {
bool ReplayKitCaptureSource::isAvailable()
{
return [PAL::getRPScreenRecorderClass() sharedRecorder].isAvailable;
}
Expected<UniqueRef<DisplayCaptureSourceCocoa::Capturer>, String> ReplayKitCaptureSource::create(const String&)
{
if (!isAvailable())
return makeUnexpected("Screen capture unavailable"_s);
return UniqueRef<DisplayCaptureSourceCocoa::Capturer>(makeUniqueRef<ReplayKitCaptureSource>());
}
ReplayKitCaptureSource::ReplayKitCaptureSource()
: m_captureWatchdogTimer(*this, &ReplayKitCaptureSource::verifyCaptureIsActive)
{
}
ReplayKitCaptureSource::~ReplayKitCaptureSource()
{
[m_recorderHelper disconnect];
stop();
m_currentFrame = nullptr;
}
bool ReplayKitCaptureSource::start()
{
ASSERT(isAvailable());
auto identifier = LOGIDENTIFIER;
ALWAYS_LOG_IF(loggerPtr(), identifier);
auto *screenRecorder = [PAL::getRPScreenRecorderClass() sharedRecorder];
if (screenRecorder.recording)
return true;
// FIXME: Add support for concurrent audio capture.
[screenRecorder setMicrophoneEnabled:NO];
if (!m_recorderHelper)
m_recorderHelper = ([[WebCoreReplayKitScreenRecorderHelper alloc] initWithCallback:this]);
auto captureHandler = makeBlockPtr([this, weakThis = WeakPtr { *this }, identifier](CMSampleBufferRef _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
if (bufferType != RPSampleBufferTypeVideo)
return;
ERROR_LOG_IF(error && loggerPtr(), identifier, "startCaptureWithHandler failed ", [error localizedDescription].UTF8String);
++m_frameCount;
RunLoop::main().dispatch([weakThis, sampleBuffer = retainPtr(sampleBuffer)]() mutable {
if (!weakThis)
return;
weakThis->screenRecorderDidOutputVideoSample(WTFMove(sampleBuffer));
});
});
auto completionHandler = makeBlockPtr([this, weakThis = WeakPtr { *this }, identifier](NSError * _Nullable error) {
// FIXME: It should be safe to call `videoSampleAvailable` from any thread. Test this and get rid of this main thread hop.
RunLoop::main().dispatch([this, weakThis, error = retainPtr(error), identifier]() mutable {
if (!weakThis || !error)
return;
ERROR_LOG_IF(loggerPtr(), identifier, "completionHandler failed ", [error localizedDescription].UTF8String);
weakThis->stop();
});
});
[screenRecorder startCaptureWithHandler:captureHandler.get() completionHandler:completionHandler.get()];
return true;
}
void ReplayKitCaptureSource::screenRecorderDidOutputVideoSample(RetainPtr<CMSampleBufferRef>&& sampleBuffer)
{
m_currentFrame = sampleBuffer.get();
m_intrinsicSize = IntSize(PAL::CMVideoFormatDescriptionGetPresentationDimensions(PAL::CMSampleBufferGetFormatDescription(m_currentFrame.get()), true, true));
}
void ReplayKitCaptureSource::captureStateDidChange()
{
RunLoop::main().dispatch([this, weakThis = WeakPtr { *this }, identifier = LOGIDENTIFIER]() mutable {
if (!weakThis)
return;
bool isRecording = !![[PAL::getRPScreenRecorderClass() sharedRecorder] isRecording];
if (m_isRunning == (isRecording && !m_interrupted))
return;
m_isRunning = isRecording && !m_interrupted;
ALWAYS_LOG_IF(loggerPtr(), identifier, m_isRunning);
isRunningChanged(m_isRunning);
});
}
void ReplayKitCaptureSource::stop()
{
auto identifier = LOGIDENTIFIER;
ALWAYS_LOG_IF(loggerPtr(), identifier);
m_captureWatchdogTimer.stop();
m_interrupted = false;
m_isRunning = false;
auto *screenRecorder = [PAL::getRPScreenRecorderClass() sharedRecorder];
if (screenRecorder.recording) {
[screenRecorder stopCaptureWithHandler:^(NSError * _Nullable error) {
ERROR_LOG_IF(error && loggerPtr(), identifier, "startCaptureWithHandler failed ", [error localizedDescription].UTF8String);
}];
}
}
DisplayCaptureSourceCocoa::DisplayFrameType ReplayKitCaptureSource::generateFrame()
{
return m_currentFrame;
}
void ReplayKitCaptureSource::verifyCaptureIsActive()
{
ASSERT(m_isRunning || m_interrupted);
auto identifier = LOGIDENTIFIER;
if (m_lastFrameCount != m_frameCount) {
m_lastFrameCount = m_frameCount;
if (m_interrupted) {
ALWAYS_LOG_IF(loggerPtr(), identifier, "frame received after interruption, unmuting");
m_interrupted = false;
captureStateDidChange();
}
return;
}
ALWAYS_LOG_IF(loggerPtr(), identifier, "no frame received in ", static_cast<int>(m_captureWatchdogTimer.repeatInterval().value()), " seconds, muting");
m_interrupted = true;
captureStateDidChange();
}
void ReplayKitCaptureSource::startCaptureWatchdogTimer()
{
static constexpr Seconds verifyCaptureInterval = 2_s;
if (m_captureWatchdogTimer.isActive())
return;
m_captureWatchdogTimer.startRepeating(verifyCaptureInterval);
m_lastFrameCount = m_frameCount;
}
static String screenDeviceUUID()
{
static NeverDestroyed<String> screenID = createCanonicalUUIDString();
return screenID;
}
static CaptureDevice& screenDevice()
{
static NeverDestroyed<CaptureDevice> device = { screenDeviceUUID(), CaptureDevice::DeviceType::Screen, makeString("Screen 1"), emptyString(), true };
return device;
}
std::optional<CaptureDevice> ReplayKitCaptureSource::screenCaptureDeviceWithPersistentID(const String& displayID)
{
if (!isAvailable()) {
RELEASE_LOG_ERROR(WebRTC, "ReplayKitCaptureSource::screenCaptureDeviceWithPersistentID: screen capture unavailable");
return std::nullopt;
}
if (displayID != screenDeviceUUID()) {
RELEASE_LOG_ERROR(WebRTC, "ReplayKitCaptureSource::screenCaptureDeviceWithPersistentID: invalid display ID");
return std::nullopt;
}
return screenDevice();
}
void ReplayKitCaptureSource::screenCaptureDevices(Vector<CaptureDevice>& displays)
{
if (isAvailable())
displays.append(screenDevice());
}
} // namespace WebCore
#endif // ENABLE(MEDIA_STREAM) && PLATFORM(IOS)