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.
Files changed (235) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/NOTICES.md +18 -7
  3. package/README.md +43 -0
  4. package/SECURITY.md +196 -12
  5. package/node_modules/@waterplane/shared/dist/desktopfiles.d.ts +24 -2
  6. package/node_modules/@waterplane/shared/dist/desktopfiles.js +73 -3
  7. package/node_modules/@waterplane/shared/dist/desktopfiles.js.map +1 -1
  8. package/node_modules/@waterplane/shared/dist/hosts.d.ts +31 -0
  9. package/node_modules/@waterplane/shared/dist/hosts.js +46 -1
  10. package/node_modules/@waterplane/shared/dist/hosts.js.map +1 -1
  11. package/node_modules/@waterplane/shared/dist/hosturl.d.ts +55 -0
  12. package/node_modules/@waterplane/shared/dist/hosturl.js +85 -0
  13. package/node_modules/@waterplane/shared/dist/hosturl.js.map +1 -1
  14. package/node_modules/@waterplane/shared/dist/index.d.ts +4 -0
  15. package/node_modules/@waterplane/shared/dist/index.js +4 -0
  16. package/node_modules/@waterplane/shared/dist/index.js.map +1 -1
  17. package/node_modules/@waterplane/shared/dist/installrow.d.ts +6 -1
  18. package/node_modules/@waterplane/shared/dist/installrow.js +20 -1
  19. package/node_modules/@waterplane/shared/dist/installrow.js.map +1 -1
  20. package/node_modules/@waterplane/shared/dist/lanroute.d.ts +5 -0
  21. package/node_modules/@waterplane/shared/dist/lanroute.js.map +1 -1
  22. package/node_modules/@waterplane/shared/dist/pairing.d.ts +96 -2
  23. package/node_modules/@waterplane/shared/dist/pairing.js +71 -3
  24. package/node_modules/@waterplane/shared/dist/pairing.js.map +1 -1
  25. package/node_modules/@waterplane/shared/dist/pake.d.ts +131 -0
  26. package/node_modules/@waterplane/shared/dist/pake.js +272 -0
  27. package/node_modules/@waterplane/shared/dist/pake.js.map +1 -0
  28. package/node_modules/@waterplane/shared/dist/peer.d.ts +63 -0
  29. package/node_modules/@waterplane/shared/dist/peer.js +41 -0
  30. package/node_modules/@waterplane/shared/dist/peer.js.map +1 -1
  31. package/node_modules/@waterplane/shared/dist/plugin/api.d.ts +15 -0
  32. package/node_modules/@waterplane/shared/dist/plugin/version.d.ts +5 -1
  33. package/node_modules/@waterplane/shared/dist/plugin/version.js +5 -1
  34. package/node_modules/@waterplane/shared/dist/plugin/version.js.map +1 -1
  35. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.d.ts +7 -2
  36. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js +6 -3
  37. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js.map +1 -1
  38. package/node_modules/@waterplane/shared/dist/waiting.d.ts +64 -0
  39. package/node_modules/@waterplane/shared/dist/waiting.js +86 -0
  40. package/node_modules/@waterplane/shared/dist/waiting.js.map +1 -0
  41. package/node_modules/@waterplane/shared/dist/wire/messages/hostcore.d.ts +3 -0
  42. package/node_modules/@waterplane/shared/dist/wire/notify.d.ts +2 -0
  43. package/node_modules/@waterplane/shared/dist/wire/notify.js +2 -0
  44. package/node_modules/@waterplane/shared/dist/wire/notify.js.map +1 -1
  45. package/node_modules/@waterplane/shared/dist/wire/repo.d.ts +25 -1
  46. package/node_modules/@waterplane/shared/dist/wire/repo.js.map +1 -1
  47. package/node_modules/@waterplane/shared/dist/wire/sysstat.d.ts +7 -0
  48. package/node_modules/@waterplane/shared/dist/wire/telemetry.d.ts +3 -0
  49. package/node_modules/@waterplane/shared/package.json +3 -0
  50. package/npm-shrinkwrap.json +30 -2
  51. package/package.json +2 -1
  52. package/packages/extensions/desktop/helper/Capture.swift +245 -0
  53. package/packages/extensions/desktop/helper/H264.swift +226 -0
  54. package/packages/extensions/desktop/helper/Info.plist +20 -0
  55. package/packages/extensions/desktop/helper/Input.swift +195 -0
  56. package/packages/extensions/desktop/helper/Jpeg.swift +81 -0
  57. package/packages/extensions/desktop/helper/PROTOCOL.md +366 -0
  58. package/packages/extensions/desktop/helper/Params.swift +133 -0
  59. package/packages/extensions/desktop/helper/Probe.swift +119 -0
  60. package/packages/extensions/desktop/helper/Protocol.swift +224 -0
  61. package/packages/extensions/desktop/helper/Request.swift +51 -0
  62. package/packages/extensions/desktop/helper/Responsibility.swift +133 -0
  63. package/packages/extensions/desktop/helper/Server.swift +334 -0
  64. package/packages/extensions/desktop/helper/Synthetic.swift +281 -0
  65. package/packages/extensions/desktop/helper/build.sh +53 -0
  66. package/packages/extensions/desktop/helper/main.swift +79 -0
  67. package/packages/extensions/desktop/helper/selftest-driver +626 -0
  68. package/packages/extensions/desktop/helper/selftest.sh +16 -0
  69. package/packages/extensions/desktop/helper-win/Capture.cs +299 -0
  70. package/packages/extensions/desktop/helper-win/H264.cs +340 -0
  71. package/packages/extensions/desktop/helper-win/Input.cs +257 -0
  72. package/packages/extensions/desktop/helper-win/Jpeg.cs +75 -0
  73. package/packages/extensions/desktop/helper-win/Main.cs +118 -0
  74. package/packages/extensions/desktop/helper-win/Native.cs +276 -0
  75. package/packages/extensions/desktop/helper-win/Params.cs +138 -0
  76. package/packages/extensions/desktop/helper-win/Protocol.cs +206 -0
  77. package/packages/extensions/desktop/helper-win/Server.cs +304 -0
  78. package/packages/extensions/desktop/helper-win/Synthetic.cs +88 -0
  79. package/packages/extensions/desktop/helper-win/build.sh +57 -0
  80. package/packages/extensions/desktop/helper-win/selftest.sh +36 -0
  81. package/packages/extensions/desktop/waterplane-plugin.json +24 -0
  82. package/packages/host/dist/alwayson.js +1 -1
  83. package/packages/host/dist/alwayson.js.map +1 -1
  84. package/packages/host/dist/attentionstore.d.ts +4 -0
  85. package/packages/host/dist/attentionstore.js +17 -0
  86. package/packages/host/dist/attentionstore.js.map +1 -1
  87. package/packages/host/dist/auth.d.ts +28 -0
  88. package/packages/host/dist/auth.js +65 -2
  89. package/packages/host/dist/auth.js.map +1 -1
  90. package/packages/host/dist/cli/pair.d.ts +4 -0
  91. package/packages/host/dist/cli/pair.js +18 -5
  92. package/packages/host/dist/cli/pair.js.map +1 -1
  93. package/packages/host/dist/der.d.ts +19 -0
  94. package/packages/host/dist/der.js +51 -0
  95. package/packages/host/dist/der.js.map +1 -0
  96. package/packages/host/dist/doctor.d.ts +2 -0
  97. package/packages/host/dist/doctor.js +10 -2
  98. package/packages/host/dist/doctor.js.map +1 -1
  99. package/packages/host/dist/extensions/desktop/host/auto.d.mts +71 -0
  100. package/packages/host/dist/extensions/desktop/host/auto.mjs +114 -0
  101. package/packages/host/dist/extensions/desktop/host/auto.mjs.map +1 -0
  102. package/packages/host/dist/extensions/desktop/host/desktop.d.mts +51 -0
  103. package/packages/host/dist/extensions/desktop/host/desktop.mjs +113 -0
  104. package/packages/host/dist/extensions/desktop/host/desktop.mjs.map +1 -0
  105. package/packages/host/dist/extensions/desktop/host/helperbin.d.mts +71 -0
  106. package/packages/host/dist/extensions/desktop/host/helperbin.mjs +160 -0
  107. package/packages/host/dist/extensions/desktop/host/helperbin.mjs.map +1 -0
  108. package/packages/host/dist/extensions/desktop/host/index.d.mts +4 -0
  109. package/packages/host/dist/extensions/desktop/host/index.mjs +88 -0
  110. package/packages/host/dist/extensions/desktop/host/index.mjs.map +1 -0
  111. package/packages/host/dist/extensions/desktop/host/knobs.d.mts +52 -0
  112. package/packages/host/dist/extensions/desktop/host/knobs.mjs +81 -0
  113. package/packages/host/dist/extensions/desktop/host/knobs.mjs.map +1 -0
  114. package/packages/host/dist/extensions/desktop/host/probe.d.mts +57 -0
  115. package/packages/host/dist/extensions/desktop/host/probe.mjs +119 -0
  116. package/packages/host/dist/extensions/desktop/host/probe.mjs.map +1 -0
  117. package/packages/host/dist/extensions/desktop/host/records.d.mts +40 -0
  118. package/packages/host/dist/extensions/desktop/host/records.mjs +101 -0
  119. package/packages/host/dist/extensions/desktop/host/records.mjs.map +1 -0
  120. package/packages/host/dist/extensions/desktop/host/session.d.mts +86 -0
  121. package/packages/host/dist/extensions/desktop/host/session.mjs +318 -0
  122. package/packages/host/dist/extensions/desktop/host/session.mjs.map +1 -0
  123. package/packages/host/dist/extensions/desktop/host/setup.d.mts +28 -0
  124. package/packages/host/dist/extensions/desktop/host/setup.mjs +217 -0
  125. package/packages/host/dist/extensions/desktop/host/setup.mjs.map +1 -0
  126. package/packages/host/dist/extensions/desktop/host/status.d.mts +27 -0
  127. package/packages/host/dist/extensions/desktop/host/status.mjs +62 -0
  128. package/packages/host/dist/extensions/desktop/host/status.mjs.map +1 -0
  129. package/packages/host/dist/extensions/desktop/shared/wire.d.mts +206 -0
  130. package/packages/host/dist/extensions/desktop/shared/wire.mjs +174 -0
  131. package/packages/host/dist/extensions/desktop/shared/wire.mjs.map +1 -0
  132. package/packages/host/dist/git.d.ts +23 -6
  133. package/packages/host/dist/git.js +54 -26
  134. package/packages/host/dist/git.js.map +1 -1
  135. package/packages/host/dist/gitfetchfail.d.ts +24 -0
  136. package/packages/host/dist/gitfetchfail.js +62 -0
  137. package/packages/host/dist/gitfetchfail.js.map +1 -0
  138. package/packages/host/dist/gitlog.d.ts +4 -0
  139. package/packages/host/dist/gitlog.js +8 -3
  140. package/packages/host/dist/gitlog.js.map +1 -1
  141. package/packages/host/dist/gitnotify.d.ts +7 -1
  142. package/packages/host/dist/gitnotify.js +61 -1
  143. package/packages/host/dist/gitnotify.js.map +1 -1
  144. package/packages/host/dist/host.js +5 -1
  145. package/packages/host/dist/host.js.map +1 -1
  146. package/packages/host/dist/hoststore.js +4 -4
  147. package/packages/host/dist/hoststore.js.map +1 -1
  148. package/packages/host/dist/lantls.d.ts +145 -0
  149. package/packages/host/dist/lantls.js +337 -0
  150. package/packages/host/dist/lantls.js.map +1 -0
  151. package/packages/host/dist/main.js +7 -2
  152. package/packages/host/dist/main.js.map +1 -1
  153. package/packages/host/dist/net.d.ts +7 -0
  154. package/packages/host/dist/net.js +9 -0
  155. package/packages/host/dist/net.js.map +1 -1
  156. package/packages/host/dist/origin.d.ts +4 -0
  157. package/packages/host/dist/origin.js +6 -0
  158. package/packages/host/dist/origin.js.map +1 -1
  159. package/packages/host/dist/pair.d.ts +54 -5
  160. package/packages/host/dist/pair.js +139 -34
  161. package/packages/host/dist/pair.js.map +1 -1
  162. package/packages/host/dist/pairpake.d.ts +58 -0
  163. package/packages/host/dist/pairpake.js +208 -0
  164. package/packages/host/dist/pairpake.js.map +1 -0
  165. package/packages/host/dist/passive.d.ts +2 -0
  166. package/packages/host/dist/passive.js +4 -0
  167. package/packages/host/dist/passive.js.map +1 -1
  168. package/packages/host/dist/peers/client.d.ts +3 -1
  169. package/packages/host/dist/peers/client.js +4 -0
  170. package/packages/host/dist/peers/client.js.map +1 -1
  171. package/packages/host/dist/peers/routes.d.ts +6 -1
  172. package/packages/host/dist/peers/routes.js +39 -0
  173. package/packages/host/dist/peers/routes.js.map +1 -1
  174. package/packages/host/dist/plugins/pluginapi.js +13 -0
  175. package/packages/host/dist/plugins/pluginapi.js.map +1 -1
  176. package/packages/host/dist/plugins/pluginregistry.d.ts +6 -0
  177. package/packages/host/dist/plugins/pluginregistry.js.map +1 -1
  178. package/packages/host/dist/prepushcheck.d.ts +34 -0
  179. package/packages/host/dist/prepushcheck.js +157 -0
  180. package/packages/host/dist/prepushcheck.js.map +1 -0
  181. package/packages/host/dist/prs.js +2 -1
  182. package/packages/host/dist/prs.js.map +1 -1
  183. package/packages/host/dist/routes/apigitcheck.d.ts +2 -0
  184. package/packages/host/dist/routes/apigitcheck.js +29 -0
  185. package/packages/host/dist/routes/apigitcheck.js.map +1 -0
  186. package/packages/host/dist/routes/apihost.js +2 -0
  187. package/packages/host/dist/routes/apihost.js.map +1 -1
  188. package/packages/host/dist/routes/apiio.js +2 -1
  189. package/packages/host/dist/routes/apiio.js.map +1 -1
  190. package/packages/host/dist/routes/apirepofiles.js +2 -1
  191. package/packages/host/dist/routes/apirepofiles.js.map +1 -1
  192. package/packages/host/dist/routes/apisystem.js +21 -3
  193. package/packages/host/dist/routes/apisystem.js.map +1 -1
  194. package/packages/host/dist/server/http.js +31 -5
  195. package/packages/host/dist/server/http.js.map +1 -1
  196. package/packages/host/dist/server/plugins.d.ts +13 -0
  197. package/packages/host/dist/server/plugins.js +26 -0
  198. package/packages/host/dist/server/plugins.js.map +1 -1
  199. package/packages/host/dist/server/wsops.js +3 -1
  200. package/packages/host/dist/server/wsops.js.map +1 -1
  201. package/packages/host/dist/sysstat.js +10 -1
  202. package/packages/host/dist/sysstat.js.map +1 -1
  203. package/packages/host/dist/updatekey.d.ts +22 -0
  204. package/packages/host/dist/updatekey.js +140 -0
  205. package/packages/host/dist/updatekey.js.map +1 -0
  206. package/packages/host/dist/webbundle.d.ts +15 -6
  207. package/packages/host/dist/webbundle.js +44 -7
  208. package/packages/host/dist/webbundle.js.map +1 -1
  209. package/packages/host/dist/wincpu.d.ts +13 -0
  210. package/packages/host/dist/wincpu.js +101 -0
  211. package/packages/host/dist/wincpu.js.map +1 -0
  212. package/packages/host/package.json +2 -1
  213. package/packages/web/dist/assets/demo-BLBaz4G_.js +26 -0
  214. package/packages/web/dist/assets/icons-D8RlmSLR.js +1 -0
  215. package/packages/web/dist/assets/index-BrvcQFBj.js +170 -0
  216. package/packages/web/dist/assets/index-Dx3VHnr3.css +1 -0
  217. package/packages/web/dist/assets/noble-DV-r0OkE.js +1 -0
  218. package/packages/web/dist/assets/{react-CoSyT_xY.js → react-CjmvHigY.js} +2 -2
  219. package/packages/web/dist/assets/rolldown-runtime-4tVxZq34.js +1 -0
  220. package/packages/web/dist/assets/shared-BsWIJLE-.js +51 -0
  221. package/packages/web/dist/assets/sheets-C7CXUU6R.js +250 -0
  222. package/packages/web/dist/assets/xterm-BHsKrDzh.js +16 -0
  223. package/packages/web/dist/build-id.txt +1 -1
  224. package/packages/web/dist/buildstamp.json +1 -1
  225. package/packages/web/dist/index.html +9 -9
  226. package/packages/web/dist/sw.js +2 -2
  227. package/release.json +7 -3
  228. package/packages/web/dist/assets/demo-DkCtg-SJ.js +0 -26
  229. package/packages/web/dist/assets/icons-B48F-4nA.js +0 -1
  230. package/packages/web/dist/assets/index-CpdyS6Xe.css +0 -1
  231. package/packages/web/dist/assets/index-EYc28CfH.js +0 -170
  232. package/packages/web/dist/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
  233. package/packages/web/dist/assets/shared-DjRRLGfw.js +0 -50
  234. package/packages/web/dist/assets/sheets-ByWNPa9p.js +0 -248
  235. package/packages/web/dist/assets/xterm-C8zLTNGk.js +0 -16
@@ -59,6 +59,11 @@ export interface GitStatus {
59
59
  upstream?: string;
60
60
  ahead: number;
61
61
  behind: number;
62
+ /** When this worktree last fetched (FETCH_HEAD's mtime, epoch ms) — how old `behind` is: git
63
+ * counts against the remote-tracking ref, which only a fetch moves, so a repo nobody fetched
64
+ * for a month read ↓0 while 30 commits waited (2026-11-03). ABSENT = never fetched from this
65
+ * worktree, no upstream, or an older host. The phone cuts staleness off THIS, never off `behind`. */
66
+ fetchedAt?: number;
62
67
  files: GitFile[];
63
68
  clean: boolean;
64
69
  /** What the remote's own rules demand of the default branch. ABSENT = direct
@@ -72,6 +77,14 @@ export interface GitCommit {
72
77
  author: string;
73
78
  at: number;
74
79
  subject: string;
80
+ /** Outgoing commits only: what the pre-push checks last said about exactly this commit. */
81
+ check?: GitCheck;
82
+ }
83
+ /** One run of the pre-push hook over one commit (host `prepushcheck.ts`). Information only: the
84
+ * real push still runs the hook. */
85
+ export interface GitCheck {
86
+ ok: boolean;
87
+ at: number;
75
88
  }
76
89
  /**
77
90
  * The branch's commits against its upstream, in three disjoint lists:
@@ -91,6 +104,12 @@ export interface GitLog {
91
104
  outgoingTotal: number;
92
105
  shared: GitCommit[];
93
106
  fetchedAt?: number;
107
+ /** There is a pre-push hook to run; absent = none. `testsCommit`: it tests the commit it is
108
+ * given, so any outgoing commit can be checked and pushed up to. Otherwise it tests the working
109
+ * tree and only the newest commit can be checked. */
110
+ prePush?: {
111
+ testsCommit: boolean;
112
+ };
94
113
  }
95
114
  /**
96
115
  * The push the desktop is running (or last ran) in one worktree. A push runs
@@ -111,6 +130,8 @@ export interface GitPushState {
111
130
  };
112
131
  /** What the push has printed so far — absent from a host that does not stream it. */
113
132
  progress?: GitPushProgress;
133
+ /** This job runs the hook alone and pushes nothing. */
134
+ check?: boolean;
114
135
  }
115
136
  /**
116
137
  * A push's output as it streams, read by the host (`pushprogress.ts`): the
@@ -180,6 +201,9 @@ export interface RepoTarget {
180
201
  * the branch has no upstream (nothing to count against), never 0 for that. */
181
202
  ahead?: number;
182
203
  behind?: number;
204
+ /** When this worktree last fetched (FETCH_HEAD's mtime) — how old `behind` is; same field and
205
+ * rule as `GitStatus.fetchedAt`. ABSENT = never fetched here, no upstream, or an older host. */
206
+ fetchedAt?: number;
183
207
  /** Whether the repo has an `origin` remote — where the git view pushes. `false`
184
208
  * = local only, nothing to push to (the launcher says so). ABSENT = not known
185
209
  * (an older host, or a count not taken yet) — never drawn as "no remote". */
@@ -187,7 +211,7 @@ export interface RepoTarget {
187
211
  /** A git job the desktop is running in this worktree right now — a push (its
188
212
  * pre-push checks take minutes) or a pull — so the launcher's git button can
189
213
  * spin while it runs. ABSENT = none running (or an older host). */
190
- gitRunning?: "push" | "pull";
214
+ gitRunning?: "push" | "pull" | "check";
191
215
  /** The last pull or push of a verb here FAILED and nobody has handled it —
192
216
  * derived on the host from its attention store with `needsYou`, so this
193
217
  * chip, the bell and the feed row are one predicate. `id` is the feed
@@ -1 +1 @@
1
- {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/wire/repo.ts"],"names":[],"mappings":"AAkbA;sEACsE;AACtE,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,mBAAmB,CAAC,CAAC;AAErI;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAAC,CAAkB;IAC/C,OAAO,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC/F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,CAAkB;IAChD,OAAO,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;AAChG,CAAC"}
1
+ {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/wire/repo.ts"],"names":[],"mappings":"AAycA;sEACsE;AACtE,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,mBAAmB,CAAC,CAAC;AAErI;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAAC,CAAkB;IAC/C,OAAO,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC/F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,CAAkB;IAChD,OAAO,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;AAChG,CAAC"}
@@ -61,6 +61,13 @@ export interface CpuBusy {
61
61
  /** How long the split covers, ms. A number, because a "37%" over four
62
62
  * seconds and over four minutes are different claims. */
63
63
  spanMs: number;
64
+ /** Where the reading came from, when not the host OS itself: `windows` =
65
+ * the Windows-wide figure read through WSL interop; `wsl-vm` = only the
66
+ * WSL2 VM's own load (interop unavailable), so it under-reports the PC. */
67
+ source?: "windows" | "wsl-vm";
68
+ /** `false` when only the total is known: `userPct` then holds total busy,
69
+ * `sysPct` is 0 and `cores` is empty. Absent means a real split. */
70
+ splitKnown?: false;
64
71
  }
65
72
  /**
66
73
  * Where the memory went.
@@ -238,6 +238,9 @@ export interface HostStat {
238
238
  loadavg: [number, number, number];
239
239
  /** 1-min load as a percent of cores (loadavg[0] / cores * 100), rounded. */
240
240
  cpuPct: number;
241
+ /** `windows` when `cpuPct` is the Windows-wide figure read through WSL interop
242
+ * (the load average inside the VM under-reports the PC); absent otherwise. */
243
+ cpuSource?: "windows";
241
244
  /** Total / free physical memory, bytes. */
242
245
  memTotal: number;
243
246
  memFree: number;
@@ -10,6 +10,9 @@
10
10
  "default": "./dist/index.js"
11
11
  }
12
12
  },
13
+ "dependencies": {
14
+ "@noble/curves": "2.4.0"
15
+ },
13
16
  "scripts": {
14
17
  "build": "tsc -p tsconfig.json",
15
18
  "watch": "tsc -p tsconfig.json --watch"
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "waterplane",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "waterplane",
9
- "version": "0.1.16",
9
+ "version": "0.1.18",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
+ "@noble/curves": "2.4.0",
12
13
  "@xterm/headless": "5.5.0",
13
14
  "node-pty": "1.2.0-beta.15",
14
15
  "web-push": "3.6.7",
@@ -29,6 +30,33 @@
29
30
  "version": "0.1.4",
30
31
  "inBundle": true
31
32
  },
33
+ "node_modules/@noble/curves": {
34
+ "version": "2.4.0",
35
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz",
36
+ "integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@noble/hashes": "2.4.0"
40
+ },
41
+ "engines": {
42
+ "node": ">= 20.19.0"
43
+ },
44
+ "funding": {
45
+ "url": "https://paulmillr.com/funding/"
46
+ }
47
+ },
48
+ "node_modules/@noble/hashes": {
49
+ "version": "2.4.0",
50
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
51
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
52
+ "license": "MIT",
53
+ "engines": {
54
+ "node": ">= 20.19.0"
55
+ },
56
+ "funding": {
57
+ "url": "https://paulmillr.com/funding/"
58
+ }
59
+ },
32
60
  "node_modules/@xterm/headless": {
33
61
  "version": "5.5.0",
34
62
  "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waterplane",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "description": "waterplane — phone-native remote control for Claude Code on your PC",
6
6
  "license": "MIT",
@@ -30,6 +30,7 @@
30
30
  "node": ">=22.12"
31
31
  },
32
32
  "dependencies": {
33
+ "@noble/curves": "2.4.0",
33
34
  "@xterm/headless": "5.5.0",
34
35
  "node-pty": "1.2.0-beta.15",
35
36
  "web-push": "3.6.7",
@@ -0,0 +1,245 @@
1
+ // Capture: ScreenCaptureKit on one display, one window, or the frontmost
2
+ // app's window (re-aimed at ≤1 Hz). SCK delivers a frame only when pixels
3
+ // change (SCFrameStatus.complete); a still screen sends .idle and costs nothing.
4
+ //
5
+ // SAFETY: nothing in this file runs unless CGPreflightScreenCaptureAccess()
6
+ // already says yes, so no code path here can raise the Screen Recording
7
+ // prompt. The server checks too; `start()` checks again.
8
+
9
+ import CoreGraphics
10
+ import CoreMedia
11
+ import Foundation
12
+ @preconcurrency import ScreenCaptureKit
13
+
14
+ private struct CaptureError: Error {
15
+ let sentence: String
16
+ }
17
+
18
+ final class ScreenSource: NSObject, FrameSource, SCStreamOutput, SCStreamDelegate, @unchecked Sendable {
19
+ private let q: DispatchQueue
20
+ private var p: Params
21
+ private let cb: SourceCallbacks
22
+ private var stream: SCStream?
23
+ private var geo: Geometry?
24
+ private var windowID: CGWindowID?
25
+ private var followTimer: DispatchSourceTimer?
26
+ private var stopped = false
27
+ private var retargeting = false
28
+
29
+ init(queue: DispatchQueue, params: Params, callbacks: SourceCallbacks) {
30
+ q = queue
31
+ p = params
32
+ cb = callbacks
33
+ }
34
+
35
+ // MARK: lifecycle (all on q)
36
+
37
+ func start() {
38
+ guard screenRecordingGranted() else {
39
+ cb.onError(screenRecordingSentence)
40
+ return
41
+ }
42
+ let want = p
43
+ Task {
44
+ do {
45
+ let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
46
+ let (filter, g, wid) = try self.resolve(content, want)
47
+ let s = SCStream(filter: filter, configuration: self.config(g, want), delegate: self)
48
+ try s.addStreamOutput(self, type: .screen, sampleHandlerQueue: self.q)
49
+ try await s.startCapture()
50
+ self.q.async {
51
+ if self.stopped {
52
+ s.stopCapture { _ in }
53
+ return
54
+ }
55
+ self.stream = s
56
+ self.windowID = wid
57
+ self.setGeometry(g)
58
+ if self.p != want { self.update(self.p) } // params moved on while we were starting
59
+ if wid != nil { self.startFollowing() }
60
+ }
61
+ } catch {
62
+ self.q.async { self.cb.onError(Self.sentence(for: error)) }
63
+ }
64
+ }
65
+ }
66
+
67
+ func update(_ np: Params) {
68
+ let old = p
69
+ p = np
70
+ guard let s = stream, !retargeting else { return }
71
+ if case .window(let a) = old.source, case .window(let b) = np.source, a != b {
72
+ retarget(to: b)
73
+ return
74
+ }
75
+ guard let g = geo else { return }
76
+ let ng = geometry(srcWidth: g.srcWidth, srcHeight: g.srcHeight, bounds: g.globalBounds)
77
+ s.updateConfiguration(config(ng, np)) { err in
78
+ if let err { self.q.async { self.cb.onError("capture could not change settings: \(err.localizedDescription)") } }
79
+ }
80
+ setGeometry(ng)
81
+ }
82
+
83
+ func stop() {
84
+ stopped = true
85
+ followTimer?.cancel()
86
+ followTimer = nil
87
+ stream?.stopCapture { _ in }
88
+ stream = nil
89
+ }
90
+
91
+ // MARK: SCK callbacks
92
+
93
+ func stream(_ stream: SCStream, didOutputSampleBuffer sb: CMSampleBuffer, of type: SCStreamOutputType) {
94
+ guard type == .screen, sb.isValid,
95
+ let atts = CMSampleBufferGetSampleAttachmentsArray(sb, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]],
96
+ let raw = atts.first?[.status] as? Int, let status = SCFrameStatus(rawValue: raw)
97
+ else { return }
98
+ switch status {
99
+ case .complete:
100
+ guard let pb = CMSampleBufferGetImageBuffer(sb) else { return }
101
+ let t = CMSampleBufferGetPresentationTimeStamp(sb)
102
+ let us = t.isValid && t.seconds > 0 ? UInt64(t.seconds * 1e6) : nowMicros()
103
+ cb.onFrame(pb, us)
104
+ case .idle:
105
+ cb.onIdle()
106
+ default:
107
+ break
108
+ }
109
+ }
110
+
111
+ func stream(_ stream: SCStream, didStopWithError error: Error) {
112
+ q.async {
113
+ if self.stopped { return }
114
+ self.cb.onError("capture stopped: " + Self.sentence(for: error))
115
+ }
116
+ }
117
+
118
+ // MARK: geometry
119
+
120
+ private func geometry(srcWidth: Int, srcHeight: Int, bounds: CGRect) -> Geometry {
121
+ let (w, h) = outputSize(srcW: srcWidth, srcH: srcHeight, crop: p.crop, maxWidth: p.maxWidth, codec: p.codec)
122
+ return Geometry(srcWidth: srcWidth, srcHeight: srcHeight, globalBounds: bounds, outWidth: w, outHeight: h)
123
+ }
124
+
125
+ private func setGeometry(_ g: Geometry) {
126
+ if g != geo {
127
+ geo = g
128
+ cb.onGeometry(g)
129
+ }
130
+ }
131
+
132
+ private func resolve(_ content: SCShareableContent, _ want: Params) throws -> (SCContentFilter, Geometry, CGWindowID?) {
133
+ switch want.source {
134
+ case .display(let id):
135
+ let did = id ?? CGMainDisplayID()
136
+ guard let d = content.displays.first(where: { $0.displayID == did }) else {
137
+ throw CaptureError(sentence: "Display \(did) is not active on the Mac.")
138
+ }
139
+ let mode = CGDisplayCopyDisplayMode(did)
140
+ let filter = SCContentFilter(display: d, excludingWindows: [])
141
+ let g = geometry(srcWidth: mode?.pixelWidth ?? d.width, srcHeight: mode?.pixelHeight ?? d.height,
142
+ bounds: CGDisplayBounds(did))
143
+ return (filter, g, nil)
144
+ case .window(let wid):
145
+ return try windowFilter(content, wid)
146
+ case .front:
147
+ guard let (wid, _) = windowBounds(nil) else { throw CaptureError(sentence: "No window is in front on the Mac.") }
148
+ return try windowFilter(content, wid)
149
+ case .synthetic:
150
+ throw CaptureError(sentence: "internal: the synthetic source is not a screen capture")
151
+ }
152
+ }
153
+
154
+ private func windowFilter(_ content: SCShareableContent, _ wid: CGWindowID) throws -> (SCContentFilter, Geometry, CGWindowID?) {
155
+ guard let w = content.windows.first(where: { $0.windowID == wid }) else {
156
+ throw CaptureError(sentence: "Window \(wid) is not on screen any more.")
157
+ }
158
+ let filter = SCContentFilter(desktopIndependentWindow: w)
159
+ let scale = Double(filter.pointPixelScale)
160
+ let bounds = windowBounds(wid)?.1 ?? w.frame
161
+ let g = geometry(srcWidth: Int(bounds.width * scale), srcHeight: Int(bounds.height * scale), bounds: bounds)
162
+ return (filter, g, wid)
163
+ }
164
+
165
+ private func config(_ g: Geometry, _ p: Params) -> SCStreamConfiguration {
166
+ let c = SCStreamConfiguration()
167
+ c.width = g.outWidth
168
+ c.height = g.outHeight
169
+ c.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(p.fps))
170
+ c.queueDepth = 3
171
+ c.showsCursor = p.cursor
172
+ c.capturesAudio = false
173
+ c.pixelFormat = pixelFormat(for: p.codec)
174
+ if p.codec == .h264 { c.colorMatrix = CGDisplayStream.yCbCrMatrix_ITU_R_709_2 }
175
+ // sourceRect is in points of the source (display-local or window-local).
176
+ let pw = g.globalBounds.width, ph = g.globalBounds.height
177
+ c.sourceRect = CGRect(x: p.crop.x * pw, y: p.crop.y * ph, width: p.crop.w * pw, height: p.crop.h * ph)
178
+ c.scalesToFit = true
179
+ return c
180
+ }
181
+
182
+ // MARK: following a window / the front window (≤1 Hz)
183
+
184
+ private func startFollowing() {
185
+ followTimer?.cancel()
186
+ let t = DispatchSource.makeTimerSource(queue: q)
187
+ t.schedule(deadline: .now() + 1, repeating: 1, leeway: .milliseconds(100))
188
+ t.setEventHandler { [weak self] in self?.follow() }
189
+ t.resume()
190
+ followTimer = t
191
+ }
192
+
193
+ private func follow() {
194
+ guard stream != nil, !retargeting, let g = geo else { return }
195
+ if case .front = p.source, let (wid, _) = windowBounds(nil), wid != windowID {
196
+ retarget(to: wid)
197
+ return
198
+ }
199
+ guard let wid = windowID else { return }
200
+ guard let (_, rect) = windowBounds(wid) else {
201
+ if case .front = p.source { return } // the next poll picks the new front window
202
+ cb.onError("The captured window was closed.")
203
+ stop()
204
+ return
205
+ }
206
+ if rect != g.globalBounds {
207
+ let scale = g.globalBounds.width > 0 ? Double(g.srcWidth) / g.globalBounds.width : 2
208
+ let ng = geometry(srcWidth: Int(rect.width * scale), srcHeight: Int(rect.height * scale), bounds: rect)
209
+ stream?.updateConfiguration(config(ng, p)) { _ in }
210
+ setGeometry(ng)
211
+ }
212
+ }
213
+
214
+ private func retarget(to wid: CGWindowID) {
215
+ guard let s = stream else { return }
216
+ retargeting = true
217
+ let want = p
218
+ Task {
219
+ do {
220
+ let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
221
+ let (filter, g, _) = try self.windowFilter(content, wid)
222
+ try await s.updateContentFilter(filter)
223
+ try await s.updateConfiguration(self.config(g, want))
224
+ self.q.async {
225
+ self.retargeting = false
226
+ self.windowID = wid
227
+ self.setGeometry(g)
228
+ log("capture: now following window \(wid)")
229
+ }
230
+ } catch {
231
+ self.q.async {
232
+ self.retargeting = false
233
+ self.cb.onError(Self.sentence(for: error))
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ private static func sentence(for error: Error) -> String {
240
+ if let e = error as? CaptureError { return e.sentence }
241
+ let ns = error as NSError
242
+ if ns.domain == SCStreamErrorDomain && ns.code == SCStreamError.userDeclined.rawValue { return screenRecordingSentence }
243
+ return ns.localizedDescription
244
+ }
245
+ }
@@ -0,0 +1,226 @@
1
+ // H.264 through VideoToolbox: hardware, real-time, no B-frames, Annex-B out
2
+ // with SPS+PPS in front of every keyframe, so any record that carries the key
3
+ // flag is a complete entry point for a decoder that just joined.
4
+
5
+ import CoreMedia
6
+ import CoreVideo
7
+ import Foundation
8
+ import VideoToolbox
9
+
10
+ struct EncodedFrame {
11
+ let data: Data
12
+ let key: Bool
13
+ let pts: UInt64
14
+ /// Submit → output, µs. For JPEG this is the whole synchronous encode.
15
+ let encodeMicros: UInt64
16
+ let kind: RecordKind
17
+ /// `avc1.PPCCLL` from the SPS, on H.264 keyframes only.
18
+ let avc: String?
19
+ /// The region the frame was submitted for, carried through the encoder so late output still
20
+ /// says which config produced it.
21
+ let region: Region
22
+ }
23
+
24
+ protocol FrameEncoder: AnyObject {
25
+ var width: Int { get }
26
+ var height: Int { get }
27
+ var codec: Codec { get }
28
+ /// Frames submitted and not yet out. The server drops new frames past a few.
29
+ var inFlight: Int { get }
30
+ func encode(_ pb: CVPixelBuffer, pts: UInt64, forceKey: Bool, region: Region)
31
+ /// Same size and codec, new knobs (bitrate, fps, keyframe interval, quality…).
32
+ func update(_ p: Params)
33
+ func setBitrate(_ kbps: Int)
34
+ func invalidate()
35
+ }
36
+
37
+ final class H264Encoder: FrameEncoder {
38
+ let width: Int
39
+ let height: Int
40
+ let codec = Codec.h264
41
+ private(set) var profile = "main"
42
+ private(set) var hardware = false
43
+ private var session: VTCompressionSession?
44
+ private let sink: (EncodedFrame) -> Void
45
+ private let lock = NSLock()
46
+ private var submitted: [UInt64: UInt64] = [:]
47
+ private var pending = 0
48
+ private var params: Params
49
+
50
+ var inFlight: Int {
51
+ lock.lock()
52
+ defer { lock.unlock() }
53
+ return pending
54
+ }
55
+
56
+ init?(width: Int, height: Int, params: Params, sink: @escaping (EncodedFrame) -> Void) {
57
+ self.width = width
58
+ self.height = height
59
+ self.params = params
60
+ self.sink = sink
61
+ let spec: [String: Any] = [
62
+ kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder as String: true
63
+ ]
64
+ var s: VTCompressionSession?
65
+ let st = VTCompressionSessionCreate(
66
+ allocator: nil, width: Int32(width), height: Int32(height), codecType: kCMVideoCodecType_H264,
67
+ encoderSpecification: spec as CFDictionary, imageBufferAttributes: nil, compressedDataAllocator: nil,
68
+ outputCallback: nil, refcon: nil, compressionSessionOut: &s)
69
+ guard st == noErr, let s else {
70
+ log("h264: VTCompressionSessionCreate \(width)x\(height) failed (\(st))")
71
+ return nil
72
+ }
73
+ session = s
74
+ set(kVTCompressionPropertyKey_RealTime, kCFBooleanTrue)
75
+ set(kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse)
76
+ if set(kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel, quiet: true) != noErr {
77
+ set(kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel)
78
+ profile = "baseline"
79
+ } else {
80
+ set(kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CABAC, quiet: true)
81
+ }
82
+ set(kVTCompressionPropertyKey_MaxFrameDelayCount, 0 as NSNumber, quiet: true)
83
+ set(kVTCompressionPropertyKey_PrioritizeEncodingSpeedOverQuality, kCFBooleanTrue, quiet: true)
84
+ applyRate(params.bitrateKbps)
85
+ applyTiming(params)
86
+ var hw: Unmanaged<CFTypeRef>?
87
+ if VTSessionCopyProperty(s, key: kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder,
88
+ allocator: nil, valueOut: &hw) == noErr, let b = hw?.takeRetainedValue() as? Bool
89
+ {
90
+ hardware = b
91
+ }
92
+ VTCompressionSessionPrepareToEncodeFrames(s)
93
+ log("h264: \(width)x\(height) \(profile) \(hardware ? "hardware" : "software") \(params.bitrateKbps) kbps \(params.fps) fps")
94
+ }
95
+
96
+ deinit { invalidate() }
97
+
98
+ @discardableResult
99
+ private func set(_ k: CFString, _ v: CFTypeRef?, quiet: Bool = false) -> OSStatus {
100
+ guard let session else { return -1 }
101
+ let st = VTSessionSetProperty(session, key: k, value: v)
102
+ if st != noErr && !quiet { log("h264: setting \(k) failed (\(st))") }
103
+ return st
104
+ }
105
+
106
+ private func applyRate(_ kbps: Int) {
107
+ let bps = kbps * 1000
108
+ set(kVTCompressionPropertyKey_AverageBitRate, bps as NSNumber)
109
+ // Hard ceiling: 1.5× the average over any 1 s window.
110
+ let limits = [NSNumber(value: Double(bps) / 8 * 1.5), NSNumber(value: 1.0)] as CFArray
111
+ set(kVTCompressionPropertyKey_DataRateLimits, limits)
112
+ }
113
+
114
+ private func applyTiming(_ p: Params) {
115
+ set(kVTCompressionPropertyKey_ExpectedFrameRate, p.fps as NSNumber)
116
+ if p.keyframeEverySec > 0 {
117
+ set(kVTCompressionPropertyKey_MaxKeyFrameInterval, max(1, Int((p.keyframeEverySec * Double(p.fps)).rounded())) as NSNumber)
118
+ set(kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, p.keyframeEverySec as NSNumber)
119
+ } else {
120
+ // On demand only: an interval nobody reaches (a day of frames at 60 fps).
121
+ set(kVTCompressionPropertyKey_MaxKeyFrameInterval, 5_184_000 as NSNumber)
122
+ set(kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, 86_400 as NSNumber)
123
+ }
124
+ }
125
+
126
+ func update(_ p: Params) {
127
+ if p.bitrateKbps != params.bitrateKbps { applyRate(p.bitrateKbps) }
128
+ if p.fps != params.fps || p.keyframeEverySec != params.keyframeEverySec { applyTiming(p) }
129
+ params = p
130
+ }
131
+
132
+ func setBitrate(_ kbps: Int) {
133
+ params.bitrateKbps = kbps
134
+ applyRate(kbps)
135
+ }
136
+
137
+ func invalidate() {
138
+ if let s = session { VTCompressionSessionInvalidate(s) }
139
+ session = nil
140
+ }
141
+
142
+ func encode(_ pb: CVPixelBuffer, pts: UInt64, forceKey: Bool, region: Region) {
143
+ guard let s = session else { return }
144
+ let t = CMTime(value: CMTimeValue(pts), timescale: 1_000_000)
145
+ let props: CFDictionary? =
146
+ forceKey ? [kVTEncodeFrameOptionKey_ForceKeyFrame as String: true] as CFDictionary : nil
147
+ lock.lock()
148
+ submitted[pts] = nowMicros()
149
+ pending += 1
150
+ lock.unlock()
151
+ let st = VTCompressionSessionEncodeFrame(
152
+ s, imageBuffer: pb, presentationTimeStamp: t, duration: .invalid, frameProperties: props, infoFlagsOut: nil
153
+ ) { [weak self] status, flags, sb in
154
+ self?.output(status, flags, sb, pts, region)
155
+ }
156
+ if st != noErr {
157
+ log("h264: encode failed (\(st))")
158
+ lock.lock()
159
+ submitted[pts] = nil
160
+ pending -= 1
161
+ lock.unlock()
162
+ }
163
+ }
164
+
165
+ private func output(_ status: OSStatus, _ flags: VTEncodeInfoFlags, _ sb: CMSampleBuffer?, _ pts: UInt64, _ region: Region) {
166
+ lock.lock()
167
+ let sub = submitted.removeValue(forKey: pts)
168
+ pending -= 1
169
+ lock.unlock()
170
+ guard status == noErr, let sb, let bb = CMSampleBufferGetDataBuffer(sb) else {
171
+ if status != noErr { log("h264: encoder returned \(status)") }
172
+ return
173
+ }
174
+ var key = true
175
+ if let arr = CMSampleBufferGetSampleAttachmentsArray(sb, createIfNecessary: false) as? [[CFString: Any]],
176
+ let a = arr.first, let notSync = a[kCMSampleAttachmentKey_NotSync] as? Bool
177
+ {
178
+ key = !notSync
179
+ }
180
+ let total = CMBlockBufferGetDataLength(bb)
181
+ var avcc = Data(count: total)
182
+ let copied = avcc.withUnsafeMutableBytes { CMBlockBufferCopyDataBytes(bb, atOffset: 0, dataLength: total, destination: $0.baseAddress!) }
183
+ guard copied == kCMBlockBufferNoErr else { return }
184
+
185
+ let startCode: [UInt8] = [0, 0, 0, 1]
186
+ var out = Data(capacity: total + 64)
187
+ var nalLen: Int32 = 4
188
+ var avc: String?
189
+ if let fd = CMSampleBufferGetFormatDescription(sb) {
190
+ var count = 0
191
+ CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
192
+ fd, parameterSetIndex: 0, parameterSetPointerOut: nil, parameterSetSizeOut: nil,
193
+ parameterSetCountOut: &count, nalUnitHeaderLengthOut: &nalLen)
194
+ if key {
195
+ for i in 0..<count {
196
+ var p: UnsafePointer<UInt8>?
197
+ var n = 0
198
+ guard CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
199
+ fd, parameterSetIndex: i, parameterSetPointerOut: &p, parameterSetSizeOut: &n,
200
+ parameterSetCountOut: nil, nalUnitHeaderLengthOut: nil) == noErr, let p
201
+ else { continue }
202
+ out.append(contentsOf: startCode)
203
+ out.append(p, count: n)
204
+ if i == 0 && n >= 4 { avc = String(format: "avc1.%02x%02x%02x", p[1], p[2], p[3]) }
205
+ }
206
+ }
207
+ }
208
+ // AVCC length prefixes → Annex-B start codes.
209
+ let L = Int(nalLen)
210
+ avcc.withUnsafeBytes { raw in
211
+ let b = raw.bindMemory(to: UInt8.self)
212
+ var off = 0
213
+ while off + L <= total {
214
+ var len = 0
215
+ for i in 0..<L { len = (len << 8) | Int(b[off + i]) }
216
+ off += L
217
+ guard len > 0, off + len <= total else { break }
218
+ out.append(contentsOf: startCode)
219
+ out.append(b.baseAddress! + off, count: len)
220
+ off += len
221
+ }
222
+ }
223
+ let enc = sub.map { nowMicros() &- $0 } ?? 0
224
+ sink(EncodedFrame(data: out, key: key, pts: pts, encodeMicros: enc, kind: .h264, avc: avc, region: region))
225
+ }
226
+ }
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleIdentifier</key>
6
+ <string>@WP_DESKTOP_ID@</string>
7
+ <key>CFBundleName</key>
8
+ <string>wp-desktop</string>
9
+ <key>CFBundleDisplayName</key>
10
+ <string>@WP_DESKTOP_NAME@</string>
11
+ <key>CFBundleShortVersionString</key>
12
+ <string>0.1.0</string>
13
+ <key>CFBundleVersion</key>
14
+ <string>1</string>
15
+ <key>CFBundleInfoDictionaryVersion</key>
16
+ <string>6.0</string>
17
+ <key>LSMinimumSystemVersion</key>
18
+ <string>14.0</string>
19
+ </dict>
20
+ </plist>