@houwert/conductor 0.24.1 → 0.26.0

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.
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ /**
3
+ * H.264 Annex B streaming parser (daemon side).
4
+ *
5
+ * Splits the raw Annex B byte stream from the capture backend into access units,
6
+ * extracts SPS/PPS to build the stream config, and preserves each access unit
7
+ * **in Annex B form** (start codes intact) for forwarding to subscribers — the
8
+ * wire contract is "config + a sequence of Annex B access units, keyframe-led".
9
+ *
10
+ * Ported from Argus's electron/services/simulator/h264-parser.ts; the SPS
11
+ * bit-reader is identical, but access units are emitted as Annex B rather than
12
+ * converted to AVCC length-prefix (that conversion is left to WebCodecs clients).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.H264AnnexBParser = void 0;
16
+ const NAL_SLICE = 1;
17
+ const NAL_IDR = 5;
18
+ const NAL_SPS = 7;
19
+ const NAL_PPS = 8;
20
+ // ── Exp-Golomb reader (minimal, for SPS parsing) ────────────────────────────
21
+ class ExpGolombReader {
22
+ constructor(data) {
23
+ this.data = data;
24
+ this.byteOffset = 0;
25
+ this.bitOffset = 0;
26
+ }
27
+ readBit() {
28
+ if (this.byteOffset >= this.data.length)
29
+ return 0;
30
+ const bit = (this.data[this.byteOffset] >> (7 - this.bitOffset)) & 1;
31
+ this.bitOffset++;
32
+ if (this.bitOffset === 8) {
33
+ this.bitOffset = 0;
34
+ this.byteOffset++;
35
+ }
36
+ return bit;
37
+ }
38
+ readBits(n) {
39
+ let val = 0;
40
+ for (let i = 0; i < n; i++)
41
+ val = (val << 1) | this.readBit();
42
+ return val;
43
+ }
44
+ readUE() {
45
+ let zeros = 0;
46
+ while (this.readBit() === 0 && zeros < 31)
47
+ zeros++;
48
+ if (zeros === 0)
49
+ return 0;
50
+ return (1 << zeros) - 1 + this.readBits(zeros);
51
+ }
52
+ readSE() {
53
+ const val = this.readUE();
54
+ return val % 2 === 0 ? -(val >> 1) : (val + 1) >> 1;
55
+ }
56
+ }
57
+ /** Parse the SPS fields needed for config. `body` is the NAL after the header byte. */
58
+ function parseSps(body) {
59
+ const r = new ExpGolombReader(body);
60
+ const profileIdc = r.readBits(8);
61
+ const constraintFlags = r.readBits(8);
62
+ const levelIdc = r.readBits(8);
63
+ r.readUE(); // seq_parameter_set_id
64
+ if ([100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134].includes(profileIdc)) {
65
+ const chromaFormatIdc = r.readUE();
66
+ if (chromaFormatIdc === 3)
67
+ r.readBits(1);
68
+ r.readUE(); // bit_depth_luma_minus8
69
+ r.readUE(); // bit_depth_chroma_minus8
70
+ r.readBits(1); // qpprime_y_zero_transform_bypass_flag
71
+ if (r.readBits(1)) {
72
+ const count = chromaFormatIdc !== 3 ? 8 : 12;
73
+ for (let i = 0; i < count; i++) {
74
+ if (r.readBits(1)) {
75
+ const size = i < 6 ? 16 : 64;
76
+ let lastScale = 8;
77
+ let nextScale = 8;
78
+ for (let j = 0; j < size; j++) {
79
+ if (nextScale !== 0)
80
+ nextScale = (lastScale + r.readSE() + 256) % 256;
81
+ lastScale = nextScale === 0 ? lastScale : nextScale;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ r.readUE(); // log2_max_frame_num_minus4
88
+ const picOrderCntType = r.readUE();
89
+ if (picOrderCntType === 0) {
90
+ r.readUE();
91
+ }
92
+ else if (picOrderCntType === 1) {
93
+ r.readBits(1);
94
+ r.readSE();
95
+ r.readSE();
96
+ const n = r.readUE();
97
+ for (let i = 0; i < n; i++)
98
+ r.readSE();
99
+ }
100
+ r.readUE(); // max_num_ref_frames
101
+ r.readBits(1); // gaps_in_frame_num_value_allowed_flag
102
+ const picWidthInMbsMinus1 = r.readUE();
103
+ const picHeightInMapUnitsMinus1 = r.readUE();
104
+ const frameMbsOnlyFlag = r.readBits(1);
105
+ let width = (picWidthInMbsMinus1 + 1) * 16;
106
+ let height = (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16;
107
+ if (!frameMbsOnlyFlag)
108
+ r.readBits(1);
109
+ r.readBits(1); // direct_8x8_inference_flag
110
+ if (r.readBits(1)) {
111
+ // frame_cropping_flag — 4:2:0 crop units are 2px
112
+ const cl = r.readUE();
113
+ const cr = r.readUE();
114
+ const ct = r.readUE();
115
+ const cb = r.readUE();
116
+ width -= (cl + cr) * 2;
117
+ height -= (ct + cb) * 2;
118
+ }
119
+ return { profileIdc, constraintFlags, levelIdc, width, height };
120
+ }
121
+ function buildCodecString(info) {
122
+ const pp = info.profileIdc.toString(16).padStart(2, '0');
123
+ const cc = info.constraintFlags.toString(16).padStart(2, '0');
124
+ const ll = info.levelIdc.toString(16).padStart(2, '0');
125
+ return `avc1.${pp}${cc}${ll}`;
126
+ }
127
+ /** AVCDecoderConfigurationRecord from raw SPS/PPS NAL bodies (with NAL header byte). */
128
+ function buildAvcC(sps, pps) {
129
+ const buf = Buffer.alloc(11 + sps.length + pps.length);
130
+ buf[0] = 1; // configurationVersion
131
+ buf[1] = sps[1]; // profile
132
+ buf[2] = sps[2]; // compatibility
133
+ buf[3] = sps[3]; // level
134
+ buf[4] = 0xff; // lengthSizeMinusOne = 3
135
+ buf[5] = 0xe1; // numOfSPS = 1
136
+ buf.writeUInt16BE(sps.length, 6);
137
+ Buffer.from(sps).copy(buf, 8);
138
+ const off = 8 + sps.length;
139
+ buf[off] = 1; // numOfPPS
140
+ buf.writeUInt16BE(pps.length, off + 1);
141
+ Buffer.from(pps).copy(buf, off + 3);
142
+ return buf;
143
+ }
144
+ /** Find the next Annex B start code (00 00 01 or 00 00 00 01). */
145
+ function findStartCode(buf, offset) {
146
+ for (let i = offset; i < buf.length - 2; i++) {
147
+ if (buf[i] === 0 && buf[i + 1] === 0) {
148
+ if (buf[i + 2] === 1)
149
+ return { index: i, length: 3 };
150
+ if (buf[i + 2] === 0 && i + 3 < buf.length && buf[i + 3] === 1)
151
+ return { index: i, length: 4 };
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Streaming Annex B parser. Feed raw capture bytes with push(); emits config
158
+ * once SPS+PPS are seen, then one Annex B access unit per VCL frame.
159
+ */
160
+ class H264AnnexBParser {
161
+ constructor(cbs, fps = 30) {
162
+ this.cbs = cbs;
163
+ this.buffer = Buffer.alloc(0);
164
+ this.sps = null;
165
+ this.pps = null;
166
+ this.config = null;
167
+ // NALs (each Annex-B, start-code prefixed) buffered for the current access unit.
168
+ this.au = [];
169
+ this.auIsKey = false;
170
+ this.fps = fps;
171
+ }
172
+ reset() {
173
+ this.buffer = Buffer.alloc(0);
174
+ this.sps = null;
175
+ this.pps = null;
176
+ this.config = null;
177
+ this.au = [];
178
+ this.auIsKey = false;
179
+ }
180
+ push(chunk) {
181
+ this.buffer = this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
182
+ this.processBuffer();
183
+ }
184
+ processBuffer() {
185
+ let sc = findStartCode(this.buffer, 0);
186
+ if (!sc)
187
+ return;
188
+ while (true) {
189
+ const next = findStartCode(this.buffer, sc.index + sc.length);
190
+ if (!next)
191
+ break; // incomplete NAL — wait for more data
192
+ const nal = this.buffer.subarray(sc.index + sc.length, next.index);
193
+ // Keep the whole unit including start code so subscribers get Annex B.
194
+ const withStartCode = this.buffer.subarray(sc.index, next.index);
195
+ this.processNal(nal, withStartCode);
196
+ sc = next;
197
+ }
198
+ // Retain the trailing (unterminated) NAL for the next push.
199
+ if (sc.index > 0)
200
+ this.buffer = Buffer.from(this.buffer.subarray(sc.index));
201
+ // Guard against unbounded growth on a stuck stream.
202
+ if (this.buffer.length > 4 * 1024 * 1024) {
203
+ this.buffer = Buffer.from(this.buffer.subarray(-1024 * 1024));
204
+ }
205
+ }
206
+ processNal(nal, withStartCode) {
207
+ if (nal.length === 0)
208
+ return;
209
+ const nalType = nal[0] & 0x1f;
210
+ if (nalType === NAL_SPS) {
211
+ this.sps = Buffer.from(nal);
212
+ this.emitConfigIfReady();
213
+ return;
214
+ }
215
+ if (nalType === NAL_PPS) {
216
+ this.pps = Buffer.from(nal);
217
+ this.emitConfigIfReady();
218
+ return;
219
+ }
220
+ if (nalType === NAL_IDR || nalType === NAL_SLICE) {
221
+ // Flush any previously buffered AU before starting this VCL frame.
222
+ if (this.au.length > 0)
223
+ this.emitAccessUnit();
224
+ this.auIsKey = nalType === NAL_IDR;
225
+ this.au.push(Buffer.from(withStartCode));
226
+ this.emitAccessUnit();
227
+ return;
228
+ }
229
+ // SEI / AUD / other — attach to the current access unit.
230
+ this.au.push(Buffer.from(withStartCode));
231
+ }
232
+ emitAccessUnit() {
233
+ if (this.au.length === 0)
234
+ return;
235
+ if (!this.config) {
236
+ // Can't decode without config yet — drop until SPS/PPS arrive.
237
+ this.au = [];
238
+ this.auIsKey = false;
239
+ return;
240
+ }
241
+ const data = this.au.length === 1 ? this.au[0] : Buffer.concat(this.au);
242
+ this.cbs.onFrame(data, this.auIsKey);
243
+ this.au = [];
244
+ this.auIsKey = false;
245
+ }
246
+ emitConfigIfReady() {
247
+ if (this.config || !this.sps || !this.pps)
248
+ return;
249
+ const info = parseSps(this.sps.subarray(1));
250
+ this.config = {
251
+ codec: 'h264',
252
+ width: info.width,
253
+ height: info.height,
254
+ rotation: 0,
255
+ fps: this.fps,
256
+ codecString: buildCodecString(info),
257
+ sps: this.sps,
258
+ pps: this.pps,
259
+ avcC: buildAvcC(this.sps, this.pps),
260
+ };
261
+ this.cbs.onConfig(this.config);
262
+ }
263
+ }
264
+ exports.H264AnnexBParser = H264AnnexBParser;
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.iosBackend = iosBackend;
4
+ exports.androidBackend = androidBackend;
5
+ const clampNorm = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
6
+ // ── iOS / tvOS (XCUITest driver) ──────────────────────────────────────────────
7
+ // key-name → XCUITest pressKey value
8
+ const IOS_KEY = {
9
+ Backspace: 'delete',
10
+ Delete: 'delete',
11
+ Enter: 'enter',
12
+ Return: 'enter',
13
+ Tab: 'tab',
14
+ Space: 'space',
15
+ ' ': 'space',
16
+ };
17
+ // button/key-name → XCUITest pressButton value
18
+ const IOS_BUTTON = {
19
+ home: 'home',
20
+ Home: 'home',
21
+ lock: 'lock',
22
+ Lock: 'lock',
23
+ power: 'lock',
24
+ Power: 'lock',
25
+ // tvOS remote (also reachable via the tvremote frame)
26
+ up: 'up',
27
+ down: 'down',
28
+ left: 'left',
29
+ right: 'right',
30
+ select: 'select',
31
+ menu: 'menu',
32
+ playPause: 'playPause',
33
+ ArrowUp: 'up',
34
+ ArrowDown: 'down',
35
+ ArrowLeft: 'left',
36
+ ArrowRight: 'right',
37
+ };
38
+ function iosBackend(driver) {
39
+ let size = null;
40
+ const dims = async () => {
41
+ if (!size) {
42
+ const info = await driver.deviceInfo();
43
+ size = { w: info.widthPoints, h: info.heightPoints };
44
+ }
45
+ return size;
46
+ };
47
+ const tvos = driver.platform === 'tvos';
48
+ return {
49
+ platform: tvos ? 'tvos' : 'ios',
50
+ capabilities(liveDrag) {
51
+ return {
52
+ touch: true,
53
+ drag: true,
54
+ multitouch: true,
55
+ buttons: ['home', 'lock'],
56
+ keyboard: true,
57
+ text: true,
58
+ tvRemote: tvos,
59
+ springboard: true,
60
+ liveDrag,
61
+ binaryPointer: false,
62
+ coord: 'normalized',
63
+ };
64
+ },
65
+ async tap(nx, ny, durationMs) {
66
+ const { w, h } = await dims();
67
+ await driver.tap(clampNorm(nx) * w, clampNorm(ny) * h, durationMs ? durationMs / 1000 : undefined);
68
+ },
69
+ async gesture(paths) {
70
+ const { w, h } = await dims();
71
+ const converted = paths.map((p) => ({
72
+ steps: p.steps.map((s) => ({
73
+ x: clampNorm(s.nx) * w,
74
+ y: clampNorm(s.ny) * h,
75
+ dt: s.tMs / 1000,
76
+ })),
77
+ }));
78
+ await driver.gesturePath(converted);
79
+ },
80
+ async swipe(nsx, nsy, nex, ney, durationMs) {
81
+ const { w, h } = await dims();
82
+ await driver.swipe(clampNorm(nsx) * w, clampNorm(nsy) * h, clampNorm(nex) * w, clampNorm(ney) * h, durationMs / 1000);
83
+ },
84
+ async text(value) {
85
+ await driver.inputText(value);
86
+ },
87
+ async key(code, opts) {
88
+ // XCUITest pressKey is atomic — act on key-down, ignore the paired up.
89
+ if (opts && opts.down === false)
90
+ return;
91
+ const k = IOS_KEY[code];
92
+ if (k) {
93
+ await driver.pressKey(k);
94
+ return;
95
+ }
96
+ const b = IOS_BUTTON[code];
97
+ if (b)
98
+ await driver.pressButton(b);
99
+ },
100
+ async button(name, holdMs) {
101
+ const b = IOS_BUTTON[name];
102
+ if (b)
103
+ await driver.pressButton(b, holdMs ? holdMs / 1000 : undefined);
104
+ },
105
+ };
106
+ }
107
+ // ── Android (gRPC APK + adb) ───────────────────────────────────────────────────
108
+ const ANDROID_KEYCODE = {
109
+ Home: 3,
110
+ home: 3,
111
+ Back: 4,
112
+ back: 4,
113
+ Enter: 66,
114
+ Return: 66,
115
+ Backspace: 67,
116
+ Delete: 67,
117
+ Tab: 61,
118
+ Space: 62,
119
+ ' ': 62,
120
+ Escape: 111,
121
+ Lock: 26,
122
+ Power: 26,
123
+ power: 26,
124
+ VolumeUp: 24,
125
+ volumeUp: 24,
126
+ VolumeDown: 25,
127
+ volumeDown: 25,
128
+ Menu: 82,
129
+ menu: 82,
130
+ Search: 84,
131
+ ArrowUp: 19,
132
+ ArrowDown: 20,
133
+ ArrowLeft: 21,
134
+ ArrowRight: 22,
135
+ up: 19,
136
+ down: 20,
137
+ left: 21,
138
+ right: 22,
139
+ select: 23,
140
+ playPause: 85,
141
+ };
142
+ function androidBackend(driver) {
143
+ let size = null;
144
+ const dims = async () => {
145
+ if (!size) {
146
+ const info = await driver.deviceInfo();
147
+ size = { w: info.widthPixels, h: info.heightPixels };
148
+ }
149
+ return size;
150
+ };
151
+ return {
152
+ platform: 'android',
153
+ capabilities(liveDrag) {
154
+ return {
155
+ touch: true,
156
+ drag: true,
157
+ multitouch: true,
158
+ buttons: ['home', 'back', 'menu', 'power', 'volumeUp', 'volumeDown'],
159
+ keyboard: true,
160
+ text: true,
161
+ tvRemote: true, // Android-TV dpad via keyevents
162
+ springboard: false,
163
+ liveDrag,
164
+ binaryPointer: false,
165
+ coord: 'normalized',
166
+ };
167
+ },
168
+ async tap(nx, ny) {
169
+ const { w, h } = await dims();
170
+ await driver.tap(clampNorm(nx) * w, clampNorm(ny) * h);
171
+ },
172
+ async gesture(paths) {
173
+ const { w, h } = await dims();
174
+ const converted = paths.map((p) => ({
175
+ steps: p.steps.map((s) => ({
176
+ x: clampNorm(s.nx) * w,
177
+ y: clampNorm(s.ny) * h,
178
+ dt_ms: Math.round(s.tMs),
179
+ })),
180
+ }));
181
+ await driver.gesturePath(converted);
182
+ },
183
+ async swipe(nsx, nsy, nex, ney, durationMs) {
184
+ const { w, h } = await dims();
185
+ await driver.swipe(clampNorm(nsx) * w, clampNorm(nsy) * h, clampNorm(nex) * w, clampNorm(ney) * h, durationMs);
186
+ },
187
+ async text(value) {
188
+ await driver.inputText(value);
189
+ },
190
+ async key(code, opts) {
191
+ if (opts && opts.down === false)
192
+ return;
193
+ const kc = ANDROID_KEYCODE[code];
194
+ if (kc !== undefined)
195
+ await driver.pressKeyEvent(kc);
196
+ },
197
+ async button(name) {
198
+ const kc = ANDROID_KEYCODE[name];
199
+ if (kc !== undefined)
200
+ await driver.pressKeyEvent(kc);
201
+ },
202
+ };
203
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Streaming device-input protocol (conductor ⇄ Argus).
4
+ *
5
+ * A persistent per-device WebSocket carries pointer/key/button/scroll/remote
6
+ * frames so continuous drags and fast typing stay low-latency, instead of the
7
+ * per-event HTTP the web driver uses. Conductor owns coord→device translation
8
+ * and keymaps; the client streams normalized 0..1 coordinates.
9
+ *
10
+ * See docs/device-input-migration.md for the design.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.INPUT_PROTOCOL_VERSION = void 0;
14
+ exports.decodeClientFrame = decodeClientFrame;
15
+ exports.INPUT_PROTOCOL_VERSION = 1;
16
+ const CLIENT_FRAME_TYPES = new Set([
17
+ 'select',
18
+ 'pointer',
19
+ 'key',
20
+ 'text',
21
+ 'button',
22
+ 'scroll',
23
+ 'tvremote',
24
+ ]);
25
+ /** Parse and shallow-validate one text frame. Returns null on malformed input. */
26
+ function decodeClientFrame(raw) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ if (typeof parsed !== 'object' || parsed === null)
35
+ return null;
36
+ const t = parsed.t;
37
+ if (typeof t !== 'string' || !CLIENT_FRAME_TYPES.has(t))
38
+ return null;
39
+ return parsed;
40
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InputRouter = void 0;
4
+ /** Movement below this (normalized) counts as a tap, not a drag. */
5
+ const TAP_EPSILON = 0.01;
6
+ /** Default scroll gesture duration. */
7
+ const SCROLL_DURATION_MS = 200;
8
+ class InputRouter {
9
+ constructor(backend, opts = {}) {
10
+ this.backend = backend;
11
+ this.opts = opts;
12
+ this.sessions = new Map();
13
+ this.liveOpen = new Set();
14
+ this.now = opts.now ?? (() => Date.now());
15
+ }
16
+ get liveDrag() {
17
+ return this.opts.livePointer ? 'native' : 'buffered';
18
+ }
19
+ capabilities() {
20
+ return this.backend.capabilities(this.liveDrag);
21
+ }
22
+ /** Dispatch one frame. `select` is handled by the server, not here. */
23
+ async dispatch(frame) {
24
+ switch (frame.t) {
25
+ case 'pointer':
26
+ return this.handlePointer(frame.id ?? 0, frame.phase, frame.x, frame.y);
27
+ case 'key':
28
+ return this.backend.key(frame.code, { down: frame.down, mods: frame.mods });
29
+ case 'text':
30
+ return this.backend.text(frame.value);
31
+ case 'button':
32
+ return this.backend.button(frame.name, frame.holdMs);
33
+ case 'scroll':
34
+ return this.backend.swipe(frame.x, frame.y, frame.x - frame.dx, frame.y - frame.dy, SCROLL_DURATION_MS);
35
+ case 'tvremote':
36
+ return this.backend.button(frame.button, frame.holdMs);
37
+ case 'select':
38
+ return;
39
+ }
40
+ }
41
+ async handlePointer(id, phase, nx, ny) {
42
+ // Native held-touch path: stream straight through.
43
+ if (this.opts.livePointer) {
44
+ if (phase === 'down')
45
+ this.liveOpen.add(id);
46
+ if (phase === 'up' || phase === 'cancel')
47
+ this.liveOpen.delete(id);
48
+ return this.opts.livePointer.pointer(id, phase, nx, ny);
49
+ }
50
+ // Buffered path.
51
+ const t = this.now();
52
+ if (phase === 'down') {
53
+ this.sessions.set(id, {
54
+ steps: [{ nx, ny, tMs: 0 }],
55
+ lastStepAt: t,
56
+ startNx: nx,
57
+ startNy: ny,
58
+ moved: false,
59
+ done: false,
60
+ canceled: false,
61
+ });
62
+ return;
63
+ }
64
+ const s = this.sessions.get(id);
65
+ if (!s)
66
+ return; // move/up without a down — ignore
67
+ s.steps.push({ nx, ny, tMs: Math.max(0, t - s.lastStepAt) });
68
+ s.lastStepAt = t;
69
+ if (Math.hypot(nx - s.startNx, ny - s.startNy) > TAP_EPSILON)
70
+ s.moved = true;
71
+ if (phase === 'up' || phase === 'cancel') {
72
+ s.done = true;
73
+ s.canceled = phase === 'cancel';
74
+ if ([...this.sessions.values()].every((x) => x.done))
75
+ await this.flush();
76
+ }
77
+ }
78
+ async flush() {
79
+ const sessions = [...this.sessions.values()];
80
+ this.sessions.clear();
81
+ const active = sessions.filter((s) => !s.canceled);
82
+ if (active.length === 0)
83
+ return;
84
+ if (active.length === 1 && !active[0].moved) {
85
+ await this.backend.tap(active[0].startNx, active[0].startNy);
86
+ return;
87
+ }
88
+ await this.backend.gesture(active.map((s) => ({ steps: s.steps })));
89
+ }
90
+ /**
91
+ * Release any touch still held when the socket closes, so no finger is left
92
+ * stuck down. Buffered sessions flush as-is; live touches get a cancel.
93
+ */
94
+ async onClose() {
95
+ if (this.opts.livePointer) {
96
+ const open = [...this.liveOpen];
97
+ this.liveOpen.clear();
98
+ for (const id of open) {
99
+ await this.opts.livePointer.pointer(id, 'cancel', 0, 0).catch(() => { });
100
+ }
101
+ return;
102
+ }
103
+ if (this.sessions.size > 0) {
104
+ for (const s of this.sessions.values())
105
+ s.done = true;
106
+ await this.flush().catch(() => { });
107
+ }
108
+ }
109
+ }
110
+ exports.InputRouter = InputRouter;