waterplane 0.1.16 → 0.1.18
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/CHANGELOG.md +28 -0
- package/NOTICES.md +18 -7
- package/README.md +43 -0
- package/SECURITY.md +196 -12
- package/node_modules/@waterplane/shared/dist/desktopfiles.d.ts +24 -2
- package/node_modules/@waterplane/shared/dist/desktopfiles.js +73 -3
- package/node_modules/@waterplane/shared/dist/desktopfiles.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/hosts.d.ts +31 -0
- package/node_modules/@waterplane/shared/dist/hosts.js +46 -1
- package/node_modules/@waterplane/shared/dist/hosts.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/hosturl.d.ts +55 -0
- package/node_modules/@waterplane/shared/dist/hosturl.js +85 -0
- package/node_modules/@waterplane/shared/dist/hosturl.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/index.d.ts +4 -0
- package/node_modules/@waterplane/shared/dist/index.js +4 -0
- package/node_modules/@waterplane/shared/dist/index.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/installrow.d.ts +6 -1
- package/node_modules/@waterplane/shared/dist/installrow.js +20 -1
- package/node_modules/@waterplane/shared/dist/installrow.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/lanroute.d.ts +5 -0
- package/node_modules/@waterplane/shared/dist/lanroute.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/pairing.d.ts +96 -2
- package/node_modules/@waterplane/shared/dist/pairing.js +71 -3
- package/node_modules/@waterplane/shared/dist/pairing.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/pake.d.ts +131 -0
- package/node_modules/@waterplane/shared/dist/pake.js +272 -0
- package/node_modules/@waterplane/shared/dist/pake.js.map +1 -0
- package/node_modules/@waterplane/shared/dist/peer.d.ts +63 -0
- package/node_modules/@waterplane/shared/dist/peer.js +41 -0
- package/node_modules/@waterplane/shared/dist/peer.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/plugin/api.d.ts +15 -0
- package/node_modules/@waterplane/shared/dist/plugin/version.d.ts +5 -1
- package/node_modules/@waterplane/shared/dist/plugin/version.js +5 -1
- package/node_modules/@waterplane/shared/dist/plugin/version.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.d.ts +7 -2
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js +6 -3
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/waiting.d.ts +64 -0
- package/node_modules/@waterplane/shared/dist/waiting.js +86 -0
- package/node_modules/@waterplane/shared/dist/waiting.js.map +1 -0
- package/node_modules/@waterplane/shared/dist/wire/messages/hostcore.d.ts +3 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.d.ts +2 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.js +2 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/wire/repo.d.ts +25 -1
- package/node_modules/@waterplane/shared/dist/wire/repo.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/wire/sysstat.d.ts +7 -0
- package/node_modules/@waterplane/shared/dist/wire/telemetry.d.ts +3 -0
- package/node_modules/@waterplane/shared/package.json +3 -0
- package/npm-shrinkwrap.json +30 -2
- package/package.json +2 -1
- package/packages/extensions/desktop/helper/Capture.swift +245 -0
- package/packages/extensions/desktop/helper/H264.swift +226 -0
- package/packages/extensions/desktop/helper/Info.plist +20 -0
- package/packages/extensions/desktop/helper/Input.swift +195 -0
- package/packages/extensions/desktop/helper/Jpeg.swift +81 -0
- package/packages/extensions/desktop/helper/PROTOCOL.md +366 -0
- package/packages/extensions/desktop/helper/Params.swift +133 -0
- package/packages/extensions/desktop/helper/Probe.swift +119 -0
- package/packages/extensions/desktop/helper/Protocol.swift +224 -0
- package/packages/extensions/desktop/helper/Request.swift +51 -0
- package/packages/extensions/desktop/helper/Responsibility.swift +133 -0
- package/packages/extensions/desktop/helper/Server.swift +334 -0
- package/packages/extensions/desktop/helper/Synthetic.swift +281 -0
- package/packages/extensions/desktop/helper/build.sh +53 -0
- package/packages/extensions/desktop/helper/main.swift +79 -0
- package/packages/extensions/desktop/helper/selftest-driver +626 -0
- package/packages/extensions/desktop/helper/selftest.sh +16 -0
- package/packages/extensions/desktop/helper-win/Capture.cs +299 -0
- package/packages/extensions/desktop/helper-win/H264.cs +340 -0
- package/packages/extensions/desktop/helper-win/Input.cs +257 -0
- package/packages/extensions/desktop/helper-win/Jpeg.cs +75 -0
- package/packages/extensions/desktop/helper-win/Main.cs +118 -0
- package/packages/extensions/desktop/helper-win/Native.cs +276 -0
- package/packages/extensions/desktop/helper-win/Params.cs +138 -0
- package/packages/extensions/desktop/helper-win/Protocol.cs +206 -0
- package/packages/extensions/desktop/helper-win/Server.cs +304 -0
- package/packages/extensions/desktop/helper-win/Synthetic.cs +88 -0
- package/packages/extensions/desktop/helper-win/build.sh +57 -0
- package/packages/extensions/desktop/helper-win/selftest.sh +36 -0
- package/packages/extensions/desktop/waterplane-plugin.json +24 -0
- package/packages/host/dist/alwayson.js +1 -1
- package/packages/host/dist/alwayson.js.map +1 -1
- package/packages/host/dist/attentionstore.d.ts +4 -0
- package/packages/host/dist/attentionstore.js +17 -0
- package/packages/host/dist/attentionstore.js.map +1 -1
- package/packages/host/dist/auth.d.ts +28 -0
- package/packages/host/dist/auth.js +65 -2
- package/packages/host/dist/auth.js.map +1 -1
- package/packages/host/dist/cli/pair.d.ts +4 -0
- package/packages/host/dist/cli/pair.js +18 -5
- package/packages/host/dist/cli/pair.js.map +1 -1
- package/packages/host/dist/der.d.ts +19 -0
- package/packages/host/dist/der.js +51 -0
- package/packages/host/dist/der.js.map +1 -0
- package/packages/host/dist/doctor.d.ts +2 -0
- package/packages/host/dist/doctor.js +10 -2
- package/packages/host/dist/doctor.js.map +1 -1
- package/packages/host/dist/extensions/desktop/host/auto.d.mts +71 -0
- package/packages/host/dist/extensions/desktop/host/auto.mjs +114 -0
- package/packages/host/dist/extensions/desktop/host/auto.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/desktop.d.mts +51 -0
- package/packages/host/dist/extensions/desktop/host/desktop.mjs +113 -0
- package/packages/host/dist/extensions/desktop/host/desktop.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.d.mts +71 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.mjs +160 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/index.d.mts +4 -0
- package/packages/host/dist/extensions/desktop/host/index.mjs +88 -0
- package/packages/host/dist/extensions/desktop/host/index.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/knobs.d.mts +52 -0
- package/packages/host/dist/extensions/desktop/host/knobs.mjs +81 -0
- package/packages/host/dist/extensions/desktop/host/knobs.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/probe.d.mts +57 -0
- package/packages/host/dist/extensions/desktop/host/probe.mjs +119 -0
- package/packages/host/dist/extensions/desktop/host/probe.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/records.d.mts +40 -0
- package/packages/host/dist/extensions/desktop/host/records.mjs +101 -0
- package/packages/host/dist/extensions/desktop/host/records.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/session.d.mts +86 -0
- package/packages/host/dist/extensions/desktop/host/session.mjs +318 -0
- package/packages/host/dist/extensions/desktop/host/session.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/setup.d.mts +28 -0
- package/packages/host/dist/extensions/desktop/host/setup.mjs +217 -0
- package/packages/host/dist/extensions/desktop/host/setup.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/status.d.mts +27 -0
- package/packages/host/dist/extensions/desktop/host/status.mjs +62 -0
- package/packages/host/dist/extensions/desktop/host/status.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/shared/wire.d.mts +206 -0
- package/packages/host/dist/extensions/desktop/shared/wire.mjs +174 -0
- package/packages/host/dist/extensions/desktop/shared/wire.mjs.map +1 -0
- package/packages/host/dist/git.d.ts +23 -6
- package/packages/host/dist/git.js +54 -26
- package/packages/host/dist/git.js.map +1 -1
- package/packages/host/dist/gitfetchfail.d.ts +24 -0
- package/packages/host/dist/gitfetchfail.js +62 -0
- package/packages/host/dist/gitfetchfail.js.map +1 -0
- package/packages/host/dist/gitlog.d.ts +4 -0
- package/packages/host/dist/gitlog.js +8 -3
- package/packages/host/dist/gitlog.js.map +1 -1
- package/packages/host/dist/gitnotify.d.ts +7 -1
- package/packages/host/dist/gitnotify.js +61 -1
- package/packages/host/dist/gitnotify.js.map +1 -1
- package/packages/host/dist/host.js +5 -1
- package/packages/host/dist/host.js.map +1 -1
- package/packages/host/dist/hoststore.js +4 -4
- package/packages/host/dist/hoststore.js.map +1 -1
- package/packages/host/dist/lantls.d.ts +145 -0
- package/packages/host/dist/lantls.js +337 -0
- package/packages/host/dist/lantls.js.map +1 -0
- package/packages/host/dist/main.js +7 -2
- package/packages/host/dist/main.js.map +1 -1
- package/packages/host/dist/net.d.ts +7 -0
- package/packages/host/dist/net.js +9 -0
- package/packages/host/dist/net.js.map +1 -1
- package/packages/host/dist/origin.d.ts +4 -0
- package/packages/host/dist/origin.js +6 -0
- package/packages/host/dist/origin.js.map +1 -1
- package/packages/host/dist/pair.d.ts +54 -5
- package/packages/host/dist/pair.js +139 -34
- package/packages/host/dist/pair.js.map +1 -1
- package/packages/host/dist/pairpake.d.ts +58 -0
- package/packages/host/dist/pairpake.js +208 -0
- package/packages/host/dist/pairpake.js.map +1 -0
- package/packages/host/dist/passive.d.ts +2 -0
- package/packages/host/dist/passive.js +4 -0
- package/packages/host/dist/passive.js.map +1 -1
- package/packages/host/dist/peers/client.d.ts +3 -1
- package/packages/host/dist/peers/client.js +4 -0
- package/packages/host/dist/peers/client.js.map +1 -1
- package/packages/host/dist/peers/routes.d.ts +6 -1
- package/packages/host/dist/peers/routes.js +39 -0
- package/packages/host/dist/peers/routes.js.map +1 -1
- package/packages/host/dist/plugins/pluginapi.js +13 -0
- package/packages/host/dist/plugins/pluginapi.js.map +1 -1
- package/packages/host/dist/plugins/pluginregistry.d.ts +6 -0
- package/packages/host/dist/plugins/pluginregistry.js.map +1 -1
- package/packages/host/dist/prepushcheck.d.ts +34 -0
- package/packages/host/dist/prepushcheck.js +157 -0
- package/packages/host/dist/prepushcheck.js.map +1 -0
- package/packages/host/dist/prs.js +2 -1
- package/packages/host/dist/prs.js.map +1 -1
- package/packages/host/dist/routes/apigitcheck.d.ts +2 -0
- package/packages/host/dist/routes/apigitcheck.js +29 -0
- package/packages/host/dist/routes/apigitcheck.js.map +1 -0
- package/packages/host/dist/routes/apihost.js +2 -0
- package/packages/host/dist/routes/apihost.js.map +1 -1
- package/packages/host/dist/routes/apiio.js +2 -1
- package/packages/host/dist/routes/apiio.js.map +1 -1
- package/packages/host/dist/routes/apirepofiles.js +2 -1
- package/packages/host/dist/routes/apirepofiles.js.map +1 -1
- package/packages/host/dist/routes/apisystem.js +21 -3
- package/packages/host/dist/routes/apisystem.js.map +1 -1
- package/packages/host/dist/server/http.js +31 -5
- package/packages/host/dist/server/http.js.map +1 -1
- package/packages/host/dist/server/plugins.d.ts +13 -0
- package/packages/host/dist/server/plugins.js +26 -0
- package/packages/host/dist/server/plugins.js.map +1 -1
- package/packages/host/dist/server/wsops.js +3 -1
- package/packages/host/dist/server/wsops.js.map +1 -1
- package/packages/host/dist/sysstat.js +10 -1
- package/packages/host/dist/sysstat.js.map +1 -1
- package/packages/host/dist/updatekey.d.ts +22 -0
- package/packages/host/dist/updatekey.js +140 -0
- package/packages/host/dist/updatekey.js.map +1 -0
- package/packages/host/dist/webbundle.d.ts +15 -6
- package/packages/host/dist/webbundle.js +44 -7
- package/packages/host/dist/webbundle.js.map +1 -1
- package/packages/host/dist/wincpu.d.ts +13 -0
- package/packages/host/dist/wincpu.js +101 -0
- package/packages/host/dist/wincpu.js.map +1 -0
- package/packages/host/package.json +2 -1
- package/packages/web/dist/assets/demo-BLBaz4G_.js +26 -0
- package/packages/web/dist/assets/icons-D8RlmSLR.js +1 -0
- package/packages/web/dist/assets/index-BrvcQFBj.js +170 -0
- package/packages/web/dist/assets/index-Dx3VHnr3.css +1 -0
- package/packages/web/dist/assets/noble-DV-r0OkE.js +1 -0
- package/packages/web/dist/assets/{react-CoSyT_xY.js → react-CjmvHigY.js} +2 -2
- package/packages/web/dist/assets/rolldown-runtime-4tVxZq34.js +1 -0
- package/packages/web/dist/assets/shared-BsWIJLE-.js +51 -0
- package/packages/web/dist/assets/sheets-C7CXUU6R.js +250 -0
- package/packages/web/dist/assets/xterm-BHsKrDzh.js +16 -0
- package/packages/web/dist/build-id.txt +1 -1
- package/packages/web/dist/buildstamp.json +1 -1
- package/packages/web/dist/index.html +9 -9
- package/packages/web/dist/sw.js +2 -2
- package/release.json +7 -3
- package/packages/web/dist/assets/demo-DkCtg-SJ.js +0 -26
- package/packages/web/dist/assets/icons-B48F-4nA.js +0 -1
- package/packages/web/dist/assets/index-CpdyS6Xe.css +0 -1
- package/packages/web/dist/assets/index-EYc28CfH.js +0 -170
- package/packages/web/dist/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
- package/packages/web/dist/assets/shared-DjRRLGfw.js +0 -50
- package/packages/web/dist/assets/sheets-ByWNPa9p.js +0 -248
- package/packages/web/dist/assets/xterm-C8zLTNGk.js +0 -16
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// Server: the `serve` pipeline. One serial queue owns all state; the source
|
|
2
|
+
// delivers frames on it, commands are applied on it, and only the H.264
|
|
3
|
+
// encoder's output callback (VideoToolbox's thread) writes from elsewhere,
|
|
4
|
+
// through Output's lock.
|
|
5
|
+
|
|
6
|
+
import CoreGraphics
|
|
7
|
+
import CoreVideo
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
final class Server {
|
|
11
|
+
let q = DispatchQueue(label: "wp-desktop.server", qos: .userInteractive)
|
|
12
|
+
private let dryInput: Bool
|
|
13
|
+
private let forceSynthetic: Bool
|
|
14
|
+
private let input: InputInjector
|
|
15
|
+
private var params: Params?
|
|
16
|
+
private var source: FrameSource?
|
|
17
|
+
private var encoder: FrameEncoder?
|
|
18
|
+
private var geo: Geometry?
|
|
19
|
+
private var state = "stopped"
|
|
20
|
+
private var inConfigure = false
|
|
21
|
+
private var lastFrame: CVPixelBuffer?
|
|
22
|
+
private var lastFrameAt: UInt64 = 0
|
|
23
|
+
private var lastPts: UInt64 = 0
|
|
24
|
+
private var forceKey = false
|
|
25
|
+
/// The region the NEXT submitted frame belongs to (see `Region`): bumped by every config that
|
|
26
|
+
/// moves the crop or the source, and by every source restart.
|
|
27
|
+
private var regionSeq = 0
|
|
28
|
+
private var region = Region(seq: 0, crop: Crop())
|
|
29
|
+
/// Held only while a source is running: App Nap off and no idle system sleep. A helper that sits
|
|
30
|
+
/// in "error" or "stopped" (Screen Recording not granted, the view left open on that sentence)
|
|
31
|
+
/// must not keep the Mac awake.
|
|
32
|
+
private var activity: NSObjectProtocol?
|
|
33
|
+
private var idle = false
|
|
34
|
+
private var axCache: (at: UInt64, ok: Bool) = (0, false)
|
|
35
|
+
private var axToldAt: UInt64 = 0
|
|
36
|
+
private var statsTimer: DispatchSourceTimer?
|
|
37
|
+
private var watchdog: DispatchSourceTimer?
|
|
38
|
+
private let parentAtStart = getppid()
|
|
39
|
+
|
|
40
|
+
// Written from the encoder's thread.
|
|
41
|
+
private let statsLock = NSLock()
|
|
42
|
+
private var frames = 0, keys = 0, bytes = 0, captured = 0, dropped = 0
|
|
43
|
+
private var encSum: UInt64 = 0, encMax: UInt64 = 0
|
|
44
|
+
private var statsSince = nowMicros()
|
|
45
|
+
private var avc: String?
|
|
46
|
+
|
|
47
|
+
init(dryInput: Bool, forceSynthetic: Bool) {
|
|
48
|
+
self.dryInput = dryInput
|
|
49
|
+
self.forceSynthetic = forceSynthetic
|
|
50
|
+
input = InputInjector(dry: dryInput)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func run() {
|
|
54
|
+
StdinReader(
|
|
55
|
+
onCommand: { [self] c in q.async { self.apply(c) } },
|
|
56
|
+
onEOF: { [self] in
|
|
57
|
+
log("stdin closed, exiting")
|
|
58
|
+
// Never leave a capture running: stop on the queue, and exit regardless after 0.5 s.
|
|
59
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { exit(0) }
|
|
60
|
+
q.async {
|
|
61
|
+
self.source?.stop()
|
|
62
|
+
self.encoder?.invalidate()
|
|
63
|
+
exit(0)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
).start()
|
|
67
|
+
startTimers()
|
|
68
|
+
log("serve: ready (\(dryInput ? "dry input, " : "")\(forceSynthetic ? "synthetic source" : "screen source"))")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// MARK: commands
|
|
72
|
+
|
|
73
|
+
private func apply(_ c: Command) {
|
|
74
|
+
switch c {
|
|
75
|
+
case .config(let o): configure(o)
|
|
76
|
+
case .keyframe: requestKey()
|
|
77
|
+
case .bitrate(let k):
|
|
78
|
+
guard var p = params else { return }
|
|
79
|
+
p.bitrateKbps = Params.clampBitrate(k)
|
|
80
|
+
params = p
|
|
81
|
+
encoder?.setBitrate(p.bitrateKbps)
|
|
82
|
+
emitStatus()
|
|
83
|
+
case .input(let ev): inject(ev)
|
|
84
|
+
case .stop: stopAll(reason: nil)
|
|
85
|
+
case .probe: Output.shared.json(probeObject())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private func configure(_ o: [String: Any]) {
|
|
90
|
+
var np = Params.parse(o, warn: { log($0) })
|
|
91
|
+
if forceSynthetic, np.source.kind != "synthetic" { np.source = .synthetic(idle: false) }
|
|
92
|
+
let old = params
|
|
93
|
+
params = np
|
|
94
|
+
inConfigure = true
|
|
95
|
+
defer { inConfigure = false }
|
|
96
|
+
let restart: Bool = {
|
|
97
|
+
guard let old, source != nil, state != "error" else { return true }
|
|
98
|
+
if !old.source.sameKind(np.source) || old.codec != np.codec { return true }
|
|
99
|
+
if case .display(let a) = old.source, case .display(let b) = np.source, a != b { return true }
|
|
100
|
+
if case .synthetic(let a) = old.source, case .synthetic(let b) = np.source, a != b { return true }
|
|
101
|
+
return false
|
|
102
|
+
}()
|
|
103
|
+
if restart || old?.crop != np.crop || old?.source != np.source { regionSeq += 1 }
|
|
104
|
+
region = Region(seq: regionSeq, crop: np.crop)
|
|
105
|
+
if restart {
|
|
106
|
+
teardown()
|
|
107
|
+
if np.source.kind != "synthetic" && !screenRecordingGranted() {
|
|
108
|
+
fail(screenRecordingSentence)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
state = "starting"
|
|
112
|
+
let cb = SourceCallbacks(
|
|
113
|
+
onGeometry: { [weak self] g in self?.geometryChanged(g) },
|
|
114
|
+
onFrame: { [weak self] pb, pts in self?.frame(pb, pts) },
|
|
115
|
+
onIdle: { [weak self] in self?.idle = true },
|
|
116
|
+
onError: { [weak self] s in self?.fail(s) })
|
|
117
|
+
let s: FrameSource =
|
|
118
|
+
np.source.kind == "synthetic"
|
|
119
|
+
? SyntheticSource(queue: q, params: np, callbacks: cb)
|
|
120
|
+
: ScreenSource(queue: q, params: np, callbacks: cb)
|
|
121
|
+
source = s
|
|
122
|
+
s.start()
|
|
123
|
+
if state == "error" { return } // fail() already said why
|
|
124
|
+
holdActivity()
|
|
125
|
+
} else {
|
|
126
|
+
// A new region (crop, or another window) must start on a key OF THAT REGION: the phone
|
|
127
|
+
// treats the next key after a crop change as the new picture. Forget the old frame so a
|
|
128
|
+
// keyframe request cannot re-encode the previous region, and key the next captured one.
|
|
129
|
+
if old?.crop != np.crop || old?.source != np.source {
|
|
130
|
+
lastFrame = nil
|
|
131
|
+
forceKey = true
|
|
132
|
+
}
|
|
133
|
+
source?.update(np)
|
|
134
|
+
encoder?.update(np)
|
|
135
|
+
}
|
|
136
|
+
emitStatus() // "running" (synthetic knows its size at once) or "starting" (SCK resolving)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func holdActivity() {
|
|
140
|
+
guard activity == nil else { return }
|
|
141
|
+
// A background CLI gets App Nap timer coalescing: a 66 ms timer fired at ~180 ms.
|
|
142
|
+
activity = ProcessInfo.processInfo.beginActivity(
|
|
143
|
+
options: [.userInitiated, .latencyCritical, .idleSystemSleepDisabled], reason: "live desktop stream")
|
|
144
|
+
log("activity: held")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private func releaseActivity() {
|
|
148
|
+
guard let a = activity else { return }
|
|
149
|
+
ProcessInfo.processInfo.endActivity(a)
|
|
150
|
+
activity = nil
|
|
151
|
+
log("activity: released")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private func teardown() {
|
|
155
|
+
releaseActivity()
|
|
156
|
+
source?.stop()
|
|
157
|
+
source = nil
|
|
158
|
+
encoder?.invalidate()
|
|
159
|
+
encoder = nil
|
|
160
|
+
geo = nil
|
|
161
|
+
lastFrame = nil
|
|
162
|
+
statsLock.lock()
|
|
163
|
+
avc = nil
|
|
164
|
+
statsLock.unlock()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func stopAll(reason: String?) {
|
|
168
|
+
teardown()
|
|
169
|
+
state = "stopped"
|
|
170
|
+
emitStatus(reason: reason)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func fail(_ sentence: String) {
|
|
174
|
+
teardown()
|
|
175
|
+
state = "error"
|
|
176
|
+
log("error: \(sentence)")
|
|
177
|
+
emitStatus(reason: sentence)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private func requestKey() {
|
|
181
|
+
guard let p = params, let enc = encoder else {
|
|
182
|
+
forceKey = true
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
// On a still screen no frame will come: re-encode the last one as an IDR now.
|
|
186
|
+
let interval = UInt64(1_000_000 / p.fps)
|
|
187
|
+
if let last = lastFrame, nowMicros() - lastFrameAt > 2 * interval {
|
|
188
|
+
lastPts = max(nowMicros(), lastPts + 1)
|
|
189
|
+
enc.encode(last, pts: lastPts, forceKey: true, region: region)
|
|
190
|
+
} else {
|
|
191
|
+
forceKey = true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func inject(_ ev: [String: Any]) {
|
|
196
|
+
if !dryInput {
|
|
197
|
+
let now = nowMicros()
|
|
198
|
+
if now - axCache.at > 2_000_000 { axCache = (now, accessibilityTrusted()) }
|
|
199
|
+
if !axCache.ok {
|
|
200
|
+
if now - axToldAt > 10_000_000 {
|
|
201
|
+
axToldAt = now
|
|
202
|
+
emitStatus(reason: accessibilitySentence)
|
|
203
|
+
}
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
var bounds = geo?.globalBounds ?? CGDisplayBounds(CGMainDisplayID())
|
|
208
|
+
if bounds.width < 1 { bounds = CGRect(x: 0, y: 0, width: 1280, height: 800) }
|
|
209
|
+
if let why = input.handle(ev, bounds: bounds) { log("input ignored: \(why)") }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// MARK: source callbacks (on q)
|
|
213
|
+
|
|
214
|
+
private func geometryChanged(_ g: Geometry) {
|
|
215
|
+
geo = g
|
|
216
|
+
if state != "error" { state = "running" }
|
|
217
|
+
if !inConfigure { emitStatus() }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private func frame(_ pb: CVPixelBuffer, _ ptsIn: UInt64) {
|
|
221
|
+
guard let p = params else { return }
|
|
222
|
+
idle = false
|
|
223
|
+
statsLock.lock()
|
|
224
|
+
captured += 1
|
|
225
|
+
statsLock.unlock()
|
|
226
|
+
let w = CVPixelBufferGetWidth(pb), h = CVPixelBufferGetHeight(pb)
|
|
227
|
+
if encoder == nil || encoder!.width != w || encoder!.height != h || encoder!.codec != p.codec {
|
|
228
|
+
encoder?.invalidate()
|
|
229
|
+
let sink: (EncodedFrame) -> Void = { [weak self] f in self?.encoded(f) }
|
|
230
|
+
encoder =
|
|
231
|
+
p.codec == .h264
|
|
232
|
+
? H264Encoder(width: w, height: h, params: p, sink: sink)
|
|
233
|
+
: JpegEncoder(width: w, height: h, params: p, sink: sink)
|
|
234
|
+
if encoder == nil {
|
|
235
|
+
fail("The H.264 encoder could not start at \(w)×\(h). Try JPEG or a smaller size.")
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
forceKey = true
|
|
239
|
+
}
|
|
240
|
+
guard let enc = encoder else { return }
|
|
241
|
+
if enc.inFlight > 2 && !forceKey {
|
|
242
|
+
statsLock.lock()
|
|
243
|
+
dropped += 1
|
|
244
|
+
statsLock.unlock()
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
let pts = max(ptsIn, lastPts + 1)
|
|
248
|
+
lastPts = pts
|
|
249
|
+
lastFrame = pb
|
|
250
|
+
lastFrameAt = nowMicros()
|
|
251
|
+
let k = forceKey
|
|
252
|
+
forceKey = false
|
|
253
|
+
enc.encode(pb, pts: pts, forceKey: k, region: region)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Encoder output: H.264 on VideoToolbox's thread, JPEG on q.
|
|
257
|
+
private func encoded(_ f: EncodedFrame) {
|
|
258
|
+
var newAvc = false
|
|
259
|
+
statsLock.lock()
|
|
260
|
+
frames += 1
|
|
261
|
+
bytes += f.data.count
|
|
262
|
+
if f.key { keys += 1 }
|
|
263
|
+
encSum += f.encodeMicros
|
|
264
|
+
encMax = max(encMax, f.encodeMicros)
|
|
265
|
+
if let a = f.avc, a != avc {
|
|
266
|
+
avc = a
|
|
267
|
+
newAvc = true
|
|
268
|
+
}
|
|
269
|
+
statsLock.unlock()
|
|
270
|
+
// The region the frame was SUBMITTED for: Output announces it (a status) before its first frame.
|
|
271
|
+
Output.shared.frame(f.kind, key: f.key, pts: f.pts, payload: f.data, region: f.region)
|
|
272
|
+
if newAvc { q.async { self.emitStatus() } }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// MARK: status + stats
|
|
276
|
+
|
|
277
|
+
private func emitStatus(reason: String? = nil) {
|
|
278
|
+
let p = params
|
|
279
|
+
var o: [String: Any] = [
|
|
280
|
+
"t": "status", "state": state,
|
|
281
|
+
"width": geo?.outWidth ?? 0, "height": geo?.outHeight ?? 0,
|
|
282
|
+
"srcWidth": geo?.srcWidth ?? 0, "srcHeight": geo?.srcHeight ?? 0,
|
|
283
|
+
"fps": p?.fps ?? 0, "codec": (p?.codec ?? .h264).rawValue,
|
|
284
|
+
"bitrateKbps": p?.bitrateKbps ?? 0, "source": p?.source.kind ?? "none",
|
|
285
|
+
"screenRecording": screenRecordingGranted(), "accessibility": accessibilityTrusted(),
|
|
286
|
+
"responsibleSelf": { let r = responsibility(); return r.pid == r.responsible }(),
|
|
287
|
+
"helperVersion": helperVersion,
|
|
288
|
+
]
|
|
289
|
+
if let reason { o["reason"] = reason }
|
|
290
|
+
statsLock.lock()
|
|
291
|
+
if let avc, p?.codec == .h264 { o["avc"] = avc }
|
|
292
|
+
statsLock.unlock()
|
|
293
|
+
Output.shared.status(o)
|
|
294
|
+
log("status \(state) \(o["width"]!)x\(o["height"]!) \(o["codec"]!) \(o["fps"]!)fps\(reason.map { " — " + $0 } ?? "")")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func startTimers() {
|
|
298
|
+
let t = DispatchSource.makeTimerSource(queue: q)
|
|
299
|
+
t.schedule(deadline: .now() + 2, repeating: 2)
|
|
300
|
+
t.setEventHandler { [weak self] in self?.reportStats() }
|
|
301
|
+
t.resume()
|
|
302
|
+
statsTimer = t
|
|
303
|
+
// The host dying closes stdin, which is the main exit path. Being re-parented is the backstop.
|
|
304
|
+
let w = DispatchSource.makeTimerSource(queue: .global())
|
|
305
|
+
w.schedule(deadline: .now() + 1, repeating: 1)
|
|
306
|
+
w.setEventHandler { [parentAtStart] in
|
|
307
|
+
if getppid() != parentAtStart {
|
|
308
|
+
log("parent went away, exiting")
|
|
309
|
+
exit(0)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
w.resume()
|
|
313
|
+
watchdog = w
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private func reportStats() {
|
|
317
|
+
let now = nowMicros()
|
|
318
|
+
statsLock.lock()
|
|
319
|
+
let span = Double(now - statsSince) / 1e6
|
|
320
|
+
let s = (frames, keys, bytes, captured, dropped, encSum, encMax)
|
|
321
|
+
frames = 0; keys = 0; bytes = 0; captured = 0; dropped = 0; encSum = 0; encMax = 0
|
|
322
|
+
statsSince = now
|
|
323
|
+
statsLock.unlock()
|
|
324
|
+
guard state == "running", span > 0, s.0 > 0 || s.3 > 0 else { return }
|
|
325
|
+
let fps = Double(s.0) / span, kbps = Double(s.2) * 8 / 1000 / span
|
|
326
|
+
let encAvg = s.0 > 0 ? Double(s.5) / Double(s.0) / 1000 : 0, encMaxMs = Double(s.6) / 1000
|
|
327
|
+
log(String(format: "stats fps=%.1f kbps=%.0f keys=%d enc=%.2fms max=%.2fms captured=%d dropped=%d", fps, kbps, s.1, encAvg, encMaxMs, s.3, s.4))
|
|
328
|
+
Output.shared.json([
|
|
329
|
+
"t": "stats", "fps": (fps * 10).rounded() / 10, "kbps": kbps.rounded(), "keys": s.1,
|
|
330
|
+
"encodeMsAvg": (encAvg * 100).rounded() / 100, "encodeMsMax": (encMaxMs * 100).rounded() / 100,
|
|
331
|
+
"captured": s.3, "dropped": s.4, "periodMs": Int(span * 1000), "idle": idle,
|
|
332
|
+
])
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Synthetic source: a desktop-like test pattern rendered without any capture
|
|
2
|
+
// API, fed into the same encoder/output path as a real capture. Every test uses
|
|
3
|
+
// it, so nothing in a test can touch the real screen or ask for a permission.
|
|
4
|
+
//
|
|
5
|
+
// The pattern lives on a virtual 2560×1600 "desktop" so `crop` and `maxWidth`
|
|
6
|
+
// behave exactly as they do on a display:
|
|
7
|
+
// - a light, mostly static background and menu bar
|
|
8
|
+
// - an editor window whose text-like lines scroll ~3 lines per second
|
|
9
|
+
// - a dark terminal panel whose glyphs churn every frame (the part that costs bits)
|
|
10
|
+
// - a cursor-like square on a slow Lissajous path
|
|
11
|
+
|
|
12
|
+
import CoreGraphics
|
|
13
|
+
import CoreVideo
|
|
14
|
+
import Foundation
|
|
15
|
+
|
|
16
|
+
protocol FrameSource: AnyObject {
|
|
17
|
+
func start()
|
|
18
|
+
/// Same source kind, new params: re-steer without a restart where possible.
|
|
19
|
+
func update(_ p: Params)
|
|
20
|
+
func stop()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// All callbacks arrive on the server's queue.
|
|
24
|
+
struct SourceCallbacks {
|
|
25
|
+
let onGeometry: (Geometry) -> Void
|
|
26
|
+
let onFrame: (CVPixelBuffer, UInt64) -> Void
|
|
27
|
+
let onIdle: () -> Void
|
|
28
|
+
let onError: (String) -> Void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func pixelFormat(for codec: Codec) -> OSType {
|
|
32
|
+
codec == .h264 ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : kCVPixelFormatType_32BGRA
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@inline(__always) private func mix(_ a: Int, _ b: Int, _ c: Int) -> UInt32 {
|
|
36
|
+
var x = UInt32(truncatingIfNeeded: (a &* 73_856_093) ^ (b &* 19_349_663) ^ (c &* 83_492_791))
|
|
37
|
+
x ^= x >> 13
|
|
38
|
+
x = x &* 0x5bd1_e995
|
|
39
|
+
x ^= x >> 15
|
|
40
|
+
return x
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
final class SyntheticSource: FrameSource {
|
|
44
|
+
static let VW = 2560, VH = 1600
|
|
45
|
+
private let q: DispatchQueue
|
|
46
|
+
private var p: Params
|
|
47
|
+
private let cb: SourceCallbacks
|
|
48
|
+
private var timer: DispatchSourceTimer?
|
|
49
|
+
private var pool: CVPixelBufferPool?
|
|
50
|
+
private var poolKey: (Int, Int, OSType) = (0, 0, 0)
|
|
51
|
+
private var geo: Geometry?
|
|
52
|
+
private let t0 = nowMicros()
|
|
53
|
+
private var frameNo = 0
|
|
54
|
+
private var wasIdle = false
|
|
55
|
+
private var colMap = [Int32]()
|
|
56
|
+
private var row = [UInt8]()
|
|
57
|
+
private let yLut: [UInt8] = (0..<256).map { UInt8(16 + $0 * 219 / 255) }
|
|
58
|
+
|
|
59
|
+
init(queue: DispatchQueue, params: Params, callbacks: SourceCallbacks) {
|
|
60
|
+
q = queue
|
|
61
|
+
p = params
|
|
62
|
+
cb = callbacks
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func start() {
|
|
66
|
+
recomputeGeometry()
|
|
67
|
+
startTimer()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func update(_ np: Params) {
|
|
71
|
+
let fpsChanged = np.fps != p.fps
|
|
72
|
+
p = np
|
|
73
|
+
recomputeGeometry()
|
|
74
|
+
if fpsChanged { startTimer() }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func stop() {
|
|
78
|
+
timer?.cancel()
|
|
79
|
+
timer = nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func recomputeGeometry() {
|
|
83
|
+
var bounds = CGDisplayBounds(CGMainDisplayID())
|
|
84
|
+
if bounds.width < 1 { bounds = CGRect(x: 0, y: 0, width: 1280, height: 800) }
|
|
85
|
+
let (w, h) = outputSize(srcW: Self.VW, srcH: Self.VH, crop: p.crop, maxWidth: p.maxWidth, codec: p.codec)
|
|
86
|
+
let g = Geometry(srcWidth: Self.VW, srcHeight: Self.VH, globalBounds: bounds, outWidth: w, outHeight: h)
|
|
87
|
+
if g != geo {
|
|
88
|
+
geo = g
|
|
89
|
+
cb.onGeometry(g)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private func startTimer() {
|
|
94
|
+
timer?.cancel()
|
|
95
|
+
let t = DispatchSource.makeTimerSource(flags: .strict, queue: q)
|
|
96
|
+
let interval = 1.0 / Double(p.fps)
|
|
97
|
+
t.schedule(deadline: .now() + interval, repeating: interval, leeway: .milliseconds(1))
|
|
98
|
+
t.setEventHandler { [weak self] in self?.tick() }
|
|
99
|
+
t.resume()
|
|
100
|
+
timer = t
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private func tick() {
|
|
104
|
+
guard let g = geo else { return }
|
|
105
|
+
let now = nowMicros()
|
|
106
|
+
let t = Double(now - t0) / 1e6
|
|
107
|
+
if case .synthetic(let idle) = p.source, idle, t.truncatingRemainder(dividingBy: 6) >= 3 {
|
|
108
|
+
if !wasIdle { cb.onIdle() }
|
|
109
|
+
wasIdle = true
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
wasIdle = false
|
|
113
|
+
let fmt = pixelFormat(for: p.codec)
|
|
114
|
+
if pool == nil || poolKey != (g.outWidth, g.outHeight, fmt) {
|
|
115
|
+
pool = makePool(g.outWidth, g.outHeight, fmt)
|
|
116
|
+
poolKey = (g.outWidth, g.outHeight, fmt)
|
|
117
|
+
}
|
|
118
|
+
guard let pool else { return }
|
|
119
|
+
var pbOut: CVPixelBuffer?
|
|
120
|
+
guard CVPixelBufferPoolCreatePixelBuffer(nil, pool, &pbOut) == kCVReturnSuccess, let pb = pbOut else {
|
|
121
|
+
log("synthetic: no pixel buffer from the pool")
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
frameNo += 1
|
|
125
|
+
render(pb, g, t)
|
|
126
|
+
cb.onFrame(pb, now)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func makePool(_ w: Int, _ h: Int, _ fmt: OSType) -> CVPixelBufferPool? {
|
|
130
|
+
let attrs: [String: Any] = [
|
|
131
|
+
kCVPixelBufferPixelFormatTypeKey as String: fmt,
|
|
132
|
+
kCVPixelBufferWidthKey as String: w,
|
|
133
|
+
kCVPixelBufferHeightKey as String: h,
|
|
134
|
+
kCVPixelBufferIOSurfacePropertiesKey as String: [String: Any](),
|
|
135
|
+
]
|
|
136
|
+
var pool: CVPixelBufferPool?
|
|
137
|
+
CVPixelBufferPoolCreate(nil, nil, attrs as CFDictionary, &pool)
|
|
138
|
+
return pool
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MARK: rendering
|
|
142
|
+
|
|
143
|
+
private func render(_ pb: CVPixelBuffer, _ g: Geometry, _ t: Double) {
|
|
144
|
+
let W = g.outWidth, H = g.outHeight
|
|
145
|
+
let c = p.crop
|
|
146
|
+
let cx0 = c.x * Double(Self.VW), cw = c.w * Double(Self.VW)
|
|
147
|
+
let cy0 = c.y * Double(Self.VH), ch = c.h * Double(Self.VH)
|
|
148
|
+
if colMap.count != W {
|
|
149
|
+
colMap = [Int32](repeating: 0, count: W)
|
|
150
|
+
row = [UInt8](repeating: 0, count: W)
|
|
151
|
+
}
|
|
152
|
+
for x in 0..<W { colMap[x] = Int32(cx0 + (Double(x) + 0.5) * cw / Double(W)) }
|
|
153
|
+
let scroll = Int(t * 3 * 32) // 3 lines of 32 px per second
|
|
154
|
+
let curX = Int(1280 + 900 * sin(t * 0.7)), curY = Int(800 + 500 * sin(t * 1.1))
|
|
155
|
+
let frame = frameNo
|
|
156
|
+
let bgra = CVPixelBufferGetPixelFormatType(pb) == kCVPixelFormatType_32BGRA
|
|
157
|
+
|
|
158
|
+
CVPixelBufferLockBaseAddress(pb, [])
|
|
159
|
+
defer { CVPixelBufferUnlockBaseAddress(pb, []) }
|
|
160
|
+
let plane0 = bgra ? CVPixelBufferGetBaseAddress(pb)! : CVPixelBufferGetBaseAddressOfPlane(pb, 0)!
|
|
161
|
+
let stride0 = bgra ? CVPixelBufferGetBytesPerRow(pb) : CVPixelBufferGetBytesPerRowOfPlane(pb, 0)
|
|
162
|
+
let out = plane0.assumingMemoryBound(to: UInt8.self)
|
|
163
|
+
|
|
164
|
+
colMap.withUnsafeBufferPointer { cm in
|
|
165
|
+
row.withUnsafeMutableBufferPointer { rw in
|
|
166
|
+
for y in 0..<H {
|
|
167
|
+
let vy = Int(cy0 + (Double(y) + 0.5) * ch / Double(H))
|
|
168
|
+
fillRow(vy: vy, scroll: scroll, frame: frame, curX: curX, curY: curY, cm: cm, row: rw)
|
|
169
|
+
let dst = out + y * stride0
|
|
170
|
+
if bgra {
|
|
171
|
+
for x in 0..<W {
|
|
172
|
+
let v = rw[x]
|
|
173
|
+
dst[4 * x] = v
|
|
174
|
+
dst[4 * x + 1] = v
|
|
175
|
+
dst[4 * x + 2] = v
|
|
176
|
+
dst[4 * x + 3] = 255
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
yLut.withUnsafeBufferPointer { lut in for x in 0..<W { dst[x] = lut[Int(rw[x])] } }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Colour: the cursor square is orange so the chroma path is exercised too.
|
|
186
|
+
let ox0 = Int((Double(curX) - cx0) * Double(W) / cw), ox1 = Int((Double(curX + 28) - cx0) * Double(W) / cw)
|
|
187
|
+
let oy0 = Int((Double(curY) - cy0) * Double(H) / ch), oy1 = Int((Double(curY + 28) - cy0) * Double(H) / ch)
|
|
188
|
+
if bgra {
|
|
189
|
+
for y in max(0, oy0)..<max(max(0, oy0), min(H, oy1)) {
|
|
190
|
+
let dst = out + y * stride0
|
|
191
|
+
for x in max(0, ox0)..<max(max(0, ox0), min(W, ox1)) {
|
|
192
|
+
dst[4 * x] = 20
|
|
193
|
+
dst[4 * x + 1] = 120
|
|
194
|
+
dst[4 * x + 2] = 240
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
let uv = CVPixelBufferGetBaseAddressOfPlane(pb, 1)!.assumingMemoryBound(to: UInt8.self)
|
|
199
|
+
let strideUV = CVPixelBufferGetBytesPerRowOfPlane(pb, 1)
|
|
200
|
+
let hw = (W + 1) / 2
|
|
201
|
+
for y in 0..<((H + 1) / 2) {
|
|
202
|
+
let dst = uv + y * strideUV
|
|
203
|
+
let inCur = 2 * y >= oy0 && 2 * y < oy1
|
|
204
|
+
for x in 0..<hw {
|
|
205
|
+
if inCur && 2 * x >= ox0 && 2 * x < ox1 {
|
|
206
|
+
dst[2 * x] = 70
|
|
207
|
+
dst[2 * x + 1] = 200
|
|
208
|
+
} else {
|
|
209
|
+
dst[2 * x] = 128
|
|
210
|
+
dst[2 * x + 1] = 128
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// One row of luma (0 = black, 255 = white) at virtual row `vy`.
|
|
218
|
+
@inline(__always)
|
|
219
|
+
private func fillRow(
|
|
220
|
+
vy: Int, scroll: Int, frame: Int, curX: Int, curY: Int,
|
|
221
|
+
cm: UnsafeBufferPointer<Int32>, row: UnsafeMutableBufferPointer<UInt8>
|
|
222
|
+
) {
|
|
223
|
+
let W = row.count
|
|
224
|
+
if vy < 48 { // menu bar: static
|
|
225
|
+
for x in 0..<W { row[x] = 238 }
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
let base: UInt8 = 206 &+ UInt8((vy >> 6) & 1) &* 4
|
|
229
|
+
// Editor window rows.
|
|
230
|
+
let inEditor = vy >= 120 && vy < 1450
|
|
231
|
+
let edTitle = vy < 170
|
|
232
|
+
let ly = vy - 170 + scroll
|
|
233
|
+
let eLine = ly >> 5, eR = ly & 31
|
|
234
|
+
let eGlyphRow = !edTitle && eR >= 8 && eR < 26
|
|
235
|
+
let eGy = (eR - 8) / 3
|
|
236
|
+
let eHash = mix(eLine, 0, 7)
|
|
237
|
+
let eLen = 16 + Int(eHash % 72), eIndent = Int((eHash >> 8) % 4) * 2
|
|
238
|
+
// Terminal panel rows.
|
|
239
|
+
let inTerm = vy >= 300 && vy < 1100
|
|
240
|
+
let tTitle = vy < 340
|
|
241
|
+
let tl = vy - 340
|
|
242
|
+
let tLine = tl >> 5, tR = tl & 31
|
|
243
|
+
let tGlyphRow = !tTitle && tR >= 8 && tR < 26
|
|
244
|
+
let tGy = (tR - 8) / 3
|
|
245
|
+
let tHash = mix(tLine, frame, 11)
|
|
246
|
+
let tLen = 8 + Int(tHash % 32)
|
|
247
|
+
let inCursor = vy >= curY && vy < curY + 28
|
|
248
|
+
|
|
249
|
+
for x in 0..<W {
|
|
250
|
+
let vx = Int(cm[x])
|
|
251
|
+
var v = base
|
|
252
|
+
if inEditor && vx >= 200 && vx < 1700 {
|
|
253
|
+
if edTitle {
|
|
254
|
+
v = 214
|
|
255
|
+
} else {
|
|
256
|
+
v = 252
|
|
257
|
+
if eGlyphRow {
|
|
258
|
+
let col = (vx - 216) >> 4, cx = (vx - 216) & 15
|
|
259
|
+
if vx >= 216 && col >= eIndent && col < eIndent + eLen && cx < 12 && mix(eLine, col, 1) % 7 != 0 {
|
|
260
|
+
if (mix(eLine, col, eGy + 2) >> UInt32(cx >> 1)) & 1 == 1 { v = 28 }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} else if inTerm && vx >= 1800 && vx < 2450 {
|
|
265
|
+
if tTitle {
|
|
266
|
+
v = 64
|
|
267
|
+
} else {
|
|
268
|
+
v = 30
|
|
269
|
+
if tGlyphRow {
|
|
270
|
+
let col = (vx - 1812) >> 4, cx = (vx - 1812) & 15
|
|
271
|
+
if vx >= 1812 && col < tLen && cx < 12 && mix(tLine, col, frame) % 5 != 0 {
|
|
272
|
+
if (mix(tLine &+ frame, col, tGy + 2) >> UInt32(cx >> 1)) & 1 == 1 { v = 224 }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if inCursor && vx >= curX && vx < curX + 28 { v = 12 }
|
|
278
|
+
row[x] = v
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build wp-desktop. Usage:
|
|
3
|
+
# build.sh [OUT] compile to OUT (default: $OUT, else $TMPDIR/wp-desktop/wp-desktop), ad-hoc sign
|
|
4
|
+
# build.sh --hash print the source hash only (the host rebuilds only when it changes)
|
|
5
|
+
#
|
|
6
|
+
# A TCC grant is bound to the binary's code signature. An ad-hoc signature is
|
|
7
|
+
# its cdhash, so EVERY rebuild means granting Screen Recording and
|
|
8
|
+
# Accessibility again: rebuild only when --hash changes. The identifier is
|
|
9
|
+
# stable and the Info.plist is embedded so the System Settings row reads
|
|
10
|
+
# "wp-desktop", not a hash.
|
|
11
|
+
#
|
|
12
|
+
# The product's name is not spelled here: the host (helperbin.mts) reads it from
|
|
13
|
+
# the brand table and passes it in the environment, for --hash and for a build
|
|
14
|
+
# alike (the identifier is part of the hash):
|
|
15
|
+
# WP_DESKTOP_ID the code-signing identifier / CFBundleIdentifier (TCC keys the grant on it)
|
|
16
|
+
# WP_DESKTOP_NAME CFBundleDisplayName
|
|
17
|
+
# A hand build (selftest.sh) gets a neutral local identifier; the host then sees
|
|
18
|
+
# a different hash and builds its own.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
SOURCES=(main.swift Protocol.swift Params.swift Server.swift Capture.swift Synthetic.swift H264.swift Jpeg.swift Input.swift Probe.swift Request.swift Responsibility.swift)
|
|
22
|
+
|
|
23
|
+
ID="${WP_DESKTOP_ID:-local.wp-desktop}"
|
|
24
|
+
NAME="${WP_DESKTOP_NAME:-wp-desktop helper}"
|
|
25
|
+
|
|
26
|
+
srchash() {
|
|
27
|
+
{ (cd "$HERE" && cat "${SOURCES[@]}" Info.plist build.sh); printf '%s\n%s\n' "$ID" "$NAME"; } | shasum -a 256 | cut -c1-16
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if [ "${1:-}" = "--hash" ]; then
|
|
31
|
+
srchash
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
OUT="${1:-${OUT:-${TMPDIR:-/tmp}/wp-desktop/wp-desktop}}"
|
|
36
|
+
SWIFTC="${SWIFTC:-$(command -v swiftc || true)}"
|
|
37
|
+
SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
|
|
38
|
+
if [ -z "$SWIFTC" ] || [ -z "$SDK" ]; then
|
|
39
|
+
echo "build.sh: no swiftc or macOS SDK. Install the command line tools: xcode-select --install" >&2
|
|
40
|
+
exit 3
|
|
41
|
+
fi
|
|
42
|
+
mkdir -p "$(dirname "$OUT")"
|
|
43
|
+
TMP_OUT="$OUT.build.$$"
|
|
44
|
+
PLIST="$OUT.plist.$$"
|
|
45
|
+
trap 'rm -f "$TMP_OUT" "$PLIST"' EXIT
|
|
46
|
+
sed -e "s|@WP_DESKTOP_ID@|$ID|" -e "s|@WP_DESKTOP_NAME@|$NAME|" "$HERE/Info.plist" > "$PLIST"
|
|
47
|
+
(cd "$HERE" && "$SWIFTC" -O -swift-version 5 -target arm64-apple-macos14 -sdk "$SDK" \
|
|
48
|
+
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker "$PLIST" \
|
|
49
|
+
"${SOURCES[@]}" -o "$TMP_OUT")
|
|
50
|
+
codesign -s - --force --identifier "$ID" "$TMP_OUT"
|
|
51
|
+
mv -f "$TMP_OUT" "$OUT"
|
|
52
|
+
srchash > "$OUT.srchash"
|
|
53
|
+
echo "built $OUT ($(cat "$OUT.srchash"))" >&2
|