@linkshell/mac 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -47
- package/build/LinkShell.app.tar.gz +0 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,7 +7,38 @@ unix socket; nobody starts it by hand.
|
|
|
7
7
|
|
|
8
8
|
It has no place in the Dock (`LSUIElement`) and one window, shown only when asked for
|
|
9
9
|
(`--setup`): the one in which the user gives it its two permissions. macOS 13 or later, on
|
|
10
|
-
Apple silicon
|
|
10
|
+
Apple silicon and Intel. The universal app and its WebRTC framework contain both arm64 and x86_64;
|
|
11
|
+
macOS chooses the native architecture at launch. No separate installer or Rosetta is needed.
|
|
12
|
+
Intel real-hardware capture, encoding and end-to-end performance validation is pending.
|
|
13
|
+
|
|
14
|
+
## Current media path
|
|
15
|
+
|
|
16
|
+
ScreenCaptureKit produces NV12 `CVPixelBuffer`s backed by IOSurface. The video track excludes
|
|
17
|
+
the cursor. `ScreenEncoderFactory` defaults to the app's low-latency VideoToolbox H.264
|
|
18
|
+
adapter, retaining libwebrtc's transport, bandwidth feedback, NACK/RTX and negotiated FlexFEC.
|
|
19
|
+
Encoder initialization/operation failures fall back to stock H.264; `--encoder stock` is the
|
|
20
|
+
explicit comparison path. `playout-delay` 0/0 asks for prompt display, not zero end-to-end delay.
|
|
21
|
+
The attempted `MaxFrameDelayCount=0` property is not supported by the hardware in the recorded
|
|
22
|
+
checks and does not establish zero encoder queueing.
|
|
23
|
+
|
|
24
|
+
The media peer is an iOS native receiver or a web player, independently of the Node host's
|
|
25
|
+
werift bulk-data connection. iOS normally decodes H.264 through VideoToolbox into a latest-frame
|
|
26
|
+
mailbox, then maps the pixel buffer to Metal. Its transparent WebView still owns gestures,
|
|
27
|
+
controls and the cursor. It requests a ceiling of 120 fps, bounded by both displays, power,
|
|
28
|
+
thermal state and adaptation; standard receivers retain the 60/30 defaults. No video frames
|
|
29
|
+
cross JS. The native module shares the app's JitsiWebRTC M124 framework; this sender uses M154,
|
|
30
|
+
so codec/extension support must be established by the actual negotiation.
|
|
31
|
+
|
|
32
|
+
Failed media falls back to the host's H.264 byte stream. iOS first tries the standard WebView
|
|
33
|
+
receiver and then bypasses the bulk direct channel for that screen, using RPC. Android/Web
|
|
34
|
+
retain their existing stream path. Both Mac capture paths use ScreenCaptureKit/VideoToolbox;
|
|
35
|
+
only the byte stream includes the cursor in the picture. There is no TURN service.
|
|
36
|
+
|
|
37
|
+
See the [current architecture and diagrams](../../docs/v2/screen-realtime.md),
|
|
38
|
+
[iOS receiver](../client/modules/link-screen/README.md) and
|
|
39
|
+
[historical measurements](../../docs/v2/screen-realtime-history.md). Stable 120 fps, stable
|
|
40
|
+
4K/60 and phone-side FlexFEC recovery are not established; the recorded periodic stutter is
|
|
41
|
+
still unresolved. These are validation limits, not reasons to change defaults without evidence.
|
|
11
42
|
|
|
12
43
|
## Why it is an app
|
|
13
44
|
|
|
@@ -114,7 +145,7 @@ sleeping when the lid is closed or the user chooses Sleep.
|
|
|
114
145
|
A package manager can't carry the app as it is: npm leaves out the framework's symbolic links,
|
|
115
146
|
pnpm the program's permission to run. The package carries one file,
|
|
116
147
|
`build/LinkShell.app.tar.gz`; `@linkshell/host` has this package as an optional dependency
|
|
117
|
-
(`os: darwin`, `cpu: arm64`) and unpacks the archive with `/usr/bin/tar -xzf` into `~/.linkshell/LinkShell.app`,
|
|
148
|
+
(`os: darwin`, `cpu: arm64, x64`) and unpacks the archive with `/usr/bin/tar -xzf` into `~/.linkshell/LinkShell.app`,
|
|
118
149
|
once per archive (by its SHA-256). In a checkout the host uses `build/LinkShell.app` directly.
|
|
119
150
|
|
|
120
151
|
## Build and check
|
|
@@ -128,10 +159,10 @@ pnpm test # swift test: what needs no screen and no netw
|
|
|
128
159
|
pnpm check # that, then pass or fail for the built app, in about a minute
|
|
129
160
|
```
|
|
130
161
|
|
|
131
|
-
`build-app.mjs [--out <directory>]` builds
|
|
162
|
+
`build-app.mjs [--out <directory>] [--scratch-path <directory>]` builds a universal arm64/x86_64 executable, embeds both framework architectures, checks that neither is missing,
|
|
132
163
|
signs from the inside out (hardened runtime, timestamp), swaps the
|
|
133
164
|
finished bundle into place, packs the archive and proves that the archive unpacks into an app
|
|
134
|
-
whose signature verifies. It signs with the "Developer ID Application" identity in the keychain,
|
|
165
|
+
whose signature verifies for both architectures. It signs with the "Developer ID Application" identity in the keychain,
|
|
135
166
|
or the one `LINKSHELL_SIGN_IDENTITY` names. Without one it signs ad hoc: that app runs, but is
|
|
136
167
|
another app to the system, with permissions of its own — not for release.
|
|
137
168
|
`LINKSHELL_REQUIRE_SIGNED=1` (set by `prepack`) makes a missing identity fatal. The app is not
|
|
@@ -147,6 +178,7 @@ already, and stop if it is not rather than have the system ask.
|
|
|
147
178
|
| `stream-check.mjs` | The stream for the host's socket: record format, rates, key frames, change of size, beside a video track; decoded by ffmpeg where installed, and by Chrome's WebCodecs unless `--no-chrome`. Part of `check`. |
|
|
148
179
|
| `app-smoke.mjs` | Opens the app, asks for an offer, says what it holds. Part of `check`. |
|
|
149
180
|
| `loopback.mjs` | The video track with no viewer: the app answers its own offer, and both ends' numbers are printed. Without `--fps`, at the app's own choice of frame rate; `--fps 30,60` holds each of those. `--narrow <bit/s>:<from second>:<for seconds>` holds the bandwidth estimate down for a while, prints every second, and says what each change of frame rate cost; `--timeline`, `--seconds 10`, `--motion`, `--still`, `--encoder own\|stock`, `--json <file>`; the rest in its header. |
|
|
181
|
+
| `flexfec-check.mjs` | A short functional check over its own loopback UDP proxy: 60 ms each way, every twentieth original video packet dropped. Checks real FlexFEC packet receipt, then successful video without FlexFEC when the test receiver declines it. `--json <file>` saves the evidence. No system network changes or latency claims. |
|
|
150
182
|
| `setup-shots.mjs` | The setup window drawn into PNG files in every state, light and dark, Chinese and English (24 of them, in `build/setup-shots` or the directory named), with nothing shown and nothing asked of the system. For looking at the window after changing it. |
|
|
151
183
|
| `app.mjs`, `h264.mjs`, `pointer-shapes.swift` | Shared by the above: opening the app, reading H.264, changing the pointer's picture. |
|
|
152
184
|
|
|
@@ -168,9 +200,12 @@ The host's own tests run its real code against this app: `packages/host/test/app
|
|
|
168
200
|
| `--clock` | Shows the clock strip on a display while it is sent (see Measuring). `status` gains `clock`. |
|
|
169
201
|
| `--motion` | Shows a window of scrolling text and a sliding block on a display while it is sent: a busy screen's work for the encoder, the same every run. |
|
|
170
202
|
| `--loopback` | Each `rtc.open` is answered inside the app by a second connection that decodes the track. Adds `rtc.loopback*` (below). |
|
|
171
|
-
| `--encoder own\|stock` | The video track's H.264 encoder: the app's low-latency one (`LowLatencyEncoder.swift`), or libwebrtc's VideoToolbox encoder with the level fixed (`Encoders.swift`). Default `stock
|
|
203
|
+
| `--encoder own\|stock` | The video track's H.264 encoder: the app's low-latency one (`LowLatencyEncoder.swift`), or libwebrtc's VideoToolbox encoder with the level fixed (`Encoders.swift`). Default `own`; initialization or encoding failures switch to stock for the rest of that encoder's lifetime. |
|
|
172
204
|
| `--no-low-latency` | Never asks VideoToolbox for the low-latency rate control: encodes as a Mac without it does. |
|
|
173
205
|
| `--no-playout-delay` | Leaves out the field trial that sends playout-delay 0. |
|
|
206
|
+
| `--no-flexfec` | Disables FlexFEC advertisement and sending for an explicit comparison. |
|
|
207
|
+
| `--loopback-no-flexfec` | Makes the test receiver decline FlexFEC through codec preferences, to check an unsupported viewer's path. |
|
|
208
|
+
| `--loopback-network` | Only with `--loopback`: passes its ICE candidates to `flexfec-check.mjs`, which supplies proxy candidates. The proxy exists only while that tool runs. |
|
|
174
209
|
| `--trial <name>=<value>` | Another libwebrtc field trial (repeatable). |
|
|
175
210
|
| `--rtc-log` | libwebrtc's log, as `log` messages. |
|
|
176
211
|
|
|
@@ -230,12 +265,12 @@ and takes its place; what is then said for the earlier `v` is dropped.
|
|
|
230
265
|
|
|
231
266
|
| | Message | |
|
|
232
267
|
|---|---|---|
|
|
233
|
-
| → | `{t:"rtc.open", v, screen?, iceServers?, maxWidth?, fps?, codec?}` | `iceServers`: `[{urls, username?, credential?}]`, `urls` a string or
|
|
268
|
+
| → | `{t:"rtc.open", v, screen?, iceServers?, maxWidth?, fps?, maxFps?, codec?}` | `iceServers`: `[{urls, username?, credential?}]`, `urls` a string or list. `maxWidth`: default 1920, clamped to 160–3840 and bounded by the source display. `maxFps`: receiver ceiling, 30 / 60 / 120, default 60; automatic rate selection also considers the source display. `fps`: optional fixed 1–120 rate, overriding automatic selection (internal comparison). `codec`: `"h264"` (default) or `"hevc"`, which this build cannot encode; H.264 is offered and a log explains why. |
|
|
234
269
|
| ← | `{t:"rtc.offer", v, sdp}` | The app offers: one send-only video track, and the four data channels. |
|
|
235
270
|
| → | `{t:"rtc.answer", v, sdp}` | |
|
|
236
271
|
| ↔ | `{t:"rtc.ice", v, candidate, sdpMid, sdpMLineIndex}` | Trickled, either way. Gathering goes on for the life of the session, so a viewer that changes network is found again. |
|
|
237
272
|
| ← | `{t:"rtc.state", v, ice, connection}` | On every change of either, named as a browser names them. |
|
|
238
|
-
| ← | `{t:"rtc.stats", v, …}` | Once a second: `bitrate` (bit/s), `fps` (frames encoded), `frameRate` (the rate in force: 60
|
|
273
|
+
| ← | `{t:"rtc.stats", v, …}` | Once a second: `bitrate` (bit/s), `fps` (frames encoded), `frameRate` (the rate in force: up to 120 / 60 / 30 subject to source and receiver limits, or the fixed `fps` requested), `frameRateReason` (why it last changed, as the `log` said it; `null` while it hasn't), `width`, `height`, `encoder`, `hardware`, `codec`, `fmtp`, `keyFrames`, `qualityLimitation`, `targetBitrate`, `availableOutgoing`, `rttMs`, `packetsLost`, `nack`, `pli`, `encodeMs`, `sendDelayMs`, `hugeFrames`, `captureFps`, `repeated` (the last picture sent again), `gapMs` (the longest libwebrtc went without a picture, new or repeated), `heard` (messages received on each channel), `clock` (with `--clock`: the strip as captured against the time it was displayed, `{n, unreadable, min, median, max}` in ms). A number libwebrtc doesn't have is `null`. |
|
|
239
274
|
| ← | `{t:"rtc.error", v, message}` | The session has ended, and why. |
|
|
240
275
|
| → | `{t:"rtc.close", v}` | |
|
|
241
276
|
|
|
@@ -247,15 +282,42 @@ be made`, `the video could not be added`, `no offer: …`, `the offer was not se
|
|
|
247
282
|
The picture has no pointer in it; the viewer draws one from `cursor` and `shape`. A failed
|
|
248
283
|
connection does not end the session: the host closes it.
|
|
249
284
|
|
|
285
|
+
`rtc.stats.flexfec` reports SDP evidence: `{state, payloadType, mediaSSRC, repairSSRC}`. The
|
|
286
|
+
state is `not-offered`, `awaiting-answer`, `declined`, `missing-ssrc` or `negotiated`. The last
|
|
287
|
+
means the answer accepted the codec and the offer has an FEC-FR protection group; it does not
|
|
288
|
+
claim that protection packets have been needed or that a loss was repaired.
|
|
289
|
+
|
|
250
290
|
With `--loopback`: `← {t:"rtc.loopback", v, …}` once a second (the receiving end's numbers,
|
|
251
291
|
among them `gapMs`, the longest it went without a decoded frame, and with `--clock` `latency`
|
|
252
|
-
`{n, min, p50, p95, max}`: glass to decoded, ms)
|
|
292
|
+
`{n, min, p50, p95, max}`: glass to decoded, ms). `fecPacketsReceived` and `fecBytesReceived`
|
|
293
|
+
are the receiver's protection counters (`null` when unavailable), not recovery counts.
|
|
294
|
+
The test control messages are
|
|
253
295
|
`→ {t:"rtc.loopback.limit", v, bitrate?}` to hold the sender's bandwidth estimate to so many
|
|
254
296
|
bits a second, as a narrow network does (without its loss and delay), or without `bitrate` to
|
|
255
297
|
let it go,
|
|
256
298
|
`← {t:"rtc.loopback.channel", v, channel}` when a channel opens there,
|
|
257
299
|
`→ {t:"rtc.loopback.send", v, channel, events:[…]}` to send messages up one, and
|
|
258
300
|
`← {t:"rtc.loopback.heard", v, channel, bytes, message}` for each that came down one.
|
|
301
|
+
With `--loopback-network`, `↔ {t:"rtc.loopback.ice", v, candidate, sdpMid, sdpMLineIndex}`
|
|
302
|
+
exchanges the test receiver's ICE candidates with the tool. Normal viewers never use it.
|
|
303
|
+
|
|
304
|
+
**FlexFEC.** M154 needs both `WebRTC-FlexFEC-03-Advertised=Enabled` (send codec) and
|
|
305
|
+
`WebRTC-FlexFEC-03=Enabled` (protection SSRC and sending). They are on by default. Codec
|
|
306
|
+
preferences retain `flexfec-03`; libwebrtc negotiates it normally and produces the FEC-FR
|
|
307
|
+
group for our single video stream. A viewer which declines it continues with H.264 and
|
|
308
|
+
NACK/RTX. There is no SDP rewriting to pretend a viewer supports it. See M154's
|
|
309
|
+
[video engine](https://webrtc.googlesource.com/src/+/refs/branch-heads/8037/media/engine/webrtc_video_engine.cc),
|
|
310
|
+
[stream negotiation](https://webrtc.googlesource.com/src/+/refs/branch-heads/8037/pc/media_session.cc), and
|
|
311
|
+
[FEC sender creation](https://webrtc.googlesource.com/src/+/refs/branch-heads/8037/call/rtp_video_sender.cc).
|
|
312
|
+
|
|
313
|
+
Upstream [M124](https://webrtc.googlesource.com/src/+/refs/branch-heads/6367/media/engine/webrtc_video_engine.cc)
|
|
314
|
+
already exposes FlexFEC receive support unless explicitly disabled; the installed Jitsi build
|
|
315
|
+
and WKWebView still have to prove their capability in their actual answers. A clean network
|
|
316
|
+
can negotiate FlexFEC and send no repair packets, because M154's protection controller uses
|
|
317
|
+
loss and RTT to choose redundancy. The advertised `repair-window=10000000` is a required
|
|
318
|
+
format parameter which this upstream implementation does not use as a ten-second playback
|
|
319
|
+
wait. `flexfec-check.mjs` checks actual packet receipt under injected loss; it does not measure
|
|
320
|
+
real-device recovery, visual stalls or latency. Those remain phone acceptance checks.
|
|
259
321
|
|
|
260
322
|
### The data channels
|
|
261
323
|
|
|
@@ -291,7 +353,7 @@ and a stream beside an `rtc` session, run independently.
|
|
|
291
353
|
|
|
292
354
|
| | Message | |
|
|
293
355
|
|---|---|---|
|
|
294
|
-
| → | `{t:"stream.open", v, socket, screen?, width?, fps?, bitrate?, ceiling?, gop?, profile?}` | `socket`: the path the app connects to and writes records on. `width`: the widest the picture may be, default 1920, at least 160. `fps`: default 30, 1–
|
|
356
|
+
| → | `{t:"stream.open", v, socket, screen?, width?, fps?, bitrate?, ceiling?, gop?, profile?}` | `socket`: the path the app connects to and writes records on. `width`: the widest the picture may be, default 1920, at least 160. `fps`: default 30, 1–120 at this helper interface; the host's compatibility ladder requests only 8–20 fps. `bitrate`: bit/s on average, default 2 000 000, at least 50 000. `ceiling`: the most in any one second, never below `bitrate`. `gop`: seconds between key frames at most, default 1, at least 0.2. `profile`: `"baseline"` or `"high"`; left out, High from the low-latency encoder and Baseline from the usual one. |
|
|
295
357
|
| → | `{t:"stream.set", v, width?, fps?, bitrate?, ceiling?}` | Takes effect without the stream stopping. |
|
|
296
358
|
| → | `{t:"stream.key", v}` | The next frame is a key frame, and goes now. |
|
|
297
359
|
| ← | `{t:"stream.started", v, width, height, fps, generation, mode, profile, hardware}` | After the first frame of each generation is written. `mode`: `"low-latency"` or `"real-time"`, the rate control the encoder gave. |
|
|
@@ -328,44 +390,48 @@ again once.
|
|
|
328
390
|
|
|
329
391
|
## Tuning
|
|
330
392
|
|
|
331
|
-
`Sources/LinkShell/Tuning.swift`
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
(
|
|
393
|
+
`Sources/LinkShell/Tuning.swift` defines the shared limits. The video defaults to a maximum
|
|
394
|
+
width of 1920, with a 3840 cap; an ordinary receiver gets a 60 fps ceiling, while `maxFps`
|
|
395
|
+
can request 30 / 60 / 120 without disabling adaptation. The source display bounds the starting
|
|
396
|
+
rate. The helper's separate byte-stream interface defaults to 30 fps, but `screen-pacer.ts`
|
|
397
|
+
selects its own 8–20 fps ladder (1280-wide / 12 fps / 900 kbit/s target when `q=low`). These
|
|
398
|
+
are different paths; selecting a video width does not override the compatibility ladder.
|
|
399
|
+
|
|
400
|
+
The video bitrate ceiling starts from 8 Mbit/s for 1920×1080 at 30 fps. Pixel counts below
|
|
401
|
+
1080p scale proportionally; above it they scale by their square root. The frame factor is 1
|
|
402
|
+
through 30 fps, 1.5 through 60, and `1.5 × fps / 60` above 60. The result is clamped to
|
|
403
|
+
2–30 Mbit/s: 1080p30/60/120 ceilings are 8/12/24 Mbit/s. Actual bitrate follows WebRTC's
|
|
404
|
+
estimate, initialized at 2 Mbit/s, and the content.
|
|
405
|
+
|
|
406
|
+
`maintainFramerate` permits WebRTC to reduce resolution under resource pressure. Separately,
|
|
407
|
+
`ScreenEncoderFactory.scaling` returns no QP thresholds, suppressing QP-only quality scaling
|
|
408
|
+
that unnecessarily shrank text. `FrameRate.swift` judges the connection once a second and
|
|
409
|
+
reduces rate when the full picture/rate cannot be carried:
|
|
410
|
+
|
|
411
|
+
- Start at the receiver/source ceiling, normally 120 or 60, with lower stages at 60 and 30
|
|
412
|
+
where applicable. A source below these rates bounds the starting rate further. Explicit
|
|
413
|
+
`fps` is a fixed internal override.
|
|
414
|
+
- Reduce after 3 consecutive troubled seconds: estimated bandwidth below the full-rate
|
|
415
|
+
ceiling × 0.2 × current/full, sent pixel share below 0.9, or encoded output below 0.75 of
|
|
416
|
+
the requested active input while capture supplies at least 0.8 of the current rate.
|
|
417
|
+
Packet loss alone and time per encoded frame are not independent triggers.
|
|
418
|
+
- Raise to the next stage after 10 good seconds: full picture, no quality limitation,
|
|
419
|
+
sufficient encoded output and estimated bandwidth at least the full-rate ceiling × 0.4
|
|
420
|
+
× next/full. If a raised rate fails again within 30 seconds, the next wait doubles, up
|
|
421
|
+
to 160 seconds.
|
|
422
|
+
- The first 4 seconds and the 4 after a change are settling periods.
|
|
423
|
+
|
|
424
|
+
Rate changes update ScreenCaptureKit, sender parameters and the bitrate ceiling without
|
|
425
|
+
renegotiating the media connection. A decrease requests restoration of the captured dimensions;
|
|
426
|
+
the sender can adapt them again later. An increase eases the first second's bitrate using
|
|
427
|
+
half the previous observed bitrate, with a 1 Mbit/s floor. Neither change guarantees physical
|
|
428
|
+
presentation cadence.
|
|
429
|
+
|
|
430
|
+
Other settings include capture rate slack (1.1×), capture surface queue depth (5), static-frame
|
|
431
|
+
repeats (0.1 s for a second, then 0.5 s), cursor position/shape polling (60/5 Hz), playout-delay
|
|
432
|
+
0/0 and FlexFEC field trials. Surface pool sizes are not measured queued-frame counts.
|
|
433
|
+
`VideoCompressor.swift` controls keyframe sizing, `Control.swift` input state and scroll bounds,
|
|
434
|
+
and `Keys.swift` the temporary input-source switch for text.
|
|
369
435
|
|
|
370
436
|
## Measuring
|
|
371
437
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkshell/mac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "LinkShell.app: the Mac side of the screen — capture, encoding, WebRTC and input, under the app's own permissions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"darwin"
|
|
14
14
|
],
|
|
15
15
|
"cpu": [
|
|
16
|
-
"arm64"
|
|
16
|
+
"arm64",
|
|
17
|
+
"x64"
|
|
17
18
|
],
|
|
18
19
|
"files": [
|
|
19
20
|
"build/LinkShell.app.tar.gz",
|