@mindexec/cli 0.2.478 → 0.2.479
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +323 -323
- package/codex-model-catalog.js +202 -202
- package/codex-runtime.js +1608 -1608
- package/desktop-workspace-state.cjs +120 -120
- package/electron/main.cjs +517 -517
- package/electron/preload.cjs +17 -17
- package/electron/recurring-update-owner-smoke.mjs +55 -55
- package/electron/recurring-update-owner.cjs +97 -97
- package/electron/source-smoke.mjs +108 -108
- package/electron/update-feed-resolver.cjs +79 -79
- package/electron/update-feed.json +10 -10
- package/electron/update-manager-smoke.mjs +146 -146
- package/electron/update-manager.cjs +297 -297
- package/electron/windows-package-smoke.mjs +156 -156
- package/launch-bridge.cjs +236 -236
- package/lzo1x.js +110 -110
- package/mode4-atlas-worker.js +440 -440
- package/mode4-atlas.js +130 -130
- package/package.json +1 -1
- package/port-guard.cjs +285 -285
- package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/remote-hub.js +3698 -3698
- package/scripts/auth-session-smoke.mjs +262 -262
- package/scripts/board-render-ownership-smoke.mjs +85 -85
- package/scripts/codex-model-catalog-smoke.mjs +150 -150
- package/scripts/codex-runtime-home-smoke.mjs +58 -58
- package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
- package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
- package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
- package/scripts/desktop-update-publisher-smoke.mjs +89 -89
- package/scripts/desktop-update-worker-smoke.mjs +72 -72
- package/scripts/desktop-workspace-state-smoke.mjs +81 -81
- package/scripts/drive-installed-update-smoke.mjs +67 -67
- package/scripts/npm-fresh-install-smoke.mjs +216 -216
- package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
- package/scripts/remote-agent-managed-smoke.mjs +318 -318
- package/scripts/remote-agent-package-smoke.mjs +64 -64
- package/scripts/remote-agent-ws-smoke.mjs +269 -269
- package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
- package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
- package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
- package/scripts/remote-frame-ws-smoke.mjs +234 -234
- package/scripts/remote-http-smoke.mjs +592 -592
- package/scripts/remote-hub-identity-smoke.mjs +146 -146
- package/scripts/remote-hub-scale-smoke.mjs +124 -124
- package/scripts/remote-hub-smoke.mjs +699 -699
- package/scripts/remote-input-ws-smoke.mjs +263 -263
- package/scripts/remote-registry-follower-smoke.mjs +815 -815
- package/scripts/setup-tree-sitter-grammars.mjs +59 -59
- package/server.js +7510 -7510
- package/start-bridge.bat +32 -32
- package/start-bridge.sh +81 -81
- package/tree-sitter-grammars/README.md +18 -0
- package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
- package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
- package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
- package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
- package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
- package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
- package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
- package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10506
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +1059 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +434 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7078 -7078
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9958 -9958
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +372 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +1258 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
- package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
- package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
- package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
- package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
- package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
- package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
- package/wwwroot/_framework/{Google.Protobuf.33g2gb5is0.dll → Google.Protobuf.9h59ukbel7.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Core.z100xo4mg0.dll → MindExecution.Core.xxsj2s456b.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.i9cldrx8yc.dll → MindExecution.Kernel.5jofnc0wrd.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.48lzqddb0a.dll → MindExecution.Plugins.Admin.t7m4jnqjg3.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.7m8u1q142p.dll → MindExecution.Plugins.Business.6yjlhhavap.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.cl5wmr53od.dll → MindExecution.Plugins.Concept.iikqq5zxu0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.slipi39041.dll → MindExecution.Plugins.Directory.c5ribyne4m.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.qfge05l671.dll → MindExecution.Plugins.PlanMaster.365bbuifr9.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.tn7v29ut77.dll → MindExecution.Plugins.YouTube.ax415cilpk.dll} +0 -0
- package/wwwroot/_framework/MindExecution.Shared.21e2nvf86u.dll +0 -0
- package/wwwroot/_framework/{MindExecution.Web.rwawrxxk38.dll → MindExecution.Web.ppgm8rey59.dll} +0 -0
- package/wwwroot/_framework/{System.Collections.23yxgetbju.dll → System.Collections.zyvbaq6mpd.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +25 -25
- package/wwwroot/_headers +40 -40
- package/wwwroot/_redirects +1 -1
- package/wwwroot/index.html +772 -772
- package/wwwroot/js/marketing-tool.js +180 -180
- package/wwwroot/manifest.webmanifest +23 -23
- package/wwwroot/service-worker-assets.js +109 -81
- package/wwwroot/service-worker.js +33 -33
- package/wwwroot/_framework/MindExecution.Shared.rjqlf0umjn.dll +0 -0
|
@@ -1,339 +1,339 @@
|
|
|
1
|
-
import CoreFoundation
|
|
2
|
-
import CoreGraphics
|
|
3
|
-
import CoreMedia
|
|
4
|
-
import CoreVideo
|
|
5
|
-
import Foundation
|
|
6
|
-
import IOSurface
|
|
7
|
-
import ScreenCaptureKit
|
|
8
|
-
import VideoToolbox
|
|
9
|
-
|
|
10
|
-
@available(macOS 12.3, *)
|
|
11
|
-
final class LiveDeskScreenCaptureKitEncoder: NSObject, SCStreamOutput, SCStreamDelegate {
|
|
12
|
-
private let displayIndex: Int
|
|
13
|
-
private let width: Int
|
|
14
|
-
private let height: Int
|
|
15
|
-
private let fps: Int
|
|
16
|
-
private let bitrateKbps: Int
|
|
17
|
-
private let queue = DispatchQueue(label: "dev.livedesk.screencapturekit.frames", qos: .userInteractive)
|
|
18
|
-
private let writeLock = NSLock()
|
|
19
|
-
private var stream: SCStream?
|
|
20
|
-
private var compressionSession: VTCompressionSession?
|
|
21
|
-
private var sampleCount: Int = 0
|
|
22
|
-
private var frameCount: Int = 0
|
|
23
|
-
private var skippedCount: Int = 0
|
|
24
|
-
private var encodedCount: Int = 0
|
|
25
|
-
private var encodedKeyCount: Int = 0
|
|
26
|
-
private var encodedDeltaCount: Int = 0
|
|
27
|
-
private var loggedInputSurface = false
|
|
28
|
-
|
|
29
|
-
init(displayIndex: Int, width: Int, height: Int, fps: Int, bitrateKbps: Int) {
|
|
30
|
-
self.displayIndex = max(0, displayIndex)
|
|
31
|
-
self.width = max(2, width)
|
|
32
|
-
self.height = max(2, height)
|
|
33
|
-
self.fps = max(1, fps)
|
|
34
|
-
self.bitrateKbps = max(500, bitrateKbps)
|
|
35
|
-
super.init()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
func start() async throws {
|
|
39
|
-
try createCompressionSession()
|
|
40
|
-
|
|
41
|
-
let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
|
|
42
|
-
guard !shareableContent.displays.isEmpty else {
|
|
43
|
-
throw LiveDeskSCKError.noDisplays
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let mainDisplayID = CGMainDisplayID()
|
|
47
|
-
let orderedDisplays = shareableContent.displays.sorted { left, right in
|
|
48
|
-
let leftIsMain = left.displayID == mainDisplayID
|
|
49
|
-
let rightIsMain = right.displayID == mainDisplayID
|
|
50
|
-
if leftIsMain != rightIsMain {
|
|
51
|
-
return leftIsMain
|
|
52
|
-
}
|
|
53
|
-
return left.displayID < right.displayID
|
|
54
|
-
}
|
|
55
|
-
let display = orderedDisplays[min(displayIndex, orderedDisplays.count - 1)]
|
|
56
|
-
let filter = SCContentFilter(display: display, excludingWindows: [])
|
|
57
|
-
let configuration = SCStreamConfiguration()
|
|
58
|
-
configuration.width = width
|
|
59
|
-
configuration.height = height
|
|
60
|
-
configuration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
61
|
-
configuration.queueDepth = 3
|
|
62
|
-
configuration.showsCursor = false
|
|
63
|
-
configuration.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
|
64
|
-
|
|
65
|
-
let stream = SCStream(filter: filter, configuration: configuration, delegate: self)
|
|
66
|
-
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: queue)
|
|
67
|
-
self.stream = stream
|
|
68
|
-
try await stream.startCapture()
|
|
69
|
-
|
|
70
|
-
fputs("LiveDeskSCK started displayIndex=\(displayIndex) displayID=\(display.displayID) size=\(width)x\(height) fps=\(fps) bitrateKbps=\(bitrateKbps)\n", stderr)
|
|
71
|
-
while !Task.isCancelled {
|
|
72
|
-
try await Task.sleep(nanoseconds: 1_000_000_000)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private func createCompressionSession() throws {
|
|
77
|
-
var session: VTCompressionSession?
|
|
78
|
-
let encoderSpecification: [CFString: Any] = [
|
|
79
|
-
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder: true
|
|
80
|
-
]
|
|
81
|
-
let imageBufferAttributes: [CFString: Any] = [
|
|
82
|
-
kCVPixelBufferPixelFormatTypeKey: NSNumber(value: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange),
|
|
83
|
-
kCVPixelBufferWidthKey: NSNumber(value: width),
|
|
84
|
-
kCVPixelBufferHeightKey: NSNumber(value: height),
|
|
85
|
-
kCVPixelBufferMetalCompatibilityKey: true,
|
|
86
|
-
kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary
|
|
87
|
-
]
|
|
88
|
-
let status = VTCompressionSessionCreate(
|
|
89
|
-
allocator: kCFAllocatorDefault,
|
|
90
|
-
width: Int32(width),
|
|
91
|
-
height: Int32(height),
|
|
92
|
-
codecType: kCMVideoCodecType_H264,
|
|
93
|
-
encoderSpecification: encoderSpecification as CFDictionary,
|
|
94
|
-
imageBufferAttributes: imageBufferAttributes as CFDictionary,
|
|
95
|
-
compressedDataAllocator: nil,
|
|
96
|
-
outputCallback: Self.compressionCallback,
|
|
97
|
-
refcon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
|
|
98
|
-
compressionSessionOut: &session)
|
|
99
|
-
|
|
100
|
-
guard status == noErr, let session else {
|
|
101
|
-
throw LiveDeskSCKError.encoderCreate(status)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue)
|
|
105
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_PrioritizeEncodingSpeedOverQuality, value: kCFBooleanTrue)
|
|
106
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowTemporalCompression, value: kCFBooleanTrue)
|
|
107
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse)
|
|
108
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxFrameDelayCount, value: NSNumber(value: 0))
|
|
109
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Baseline_AutoLevel)
|
|
110
|
-
let recoveryKeyInterval = max(1, fps / 2)
|
|
111
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: NSNumber(value: recoveryKeyInterval))
|
|
112
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, value: NSNumber(value: 0.5))
|
|
113
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: fps))
|
|
114
|
-
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: NSNumber(value: bitrateKbps * 1000))
|
|
115
|
-
VTCompressionSessionPrepareToEncodeFrames(session)
|
|
116
|
-
compressionSession = session
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private static let compressionCallback: VTCompressionOutputCallback = { refCon, _, status, _, sampleBuffer in
|
|
120
|
-
guard status == noErr,
|
|
121
|
-
let refCon,
|
|
122
|
-
let sampleBuffer,
|
|
123
|
-
CMSampleBufferDataIsReady(sampleBuffer) else {
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let encoder = Unmanaged<LiveDeskScreenCaptureKitEncoder>.fromOpaque(refCon).takeUnretainedValue()
|
|
128
|
-
encoder.writeEncodedSampleBuffer(sampleBuffer)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
|
132
|
-
guard type == .screen,
|
|
133
|
-
CMSampleBufferDataIsReady(sampleBuffer),
|
|
134
|
-
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer),
|
|
135
|
-
let session = compressionSession else {
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
sampleCount += 1
|
|
140
|
-
guard isEncodableFrame(sampleBuffer) else {
|
|
141
|
-
skippedCount += 1
|
|
142
|
-
logCaptureProgressIfNeeded()
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
frameCount += 1
|
|
147
|
-
if !loggedInputSurface {
|
|
148
|
-
loggedInputSurface = true
|
|
149
|
-
let pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer)
|
|
150
|
-
let hasIOSurface = CVPixelBufferGetIOSurface(imageBuffer) != nil
|
|
151
|
-
fputs("LiveDeskSCK input pixelFormat=\(pixelFormat) iosurface=\(hasIOSurface ? 1 : 0) queueDepth=3 zeroCopyCandidate=\(hasIOSurface ? 1 : 0)\n", stderr)
|
|
152
|
-
}
|
|
153
|
-
let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
|
154
|
-
let duration = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
155
|
-
var frameProperties: CFDictionary?
|
|
156
|
-
if frameCount == 1 {
|
|
157
|
-
let properties = NSMutableDictionary()
|
|
158
|
-
properties[kVTEncodeFrameOptionKey_ForceKeyFrame] = kCFBooleanTrue
|
|
159
|
-
frameProperties = properties
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let status = VTCompressionSessionEncodeFrame(
|
|
163
|
-
session,
|
|
164
|
-
imageBuffer: imageBuffer,
|
|
165
|
-
presentationTimeStamp: pts,
|
|
166
|
-
duration: duration,
|
|
167
|
-
frameProperties: frameProperties,
|
|
168
|
-
sourceFrameRefcon: nil,
|
|
169
|
-
infoFlagsOut: nil)
|
|
170
|
-
|
|
171
|
-
if status != noErr && (frameCount == 1 || frameCount % 120 == 0) {
|
|
172
|
-
fputs("LiveDeskSCK encode status=\(status) frames=\(frameCount)\n", stderr)
|
|
173
|
-
}
|
|
174
|
-
logCaptureProgressIfNeeded()
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
func stream(_ stream: SCStream, didStopWithError error: Error) {
|
|
178
|
-
fputs("LiveDeskSCK stream stopped: \(error)\n", stderr)
|
|
179
|
-
exit(3)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private func isEncodableFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
183
|
-
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
184
|
-
let first = attachments.firstObject as? NSDictionary,
|
|
185
|
-
let rawStatus = first[SCStreamFrameInfo.status] as? Int else {
|
|
186
|
-
return true
|
|
187
|
-
}
|
|
188
|
-
return rawStatus == SCFrameStatus.complete.rawValue
|
|
189
|
-
|| rawStatus == SCFrameStatus.idle.rawValue
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private func logCaptureProgressIfNeeded() {
|
|
193
|
-
let interval = max(1, fps * 5)
|
|
194
|
-
if sampleCount == 1 || sampleCount % interval == 0 {
|
|
195
|
-
fputs("LiveDeskSCK capture samples=\(sampleCount) complete=\(frameCount) skipped=\(skippedCount)\n", stderr)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private func writeEncodedSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
|
200
|
-
guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else {
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let keyFrame = isKeyFrame(sampleBuffer)
|
|
205
|
-
var output = Data()
|
|
206
|
-
appendAccessUnitDelimiter(to: &output)
|
|
207
|
-
if keyFrame, let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) {
|
|
208
|
-
appendParameterSet(formatDescription, index: 0, to: &output)
|
|
209
|
-
appendParameterSet(formatDescription, index: 1, to: &output)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
var lengthAtOffset = 0
|
|
213
|
-
var totalLength = 0
|
|
214
|
-
var dataPointer: UnsafeMutablePointer<Int8>?
|
|
215
|
-
let status = CMBlockBufferGetDataPointer(
|
|
216
|
-
blockBuffer,
|
|
217
|
-
atOffset: 0,
|
|
218
|
-
lengthAtOffsetOut: &lengthAtOffset,
|
|
219
|
-
totalLengthOut: &totalLength,
|
|
220
|
-
dataPointerOut: &dataPointer)
|
|
221
|
-
|
|
222
|
-
guard status == noErr, let dataPointer, totalLength > 4 else {
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
var offset = 0
|
|
227
|
-
while offset + 4 <= totalLength {
|
|
228
|
-
var nalLength: UInt32 = 0
|
|
229
|
-
memcpy(&nalLength, dataPointer.advanced(by: offset), 4)
|
|
230
|
-
nalLength = CFSwapInt32BigToHost(nalLength)
|
|
231
|
-
let length = Int(nalLength)
|
|
232
|
-
if length <= 0 || offset + 4 + length > totalLength {
|
|
233
|
-
break
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
appendStartCode(to: &output)
|
|
237
|
-
output.append(UnsafeRawPointer(dataPointer.advanced(by: offset + 4)).assumingMemoryBound(to: UInt8.self), count: length)
|
|
238
|
-
offset += 4 + length
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
guard !output.isEmpty else {
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
writeLock.lock()
|
|
246
|
-
FileHandle.standardOutput.write(output)
|
|
247
|
-
writeLock.unlock()
|
|
248
|
-
|
|
249
|
-
encodedCount += 1
|
|
250
|
-
if keyFrame {
|
|
251
|
-
encodedKeyCount += 1
|
|
252
|
-
} else {
|
|
253
|
-
encodedDeltaCount += 1
|
|
254
|
-
}
|
|
255
|
-
if encodedCount == 1 || encodedCount % max(1, fps * 5) == 0 {
|
|
256
|
-
fputs("LiveDeskSCK encoded frames=\(encodedCount) key=\(encodedKeyCount) delta=\(encodedDeltaCount) bytes=\(output.count)\n", stderr)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private func isKeyFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
261
|
-
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
262
|
-
let first = attachments.firstObject as? NSDictionary else {
|
|
263
|
-
return true
|
|
264
|
-
}
|
|
265
|
-
return !(first[kCMSampleAttachmentKey_NotSync] as? Bool ?? false)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private func appendParameterSet(_ formatDescription: CMFormatDescription, index: Int, to output: inout Data) {
|
|
269
|
-
var pointer: UnsafePointer<UInt8>?
|
|
270
|
-
var size = 0
|
|
271
|
-
var count = 0
|
|
272
|
-
var nalHeaderLength: Int32 = 0
|
|
273
|
-
let status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
|
274
|
-
formatDescription,
|
|
275
|
-
parameterSetIndex: index,
|
|
276
|
-
parameterSetPointerOut: &pointer,
|
|
277
|
-
parameterSetSizeOut: &size,
|
|
278
|
-
parameterSetCountOut: &count,
|
|
279
|
-
nalUnitHeaderLengthOut: &nalHeaderLength)
|
|
280
|
-
|
|
281
|
-
guard status == noErr, let pointer, size > 0 else {
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
appendStartCode(to: &output)
|
|
286
|
-
output.append(pointer, count: size)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private func appendStartCode(to output: inout Data) {
|
|
290
|
-
output.append(contentsOf: [0, 0, 0, 1])
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private func appendAccessUnitDelimiter(to output: inout Data) {
|
|
294
|
-
appendStartCode(to: &output)
|
|
295
|
-
output.append(contentsOf: [0x09, 0xF0])
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
enum LiveDeskSCKError: Error {
|
|
300
|
-
case noDisplays
|
|
301
|
-
case encoderCreate(OSStatus)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
func intArg(_ name: String, default defaultValue: Int) -> Int {
|
|
305
|
-
let args = CommandLine.arguments
|
|
306
|
-
guard let index = args.firstIndex(of: name), index + 1 < args.count else {
|
|
307
|
-
return defaultValue
|
|
308
|
-
}
|
|
309
|
-
return Int(args[index + 1]) ?? defaultValue
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
@main
|
|
313
|
-
struct LiveDeskSCKMain {
|
|
314
|
-
static func main() async {
|
|
315
|
-
guard #available(macOS 12.3, *) else {
|
|
316
|
-
fputs("LiveDeskSCK requires macOS 12.3 or later.\n", stderr)
|
|
317
|
-
exit(2)
|
|
318
|
-
}
|
|
319
|
-
guard CGPreflightScreenCaptureAccess() else {
|
|
320
|
-
fputs("LiveDeskSCK screen recording permission is not granted.\n", stderr)
|
|
321
|
-
exit(10)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
let displayIndex = intArg("--display-index", default: 0)
|
|
325
|
-
let width = intArg("--width", default: 960)
|
|
326
|
-
let height = intArg("--height", default: 540)
|
|
327
|
-
let fps = intArg("--fps", default: 30)
|
|
328
|
-
let bitrateKbps = intArg("--bitrate-kbps", default: max(3500, width * height * fps / 4000))
|
|
329
|
-
|
|
330
|
-
do {
|
|
331
|
-
let encoder = LiveDeskScreenCaptureKitEncoder(displayIndex: displayIndex, width: width, height: height, fps: fps, bitrateKbps: bitrateKbps)
|
|
332
|
-
try await encoder.start()
|
|
333
|
-
} catch {
|
|
334
|
-
fputs("LiveDeskSCK failed: \(error)\n", stderr)
|
|
335
|
-
exit(1)
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
1
|
+
import CoreFoundation
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
import CoreMedia
|
|
4
|
+
import CoreVideo
|
|
5
|
+
import Foundation
|
|
6
|
+
import IOSurface
|
|
7
|
+
import ScreenCaptureKit
|
|
8
|
+
import VideoToolbox
|
|
9
|
+
|
|
10
|
+
@available(macOS 12.3, *)
|
|
11
|
+
final class LiveDeskScreenCaptureKitEncoder: NSObject, SCStreamOutput, SCStreamDelegate {
|
|
12
|
+
private let displayIndex: Int
|
|
13
|
+
private let width: Int
|
|
14
|
+
private let height: Int
|
|
15
|
+
private let fps: Int
|
|
16
|
+
private let bitrateKbps: Int
|
|
17
|
+
private let queue = DispatchQueue(label: "dev.livedesk.screencapturekit.frames", qos: .userInteractive)
|
|
18
|
+
private let writeLock = NSLock()
|
|
19
|
+
private var stream: SCStream?
|
|
20
|
+
private var compressionSession: VTCompressionSession?
|
|
21
|
+
private var sampleCount: Int = 0
|
|
22
|
+
private var frameCount: Int = 0
|
|
23
|
+
private var skippedCount: Int = 0
|
|
24
|
+
private var encodedCount: Int = 0
|
|
25
|
+
private var encodedKeyCount: Int = 0
|
|
26
|
+
private var encodedDeltaCount: Int = 0
|
|
27
|
+
private var loggedInputSurface = false
|
|
28
|
+
|
|
29
|
+
init(displayIndex: Int, width: Int, height: Int, fps: Int, bitrateKbps: Int) {
|
|
30
|
+
self.displayIndex = max(0, displayIndex)
|
|
31
|
+
self.width = max(2, width)
|
|
32
|
+
self.height = max(2, height)
|
|
33
|
+
self.fps = max(1, fps)
|
|
34
|
+
self.bitrateKbps = max(500, bitrateKbps)
|
|
35
|
+
super.init()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func start() async throws {
|
|
39
|
+
try createCompressionSession()
|
|
40
|
+
|
|
41
|
+
let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
|
|
42
|
+
guard !shareableContent.displays.isEmpty else {
|
|
43
|
+
throw LiveDeskSCKError.noDisplays
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let mainDisplayID = CGMainDisplayID()
|
|
47
|
+
let orderedDisplays = shareableContent.displays.sorted { left, right in
|
|
48
|
+
let leftIsMain = left.displayID == mainDisplayID
|
|
49
|
+
let rightIsMain = right.displayID == mainDisplayID
|
|
50
|
+
if leftIsMain != rightIsMain {
|
|
51
|
+
return leftIsMain
|
|
52
|
+
}
|
|
53
|
+
return left.displayID < right.displayID
|
|
54
|
+
}
|
|
55
|
+
let display = orderedDisplays[min(displayIndex, orderedDisplays.count - 1)]
|
|
56
|
+
let filter = SCContentFilter(display: display, excludingWindows: [])
|
|
57
|
+
let configuration = SCStreamConfiguration()
|
|
58
|
+
configuration.width = width
|
|
59
|
+
configuration.height = height
|
|
60
|
+
configuration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
61
|
+
configuration.queueDepth = 3
|
|
62
|
+
configuration.showsCursor = false
|
|
63
|
+
configuration.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
|
64
|
+
|
|
65
|
+
let stream = SCStream(filter: filter, configuration: configuration, delegate: self)
|
|
66
|
+
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: queue)
|
|
67
|
+
self.stream = stream
|
|
68
|
+
try await stream.startCapture()
|
|
69
|
+
|
|
70
|
+
fputs("LiveDeskSCK started displayIndex=\(displayIndex) displayID=\(display.displayID) size=\(width)x\(height) fps=\(fps) bitrateKbps=\(bitrateKbps)\n", stderr)
|
|
71
|
+
while !Task.isCancelled {
|
|
72
|
+
try await Task.sleep(nanoseconds: 1_000_000_000)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func createCompressionSession() throws {
|
|
77
|
+
var session: VTCompressionSession?
|
|
78
|
+
let encoderSpecification: [CFString: Any] = [
|
|
79
|
+
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder: true
|
|
80
|
+
]
|
|
81
|
+
let imageBufferAttributes: [CFString: Any] = [
|
|
82
|
+
kCVPixelBufferPixelFormatTypeKey: NSNumber(value: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange),
|
|
83
|
+
kCVPixelBufferWidthKey: NSNumber(value: width),
|
|
84
|
+
kCVPixelBufferHeightKey: NSNumber(value: height),
|
|
85
|
+
kCVPixelBufferMetalCompatibilityKey: true,
|
|
86
|
+
kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary
|
|
87
|
+
]
|
|
88
|
+
let status = VTCompressionSessionCreate(
|
|
89
|
+
allocator: kCFAllocatorDefault,
|
|
90
|
+
width: Int32(width),
|
|
91
|
+
height: Int32(height),
|
|
92
|
+
codecType: kCMVideoCodecType_H264,
|
|
93
|
+
encoderSpecification: encoderSpecification as CFDictionary,
|
|
94
|
+
imageBufferAttributes: imageBufferAttributes as CFDictionary,
|
|
95
|
+
compressedDataAllocator: nil,
|
|
96
|
+
outputCallback: Self.compressionCallback,
|
|
97
|
+
refcon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
|
|
98
|
+
compressionSessionOut: &session)
|
|
99
|
+
|
|
100
|
+
guard status == noErr, let session else {
|
|
101
|
+
throw LiveDeskSCKError.encoderCreate(status)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue)
|
|
105
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_PrioritizeEncodingSpeedOverQuality, value: kCFBooleanTrue)
|
|
106
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowTemporalCompression, value: kCFBooleanTrue)
|
|
107
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse)
|
|
108
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxFrameDelayCount, value: NSNumber(value: 0))
|
|
109
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Baseline_AutoLevel)
|
|
110
|
+
let recoveryKeyInterval = max(1, fps / 2)
|
|
111
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: NSNumber(value: recoveryKeyInterval))
|
|
112
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, value: NSNumber(value: 0.5))
|
|
113
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: fps))
|
|
114
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: NSNumber(value: bitrateKbps * 1000))
|
|
115
|
+
VTCompressionSessionPrepareToEncodeFrames(session)
|
|
116
|
+
compressionSession = session
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static let compressionCallback: VTCompressionOutputCallback = { refCon, _, status, _, sampleBuffer in
|
|
120
|
+
guard status == noErr,
|
|
121
|
+
let refCon,
|
|
122
|
+
let sampleBuffer,
|
|
123
|
+
CMSampleBufferDataIsReady(sampleBuffer) else {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let encoder = Unmanaged<LiveDeskScreenCaptureKitEncoder>.fromOpaque(refCon).takeUnretainedValue()
|
|
128
|
+
encoder.writeEncodedSampleBuffer(sampleBuffer)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
|
132
|
+
guard type == .screen,
|
|
133
|
+
CMSampleBufferDataIsReady(sampleBuffer),
|
|
134
|
+
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer),
|
|
135
|
+
let session = compressionSession else {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
sampleCount += 1
|
|
140
|
+
guard isEncodableFrame(sampleBuffer) else {
|
|
141
|
+
skippedCount += 1
|
|
142
|
+
logCaptureProgressIfNeeded()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
frameCount += 1
|
|
147
|
+
if !loggedInputSurface {
|
|
148
|
+
loggedInputSurface = true
|
|
149
|
+
let pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer)
|
|
150
|
+
let hasIOSurface = CVPixelBufferGetIOSurface(imageBuffer) != nil
|
|
151
|
+
fputs("LiveDeskSCK input pixelFormat=\(pixelFormat) iosurface=\(hasIOSurface ? 1 : 0) queueDepth=3 zeroCopyCandidate=\(hasIOSurface ? 1 : 0)\n", stderr)
|
|
152
|
+
}
|
|
153
|
+
let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
|
154
|
+
let duration = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
155
|
+
var frameProperties: CFDictionary?
|
|
156
|
+
if frameCount == 1 {
|
|
157
|
+
let properties = NSMutableDictionary()
|
|
158
|
+
properties[kVTEncodeFrameOptionKey_ForceKeyFrame] = kCFBooleanTrue
|
|
159
|
+
frameProperties = properties
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let status = VTCompressionSessionEncodeFrame(
|
|
163
|
+
session,
|
|
164
|
+
imageBuffer: imageBuffer,
|
|
165
|
+
presentationTimeStamp: pts,
|
|
166
|
+
duration: duration,
|
|
167
|
+
frameProperties: frameProperties,
|
|
168
|
+
sourceFrameRefcon: nil,
|
|
169
|
+
infoFlagsOut: nil)
|
|
170
|
+
|
|
171
|
+
if status != noErr && (frameCount == 1 || frameCount % 120 == 0) {
|
|
172
|
+
fputs("LiveDeskSCK encode status=\(status) frames=\(frameCount)\n", stderr)
|
|
173
|
+
}
|
|
174
|
+
logCaptureProgressIfNeeded()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func stream(_ stream: SCStream, didStopWithError error: Error) {
|
|
178
|
+
fputs("LiveDeskSCK stream stopped: \(error)\n", stderr)
|
|
179
|
+
exit(3)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private func isEncodableFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
183
|
+
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
184
|
+
let first = attachments.firstObject as? NSDictionary,
|
|
185
|
+
let rawStatus = first[SCStreamFrameInfo.status] as? Int else {
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
return rawStatus == SCFrameStatus.complete.rawValue
|
|
189
|
+
|| rawStatus == SCFrameStatus.idle.rawValue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func logCaptureProgressIfNeeded() {
|
|
193
|
+
let interval = max(1, fps * 5)
|
|
194
|
+
if sampleCount == 1 || sampleCount % interval == 0 {
|
|
195
|
+
fputs("LiveDeskSCK capture samples=\(sampleCount) complete=\(frameCount) skipped=\(skippedCount)\n", stderr)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private func writeEncodedSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
|
200
|
+
guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let keyFrame = isKeyFrame(sampleBuffer)
|
|
205
|
+
var output = Data()
|
|
206
|
+
appendAccessUnitDelimiter(to: &output)
|
|
207
|
+
if keyFrame, let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) {
|
|
208
|
+
appendParameterSet(formatDescription, index: 0, to: &output)
|
|
209
|
+
appendParameterSet(formatDescription, index: 1, to: &output)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
var lengthAtOffset = 0
|
|
213
|
+
var totalLength = 0
|
|
214
|
+
var dataPointer: UnsafeMutablePointer<Int8>?
|
|
215
|
+
let status = CMBlockBufferGetDataPointer(
|
|
216
|
+
blockBuffer,
|
|
217
|
+
atOffset: 0,
|
|
218
|
+
lengthAtOffsetOut: &lengthAtOffset,
|
|
219
|
+
totalLengthOut: &totalLength,
|
|
220
|
+
dataPointerOut: &dataPointer)
|
|
221
|
+
|
|
222
|
+
guard status == noErr, let dataPointer, totalLength > 4 else {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
var offset = 0
|
|
227
|
+
while offset + 4 <= totalLength {
|
|
228
|
+
var nalLength: UInt32 = 0
|
|
229
|
+
memcpy(&nalLength, dataPointer.advanced(by: offset), 4)
|
|
230
|
+
nalLength = CFSwapInt32BigToHost(nalLength)
|
|
231
|
+
let length = Int(nalLength)
|
|
232
|
+
if length <= 0 || offset + 4 + length > totalLength {
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
appendStartCode(to: &output)
|
|
237
|
+
output.append(UnsafeRawPointer(dataPointer.advanced(by: offset + 4)).assumingMemoryBound(to: UInt8.self), count: length)
|
|
238
|
+
offset += 4 + length
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
guard !output.isEmpty else {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
writeLock.lock()
|
|
246
|
+
FileHandle.standardOutput.write(output)
|
|
247
|
+
writeLock.unlock()
|
|
248
|
+
|
|
249
|
+
encodedCount += 1
|
|
250
|
+
if keyFrame {
|
|
251
|
+
encodedKeyCount += 1
|
|
252
|
+
} else {
|
|
253
|
+
encodedDeltaCount += 1
|
|
254
|
+
}
|
|
255
|
+
if encodedCount == 1 || encodedCount % max(1, fps * 5) == 0 {
|
|
256
|
+
fputs("LiveDeskSCK encoded frames=\(encodedCount) key=\(encodedKeyCount) delta=\(encodedDeltaCount) bytes=\(output.count)\n", stderr)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private func isKeyFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
261
|
+
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
262
|
+
let first = attachments.firstObject as? NSDictionary else {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
return !(first[kCMSampleAttachmentKey_NotSync] as? Bool ?? false)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private func appendParameterSet(_ formatDescription: CMFormatDescription, index: Int, to output: inout Data) {
|
|
269
|
+
var pointer: UnsafePointer<UInt8>?
|
|
270
|
+
var size = 0
|
|
271
|
+
var count = 0
|
|
272
|
+
var nalHeaderLength: Int32 = 0
|
|
273
|
+
let status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
|
274
|
+
formatDescription,
|
|
275
|
+
parameterSetIndex: index,
|
|
276
|
+
parameterSetPointerOut: &pointer,
|
|
277
|
+
parameterSetSizeOut: &size,
|
|
278
|
+
parameterSetCountOut: &count,
|
|
279
|
+
nalUnitHeaderLengthOut: &nalHeaderLength)
|
|
280
|
+
|
|
281
|
+
guard status == noErr, let pointer, size > 0 else {
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
appendStartCode(to: &output)
|
|
286
|
+
output.append(pointer, count: size)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private func appendStartCode(to output: inout Data) {
|
|
290
|
+
output.append(contentsOf: [0, 0, 0, 1])
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private func appendAccessUnitDelimiter(to output: inout Data) {
|
|
294
|
+
appendStartCode(to: &output)
|
|
295
|
+
output.append(contentsOf: [0x09, 0xF0])
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
enum LiveDeskSCKError: Error {
|
|
300
|
+
case noDisplays
|
|
301
|
+
case encoderCreate(OSStatus)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func intArg(_ name: String, default defaultValue: Int) -> Int {
|
|
305
|
+
let args = CommandLine.arguments
|
|
306
|
+
guard let index = args.firstIndex(of: name), index + 1 < args.count else {
|
|
307
|
+
return defaultValue
|
|
308
|
+
}
|
|
309
|
+
return Int(args[index + 1]) ?? defaultValue
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@main
|
|
313
|
+
struct LiveDeskSCKMain {
|
|
314
|
+
static func main() async {
|
|
315
|
+
guard #available(macOS 12.3, *) else {
|
|
316
|
+
fputs("LiveDeskSCK requires macOS 12.3 or later.\n", stderr)
|
|
317
|
+
exit(2)
|
|
318
|
+
}
|
|
319
|
+
guard CGPreflightScreenCaptureAccess() else {
|
|
320
|
+
fputs("LiveDeskSCK screen recording permission is not granted.\n", stderr)
|
|
321
|
+
exit(10)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let displayIndex = intArg("--display-index", default: 0)
|
|
325
|
+
let width = intArg("--width", default: 960)
|
|
326
|
+
let height = intArg("--height", default: 540)
|
|
327
|
+
let fps = intArg("--fps", default: 30)
|
|
328
|
+
let bitrateKbps = intArg("--bitrate-kbps", default: max(3500, width * height * fps / 4000))
|
|
329
|
+
|
|
330
|
+
do {
|
|
331
|
+
let encoder = LiveDeskScreenCaptureKitEncoder(displayIndex: displayIndex, width: width, height: height, fps: fps, bitrateKbps: bitrateKbps)
|
|
332
|
+
try await encoder.start()
|
|
333
|
+
} catch {
|
|
334
|
+
fputs("LiveDeskSCK failed: \(error)\n", stderr)
|
|
335
|
+
exit(1)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|