Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions Apps/VoidDisplay/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -323,13 +323,13 @@
}
}
},
"Automatic balances H.264 compatibility, clarity, and encoder load." : {
"Automatic and Smooth send AV1 at source resolution on LAN. Actual frame rate follows encoder capability." : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "自动模式会平衡 H.264 兼容性、清晰度与编码负载。"
"value" : "自动和流畅会在局域网按源分辨率发送 AV1。实际帧率取决于编码能力。"
}
}
}
Expand Down Expand Up @@ -2916,16 +2916,6 @@
}
}
},
"This Mac's WebRTC stack did not expose H.264 video encoding." : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "本机 WebRTC 链路未暴露 H.264 视频编码能力。"
}
}
}
},
"Technical Details" : {
"extractionState" : "stale",
"localizations" : {
Expand Down Expand Up @@ -3058,6 +3048,17 @@
}
}
},
"This Mac's WebRTC stack did not expose AV1 video encoding." : {
"extractionState" : "stale",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "本机 WebRTC 链路未暴露 AV1 视频编码能力。"
}
}
}
},
"This resolution mode already exists." : {
"extractionState" : "stale",
"localizations" : {
Expand Down
1 change: 1 addition & 0 deletions Apps/VoidDisplay/VoidDisplay.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
"$(ROOT_DIR)/Tools/VoidDisplayRelay/go.mod",
"$(ROOT_DIR)/Tools/VoidDisplayRelay/go.sum",
"$(ROOT_DIR)/Tools/VoidDisplayRelay/cmd/voiddisplay-relay/main.go",
"$(ROOT_DIR)/Tools/VoidDisplayRelay/cmd/voiddisplay-relay/main_test.go",
"$(ROOT_DIR)/Tools/VoidDisplayRelay/internal/relay/server.go",
"$(ROOT_DIR)/Tools/VoidDisplayRelay/internal/relay/server_test.go",
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ package final class UITestCapturePreviewSession: @unchecked Sendable, DisplayCap

private final class DiagnosticsShareFrameConsumer: DisplayShareFrameConsumer {
nonisolated var hasDemand: Bool { false }
package nonisolated func updateSourceVideoSpec(_ spec: SourceVideoSpec) {
_ = spec
}
package nonisolated func updatePerformanceMode(_ mode: CapturePerformanceMode) {
_ = mode
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ package final class DisplayPreviewSubscription: Sendable {
private final class NoopDisplayShareFrameConsumer: DisplayShareFrameConsumer {
nonisolated var hasDemand: Bool { false }

package nonisolated func updateSourceVideoSpec(_ spec: SourceVideoSpec) {
_ = spec
}

package nonisolated func updatePerformanceMode(_ mode: CapturePerformanceMode) {
_ = mode
}
Expand Down
21 changes: 16 additions & 5 deletions Sources/VoidDisplayCapture/Services/DisplayCaptureSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ package final class DisplayCaptureSession: @unchecked Sendable, DisplayCaptureSe
nonisolated private let metrics: DisplayCaptureMetricsStore
nonisolated private let streamConfigurationCoordinator: DisplayCaptureStreamConfigurationCoordinator
nonisolated private let demandDriver: DisplayCaptureDemandDriver
nonisolated private let sourceVideoSpec: SourceVideoSpec

nonisolated init(
display: SCDisplay,
Expand Down Expand Up @@ -338,6 +339,8 @@ package final class DisplayCaptureSession: @unchecked Sendable, DisplayCaptureSe
let filter = try await Self.makeContentFilter(display: display)
self.stream = SCStream(filter: filter, configuration: config, delegate: output)
self.shareFrameConsumer = makeShareFrameConsumer()
self.sourceVideoSpec = captureSizeContext.sourceVideoSpec
self.shareFrameConsumer.updateSourceVideoSpec(captureSizeContext.sourceVideoSpec)
let metrics = DisplayCaptureMetricsStore()
self.metrics = metrics
let streamConfigurationCoordinator = DisplayCaptureStreamConfigurationCoordinator(
Expand Down Expand Up @@ -403,6 +406,7 @@ package final class DisplayCaptureSession: @unchecked Sendable, DisplayCaptureSe
}

package nonisolated func setDemand(_ demand: DisplayCaptureDemandSnapshot) async throws {
shareFrameConsumer.updateSourceVideoSpec(sourceVideoSpec)
shareFrameConsumer.updatePerformanceMode(demand.performanceMode)
try await demandDriver.setDemand(demand)
}
Expand Down Expand Up @@ -466,8 +470,12 @@ package extension DisplayCaptureSession {
}

nonisolated static func clampedPreviewFramesPerSecond(for refreshRate: Double) -> Int {
sourceFramesPerSecond(for: refreshRate)
}

nonisolated static func sourceFramesPerSecond(for refreshRate: Double) -> Int {
let normalizedRefreshRate = refreshRate > 0 ? refreshRate : 60.0
return max(1, Int(min(normalizedRefreshRate, 60.0).rounded()))
return max(1, Int(normalizedRefreshRate.rounded()))
}

nonisolated private static func makeStreamConfigurationState(
Expand All @@ -482,10 +490,11 @@ package extension DisplayCaptureSession {
for: initialProfile,
performanceMode: initialPerformanceMode
)
let previewFramesPerSecond = clampedPreviewFramesPerSecond(for: displayMode?.refreshRate ?? 60.0)
let previewFramesPerSecond = sourceFramesPerSecond(for: displayMode?.refreshRate ?? 60.0)
let initialFrameRateTier = DisplayCaptureConfigurationStateMachine.defaultFrameRateTier(
for: initialProfile,
performanceMode: initialPerformanceMode
performanceMode: initialPerformanceMode,
sourceFramesPerSecond: captureSizeContext.sourceVideoSpec.framesPerSecond
)

let state = DisplayCaptureStreamConfigurationState(
Expand All @@ -501,7 +510,7 @@ package extension DisplayCaptureSession {
shareCursorOverrideCount: 0
)
AppLog.capture.notice(
"Capture config display=\(display.displayID, privacy: .public) size=\(captureSize.width)x\(captureSize.height, privacy: .public) logical=\(captureSizeContext.logicalSize.width)x\(captureSizeContext.logicalSize.height, privacy: .public) physical=\(captureSizeContext.physicalSize.width)x\(captureSizeContext.physicalSize.height, privacy: .public)"
"Capture config display=\(display.displayID, privacy: .public) size=\(captureSize.width)x\(captureSize.height, privacy: .public) logical=\(captureSizeContext.logicalSize.width)x\(captureSizeContext.logicalSize.height, privacy: .public) physical=\(captureSizeContext.physicalSize.width)x\(captureSizeContext.physicalSize.height, privacy: .public) sourceFps=\(captureSizeContext.sourceVideoSpec.framesPerSecond, privacy: .public)"
)
return state
}
Expand All @@ -515,6 +524,7 @@ package extension DisplayCaptureSession {
let modeLogicalWidth = displayMode.map { $0.width } ?? modePixelWidth
let modeLogicalHeight = displayMode.map { $0.height } ?? modePixelHeight
let backingScale = screenBackingScaleFactor(for: display.displayID)
let sourceFramesPerSecond = sourceFramesPerSecond(for: displayMode?.refreshRate ?? 60.0)

let scaledLogicalWidth = max(1, Int((CGFloat(modeLogicalWidth) * backingScale).rounded()))
let scaledLogicalHeight = max(1, Int((CGFloat(modeLogicalHeight) * backingScale).rounded()))
Expand All @@ -527,7 +537,8 @@ package extension DisplayCaptureSession {
physicalSize: DisplayCaptureDimensions(
width: max(modePixelWidth, scaledLogicalWidth),
height: max(modePixelHeight, scaledLogicalHeight)
)
),
sourceFramesPerSecond: sourceFramesPerSecond
)
}

Expand Down
54 changes: 39 additions & 15 deletions Sources/VoidDisplayCapture/Services/DisplayCaptureTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import CoreGraphics
import CoreMedia
import Foundation
import ScreenCaptureKit
package nonisolated enum DisplayCaptureFrameRateTier: Int, CaseIterable, Sendable, Equatable {
case fps30 = 30
case fps45 = 45
case fps60 = 60
package nonisolated struct DisplayCaptureFrameRateTier: Sendable, Equatable, Hashable {
package static let fps30 = DisplayCaptureFrameRateTier(framesPerSecond: 30)
package static let fps45 = DisplayCaptureFrameRateTier(framesPerSecond: 45)
package static let fps60 = DisplayCaptureFrameRateTier(framesPerSecond: 60)

package var framesPerSecond: Int {
rawValue
package let framesPerSecond: Int

package init(framesPerSecond: Int) {
self.framesPerSecond = max(1, framesPerSecond)
}
}

Expand All @@ -20,18 +22,37 @@ package typealias DisplayCaptureDimensions = CapturePixelDimensions
package nonisolated struct DisplayCaptureSizeContext: Sendable, Equatable {
package static let defaultShared = DisplayCaptureSizeContext(
logicalSize: .defaultShared,
physicalSize: .defaultShared
physicalSize: .defaultShared,
sourceVideoSpec: .defaultShared
)

package let logicalSize: DisplayCaptureDimensions
package let physicalSize: DisplayCaptureDimensions
package let sourceVideoSpec: SourceVideoSpec

package init(
logicalSize: DisplayCaptureDimensions,
physicalSize: DisplayCaptureDimensions,
sourceFramesPerSecond: Int = 60
) {
self.init(
logicalSize: logicalSize,
physicalSize: physicalSize,
sourceVideoSpec: SourceVideoSpec(
dimensions: physicalSize,
framesPerSecond: sourceFramesPerSecond
)
)
}

package init(
logicalSize: DisplayCaptureDimensions,
physicalSize: DisplayCaptureDimensions
physicalSize: DisplayCaptureDimensions,
sourceVideoSpec: SourceVideoSpec
) {
self.logicalSize = logicalSize
self.physicalSize = physicalSize
self.sourceVideoSpec = sourceVideoSpec
}

package func captureSize(
Expand Down Expand Up @@ -273,19 +294,21 @@ package nonisolated enum DisplayCaptureConfigurationDecision: Sendable, Equatabl
package nonisolated enum DisplayCaptureConfigurationStateMachine {
nonisolated static func defaultFrameRateTier(
for profile: DisplayCaptureProfile,
performanceMode: CapturePerformanceMode
performanceMode: CapturePerformanceMode,
sourceFramesPerSecond: Int = 60
) -> DisplayCaptureFrameRateTier {
switch performanceMode {
let sourceFramesPerSecond = max(1, sourceFramesPerSecond)
return switch performanceMode {
case .automatic:
.fps60
DisplayCaptureFrameRateTier(framesPerSecond: sourceFramesPerSecond)
case .smooth:
.fps60
DisplayCaptureFrameRateTier(framesPerSecond: sourceFramesPerSecond)
case .powerEfficient:
switch profile {
case .previewOnly:
.fps45
DisplayCaptureFrameRateTier(framesPerSecond: min(sourceFramesPerSecond, 45))
case .shareOnly, .mixed:
.fps30
DisplayCaptureFrameRateTier(framesPerSecond: min(sourceFramesPerSecond, 30))
}
}
}
Expand All @@ -301,7 +324,8 @@ package nonisolated enum DisplayCaptureConfigurationStateMachine {
profile: profile,
frameRateTier: defaultFrameRateTier(
for: profile,
performanceMode: demand.performanceMode
performanceMode: demand.performanceMode,
sourceFramesPerSecond: captureSizeContext.sourceVideoSpec.framesPerSecond
),
captureSize: captureSizeContext.captureSize(
for: profile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,27 @@ package nonisolated struct CapturePixelDimensions: Sendable, Equatable {
}
}

package nonisolated struct SourceVideoSpec: Sendable, Equatable {
package let dimensions: CapturePixelDimensions
package let framesPerSecond: Int

package init(width: Int, height: Int, framesPerSecond: Int) {
self.dimensions = CapturePixelDimensions(width: width, height: height)
self.framesPerSecond = max(1, framesPerSecond)
}

package init(dimensions: CapturePixelDimensions, framesPerSecond: Int) {
self.dimensions = dimensions
self.framesPerSecond = max(1, framesPerSecond)
}

package static let defaultShared = SourceVideoSpec(
dimensions: .defaultShared,
framesPerSecond: 60
)
}

package nonisolated struct SharedCapturePerformanceBudget: Sendable, Equatable {
package static let automaticPixelBudgetPerSecond: Int64 = 221_184_000
package static let powerEfficientPixelBudgetPerSecond: Int64 = 62_208_000

package let framesPerSecond: Int
Expand All @@ -78,7 +97,7 @@ package nonisolated struct SharedCapturePerformanceBudget: Sendable, Equatable {
case .automatic:
self.init(
framesPerSecond: 60,
pixelBudgetPerSecond: Self.automaticPixelBudgetPerSecond
pixelBudgetPerSecond: nil
)
case .smooth:
self.init(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Synchronization

package protocol DisplayShareFrameConsumer: AnyObject, Sendable {
nonisolated var hasDemand: Bool { get }
nonisolated func updateSourceVideoSpec(_ spec: SourceVideoSpec)
nonisolated func updatePerformanceMode(_ mode: CapturePerformanceMode)
nonisolated func stopSharing()
nonisolated func submitFrame(pixelBuffer: CVPixelBuffer, ptsUs: UInt64)
Expand Down
Loading
Loading