// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/media/capture/desktop_capture_device_mac.h"
#include <CoreGraphics/CoreGraphics.h>
#include "base/task/single_thread_task_runner.h"
#include "content/browser/media/capture/io_surface_capture_device_base_mac.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
#include "ui/gfx/native_widget_types.h"
namespace content {
namespace {
class DesktopCaptureDeviceMac : public IOSurfaceCaptureDeviceBase {
public:
DesktopCaptureDeviceMac(CGDirectDisplayID display_id)
: display_id_(display_id),
device_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()),
weak_factory_(this) {}
DesktopCaptureDeviceMac(const DesktopCaptureDeviceMac&) = delete;
DesktopCaptureDeviceMac& operator=(const DesktopCaptureDeviceMac&) = delete;
~DesktopCaptureDeviceMac() override = default;
// IOSurfaceCaptureDeviceBase:
void OnStart() override {
requested_format_ = capture_params().requested_format;
requested_format_.pixel_format = media::PIXEL_FORMAT_NV12;
DCHECK_GT(requested_format_.frame_size.GetArea(), 0);
DCHECK_GT(requested_format_.frame_rate, 0);
base::RepeatingCallback<void(gfx::ScopedInUseIOSurface)>
received_io_surface_callback = base::BindRepeating(
&DesktopCaptureDeviceMac::OnFrame, weak_factory_.GetWeakPtr());
CGDisplayStreamFrameAvailableHandler handler =
^(CGDisplayStreamFrameStatus status, uint64_t display_time,
IOSurfaceRef frame_surface, CGDisplayStreamUpdateRef update_ref) {
gfx::ScopedInUseIOSurface io_surface(frame_surface,
base::scoped_policy::RETAIN);
if (status == kCGDisplayStreamFrameStatusFrameComplete) {
device_task_runner_->PostTask(
FROM_HERE,
base::BindRepeating(received_io_surface_callback, io_surface));
}
};
// Retrieve the source display's size.
base::apple::ScopedCFTypeRef<CGDisplayModeRef> mode(
CGDisplayCopyDisplayMode(display_id_));
const gfx::Size source_size =
mode ? gfx::Size(CGDisplayModeGetWidth(mode.get()),
CGDisplayModeGetHeight(mode.get()))
: requested_format_.frame_size;
// Compute the destination frame size using CaptureResolutionChooser.
gfx::RectF dest_rect_in_frame;
ComputeFrameSizeAndDestRect(source_size, requested_format_.frame_size,
dest_rect_in_frame);
base::apple::ScopedCFTypeRef<CFDictionaryRef> properties;
{
float max_frame_time = 1.f / requested_format_.frame_rate;
base::apple::ScopedCFTypeRef<CFNumberRef> cf_max_frame_time(
CFNumberCreate(nullptr, kCFNumberFloat32Type, &max_frame_time));
base::apple::ScopedCFTypeRef<CGColorSpaceRef> cg_color_space(
CGColorSpaceCreateWithName(kCGColorSpaceSRGB));
base::apple::ScopedCFTypeRef<CFDictionaryRef> dest_rect_in_frame_dict(
CGRectCreateDictionaryRepresentation(dest_rect_in_frame.ToCGRect()));
const size_t kNumKeys = 5;
const void* keys[kNumKeys] = {
kCGDisplayStreamShowCursor, kCGDisplayStreamPreserveAspectRatio,
kCGDisplayStreamMinimumFrameTime, kCGDisplayStreamColorSpace,
kCGDisplayStreamDestinationRect,
};
const void* values[kNumKeys] = {
kCFBooleanTrue,
kCFBooleanFalse,
cf_max_frame_time.get(),
cg_color_space.get(),
dest_rect_in_frame_dict.get(),
};
properties.reset(CFDictionaryCreate(
kCFAllocatorDefault, keys, values, kNumKeys,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
}
display_stream_.reset(
CGDisplayStreamCreate(display_id_, requested_format_.frame_size.width(),
requested_format_.frame_size.height(),
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
properties.get(), handler));
if (!display_stream_) {
client()->OnError(
media::VideoCaptureError::kDesktopCaptureDeviceMacFailedStreamCreate,
FROM_HERE, "CGDisplayStreamCreate failed");
return;
}
CGError error = CGDisplayStreamStart(display_stream_.get());
if (error != kCGErrorSuccess) {
client()->OnError(
media::VideoCaptureError::kDesktopCaptureDeviceMacFailedStreamStart,
FROM_HERE, "CGDisplayStreamStart failed");
return;
}
// Use CFRunLoopGetMain instead of CFRunLoopGetCurrent because in some
// circumstances (e.g, streaming to ChromeCast), this is called on a
// worker thread where the CFRunLoop does not get serviced.
// https://crbug.com/1185388
CFRunLoopAddSource(CFRunLoopGetMain(),
CGDisplayStreamGetRunLoopSource(display_stream_.get()),
kCFRunLoopCommonModes);
client()->OnStarted();
}
void OnStop() override {
weak_factory_.InvalidateWeakPtrs();
if (display_stream_) {
CFRunLoopRemoveSource(
CFRunLoopGetMain(),
CGDisplayStreamGetRunLoopSource(display_stream_.get()),
kCFRunLoopCommonModes);
CGDisplayStreamStop(display_stream_.get());
}
display_stream_.reset();
}
private:
void OnFrame(gfx::ScopedInUseIOSurface io_surface) {
OnReceivedIOSurfaceFromStream(io_surface, requested_format_,
gfx::Rect(requested_format_.frame_size));
}
const CGDirectDisplayID display_id_;
const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
base::apple::ScopedCFTypeRef<CGDisplayStreamRef> display_stream_;
media::VideoCaptureFormat requested_format_;
base::WeakPtrFactory<DesktopCaptureDeviceMac> weak_factory_;
};
} // namespace
std::unique_ptr<media::VideoCaptureDevice> CreateDesktopCaptureDeviceMac(
const DesktopMediaID& source) {
// DesktopCaptureDeviceMac supports only TYPE_SCREEN.
// https://crbug.com/1176900
if (source.type != DesktopMediaID::TYPE_SCREEN)
return nullptr;
// DesktopCaptureDeviceMac only supports a single display at a time. It
// will not stitch desktops together.
// https://crbug.com/1178360
if (source.id == webrtc::kFullDesktopScreenId ||
source.id == webrtc::kInvalidScreenId) {
return nullptr;
}
IncrementDesktopCaptureCounter(SCREEN_CAPTURER_CREATED);
IncrementDesktopCaptureCounter(source.audio_share
? SCREEN_CAPTURER_CREATED_WITH_AUDIO
: SCREEN_CAPTURER_CREATED_WITHOUT_AUDIO);
return std::make_unique<DesktopCaptureDeviceMac>(source.id);
}
} // namespace content