|
6 | 6 | #import <ScreenCaptureKit/ScreenCaptureKit.h> |
7 | 7 |
|
8 | 8 | #include <mach/mach_time.h> |
| 9 | +#include <unistd.h> |
9 | 10 |
|
10 | 11 | #include <atomic> |
11 | 12 | #include <chrono> |
| 13 | +#include <format> |
12 | 14 | #include <memory> |
13 | 15 | #include <optional> |
14 | 16 |
|
@@ -295,6 +297,79 @@ void stop() override { |
295 | 297 | std::shared_ptr<StreamChannel> channel_; |
296 | 298 | }; |
297 | 299 |
|
| 300 | +// Starts capture of whatever the filter selects, at its native pixel size. |
| 301 | +std::expected<std::unique_ptr<backend::IWindowStreamBackend>, Error> |
| 302 | +startFilteredStream( |
| 303 | + SCContentFilter* filter, const std::uint32_t windowId, |
| 304 | + const WindowStreamOptions& options, VideoFrameCallback onFrame, |
| 305 | + StreamErrorCallback onError |
| 306 | +) { |
| 307 | + SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init]; |
| 308 | + const double scale = static_cast<double>(filter.pointPixelScale); |
| 309 | + config.width = static_cast<size_t>(filter.contentRect.size.width * scale); |
| 310 | + config.height = static_cast<size_t>(filter.contentRect.size.height * scale); |
| 311 | + config.pixelFormat = kCVPixelFormatType_32BGRA; |
| 312 | + config.minimumFrameInterval = |
| 313 | + CMTimeMake(1, static_cast<std::int32_t>(options.maxFps)); |
| 314 | + config.queueDepth = 3; |
| 315 | + config.showsCursor = options.showsCursor ? YES : NO; |
| 316 | + |
| 317 | + dispatch_queue_t queue = |
| 318 | + dispatch_queue_create("robot.window-stream", DISPATCH_QUEUE_SERIAL); |
| 319 | + auto channel = std::make_shared<StreamChannel>(); |
| 320 | + channel->onFrame = std::move(onFrame); |
| 321 | + channel->onError = std::move(onError); |
| 322 | + channel->queue = queue; |
| 323 | + channel->windowId = windowId; |
| 324 | + channel->active.store(true); |
| 325 | + |
| 326 | + RobotWindowStreamOutput* output = [[RobotWindowStreamOutput alloc] init]; |
| 327 | + output->channel = channel; |
| 328 | + SCStream* stream = [[SCStream alloc] initWithFilter:filter |
| 329 | + configuration:config |
| 330 | + delegate:output]; |
| 331 | + |
| 332 | + NSError* addError = nil; |
| 333 | + [stream addStreamOutput:output |
| 334 | + type:SCStreamOutputTypeScreen |
| 335 | + sampleHandlerQueue:queue |
| 336 | + error:&addError]; |
| 337 | + if (addError != nil) { |
| 338 | + return std::unexpected( |
| 339 | + Error::captureFailed(addError.localizedDescription.UTF8String) |
| 340 | + ); |
| 341 | + } |
| 342 | + |
| 343 | + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); |
| 344 | + __block NSError* startError = nil; |
| 345 | + [stream startCaptureWithCompletionHandler:^(NSError* err) { |
| 346 | + startError = err; |
| 347 | + dispatch_semaphore_signal(semaphore); |
| 348 | + }]; |
| 349 | + const auto deadline = |
| 350 | + dispatch_time(DISPATCH_TIME_NOW, 5 * static_cast<int64_t>(NSEC_PER_SEC)); |
| 351 | + if (dispatch_semaphore_wait(semaphore, deadline) != 0) { |
| 352 | + // Startup may still complete later; deactivate the channel and issue a |
| 353 | + // fire-and-forget stop so a late success never delivers frames. |
| 354 | + // startError is deliberately not read here - the completion handler may |
| 355 | + // still be about to write it. |
| 356 | + channel->active.store(false); |
| 357 | + [stream stopCaptureWithCompletionHandler:^(NSError*) { |
| 358 | + }]; |
| 359 | + return std::unexpected(Error::captureFailed("timed out starting capture")); |
| 360 | + } |
| 361 | + if (startError != nil) { |
| 362 | + channel->active.store(false); |
| 363 | + return std::unexpected( |
| 364 | + Error::captureFailed(startError.localizedDescription.UTF8String) |
| 365 | + ); |
| 366 | + } |
| 367 | + |
| 368 | + return std::make_unique<MacWindowStream>( |
| 369 | + stream, output, queue, std::move(channel) |
| 370 | + ); |
| 371 | +} |
| 372 | + |
298 | 373 | } // namespace |
299 | 374 |
|
300 | 375 | std::expected<std::vector<Window>, Error> MacWindowBackend::enumerateWindows() { |
@@ -444,69 +519,55 @@ void stop() override { |
444 | 519 |
|
445 | 520 | SCContentFilter* filter = |
446 | 521 | [[SCContentFilter alloc] initWithDesktopIndependentWindow:scWindow]; |
447 | | - SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init]; |
448 | | - const double scale = static_cast<double>(filter.pointPixelScale); |
449 | | - config.width = static_cast<size_t>(filter.contentRect.size.width * scale); |
450 | | - config.height = static_cast<size_t>(filter.contentRect.size.height * scale); |
451 | | - config.pixelFormat = kCVPixelFormatType_32BGRA; |
452 | | - config.minimumFrameInterval = |
453 | | - CMTimeMake(1, static_cast<std::int32_t>(options.maxFps)); |
454 | | - config.queueDepth = 3; |
455 | | - config.showsCursor = options.showsCursor ? YES : NO; |
456 | | - |
457 | | - dispatch_queue_t queue = |
458 | | - dispatch_queue_create("robot.window-stream", DISPATCH_QUEUE_SERIAL); |
459 | | - auto channel = std::make_shared<StreamChannel>(); |
460 | | - channel->onFrame = std::move(onFrame); |
461 | | - channel->onError = std::move(onError); |
462 | | - channel->queue = queue; |
463 | | - channel->windowId = windowId; |
464 | | - channel->active.store(true); |
465 | 522 |
|
466 | | - RobotWindowStreamOutput* output = [[RobotWindowStreamOutput alloc] init]; |
467 | | - output->channel = channel; |
468 | | - SCStream* stream = [[SCStream alloc] initWithFilter:filter |
469 | | - configuration:config |
470 | | - delegate:output]; |
| 523 | + return startFilteredStream( |
| 524 | + filter, windowId, options, std::move(onFrame), std::move(onError) |
| 525 | + ); |
| 526 | +} |
471 | 527 |
|
472 | | - NSError* addError = nil; |
473 | | - [stream addStreamOutput:output |
474 | | - type:SCStreamOutputTypeScreen |
475 | | - sampleHandlerQueue:queue |
476 | | - error:&addError]; |
477 | | - if (addError != nil) { |
| 528 | +std::expected<std::unique_ptr<backend::IWindowStreamBackend>, Error> |
| 529 | +MacWindowBackend::startMonitorStream( |
| 530 | + const std::uint32_t monitorId, const WindowStreamOptions& options, |
| 531 | + VideoFrameCallback onFrame, StreamErrorCallback onError |
| 532 | +) { |
| 533 | + if (@available(macOS 14.0, *)) { |
| 534 | + // Streaming is available; continue below. |
| 535 | + } else { |
478 | 536 | return std::unexpected( |
479 | | - Error::captureFailed(addError.localizedDescription.UTF8String) |
| 537 | + Error::unsupported("monitor streaming requires macOS 14 or newer") |
480 | 538 | ); |
481 | 539 | } |
482 | 540 |
|
483 | | - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); |
484 | | - __block NSError* startError = nil; |
485 | | - [stream startCaptureWithCompletionHandler:^(NSError* err) { |
486 | | - startError = err; |
487 | | - dispatch_semaphore_signal(semaphore); |
488 | | - }]; |
489 | | - const auto deadline = |
490 | | - dispatch_time(DISPATCH_TIME_NOW, 5 * static_cast<int64_t>(NSEC_PER_SEC)); |
491 | | - if (dispatch_semaphore_wait(semaphore, deadline) != 0) { |
492 | | - // Startup may still complete later; deactivate the channel and issue a |
493 | | - // fire-and-forget stop so a late success never delivers frames. |
494 | | - // startError is deliberately not read here - the completion handler may |
495 | | - // still be about to write it. |
496 | | - channel->active.store(false); |
497 | | - [stream stopCaptureWithCompletionHandler:^(NSError*) { |
498 | | - }]; |
499 | | - return std::unexpected(Error::captureFailed("timed out starting capture")); |
| 541 | + auto contentResult = fetchShareableContent(); |
| 542 | + if (!contentResult) return std::unexpected(contentResult.error()); |
| 543 | + |
| 544 | + SCDisplay* scDisplay = nil; |
| 545 | + for (SCDisplay* display in (*contentResult).displays) { |
| 546 | + if (display.displayID == monitorId) { |
| 547 | + scDisplay = display; |
| 548 | + break; |
| 549 | + } |
500 | 550 | } |
501 | | - if (startError != nil) { |
502 | | - channel->active.store(false); |
503 | | - return std::unexpected( |
504 | | - Error::captureFailed(startError.localizedDescription.UTF8String) |
505 | | - ); |
| 551 | + if (scDisplay == nil) { |
| 552 | + return std::unexpected(Error::invalidArgument( |
| 553 | + std::format("no monitor with id {}", monitorId) |
| 554 | + )); |
506 | 555 | } |
507 | 556 |
|
508 | | - return std::make_unique<MacWindowStream>( |
509 | | - stream, output, queue, std::move(channel) |
| 557 | + NSMutableArray<SCRunningApplication*>* excluded = [NSMutableArray array]; |
| 558 | + if (options.excludesOwnWindows) { |
| 559 | + for (SCRunningApplication* app in (*contentResult).applications) { |
| 560 | + if (app.processID == getpid()) [excluded addObject:app]; |
| 561 | + } |
| 562 | + } |
| 563 | + |
| 564 | + SCContentFilter* filter = |
| 565 | + [[SCContentFilter alloc] initWithDisplay:scDisplay |
| 566 | + excludingApplications:excluded |
| 567 | + exceptingWindows:@[]]; |
| 568 | + |
| 569 | + return startFilteredStream( |
| 570 | + filter, 0, options, std::move(onFrame), std::move(onError) |
510 | 571 | ); |
511 | 572 | } |
512 | 573 |
|
|
0 commit comments