@livedesk/client 0.1.71 → 0.1.73
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/fast/linux-x64/livedesk-client-fast.dll +0 -0
- package/fast/linux-x64/livedesk-client-fast.pdb +0 -0
- package/fast/osx-arm64/MindExec.RemoteAgent.Fast/MacScreenCaptureKitH264.swift +260 -0
- package/fast/osx-arm64/livedesk-client-fast.dll +0 -0
- package/fast/osx-arm64/livedesk-client-fast.pdb +0 -0
- package/fast/osx-x64/MindExec.RemoteAgent.Fast/MacScreenCaptureKitH264.swift +260 -0
- package/fast/osx-x64/livedesk-client-fast.dll +0 -0
- package/fast/osx-x64/livedesk-client-fast.pdb +0 -0
- package/fast/win-x64/livedesk-client-fast.dll +0 -0
- package/fast/win-x64/livedesk-client-fast.pdb +0 -0
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import CoreFoundation
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
import CoreMedia
|
|
4
|
+
import CoreVideo
|
|
5
|
+
import Foundation
|
|
6
|
+
import ScreenCaptureKit
|
|
7
|
+
import VideoToolbox
|
|
8
|
+
|
|
9
|
+
@available(macOS 12.3, *)
|
|
10
|
+
final class LiveDeskScreenCaptureKitEncoder: NSObject, SCStreamOutput, SCStreamDelegate {
|
|
11
|
+
private let displayIndex: Int
|
|
12
|
+
private let width: Int
|
|
13
|
+
private let height: Int
|
|
14
|
+
private let fps: Int
|
|
15
|
+
private let queue = DispatchQueue(label: "dev.livedesk.screencapturekit.frames")
|
|
16
|
+
private let writeLock = NSLock()
|
|
17
|
+
private var stream: SCStream?
|
|
18
|
+
private var compressionSession: VTCompressionSession?
|
|
19
|
+
private var frameCount: Int = 0
|
|
20
|
+
private var encodedCount: Int = 0
|
|
21
|
+
|
|
22
|
+
init(displayIndex: Int, width: Int, height: Int, fps: Int) {
|
|
23
|
+
self.displayIndex = max(0, displayIndex)
|
|
24
|
+
self.width = max(2, width)
|
|
25
|
+
self.height = max(2, height)
|
|
26
|
+
self.fps = max(1, fps)
|
|
27
|
+
super.init()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func start() async throws {
|
|
31
|
+
try createCompressionSession()
|
|
32
|
+
|
|
33
|
+
let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
|
|
34
|
+
guard !shareableContent.displays.isEmpty else {
|
|
35
|
+
throw LiveDeskSCKError.noDisplays
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let display = shareableContent.displays[min(displayIndex, shareableContent.displays.count - 1)]
|
|
39
|
+
let filter = SCContentFilter(display: display, excludingWindows: [])
|
|
40
|
+
let configuration = SCStreamConfiguration()
|
|
41
|
+
configuration.width = width
|
|
42
|
+
configuration.height = height
|
|
43
|
+
configuration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
44
|
+
configuration.queueDepth = 3
|
|
45
|
+
configuration.showsCursor = false
|
|
46
|
+
configuration.pixelFormat = kCVPixelFormatType_32BGRA
|
|
47
|
+
|
|
48
|
+
let stream = SCStream(filter: filter, configuration: configuration, delegate: self)
|
|
49
|
+
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: queue)
|
|
50
|
+
self.stream = stream
|
|
51
|
+
try await stream.startCapture()
|
|
52
|
+
|
|
53
|
+
fputs("LiveDeskSCK started displayIndex=\(displayIndex) displayID=\(display.displayID) size=\(width)x\(height) fps=\(fps)\n", stderr)
|
|
54
|
+
RunLoop.main.run()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func createCompressionSession() throws {
|
|
58
|
+
var session: VTCompressionSession?
|
|
59
|
+
let status = VTCompressionSessionCreate(
|
|
60
|
+
allocator: kCFAllocatorDefault,
|
|
61
|
+
width: Int32(width),
|
|
62
|
+
height: Int32(height),
|
|
63
|
+
codecType: kCMVideoCodecType_H264,
|
|
64
|
+
encoderSpecification: nil,
|
|
65
|
+
imageBufferAttributes: nil,
|
|
66
|
+
compressedDataAllocator: nil,
|
|
67
|
+
outputCallback: Self.compressionCallback,
|
|
68
|
+
refcon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
|
|
69
|
+
compressionSessionOut: &session)
|
|
70
|
+
|
|
71
|
+
guard status == noErr, let session else {
|
|
72
|
+
throw LiveDeskSCKError.encoderCreate(status)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue)
|
|
76
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse)
|
|
77
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Baseline_AutoLevel)
|
|
78
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: NSNumber(value: fps))
|
|
79
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, value: NSNumber(value: 1))
|
|
80
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: fps))
|
|
81
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: NSNumber(value: max(2_500_000, width * height * fps / 6)))
|
|
82
|
+
VTCompressionSessionPrepareToEncodeFrames(session)
|
|
83
|
+
compressionSession = session
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static let compressionCallback: VTCompressionOutputCallback = { refCon, _, status, _, sampleBuffer in
|
|
87
|
+
guard status == noErr,
|
|
88
|
+
let refCon,
|
|
89
|
+
let sampleBuffer,
|
|
90
|
+
CMSampleBufferDataIsReady(sampleBuffer) else {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let encoder = Unmanaged<LiveDeskScreenCaptureKitEncoder>.fromOpaque(refCon).takeUnretainedValue()
|
|
95
|
+
encoder.writeEncodedSampleBuffer(sampleBuffer)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
|
99
|
+
guard type == .screen,
|
|
100
|
+
CMSampleBufferDataIsReady(sampleBuffer),
|
|
101
|
+
isCompleteFrame(sampleBuffer),
|
|
102
|
+
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer),
|
|
103
|
+
let session = compressionSession else {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
frameCount += 1
|
|
108
|
+
let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
|
109
|
+
let duration = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
110
|
+
let status = VTCompressionSessionEncodeFrame(
|
|
111
|
+
session,
|
|
112
|
+
imageBuffer: imageBuffer,
|
|
113
|
+
presentationTimeStamp: pts,
|
|
114
|
+
duration: duration,
|
|
115
|
+
frameProperties: nil,
|
|
116
|
+
sourceFrameRefcon: nil,
|
|
117
|
+
infoFlagsOut: nil)
|
|
118
|
+
|
|
119
|
+
if status != noErr && (frameCount == 1 || frameCount % 120 == 0) {
|
|
120
|
+
fputs("LiveDeskSCK encode status=\(status) frames=\(frameCount)\n", stderr)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func stream(_ stream: SCStream, didStopWithError error: Error) {
|
|
125
|
+
fputs("LiveDeskSCK stream stopped: \(error)\n", stderr)
|
|
126
|
+
exit(3)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func isCompleteFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
130
|
+
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
131
|
+
let first = attachments.firstObject as? NSDictionary,
|
|
132
|
+
let rawStatus = first[SCStreamFrameInfo.status] as? Int else {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
return rawStatus == SCFrameStatus.complete.rawValue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private func writeEncodedSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
|
139
|
+
guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var output = Data()
|
|
144
|
+
if isKeyFrame(sampleBuffer), let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) {
|
|
145
|
+
appendParameterSet(formatDescription, index: 0, to: &output)
|
|
146
|
+
appendParameterSet(formatDescription, index: 1, to: &output)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var lengthAtOffset = 0
|
|
150
|
+
var totalLength = 0
|
|
151
|
+
var dataPointer: UnsafeMutablePointer<Int8>?
|
|
152
|
+
let status = CMBlockBufferGetDataPointer(
|
|
153
|
+
blockBuffer,
|
|
154
|
+
atOffset: 0,
|
|
155
|
+
lengthAtOffsetOut: &lengthAtOffset,
|
|
156
|
+
totalLengthOut: &totalLength,
|
|
157
|
+
dataPointerOut: &dataPointer)
|
|
158
|
+
|
|
159
|
+
guard status == noErr, let dataPointer, totalLength > 4 else {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var offset = 0
|
|
164
|
+
while offset + 4 <= totalLength {
|
|
165
|
+
var nalLength: UInt32 = 0
|
|
166
|
+
memcpy(&nalLength, dataPointer.advanced(by: offset), 4)
|
|
167
|
+
nalLength = CFSwapInt32BigToHost(nalLength)
|
|
168
|
+
let length = Int(nalLength)
|
|
169
|
+
if length <= 0 || offset + 4 + length > totalLength {
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
appendStartCode(to: &output)
|
|
174
|
+
output.append(UnsafeRawPointer(dataPointer.advanced(by: offset + 4)).assumingMemoryBound(to: UInt8.self), count: length)
|
|
175
|
+
offset += 4 + length
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
guard !output.isEmpty else {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
writeLock.lock()
|
|
183
|
+
FileHandle.standardOutput.write(output)
|
|
184
|
+
writeLock.unlock()
|
|
185
|
+
|
|
186
|
+
encodedCount += 1
|
|
187
|
+
if encodedCount == 1 || encodedCount % max(1, fps * 5) == 0 {
|
|
188
|
+
fputs("LiveDeskSCK encoded frames=\(encodedCount) bytes=\(output.count)\n", stderr)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func isKeyFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
193
|
+
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
194
|
+
let first = attachments.firstObject as? NSDictionary else {
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
return !(first[kCMSampleAttachmentKey_NotSync] as? Bool ?? false)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private func appendParameterSet(_ formatDescription: CMFormatDescription, index: Int, to output: inout Data) {
|
|
201
|
+
var pointer: UnsafePointer<UInt8>?
|
|
202
|
+
var size = 0
|
|
203
|
+
var count = 0
|
|
204
|
+
var nalHeaderLength: Int32 = 0
|
|
205
|
+
let status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
|
206
|
+
formatDescription,
|
|
207
|
+
parameterSetIndex: index,
|
|
208
|
+
parameterSetPointerOut: &pointer,
|
|
209
|
+
parameterSetSizeOut: &size,
|
|
210
|
+
parameterSetCountOut: &count,
|
|
211
|
+
nalUnitHeaderLengthOut: &nalHeaderLength)
|
|
212
|
+
|
|
213
|
+
guard status == noErr, let pointer, size > 0 else {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
appendStartCode(to: &output)
|
|
218
|
+
output.append(pointer, count: size)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private func appendStartCode(to output: inout Data) {
|
|
222
|
+
output.append(contentsOf: [0, 0, 0, 1])
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
enum LiveDeskSCKError: Error {
|
|
227
|
+
case noDisplays
|
|
228
|
+
case encoderCreate(OSStatus)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func intArg(_ name: String, default defaultValue: Int) -> Int {
|
|
232
|
+
let args = CommandLine.arguments
|
|
233
|
+
guard let index = args.firstIndex(of: name), index + 1 < args.count else {
|
|
234
|
+
return defaultValue
|
|
235
|
+
}
|
|
236
|
+
return Int(args[index + 1]) ?? defaultValue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@main
|
|
240
|
+
struct LiveDeskSCKMain {
|
|
241
|
+
static func main() async {
|
|
242
|
+
guard #available(macOS 12.3, *) else {
|
|
243
|
+
fputs("LiveDeskSCK requires macOS 12.3 or later.\n", stderr)
|
|
244
|
+
exit(2)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let displayIndex = intArg("--display-index", default: 0)
|
|
248
|
+
let width = intArg("--width", default: 960)
|
|
249
|
+
let height = intArg("--height", default: 540)
|
|
250
|
+
let fps = intArg("--fps", default: 30)
|
|
251
|
+
|
|
252
|
+
do {
|
|
253
|
+
let encoder = LiveDeskScreenCaptureKitEncoder(displayIndex: displayIndex, width: width, height: height, fps: fps)
|
|
254
|
+
try await encoder.start()
|
|
255
|
+
} catch {
|
|
256
|
+
fputs("LiveDeskSCK failed: \(error)\n", stderr)
|
|
257
|
+
exit(1)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import CoreFoundation
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
import CoreMedia
|
|
4
|
+
import CoreVideo
|
|
5
|
+
import Foundation
|
|
6
|
+
import ScreenCaptureKit
|
|
7
|
+
import VideoToolbox
|
|
8
|
+
|
|
9
|
+
@available(macOS 12.3, *)
|
|
10
|
+
final class LiveDeskScreenCaptureKitEncoder: NSObject, SCStreamOutput, SCStreamDelegate {
|
|
11
|
+
private let displayIndex: Int
|
|
12
|
+
private let width: Int
|
|
13
|
+
private let height: Int
|
|
14
|
+
private let fps: Int
|
|
15
|
+
private let queue = DispatchQueue(label: "dev.livedesk.screencapturekit.frames")
|
|
16
|
+
private let writeLock = NSLock()
|
|
17
|
+
private var stream: SCStream?
|
|
18
|
+
private var compressionSession: VTCompressionSession?
|
|
19
|
+
private var frameCount: Int = 0
|
|
20
|
+
private var encodedCount: Int = 0
|
|
21
|
+
|
|
22
|
+
init(displayIndex: Int, width: Int, height: Int, fps: Int) {
|
|
23
|
+
self.displayIndex = max(0, displayIndex)
|
|
24
|
+
self.width = max(2, width)
|
|
25
|
+
self.height = max(2, height)
|
|
26
|
+
self.fps = max(1, fps)
|
|
27
|
+
super.init()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func start() async throws {
|
|
31
|
+
try createCompressionSession()
|
|
32
|
+
|
|
33
|
+
let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
|
|
34
|
+
guard !shareableContent.displays.isEmpty else {
|
|
35
|
+
throw LiveDeskSCKError.noDisplays
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let display = shareableContent.displays[min(displayIndex, shareableContent.displays.count - 1)]
|
|
39
|
+
let filter = SCContentFilter(display: display, excludingWindows: [])
|
|
40
|
+
let configuration = SCStreamConfiguration()
|
|
41
|
+
configuration.width = width
|
|
42
|
+
configuration.height = height
|
|
43
|
+
configuration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
44
|
+
configuration.queueDepth = 3
|
|
45
|
+
configuration.showsCursor = false
|
|
46
|
+
configuration.pixelFormat = kCVPixelFormatType_32BGRA
|
|
47
|
+
|
|
48
|
+
let stream = SCStream(filter: filter, configuration: configuration, delegate: self)
|
|
49
|
+
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: queue)
|
|
50
|
+
self.stream = stream
|
|
51
|
+
try await stream.startCapture()
|
|
52
|
+
|
|
53
|
+
fputs("LiveDeskSCK started displayIndex=\(displayIndex) displayID=\(display.displayID) size=\(width)x\(height) fps=\(fps)\n", stderr)
|
|
54
|
+
RunLoop.main.run()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func createCompressionSession() throws {
|
|
58
|
+
var session: VTCompressionSession?
|
|
59
|
+
let status = VTCompressionSessionCreate(
|
|
60
|
+
allocator: kCFAllocatorDefault,
|
|
61
|
+
width: Int32(width),
|
|
62
|
+
height: Int32(height),
|
|
63
|
+
codecType: kCMVideoCodecType_H264,
|
|
64
|
+
encoderSpecification: nil,
|
|
65
|
+
imageBufferAttributes: nil,
|
|
66
|
+
compressedDataAllocator: nil,
|
|
67
|
+
outputCallback: Self.compressionCallback,
|
|
68
|
+
refcon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
|
|
69
|
+
compressionSessionOut: &session)
|
|
70
|
+
|
|
71
|
+
guard status == noErr, let session else {
|
|
72
|
+
throw LiveDeskSCKError.encoderCreate(status)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue)
|
|
76
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse)
|
|
77
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Baseline_AutoLevel)
|
|
78
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameInterval, value: NSNumber(value: fps))
|
|
79
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, value: NSNumber(value: 1))
|
|
80
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: fps))
|
|
81
|
+
VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AverageBitRate, value: NSNumber(value: max(2_500_000, width * height * fps / 6)))
|
|
82
|
+
VTCompressionSessionPrepareToEncodeFrames(session)
|
|
83
|
+
compressionSession = session
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static let compressionCallback: VTCompressionOutputCallback = { refCon, _, status, _, sampleBuffer in
|
|
87
|
+
guard status == noErr,
|
|
88
|
+
let refCon,
|
|
89
|
+
let sampleBuffer,
|
|
90
|
+
CMSampleBufferDataIsReady(sampleBuffer) else {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let encoder = Unmanaged<LiveDeskScreenCaptureKitEncoder>.fromOpaque(refCon).takeUnretainedValue()
|
|
95
|
+
encoder.writeEncodedSampleBuffer(sampleBuffer)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
|
99
|
+
guard type == .screen,
|
|
100
|
+
CMSampleBufferDataIsReady(sampleBuffer),
|
|
101
|
+
isCompleteFrame(sampleBuffer),
|
|
102
|
+
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer),
|
|
103
|
+
let session = compressionSession else {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
frameCount += 1
|
|
108
|
+
let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
|
109
|
+
let duration = CMTime(value: 1, timescale: CMTimeScale(fps))
|
|
110
|
+
let status = VTCompressionSessionEncodeFrame(
|
|
111
|
+
session,
|
|
112
|
+
imageBuffer: imageBuffer,
|
|
113
|
+
presentationTimeStamp: pts,
|
|
114
|
+
duration: duration,
|
|
115
|
+
frameProperties: nil,
|
|
116
|
+
sourceFrameRefcon: nil,
|
|
117
|
+
infoFlagsOut: nil)
|
|
118
|
+
|
|
119
|
+
if status != noErr && (frameCount == 1 || frameCount % 120 == 0) {
|
|
120
|
+
fputs("LiveDeskSCK encode status=\(status) frames=\(frameCount)\n", stderr)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func stream(_ stream: SCStream, didStopWithError error: Error) {
|
|
125
|
+
fputs("LiveDeskSCK stream stopped: \(error)\n", stderr)
|
|
126
|
+
exit(3)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func isCompleteFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
130
|
+
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
131
|
+
let first = attachments.firstObject as? NSDictionary,
|
|
132
|
+
let rawStatus = first[SCStreamFrameInfo.status] as? Int else {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
return rawStatus == SCFrameStatus.complete.rawValue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private func writeEncodedSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
|
139
|
+
guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var output = Data()
|
|
144
|
+
if isKeyFrame(sampleBuffer), let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) {
|
|
145
|
+
appendParameterSet(formatDescription, index: 0, to: &output)
|
|
146
|
+
appendParameterSet(formatDescription, index: 1, to: &output)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var lengthAtOffset = 0
|
|
150
|
+
var totalLength = 0
|
|
151
|
+
var dataPointer: UnsafeMutablePointer<Int8>?
|
|
152
|
+
let status = CMBlockBufferGetDataPointer(
|
|
153
|
+
blockBuffer,
|
|
154
|
+
atOffset: 0,
|
|
155
|
+
lengthAtOffsetOut: &lengthAtOffset,
|
|
156
|
+
totalLengthOut: &totalLength,
|
|
157
|
+
dataPointerOut: &dataPointer)
|
|
158
|
+
|
|
159
|
+
guard status == noErr, let dataPointer, totalLength > 4 else {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var offset = 0
|
|
164
|
+
while offset + 4 <= totalLength {
|
|
165
|
+
var nalLength: UInt32 = 0
|
|
166
|
+
memcpy(&nalLength, dataPointer.advanced(by: offset), 4)
|
|
167
|
+
nalLength = CFSwapInt32BigToHost(nalLength)
|
|
168
|
+
let length = Int(nalLength)
|
|
169
|
+
if length <= 0 || offset + 4 + length > totalLength {
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
appendStartCode(to: &output)
|
|
174
|
+
output.append(UnsafeRawPointer(dataPointer.advanced(by: offset + 4)).assumingMemoryBound(to: UInt8.self), count: length)
|
|
175
|
+
offset += 4 + length
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
guard !output.isEmpty else {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
writeLock.lock()
|
|
183
|
+
FileHandle.standardOutput.write(output)
|
|
184
|
+
writeLock.unlock()
|
|
185
|
+
|
|
186
|
+
encodedCount += 1
|
|
187
|
+
if encodedCount == 1 || encodedCount % max(1, fps * 5) == 0 {
|
|
188
|
+
fputs("LiveDeskSCK encoded frames=\(encodedCount) bytes=\(output.count)\n", stderr)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func isKeyFrame(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
|
193
|
+
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as NSArray?,
|
|
194
|
+
let first = attachments.firstObject as? NSDictionary else {
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
return !(first[kCMSampleAttachmentKey_NotSync] as? Bool ?? false)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private func appendParameterSet(_ formatDescription: CMFormatDescription, index: Int, to output: inout Data) {
|
|
201
|
+
var pointer: UnsafePointer<UInt8>?
|
|
202
|
+
var size = 0
|
|
203
|
+
var count = 0
|
|
204
|
+
var nalHeaderLength: Int32 = 0
|
|
205
|
+
let status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
|
206
|
+
formatDescription,
|
|
207
|
+
parameterSetIndex: index,
|
|
208
|
+
parameterSetPointerOut: &pointer,
|
|
209
|
+
parameterSetSizeOut: &size,
|
|
210
|
+
parameterSetCountOut: &count,
|
|
211
|
+
nalUnitHeaderLengthOut: &nalHeaderLength)
|
|
212
|
+
|
|
213
|
+
guard status == noErr, let pointer, size > 0 else {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
appendStartCode(to: &output)
|
|
218
|
+
output.append(pointer, count: size)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private func appendStartCode(to output: inout Data) {
|
|
222
|
+
output.append(contentsOf: [0, 0, 0, 1])
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
enum LiveDeskSCKError: Error {
|
|
227
|
+
case noDisplays
|
|
228
|
+
case encoderCreate(OSStatus)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func intArg(_ name: String, default defaultValue: Int) -> Int {
|
|
232
|
+
let args = CommandLine.arguments
|
|
233
|
+
guard let index = args.firstIndex(of: name), index + 1 < args.count else {
|
|
234
|
+
return defaultValue
|
|
235
|
+
}
|
|
236
|
+
return Int(args[index + 1]) ?? defaultValue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@main
|
|
240
|
+
struct LiveDeskSCKMain {
|
|
241
|
+
static func main() async {
|
|
242
|
+
guard #available(macOS 12.3, *) else {
|
|
243
|
+
fputs("LiveDeskSCK requires macOS 12.3 or later.\n", stderr)
|
|
244
|
+
exit(2)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let displayIndex = intArg("--display-index", default: 0)
|
|
248
|
+
let width = intArg("--width", default: 960)
|
|
249
|
+
let height = intArg("--height", default: 540)
|
|
250
|
+
let fps = intArg("--fps", default: 30)
|
|
251
|
+
|
|
252
|
+
do {
|
|
253
|
+
let encoder = LiveDeskScreenCaptureKitEncoder(displayIndex: displayIndex, width: width, height: height, fps: fps)
|
|
254
|
+
try await encoder.start()
|
|
255
|
+
} catch {
|
|
256
|
+
fputs("LiveDeskSCK failed: \(error)\n", stderr)
|
|
257
|
+
exit(1)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|