@basmilius/apple-devices 0.9.19 → 0.10.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.
Files changed (3) hide show
  1. package/dist/index.d.mts +1168 -15
  2. package/dist/index.mjs +1660 -190
  3. package/package.json +5 -5
package/dist/index.mjs CHANGED
@@ -1,90 +1,138 @@
1
1
  import * as AirPlay from "@basmilius/apple-airplay";
2
- import { DataStreamMessage, Proto, Protocol } from "@basmilius/apple-airplay";
2
+ import { AirPlayFeature, DataStreamMessage, Proto, Protocol } from "@basmilius/apple-airplay";
3
3
  import { EventEmitter } from "node:events";
4
4
  import { CommandError, CredentialsError, waitFor } from "@basmilius/apple-common";
5
- import { Protocol as Protocol$1, convertAttentionState } from "@basmilius/apple-companion-link";
6
- import { Plist } from "@basmilius/apple-encoding";
5
+ import { MediaControlFlag, Protocol as Protocol$1, convertAttentionState } from "@basmilius/apple-companion-link";
6
+ import { NSKeyedArchiver, Plist } from "@basmilius/apple-encoding";
7
7
 
8
8
  //#region src/airplay/player.ts
9
+ /** Offset in seconds between the Cocoa epoch (2001-01-01) and the Unix epoch (1970-01-01). */
9
10
  const COCOA_EPOCH_OFFSET = 978307200;
11
+ /** Default player identifier used by the Apple TV when no specific player is active. */
10
12
  const DEFAULT_PLAYER_ID = "MediaRemote-DefaultPlayer";
13
+ /**
14
+ * Extrapolates the current elapsed time based on a snapshot timestamp and playback rate.
15
+ * Compensates for the time passed since the timestamp was recorded, scaled by the playback rate.
16
+ *
17
+ * @param elapsed - The elapsed time at the moment of the snapshot, in seconds.
18
+ * @param cocoaTimestamp - The Cocoa epoch timestamp when the snapshot was taken.
19
+ * @param rate - The playback rate (e.g. 1.0 for normal speed, 0 for paused).
20
+ * @returns The extrapolated elapsed time in seconds, clamped to a minimum of 0.
21
+ */
11
22
  const extrapolateElapsed = (elapsed, cocoaTimestamp, rate) => {
12
23
  if (!rate) return elapsed;
13
24
  const timestampUnix = cocoaTimestamp + COCOA_EPOCH_OFFSET;
14
25
  const delta = (Date.now() / 1e3 - timestampUnix) * rate;
15
26
  return Math.max(0, elapsed + delta);
16
27
  };
28
+ /**
29
+ * Represents a single media player within an app on the Apple TV.
30
+ * Each app (Client) can have multiple players (e.g. picture-in-picture).
31
+ * Tracks now-playing metadata, playback state, and provides elapsed time extrapolation
32
+ * based on Cocoa timestamps and playback rate.
33
+ */
17
34
  var Player = class {
35
+ /** Unique identifier for this player (e.g. a player path). */
18
36
  get identifier() {
19
37
  return this.#identifier;
20
38
  }
39
+ /** Human-readable display name for this player. */
21
40
  get displayName() {
22
41
  return this.#displayName;
23
42
  }
43
+ /** Whether this is the default fallback player (MediaRemote-DefaultPlayer). */
24
44
  get isDefaultPlayer() {
25
45
  return this.#identifier === DEFAULT_PLAYER_ID;
26
46
  }
47
+ /** Raw now-playing info from the Apple TV, or null if unavailable. */
27
48
  get nowPlayingInfo() {
28
49
  return this.#nowPlayingInfo;
29
50
  }
51
+ /** Current playback queue, or null if unavailable. */
30
52
  get playbackQueue() {
31
53
  return this.#playbackQueue;
32
54
  }
55
+ /**
56
+ * Effective playback state. Corrects for the edge case where the Apple TV
57
+ * reports Playing but the playback rate is 0 (effectively paused).
58
+ */
33
59
  get playbackState() {
34
60
  if (this.#playbackState === Proto.PlaybackState_Enum.Playing && this.playbackRate === 0) return Proto.PlaybackState_Enum.Paused;
35
61
  return this.#playbackState;
36
62
  }
63
+ /** The raw playback state as reported by the Apple TV, without corrections. */
37
64
  get rawPlaybackState() {
38
65
  return this.#playbackState;
39
66
  }
67
+ /** Timestamp of the last playback state update, used to discard stale updates. */
40
68
  get playbackStateTimestamp() {
41
69
  return this.#playbackStateTimestamp;
42
70
  }
71
+ /** List of commands supported by this player. */
43
72
  get supportedCommands() {
44
73
  return this.#supportedCommands;
45
74
  }
75
+ /** Current track title from NowPlayingInfo or content item metadata. */
46
76
  get title() {
47
77
  return this.#nowPlayingInfo?.title || this.currentItemMetadata?.title || "";
48
78
  }
79
+ /** Current track artist from NowPlayingInfo or content item metadata. */
49
80
  get artist() {
50
81
  return this.#nowPlayingInfo?.artist || this.currentItemMetadata?.trackArtistName || "";
51
82
  }
83
+ /** Current track album from NowPlayingInfo or content item metadata. */
52
84
  get album() {
53
85
  return this.#nowPlayingInfo?.album || this.currentItemMetadata?.albumName || "";
54
86
  }
87
+ /** Genre of the current content item. */
55
88
  get genre() {
56
89
  return this.currentItemMetadata?.genre || "";
57
90
  }
91
+ /** Series name for TV show content. */
58
92
  get seriesName() {
59
93
  return this.currentItemMetadata?.seriesName || "";
60
94
  }
95
+ /** Season number for TV show content, or 0 if not applicable. */
61
96
  get seasonNumber() {
62
97
  return this.currentItemMetadata?.seasonNumber || 0;
63
98
  }
99
+ /** Episode number for TV show content, or 0 if not applicable. */
64
100
  get episodeNumber() {
65
101
  return this.currentItemMetadata?.episodeNumber || 0;
66
102
  }
103
+ /** Media type of the current content item (music, video, etc.). */
67
104
  get mediaType() {
68
105
  return this.currentItemMetadata?.mediaType ?? Proto.ContentItemMetadata_MediaType.UnknownMediaType;
69
106
  }
107
+ /** Unique content identifier for the current item (e.g. iTunes store ID). */
70
108
  get contentIdentifier() {
71
109
  return this.currentItemMetadata?.contentIdentifier || "";
72
110
  }
111
+ /** Duration of the current track in seconds, from NowPlayingInfo or metadata. */
73
112
  get duration() {
74
113
  return this.#nowPlayingInfo?.duration || this.currentItemMetadata?.duration || 0;
75
114
  }
115
+ /** Current playback rate (1.0 = normal, 0 = paused, 2.0 = double speed). */
76
116
  get playbackRate() {
77
117
  return this.#nowPlayingInfo?.playbackRate ?? this.currentItemMetadata?.playbackRate ?? 0;
78
118
  }
119
+ /** Whether the player is currently playing (based on effective playback state). */
79
120
  get isPlaying() {
80
- return this.#playbackState === Proto.PlaybackState_Enum.Playing;
121
+ return this.playbackState === Proto.PlaybackState_Enum.Playing;
81
122
  }
123
+ /** Current shuffle mode, derived from the ChangeShuffleMode command info. */
82
124
  get shuffleMode() {
83
125
  return this.#supportedCommands.find((c) => c.command === Proto.Command.ChangeShuffleMode)?.shuffleMode ?? Proto.ShuffleMode_Enum.Unknown;
84
126
  }
127
+ /** Current repeat mode, derived from the ChangeRepeatMode command info. */
85
128
  get repeatMode() {
86
129
  return this.#supportedCommands.find((c) => c.command === Proto.Command.ChangeRepeatMode)?.repeatMode ?? Proto.RepeatMode_Enum.Unknown;
87
130
  }
131
+ /**
132
+ * Extrapolated elapsed time in seconds. Uses the most recent timestamp
133
+ * from either NowPlayingInfo or content item metadata, accounting for
134
+ * playback rate to provide a real-time estimate.
135
+ */
88
136
  get elapsedTime() {
89
137
  const npi = this.#nowPlayingInfo;
90
138
  const meta = this.currentItemMetadata;
@@ -98,13 +146,19 @@ var Player = class {
98
146
  if (metaValid) return extrapolateElapsed(meta.elapsedTime, meta.elapsedTimeTimestamp, meta.playbackRate);
99
147
  return npi?.elapsedTime || meta?.elapsedTime || 0;
100
148
  }
149
+ /** The currently playing content item from the playback queue, or null. */
101
150
  get currentItem() {
102
151
  if (!this.#playbackQueue || this.#playbackQueue.contentItems.length === 0) return null;
103
152
  return this.#playbackQueue.contentItems[this.#playbackQueue.location] ?? this.#playbackQueue.contentItems[0] ?? null;
104
153
  }
154
+ /** Metadata of the current content item, or null if no item is playing. */
105
155
  get currentItemMetadata() {
106
156
  return this.currentItem?.metadata ?? null;
107
157
  }
158
+ /**
159
+ * Unique identifier for the current artwork, used for change detection.
160
+ * Returns null if no artwork evidence exists.
161
+ */
108
162
  get artworkId() {
109
163
  const metadata = this.currentItemMetadata;
110
164
  if (!metadata) return null;
@@ -113,6 +167,14 @@ var Player = class {
113
167
  if (metadata.contentIdentifier) return metadata.contentIdentifier;
114
168
  return this.currentItem?.identifier ?? null;
115
169
  }
170
+ /**
171
+ * Resolves the best available artwork URL for the current item.
172
+ * Checks metadata artworkURL, remote artworks, and iTunes template URLs in order.
173
+ *
174
+ * @param width - Desired artwork width in pixels (used for template URLs).
175
+ * @param height - Desired artwork height in pixels (-1 for automatic).
176
+ * @returns The artwork URL, or null if no artwork URL is available.
177
+ */
116
178
  artworkUrl(width = 600, height = -1) {
117
179
  const metadata = this.currentItemMetadata;
118
180
  if (metadata?.artworkURL) return metadata.artworkURL;
@@ -124,6 +186,7 @@ var Player = class {
124
186
  } catch {}
125
187
  return null;
126
188
  }
189
+ /** Raw artwork data (image bytes) for the current item, or null if not embedded. */
127
190
  get currentItemArtwork() {
128
191
  const item = this.currentItem;
129
192
  if (!item) return null;
@@ -131,9 +194,11 @@ var Player = class {
131
194
  if (item.dataArtworks.length > 0 && item.dataArtworks[0].imageData?.byteLength > 0) return item.dataArtworks[0].imageData;
132
195
  return null;
133
196
  }
197
+ /** Convenience getter for the artwork URL at default dimensions (600px). */
134
198
  get currentItemArtworkUrl() {
135
199
  return this.artworkUrl();
136
200
  }
201
+ /** Lyrics for the current content item, or null if unavailable. */
137
202
  get currentItemLyrics() {
138
203
  return this.currentItem?.lyrics ?? null;
139
204
  }
@@ -144,38 +209,84 @@ var Player = class {
144
209
  #playbackState;
145
210
  #playbackStateTimestamp = 0;
146
211
  #supportedCommands = [];
212
+ /**
213
+ * Creates a new Player instance.
214
+ *
215
+ * @param identifier - Unique player identifier.
216
+ * @param displayName - Human-readable display name.
217
+ */
147
218
  constructor(identifier, displayName) {
148
219
  this.#identifier = identifier;
149
220
  this.#displayName = displayName;
150
221
  this.#playbackState = Proto.PlaybackState_Enum.Unknown;
151
222
  }
223
+ /**
224
+ * Finds a command by its command type in the supported commands list.
225
+ *
226
+ * @param command - The command to look up.
227
+ * @returns The command info, or null if not found.
228
+ */
152
229
  findCommand(command) {
153
230
  return this.#supportedCommands.find((c) => c.command === command) ?? null;
154
231
  }
232
+ /**
233
+ * Checks whether a command is supported and enabled.
234
+ *
235
+ * @param command - The command to check.
236
+ * @returns True if the command is in the supported list and enabled.
237
+ */
155
238
  isCommandSupported(command) {
156
239
  const info = this.findCommand(command);
157
240
  return info != null && info.enabled !== false;
158
241
  }
242
+ /**
243
+ * Updates the now-playing info for this player.
244
+ *
245
+ * @param nowPlayingInfo - The new now-playing info from the Apple TV.
246
+ */
159
247
  setNowPlayingInfo(nowPlayingInfo) {
160
248
  this.#nowPlayingInfo = nowPlayingInfo;
161
249
  }
250
+ /**
251
+ * Updates the playback queue for this player.
252
+ *
253
+ * @param playbackQueue - The new playback queue from the Apple TV.
254
+ */
162
255
  setPlaybackQueue(playbackQueue) {
163
256
  this.#playbackQueue = playbackQueue;
164
257
  }
258
+ /**
259
+ * Updates the playback state. Ignores updates with a timestamp older than the current one
260
+ * to prevent stale state from overwriting newer data.
261
+ *
262
+ * @param playbackState - The new playback state.
263
+ * @param playbackStateTimestamp - Timestamp of this state update.
264
+ */
165
265
  setPlaybackState(playbackState, playbackStateTimestamp) {
166
266
  if (playbackStateTimestamp < this.#playbackStateTimestamp) return;
167
267
  this.#playbackState = playbackState;
168
268
  this.#playbackStateTimestamp = playbackStateTimestamp;
169
269
  }
270
+ /**
271
+ * Replaces the list of supported commands for this player.
272
+ *
273
+ * @param supportedCommands - The new list of supported commands.
274
+ */
170
275
  setSupportedCommands(supportedCommands) {
171
276
  this.#supportedCommands = supportedCommands;
172
277
  }
278
+ /**
279
+ * Merges updated content item data into the existing playback queue.
280
+ * Updates metadata, artwork, lyrics, and info fields for the matching item.
281
+ *
282
+ * @param item - The content item with updated fields.
283
+ */
173
284
  updateContentItem(item) {
174
285
  if (!this.#playbackQueue) return;
175
286
  const existing = this.#playbackQueue.contentItems.find((i) => i.identifier === item.identifier);
176
287
  if (!existing) return;
177
288
  if (item.metadata != null && existing.metadata != null) {
178
- for (const [key, value] of Object.entries(item.metadata)) if (value != null) existing.metadata[key] = value;
289
+ for (const [key, value] of Object.entries(item.metadata)) if (value != null && !key.startsWith("$")) existing.metadata[key] = value;
179
290
  } else if (item.metadata != null) existing.metadata = item.metadata;
180
291
  if (item.artworkData != null) existing.artworkData = item.artworkData;
181
292
  if (item.lyrics != null) existing.lyrics = item.lyrics;
@@ -185,31 +296,49 @@ var Player = class {
185
296
 
186
297
  //#endregion
187
298
  //#region src/airplay/client.ts
299
+ /**
300
+ * Represents a now-playing app (client) on the Apple TV.
301
+ * Each client is identified by its bundle identifier and can contain multiple players.
302
+ * Proxies now-playing getters to the active player, merging player-specific and
303
+ * default supported commands.
304
+ */
188
305
  var Client = class {
306
+ /** Bundle identifier of the app (e.g. "com.apple.TVMusic"). */
189
307
  get bundleIdentifier() {
190
308
  return this.#bundleIdentifier;
191
309
  }
310
+ /** Human-readable display name of the app. */
192
311
  get displayName() {
193
312
  return this.#displayName;
194
313
  }
314
+ /** Map of all known players for this client, keyed by player identifier. */
195
315
  get players() {
196
316
  return this.#players;
197
317
  }
318
+ /** The currently active player, or null if none is active. Falls back to the default player. */
198
319
  get activePlayer() {
199
320
  return this.#players.get(this.#activePlayerId ?? "MediaRemote-DefaultPlayer") ?? null;
200
321
  }
322
+ /** Now-playing info from the active player, or null. */
201
323
  get nowPlayingInfo() {
202
324
  return this.activePlayer?.nowPlayingInfo ?? null;
203
325
  }
326
+ /** Playback queue from the active player, or null. */
204
327
  get playbackQueue() {
205
328
  return this.activePlayer?.playbackQueue ?? null;
206
329
  }
330
+ /** Playback state from the active player, or Unknown. */
207
331
  get playbackState() {
208
332
  return this.activePlayer?.playbackState ?? Proto.PlaybackState_Enum.Unknown;
209
333
  }
334
+ /** Timestamp of the last playback state update from the active player. */
210
335
  get playbackStateTimestamp() {
211
336
  return this.activePlayer?.playbackStateTimestamp ?? -1;
212
337
  }
338
+ /**
339
+ * Merged list of supported commands from the active player and client defaults.
340
+ * Player commands take precedence; default commands are appended if not already present.
341
+ */
213
342
  get supportedCommands() {
214
343
  const playerCommands = this.activePlayer?.supportedCommands ?? [];
215
344
  if (playerCommands.length === 0) return this.#defaultSupportedCommands;
@@ -219,69 +348,97 @@ var Client = class {
219
348
  for (const cmd of this.#defaultSupportedCommands) if (!playerCommandSet.has(cmd.command)) merged.push(cmd);
220
349
  return merged;
221
350
  }
351
+ /** Current track title from the active player. */
222
352
  get title() {
223
353
  return this.activePlayer?.title ?? "";
224
354
  }
355
+ /** Current track artist from the active player. */
225
356
  get artist() {
226
357
  return this.activePlayer?.artist ?? "";
227
358
  }
359
+ /** Current track album from the active player. */
228
360
  get album() {
229
361
  return this.activePlayer?.album ?? "";
230
362
  }
363
+ /** Genre of the current content from the active player. */
231
364
  get genre() {
232
365
  return this.activePlayer?.genre ?? "";
233
366
  }
367
+ /** Series name for TV show content from the active player. */
234
368
  get seriesName() {
235
369
  return this.activePlayer?.seriesName ?? "";
236
370
  }
371
+ /** Season number for TV show content from the active player. */
237
372
  get seasonNumber() {
238
373
  return this.activePlayer?.seasonNumber ?? 0;
239
374
  }
375
+ /** Episode number for TV show content from the active player. */
240
376
  get episodeNumber() {
241
377
  return this.activePlayer?.episodeNumber ?? 0;
242
378
  }
379
+ /** Media type of the current content from the active player. */
243
380
  get mediaType() {
244
381
  return this.activePlayer?.mediaType ?? Proto.ContentItemMetadata_MediaType.UnknownMediaType;
245
382
  }
383
+ /** Content identifier of the current item from the active player. */
246
384
  get contentIdentifier() {
247
385
  return this.activePlayer?.contentIdentifier ?? "";
248
386
  }
387
+ /** Duration of the current track in seconds from the active player. */
249
388
  get duration() {
250
389
  return this.activePlayer?.duration ?? 0;
251
390
  }
391
+ /** Playback rate from the active player (1.0 = normal, 0 = paused). */
252
392
  get playbackRate() {
253
393
  return this.activePlayer?.playbackRate ?? 0;
254
394
  }
395
+ /** Whether the active player is currently playing. */
255
396
  get isPlaying() {
256
397
  return this.activePlayer?.isPlaying ?? false;
257
398
  }
399
+ /** Current shuffle mode from the active player. */
258
400
  get shuffleMode() {
259
401
  return this.activePlayer?.shuffleMode ?? Proto.ShuffleMode_Enum.Unknown;
260
402
  }
403
+ /** Current repeat mode from the active player. */
261
404
  get repeatMode() {
262
405
  return this.activePlayer?.repeatMode ?? Proto.RepeatMode_Enum.Unknown;
263
406
  }
407
+ /** Extrapolated elapsed time in seconds from the active player. */
264
408
  get elapsedTime() {
265
409
  return this.activePlayer?.elapsedTime ?? 0;
266
410
  }
411
+ /** Artwork identifier for change detection from the active player. */
267
412
  get artworkId() {
268
413
  return this.activePlayer?.artworkId ?? null;
269
414
  }
415
+ /**
416
+ * Resolves the best available artwork URL from the active player.
417
+ *
418
+ * @param width - Desired artwork width in pixels.
419
+ * @param height - Desired artwork height in pixels (-1 for automatic).
420
+ * @returns The artwork URL, or null if unavailable.
421
+ */
270
422
  artworkUrl(width = 600, height = -1) {
271
423
  return this.activePlayer?.artworkUrl(width, height) ?? null;
272
424
  }
425
+ /** Current content item from the active player's playback queue. */
273
426
  get currentItem() {
274
427
  return this.activePlayer?.currentItem ?? null;
275
428
  }
429
+ /** Metadata of the current content item from the active player. */
276
430
  get currentItemMetadata() {
277
431
  return this.activePlayer?.currentItemMetadata ?? null;
278
432
  }
433
+ /** Raw artwork data (image bytes) from the active player. */
279
434
  get currentItemArtwork() {
280
435
  return this.activePlayer?.currentItemArtwork ?? null;
281
436
  }
437
+ /** Artwork URL at default dimensions from the active player. */
282
438
  get currentItemArtworkUrl() {
283
439
  return this.activePlayer?.currentItemArtworkUrl ?? null;
284
440
  }
441
+ /** Lyrics for the current content item from the active player. */
285
442
  get currentItemLyrics() {
286
443
  return this.activePlayer?.currentItemLyrics ?? null;
287
444
  }
@@ -290,10 +447,23 @@ var Client = class {
290
447
  #players = /* @__PURE__ */ new Map();
291
448
  #activePlayerId = null;
292
449
  #defaultSupportedCommands = [];
450
+ /**
451
+ * Creates a new Client instance.
452
+ *
453
+ * @param bundleIdentifier - Bundle identifier of the app.
454
+ * @param displayName - Human-readable app name.
455
+ */
293
456
  constructor(bundleIdentifier, displayName) {
294
457
  this.#bundleIdentifier = bundleIdentifier;
295
458
  this.#displayName = displayName;
296
459
  }
460
+ /**
461
+ * Gets an existing player or creates a new one if it does not exist.
462
+ *
463
+ * @param identifier - Unique player identifier.
464
+ * @param displayName - Human-readable player name (defaults to identifier).
465
+ * @returns The existing or newly created Player.
466
+ */
297
467
  getOrCreatePlayer(identifier, displayName) {
298
468
  let player = this.#players.get(identifier);
299
469
  if (!player) {
@@ -302,25 +472,61 @@ var Client = class {
302
472
  }
303
473
  return player;
304
474
  }
475
+ /**
476
+ * Sets the active player by identifier.
477
+ *
478
+ * @param identifier - Identifier of the player to activate.
479
+ */
305
480
  setActivePlayer(identifier) {
306
481
  this.#activePlayerId = identifier;
307
482
  }
483
+ /**
484
+ * Removes a player from this client. If the removed player was active,
485
+ * the active player is reset to null (falling back to the default player).
486
+ *
487
+ * @param identifier - Identifier of the player to remove.
488
+ */
308
489
  removePlayer(identifier) {
309
490
  this.#players.delete(identifier);
310
491
  if (this.#activePlayerId === identifier) this.#activePlayerId = null;
311
492
  }
493
+ /**
494
+ * Sets the default supported commands for this client. These are used as
495
+ * fallback when the active player has no commands of its own.
496
+ *
497
+ * @param supportedCommands - The default command list.
498
+ */
312
499
  setDefaultSupportedCommands(supportedCommands) {
313
500
  this.#defaultSupportedCommands = supportedCommands;
314
501
  }
502
+ /**
503
+ * Finds a command by type, checking the active player first,
504
+ * then falling back to the default supported commands.
505
+ *
506
+ * @param command - The command to look up.
507
+ * @returns The command info, or null if not found.
508
+ */
315
509
  findCommand(command) {
316
510
  const playerCmd = this.activePlayer?.findCommand(command) ?? null;
317
511
  if (playerCmd) return playerCmd;
318
512
  return this.#defaultSupportedCommands.find((c) => c.command === command) ?? null;
319
513
  }
514
+ /**
515
+ * Checks whether a command is supported and enabled, checking both
516
+ * the active player and default commands.
517
+ *
518
+ * @param command - The command to check.
519
+ * @returns True if the command is supported and enabled.
520
+ */
320
521
  isCommandSupported(command) {
321
522
  const info = this.findCommand(command);
322
523
  return info != null && info.enabled !== false;
323
524
  }
525
+ /**
526
+ * Updates the display name for this client.
527
+ *
528
+ * @param displayName - The new display name.
529
+ */
324
530
  updateDisplayName(displayName) {
325
531
  this.#displayName = displayName;
326
532
  }
@@ -328,16 +534,32 @@ var Client = class {
328
534
 
329
535
  //#endregion
330
536
  //#region src/airplay/const.ts
537
+ /** Interval in milliseconds between periodic feedback requests to keep the AirPlay session alive. */
331
538
  const FEEDBACK_INTERVAL = 2e3;
539
+ /** Symbol used to access the underlying AirPlay Protocol instance from an AirPlayDevice. */
332
540
  const PROTOCOL = Symbol("com.basmilius.airplay:protocol");
541
+ /** Symbol used to subscribe AirPlayState to DataStream events. */
333
542
  const STATE_SUBSCRIBE_SYMBOL = Symbol("com.basmilius.airplay:subscribe");
543
+ /** Symbol used to unsubscribe AirPlayState from DataStream events. */
334
544
  const STATE_UNSUBSCRIBE_SYMBOL = Symbol("com.basmilius.airplay:unsubscribe");
335
545
 
336
546
  //#endregion
337
547
  //#region src/airplay/remote.ts
548
+ /**
549
+ * Error thrown when a SendCommand request fails on the Apple TV side.
550
+ * Contains the specific send error and handler return status for diagnostics.
551
+ */
338
552
  var SendCommandError = class extends CommandError {
553
+ /** The send error reported by the Apple TV. */
339
554
  sendError;
555
+ /** The handler return status reported by the Apple TV. */
340
556
  handlerReturnStatus;
557
+ /**
558
+ * Creates a new SendCommandError.
559
+ *
560
+ * @param sendError - The send error code from the Apple TV.
561
+ * @param handlerReturnStatus - The handler return status from the Apple TV.
562
+ */
341
563
  constructor(sendError, handlerReturnStatus) {
342
564
  super(`SendCommand failed: sendError=${Proto.SendError_Enum[sendError]}, handlerReturnStatus=${Proto.HandlerReturnStatus_Enum[handlerReturnStatus]}`);
343
565
  this.name = "SendCommandError";
@@ -345,213 +567,398 @@ var SendCommandError = class extends CommandError {
345
567
  this.handlerReturnStatus = handlerReturnStatus;
346
568
  }
347
569
  };
570
+ /**
571
+ * Remote control for an AirPlay device.
572
+ * Provides HID-based navigation and media keys (USB usage pages: Generic Desktop 0x01
573
+ * and Consumer 0x0c), SendCommand-based media controls, keyboard/text input,
574
+ * and touch/gesture simulation.
575
+ */
348
576
  var remote_default = class {
577
+ /** @returns The DataStream for sending HID events and commands. */
349
578
  get #dataStream() {
350
579
  return this.#protocol.dataStream;
351
580
  }
581
+ /** @returns The underlying AirPlay Protocol instance. */
352
582
  get #protocol() {
353
583
  return this.#device[PROTOCOL];
354
584
  }
355
585
  #device;
586
+ /**
587
+ * Creates a new Remote controller.
588
+ *
589
+ * @param device - The AirPlay device to control.
590
+ */
356
591
  constructor(device) {
357
592
  this.#device = device;
358
593
  }
594
+ /** Sends an Up navigation key press (Generic Desktop, usage 0x8C). */
359
595
  async up() {
360
596
  await this.pressAndRelease(1, 140);
361
597
  }
598
+ /** Sends a Down navigation key press (Generic Desktop, usage 0x8D). */
362
599
  async down() {
363
600
  await this.pressAndRelease(1, 141);
364
601
  }
602
+ /** Sends a Left navigation key press (Generic Desktop, usage 0x8B). */
365
603
  async left() {
366
604
  await this.pressAndRelease(1, 139);
367
605
  }
606
+ /** Sends a Right navigation key press (Generic Desktop, usage 0x8A). */
368
607
  async right() {
369
608
  await this.pressAndRelease(1, 138);
370
609
  }
610
+ /** Sends a Menu key press (Generic Desktop, usage 0x86). */
371
611
  async menu() {
372
612
  await this.pressAndRelease(1, 134);
373
613
  }
614
+ /** Sends a Select/Enter key press (Generic Desktop, usage 0x89). */
374
615
  async select() {
375
616
  await this.pressAndRelease(1, 137);
376
617
  }
618
+ /** Sends a Home button press (Consumer, usage 0x40). */
377
619
  async home() {
378
620
  await this.pressAndRelease(12, 64);
379
621
  }
622
+ /** Sends a Suspend/Sleep key press to put the device to sleep (Generic Desktop, usage 0x82). */
380
623
  async suspend() {
381
624
  await this.pressAndRelease(1, 130);
382
625
  }
626
+ /** Sends a Wake key press to wake the device (Generic Desktop, usage 0x83). */
383
627
  async wake() {
384
628
  await this.pressAndRelease(1, 131);
385
629
  }
630
+ /** Sends a Play key press (Consumer, usage 0xB0). */
386
631
  async play() {
387
632
  await this.pressAndRelease(12, 176);
388
633
  }
634
+ /** Sends a Pause key press (Consumer, usage 0xB1). */
389
635
  async pause() {
390
636
  await this.pressAndRelease(12, 177);
391
637
  }
638
+ /** Toggles play/pause based on the current playback state. */
392
639
  async playPause() {
393
640
  if (this.#device.state.nowPlayingClient?.isPlaying) await this.pause();
394
641
  else await this.play();
395
642
  }
643
+ /** Sends a Stop key press (Consumer, usage 0xB7). */
396
644
  async stop() {
397
645
  await this.pressAndRelease(12, 183);
398
646
  }
647
+ /** Sends a Next Track key press (Consumer, usage 0xB5). */
399
648
  async next() {
400
649
  await this.pressAndRelease(12, 181);
401
650
  }
651
+ /** Sends a Previous Track key press (Consumer, usage 0xB6). */
402
652
  async previous() {
403
653
  await this.pressAndRelease(12, 182);
404
654
  }
655
+ /** Sends a Volume Up key press (Consumer, usage 0xE9). */
405
656
  async volumeUp() {
406
657
  await this.pressAndRelease(12, 233);
407
658
  }
659
+ /** Sends a Volume Down key press (Consumer, usage 0xEA). */
408
660
  async volumeDown() {
409
661
  await this.pressAndRelease(12, 234);
410
662
  }
663
+ /** Sends a Mute key press (Consumer, usage 0xE2). */
411
664
  async mute() {
412
665
  await this.pressAndRelease(12, 226);
413
666
  }
667
+ /** Sends a Top Menu key press (Consumer, usage 0x60). */
414
668
  async topMenu() {
415
669
  await this.pressAndRelease(12, 96);
416
670
  }
671
+ /** Sends a Channel Up key press (Consumer, usage 0x9C). */
417
672
  async channelUp() {
418
673
  await this.pressAndRelease(12, 156);
419
674
  }
675
+ /** Sends a Channel Down key press (Consumer, usage 0x9D). */
420
676
  async channelDown() {
421
677
  await this.pressAndRelease(12, 157);
422
678
  }
679
+ /** Sends a Play command via the MRP SendCommand protocol. */
423
680
  async commandPlay() {
424
681
  await this.#sendCommand(Proto.Command.Play);
425
682
  }
683
+ /** Sends a Pause command via the MRP SendCommand protocol. */
426
684
  async commandPause() {
427
685
  await this.#sendCommand(Proto.Command.Pause);
428
686
  }
687
+ /** Sends a TogglePlayPause command via the MRP SendCommand protocol. */
429
688
  async commandTogglePlayPause() {
430
689
  await this.#sendCommand(Proto.Command.TogglePlayPause);
431
690
  }
691
+ /** Sends a Stop command via the MRP SendCommand protocol. */
432
692
  async commandStop() {
433
693
  await this.#sendCommand(Proto.Command.Stop);
434
694
  }
695
+ /** Sends a NextTrack command via the MRP SendCommand protocol. */
435
696
  async commandNextTrack() {
436
697
  await this.#sendCommand(Proto.Command.NextTrack);
437
698
  }
699
+ /** Sends a PreviousTrack command via the MRP SendCommand protocol. */
438
700
  async commandPreviousTrack() {
439
701
  await this.#sendCommand(Proto.Command.PreviousTrack);
440
702
  }
703
+ /**
704
+ * Sends a SkipForward command with a configurable interval.
705
+ *
706
+ * @param interval - Seconds to skip forward (defaults to 15).
707
+ */
441
708
  async commandSkipForward(interval = 15) {
442
709
  await this.#sendCommandRaw(DataStreamMessage.sendCommandWithSkipInterval(Proto.Command.SkipForward, interval));
443
710
  }
711
+ /**
712
+ * Sends a SkipBackward command with a configurable interval.
713
+ *
714
+ * @param interval - Seconds to skip backward (defaults to 15).
715
+ */
444
716
  async commandSkipBackward(interval = 15) {
445
717
  await this.#sendCommandRaw(DataStreamMessage.sendCommandWithSkipInterval(Proto.Command.SkipBackward, interval));
446
718
  }
719
+ /**
720
+ * Seeks to an absolute playback position.
721
+ *
722
+ * @param position - The target position in seconds.
723
+ */
447
724
  async commandSeekToPosition(position) {
448
725
  await this.#sendCommandRaw(DataStreamMessage.sendCommandWithPlaybackPosition(Proto.Command.SeekToPlaybackPosition, position));
449
726
  }
727
+ /**
728
+ * Sets the shuffle mode.
729
+ *
730
+ * @param mode - The desired shuffle mode.
731
+ */
450
732
  async commandSetShuffleMode(mode) {
451
733
  await this.#sendCommandRaw(DataStreamMessage.sendCommandWithShuffleMode(Proto.Command.ChangeShuffleMode, mode));
452
734
  }
735
+ /**
736
+ * Sets the repeat mode.
737
+ *
738
+ * @param mode - The desired repeat mode.
739
+ */
453
740
  async commandSetRepeatMode(mode) {
454
741
  await this.#sendCommandRaw(DataStreamMessage.sendCommandWithRepeatMode(Proto.Command.ChangeRepeatMode, mode));
455
742
  }
743
+ /**
744
+ * Changes the playback rate (speed).
745
+ *
746
+ * @param rate - The desired playback rate (e.g. 1.0 for normal, 2.0 for double speed).
747
+ */
456
748
  async commandChangePlaybackRate(rate) {
457
749
  await this.#sendCommandRaw(DataStreamMessage.sendCommandWithPlaybackRate(Proto.Command.ChangePlaybackRate, rate));
458
750
  }
751
+ /** Cycles the shuffle mode to the next value. */
459
752
  async commandAdvanceShuffleMode() {
460
753
  await this.#sendCommand(Proto.Command.AdvanceShuffleMode);
461
754
  }
755
+ /** Cycles the repeat mode to the next value. */
462
756
  async commandAdvanceRepeatMode() {
463
757
  await this.#sendCommand(Proto.Command.AdvanceRepeatMode);
464
758
  }
759
+ /** Begins fast-forwarding playback. */
465
760
  async commandBeginFastForward() {
466
761
  await this.#sendCommand(Proto.Command.BeginFastForward);
467
762
  }
763
+ /** Ends fast-forwarding playback. */
468
764
  async commandEndFastForward() {
469
765
  await this.#sendCommand(Proto.Command.EndFastForward);
470
766
  }
767
+ /** Begins rewinding playback. */
471
768
  async commandBeginRewind() {
472
769
  await this.#sendCommand(Proto.Command.BeginRewind);
473
770
  }
771
+ /** Ends rewinding playback. */
474
772
  async commandEndRewind() {
475
773
  await this.#sendCommand(Proto.Command.EndRewind);
476
774
  }
775
+ /** Skips to the next chapter. */
477
776
  async commandNextChapter() {
478
777
  await this.#sendCommand(Proto.Command.NextChapter);
479
778
  }
779
+ /** Skips to the previous chapter. */
480
780
  async commandPreviousChapter() {
481
781
  await this.#sendCommand(Proto.Command.PreviousChapter);
482
782
  }
783
+ /** Marks the current track as liked. */
483
784
  async commandLikeTrack() {
484
785
  await this.#sendCommand(Proto.Command.LikeTrack);
485
786
  }
787
+ /** Marks the current track as disliked. */
486
788
  async commandDislikeTrack() {
487
789
  await this.#sendCommand(Proto.Command.DislikeTrack);
488
790
  }
791
+ /** Bookmarks the current track. */
489
792
  async commandBookmarkTrack() {
490
793
  await this.#sendCommand(Proto.Command.BookmarkTrack);
491
794
  }
795
+ /** Adds the currently playing item to the user's library. */
492
796
  async commandAddNowPlayingItemToLibrary() {
493
797
  await this.#sendCommand(Proto.Command.AddNowPlayingItemToLibrary);
494
798
  }
799
+ /**
800
+ * Sets the text input field to the given text, replacing any existing content.
801
+ *
802
+ * @param text - The text to set.
803
+ */
495
804
  async textSet(text) {
496
805
  await this.#dataStream.send(DataStreamMessage.textInput(text, Proto.ActionType_Enum.Set));
497
806
  }
807
+ /**
808
+ * Appends text to the current text input field content.
809
+ *
810
+ * @param text - The text to append.
811
+ */
498
812
  async textAppend(text) {
499
813
  await this.#dataStream.send(DataStreamMessage.textInput(text, Proto.ActionType_Enum.Insert));
500
814
  }
815
+ /** Clears the text input field. */
501
816
  async textClear() {
502
817
  await this.#dataStream.send(DataStreamMessage.textInput("", Proto.ActionType_Enum.ClearAction));
503
818
  }
819
+ /** Requests the current keyboard session state from the Apple TV. */
504
820
  async getKeyboardSession() {
505
821
  await this.#dataStream.send(DataStreamMessage.getKeyboardSession());
506
822
  }
823
+ /**
824
+ * Simulates a tap at the given coordinates.
825
+ *
826
+ * @param x - Horizontal position in the virtual touch area.
827
+ * @param y - Vertical position in the virtual touch area.
828
+ * @param finger - Finger index for multi-touch (defaults to 1).
829
+ */
507
830
  async tap(x, y, finger = 1) {
508
831
  await this.#sendTouch(x, y, 1, finger);
509
832
  await waitFor(50);
510
833
  await this.#sendTouch(x, y, 4, finger);
511
834
  }
835
+ /**
836
+ * Simulates an upward swipe gesture.
837
+ *
838
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
839
+ */
512
840
  async swipeUp(duration = 200) {
513
841
  await this.#swipe(200, 400, 200, 100, duration);
514
842
  }
843
+ /**
844
+ * Simulates a downward swipe gesture.
845
+ *
846
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
847
+ */
515
848
  async swipeDown(duration = 200) {
516
849
  await this.#swipe(200, 100, 200, 400, duration);
517
850
  }
851
+ /**
852
+ * Simulates a leftward swipe gesture.
853
+ *
854
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
855
+ */
518
856
  async swipeLeft(duration = 200) {
519
857
  await this.#swipe(400, 200, 100, 200, duration);
520
858
  }
859
+ /**
860
+ * Simulates a rightward swipe gesture.
861
+ *
862
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
863
+ */
521
864
  async swipeRight(duration = 200) {
522
865
  await this.#swipe(100, 200, 400, 200, duration);
523
866
  }
867
+ /**
868
+ * Sends a double press of a HID key (two press-and-release cycles with a 150ms gap).
869
+ *
870
+ * @param usePage - USB HID usage page (1 = Generic Desktop, 12 = Consumer).
871
+ * @param usage - USB HID usage code.
872
+ */
524
873
  async doublePress(usePage, usage) {
525
874
  await this.pressAndRelease(usePage, usage);
526
875
  await waitFor(150);
527
876
  await this.pressAndRelease(usePage, usage);
528
877
  }
878
+ /**
879
+ * Sends a long press of a HID key (hold for a configurable duration).
880
+ *
881
+ * @param usePage - USB HID usage page (1 = Generic Desktop, 12 = Consumer).
882
+ * @param usage - USB HID usage code.
883
+ * @param duration - Hold duration in milliseconds (defaults to 1000).
884
+ */
529
885
  async longPress(usePage, usage, duration = 1e3) {
530
886
  await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, true));
531
887
  await waitFor(duration);
532
888
  await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, false));
533
889
  }
890
+ /**
891
+ * Sends a single press-and-release of a HID key with a 25ms hold.
892
+ *
893
+ * @param usePage - USB HID usage page (1 = Generic Desktop, 12 = Consumer).
894
+ * @param usage - USB HID usage code.
895
+ */
534
896
  async pressAndRelease(usePage, usage) {
535
897
  await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, true));
536
898
  await waitFor(25);
537
899
  await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, false));
538
900
  }
901
+ /**
902
+ * Sends a SendCommand request and checks the result.
903
+ *
904
+ * @param command - The command to send.
905
+ * @param options - Optional command options.
906
+ * @returns The command result message, or undefined if no result was returned.
907
+ * @throws SendCommandError when the Apple TV reports a send error.
908
+ */
539
909
  async #sendCommand(command, options) {
540
910
  const response = await this.#dataStream.exchange(DataStreamMessage.sendCommand(command, options));
541
911
  return this.#checkCommandResult(response);
542
912
  }
913
+ /**
914
+ * Sends a pre-built command message and checks the result.
915
+ *
916
+ * @param message - The pre-built DataStream message to send.
917
+ * @returns The command result message, or undefined if no result was returned.
918
+ * @throws SendCommandError when the Apple TV reports a send error.
919
+ */
543
920
  async #sendCommandRaw(message) {
544
921
  const response = await this.#dataStream.exchange(message);
545
922
  return this.#checkCommandResult(response);
546
923
  }
924
+ /**
925
+ * Validates the response from a SendCommand request and throws on error.
926
+ *
927
+ * @param response - The protocol message response.
928
+ * @returns The decoded result, or undefined if the response has no result extension.
929
+ * @throws SendCommandError when the result indicates a send error.
930
+ */
547
931
  #checkCommandResult(response) {
548
- const result = DataStreamMessage.getExtension(response, Proto.sendCommandResultMessage);
932
+ let result;
933
+ try {
934
+ result = DataStreamMessage.getExtension(response, Proto.sendCommandResultMessage);
935
+ } catch {
936
+ return;
937
+ }
938
+ if (!result) return;
549
939
  if (result.sendError !== Proto.SendError_Enum.NoError) throw new SendCommandError(result.sendError, result.handlerReturnStatus);
550
940
  return result;
551
941
  }
942
+ /**
943
+ * Sends a virtual touch event at the given coordinates.
944
+ *
945
+ * @param x - Horizontal position.
946
+ * @param y - Vertical position.
947
+ * @param phase - Touch phase (1 = Began, 2 = Moved, 4 = Ended).
948
+ * @param finger - Finger index for multi-touch.
949
+ */
552
950
  async #sendTouch(x, y, phase, finger) {
553
951
  await this.#dataStream.exchange(DataStreamMessage.sendVirtualTouchEvent(x, y, phase, finger));
554
952
  }
953
+ /**
954
+ * Performs a swipe gesture by interpolating touch events between start and end coordinates.
955
+ *
956
+ * @param startX - Starting horizontal position.
957
+ * @param startY - Starting vertical position.
958
+ * @param endX - Ending horizontal position.
959
+ * @param endY - Ending vertical position.
960
+ * @param duration - Total swipe duration in milliseconds.
961
+ */
555
962
  async #swipe(startX, startY, endX, endY, duration) {
556
963
  const steps = Math.max(4, Math.floor(duration / 50));
557
964
  const deltaX = (endX - startX) / steps;
@@ -569,52 +976,81 @@ var remote_default = class {
569
976
 
570
977
  //#endregion
571
978
  //#region src/airplay/state.ts
979
+ /**
980
+ * Tracks the complete state of an AirPlay device: clients, players, now-playing,
981
+ * volume, keyboard, output devices, and cluster info.
982
+ * Listens to DataStream protocol messages and emits both low-level (1:1 with protocol)
983
+ * and high-level (deduplicated, resolved) events.
984
+ */
572
985
  var state_default = class extends EventEmitter {
986
+ /** @returns The DataStream for event subscription. */
573
987
  get #dataStream() {
574
988
  return this.#protocol.dataStream;
575
989
  }
990
+ /** @returns The underlying AirPlay Protocol instance. */
576
991
  get #protocol() {
577
992
  return this.#device[PROTOCOL];
578
993
  }
994
+ /** All known clients (apps) keyed by bundle identifier. */
579
995
  get clients() {
580
996
  return this.#clients;
581
997
  }
998
+ /** Whether a keyboard/text input session is currently active on the Apple TV. */
582
999
  get isKeyboardActive() {
583
1000
  return this.#keyboardState === Proto.KeyboardState_Enum.DidBeginEditing || this.#keyboardState === Proto.KeyboardState_Enum.Editing || this.#keyboardState === Proto.KeyboardState_Enum.TextDidChange;
584
1001
  }
1002
+ /** Text editing attributes for the active keyboard session, or null. */
585
1003
  get keyboardAttributes() {
586
1004
  return this.#keyboardAttributes;
587
1005
  }
1006
+ /** Current keyboard state enum value. */
588
1007
  get keyboardState() {
589
1008
  return this.#keyboardState;
590
1009
  }
1010
+ /** The currently active now-playing client, or null if nothing is playing. */
591
1011
  get nowPlayingClient() {
592
1012
  return this.#nowPlayingClientBundleIdentifier ? this.#clients[this.#nowPlayingClientBundleIdentifier] ?? null : null;
593
1013
  }
1014
+ /** UID of the primary output device (used for volume control and multi-room). */
594
1015
  get outputDeviceUID() {
595
1016
  return this.#outputDeviceUID;
596
1017
  }
1018
+ /** List of all output device descriptors in the current AirPlay group. */
597
1019
  get outputDevices() {
598
1020
  return this.#outputDevices;
599
1021
  }
1022
+ /** Cluster identifier for multi-room groups, or null. */
600
1023
  get clusterID() {
601
1024
  return this.#clusterID;
602
1025
  }
1026
+ /** Cluster type code (0 if not clustered). */
603
1027
  get clusterType() {
604
1028
  return this.#clusterType;
605
1029
  }
1030
+ /** Whether this device is aware of multi-room clusters. */
606
1031
  get isClusterAware() {
607
1032
  return this.#isClusterAware;
608
1033
  }
1034
+ /** Whether this device is the leader of its multi-room cluster. */
1035
+ get isClusterLeader() {
1036
+ return this.#isClusterLeader;
1037
+ }
1038
+ /** Current volume level (0.0 - 1.0). */
609
1039
  get volume() {
610
1040
  return this.#volume;
611
1041
  }
1042
+ /** Whether volume control is available on this device. */
612
1043
  get volumeAvailable() {
613
1044
  return this.#volumeAvailable;
614
1045
  }
1046
+ /** Volume capabilities (absolute, relative, both, or none). */
615
1047
  get volumeCapabilities() {
616
1048
  return this.#volumeCapabilities;
617
1049
  }
1050
+ /** Whether the device is currently muted. */
1051
+ get volumeMuted() {
1052
+ return this.#volumeMuted;
1053
+ }
618
1054
  #device;
619
1055
  #clients;
620
1056
  #keyboardAttributes;
@@ -626,13 +1062,21 @@ var state_default = class extends EventEmitter {
626
1062
  #clusterID;
627
1063
  #clusterType;
628
1064
  #isClusterAware;
1065
+ #isClusterLeader;
629
1066
  #volume;
630
1067
  #volumeAvailable;
631
1068
  #volumeCapabilities;
1069
+ #volumeMuted;
1070
+ /**
1071
+ * Creates a new AirPlayState tracker.
1072
+ *
1073
+ * @param device - The AirPlay device to track state for.
1074
+ */
632
1075
  constructor(device) {
633
1076
  super();
634
1077
  this.#device = device;
635
1078
  this.clear();
1079
+ this.onConfigureConnection = this.onConfigureConnection.bind(this);
636
1080
  this.onKeyboard = this.onKeyboard.bind(this);
637
1081
  this.onDeviceInfo = this.onDeviceInfo.bind(this);
638
1082
  this.onDeviceInfoUpdate = this.onDeviceInfoUpdate.bind(this);
@@ -641,6 +1085,7 @@ var state_default = class extends EventEmitter {
641
1085
  this.onRemoveClient = this.onRemoveClient.bind(this);
642
1086
  this.onRemovePlayer = this.onRemovePlayer.bind(this);
643
1087
  this.onSendCommandResult = this.onSendCommandResult.bind(this);
1088
+ this.onSendLyricsEvent = this.onSendLyricsEvent.bind(this);
644
1089
  this.onSetArtwork = this.onSetArtwork.bind(this);
645
1090
  this.onSetDefaultSupportedCommands = this.onSetDefaultSupportedCommands.bind(this);
646
1091
  this.onSetNowPlayingClient = this.onSetNowPlayingClient.bind(this);
@@ -654,8 +1099,11 @@ var state_default = class extends EventEmitter {
654
1099
  this.onVolumeControlAvailability = this.onVolumeControlAvailability.bind(this);
655
1100
  this.onVolumeControlCapabilitiesDidChange = this.onVolumeControlCapabilitiesDidChange.bind(this);
656
1101
  this.onVolumeDidChange = this.onVolumeDidChange.bind(this);
1102
+ this.onVolumeMutedDidChange = this.onVolumeMutedDidChange.bind(this);
657
1103
  }
1104
+ /** Subscribes to all DataStream events to track device state. Called internally via symbol. */
658
1105
  [STATE_SUBSCRIBE_SYMBOL]() {
1106
+ this.#dataStream.on("configureConnection", this.onConfigureConnection);
659
1107
  this.#dataStream.on("keyboard", this.onKeyboard);
660
1108
  this.#dataStream.on("deviceInfo", this.onDeviceInfo);
661
1109
  this.#dataStream.on("deviceInfoUpdate", this.onDeviceInfoUpdate);
@@ -664,6 +1112,7 @@ var state_default = class extends EventEmitter {
664
1112
  this.#dataStream.on("removeClient", this.onRemoveClient);
665
1113
  this.#dataStream.on("removePlayer", this.onRemovePlayer);
666
1114
  this.#dataStream.on("sendCommandResult", this.onSendCommandResult);
1115
+ this.#dataStream.on("sendLyricsEvent", this.onSendLyricsEvent);
667
1116
  this.#dataStream.on("setArtwork", this.onSetArtwork);
668
1117
  this.#dataStream.on("setDefaultSupportedCommands", this.onSetDefaultSupportedCommands);
669
1118
  this.#dataStream.on("setNowPlayingClient", this.onSetNowPlayingClient);
@@ -677,10 +1126,13 @@ var state_default = class extends EventEmitter {
677
1126
  this.#dataStream.on("volumeControlAvailability", this.onVolumeControlAvailability);
678
1127
  this.#dataStream.on("volumeControlCapabilitiesDidChange", this.onVolumeControlCapabilitiesDidChange);
679
1128
  this.#dataStream.on("volumeDidChange", this.onVolumeDidChange);
1129
+ this.#dataStream.on("volumeMutedDidChange", this.onVolumeMutedDidChange);
680
1130
  }
1131
+ /** Unsubscribes from all DataStream events. Called internally via symbol. */
681
1132
  [STATE_UNSUBSCRIBE_SYMBOL]() {
682
1133
  const dataStream = this.#dataStream;
683
1134
  if (!dataStream) return;
1135
+ dataStream.off("configureConnection", this.onConfigureConnection);
684
1136
  dataStream.off("keyboard", this.onKeyboard);
685
1137
  dataStream.off("deviceInfo", this.onDeviceInfo);
686
1138
  dataStream.off("deviceInfoUpdate", this.onDeviceInfoUpdate);
@@ -689,6 +1141,7 @@ var state_default = class extends EventEmitter {
689
1141
  dataStream.off("removeClient", this.onRemoveClient);
690
1142
  dataStream.off("removePlayer", this.onRemovePlayer);
691
1143
  dataStream.off("sendCommandResult", this.onSendCommandResult);
1144
+ dataStream.off("sendLyricsEvent", this.onSendLyricsEvent);
692
1145
  dataStream.off("setArtwork", this.onSetArtwork);
693
1146
  dataStream.off("setDefaultSupportedCommands", this.onSetDefaultSupportedCommands);
694
1147
  dataStream.off("setNowPlayingClient", this.onSetNowPlayingClient);
@@ -702,7 +1155,9 @@ var state_default = class extends EventEmitter {
702
1155
  dataStream.off("volumeControlAvailability", this.onVolumeControlAvailability);
703
1156
  dataStream.off("volumeControlCapabilitiesDidChange", this.onVolumeControlCapabilitiesDidChange);
704
1157
  dataStream.off("volumeDidChange", this.onVolumeDidChange);
1158
+ dataStream.off("volumeMutedDidChange", this.onVolumeMutedDidChange);
705
1159
  }
1160
+ /** Resets all state to initial/default values. Called on connect and reconnect. */
706
1161
  clear() {
707
1162
  this.#clients = {};
708
1163
  this.#keyboardAttributes = null;
@@ -714,127 +1169,310 @@ var state_default = class extends EventEmitter {
714
1169
  this.#clusterID = null;
715
1170
  this.#clusterType = 0;
716
1171
  this.#isClusterAware = false;
1172
+ this.#isClusterLeader = false;
717
1173
  this.#volume = 0;
718
1174
  this.#volumeAvailable = false;
719
1175
  this.#volumeCapabilities = Proto.VolumeCapabilities_Enum.None;
720
- }
1176
+ this.#volumeMuted = false;
1177
+ }
1178
+ /**
1179
+ * Handles a ConfigureConnection message from the Apple TV.
1180
+ *
1181
+ * @param message - The configure connection message.
1182
+ */
1183
+ onConfigureConnection(message) {
1184
+ this.emit("configureConnection", message);
1185
+ }
1186
+ /**
1187
+ * Handles keyboard state changes. Updates internal state and emits 'keyboard'.
1188
+ *
1189
+ * @param message - The keyboard message with state and attributes.
1190
+ */
721
1191
  onKeyboard(message) {
722
1192
  this.#keyboardState = message.state;
723
1193
  this.#keyboardAttributes = message.attributes ?? null;
724
1194
  this.emit("keyboard", message);
725
1195
  }
1196
+ /**
1197
+ * Handles initial device info. Updates output device UID and cluster info.
1198
+ *
1199
+ * @param message - The device info message.
1200
+ */
726
1201
  onDeviceInfo(message) {
727
- this.#updateOutputDeviceUID(message);
1202
+ this.#updateDeviceInfo(message);
728
1203
  this.emit("deviceInfo", message);
729
1204
  }
1205
+ /**
1206
+ * Handles device info updates (e.g. cluster changes). Updates output device UID and cluster info.
1207
+ *
1208
+ * @param message - The device info update message.
1209
+ */
730
1210
  onDeviceInfoUpdate(message) {
731
- this.#updateOutputDeviceUID(message);
1211
+ this.#updateDeviceInfo(message);
732
1212
  this.emit("deviceInfoUpdate", message);
733
1213
  }
1214
+ /**
1215
+ * Handles origin client properties updates.
1216
+ *
1217
+ * @param message - The origin client properties message.
1218
+ */
734
1219
  onOriginClientProperties(message) {
735
1220
  this.emit("originClientProperties", message);
736
1221
  }
1222
+ /**
1223
+ * Handles player client properties updates.
1224
+ *
1225
+ * @param message - The player client properties message.
1226
+ */
737
1227
  onPlayerClientProperties(message) {
738
1228
  this.emit("playerClientProperties", message);
739
1229
  }
1230
+ /**
1231
+ * Handles removal of a client (app). Clears the now-playing reference if
1232
+ * the removed client was the active one.
1233
+ *
1234
+ * @param message - The remove client message.
1235
+ */
740
1236
  onRemoveClient(message) {
1237
+ if (!message.client?.bundleIdentifier) return;
741
1238
  if (!(message.client.bundleIdentifier in this.#clients)) return;
742
1239
  const wasActive = this.#nowPlayingClientBundleIdentifier === message.client.bundleIdentifier;
743
1240
  delete this.#clients[message.client.bundleIdentifier];
744
1241
  if (wasActive) this.#nowPlayingClientBundleIdentifier = null;
745
1242
  this.emit("removeClient", message);
746
1243
  this.emit("clients", this.#clients);
747
- if (wasActive) this.#emitNowPlayingChangedIfNeeded();
1244
+ if (wasActive) {
1245
+ this.#emitActivePlayerChanged();
1246
+ this.#emitNowPlayingChangedIfNeeded();
1247
+ }
748
1248
  }
1249
+ /**
1250
+ * Handles command result notifications from the Apple TV.
1251
+ *
1252
+ * @param message - The send command result message.
1253
+ */
749
1254
  onSendCommandResult(message) {
750
1255
  this.emit("sendCommandResult", message);
751
1256
  }
1257
+ /**
1258
+ * Handles lyrics events (time-synced lyrics updates).
1259
+ *
1260
+ * @param message - The lyrics event message.
1261
+ */
1262
+ onSendLyricsEvent(message) {
1263
+ if (message.event) this.emit("lyricsEvent", message.event, message.playerPath);
1264
+ }
1265
+ /**
1266
+ * Handles artwork set notifications.
1267
+ *
1268
+ * @param message - The set artwork message.
1269
+ */
752
1270
  onSetArtwork(message) {
753
1271
  this.emit("setArtwork", message);
754
1272
  }
1273
+ /**
1274
+ * Handles default supported commands for a client. These serve as fallback
1275
+ * commands when a player has no commands of its own.
1276
+ *
1277
+ * @param message - The set default supported commands message.
1278
+ */
755
1279
  onSetDefaultSupportedCommands(message) {
756
1280
  if (message.playerPath?.client?.bundleIdentifier && message.supportedCommands) this.#client(message.playerPath.client.bundleIdentifier, message.playerPath.client.displayName).setDefaultSupportedCommands(message.supportedCommands.supportedCommands);
757
1281
  this.emit("setDefaultSupportedCommands", message);
758
1282
  }
1283
+ /**
1284
+ * Handles the now-playing client changing (e.g. user switches app).
1285
+ * Updates the active client reference and emits change events.
1286
+ *
1287
+ * @param message - The set now-playing client message.
1288
+ */
759
1289
  onSetNowPlayingClient(message) {
1290
+ const oldBundleId = this.#nowPlayingClientBundleIdentifier;
760
1291
  this.#nowPlayingClientBundleIdentifier = message.client?.bundleIdentifier ?? null;
761
1292
  if (message.client?.bundleIdentifier && message.client?.displayName) this.#client(message.client.bundleIdentifier, message.client.displayName);
762
1293
  this.emit("setNowPlayingClient", message);
1294
+ if (oldBundleId !== this.#nowPlayingClientBundleIdentifier) this.#emitActivePlayerChanged();
763
1295
  this.#emitNowPlayingChangedIfNeeded();
764
1296
  }
1297
+ /**
1298
+ * Handles the active player changing within a client (e.g. PiP player becomes active).
1299
+ * Creates the player if needed and sets it as the active player.
1300
+ *
1301
+ * @param message - The set now-playing player message.
1302
+ */
765
1303
  onSetNowPlayingPlayer(message) {
766
1304
  if (message.playerPath?.client?.bundleIdentifier && message.playerPath?.player?.identifier) {
767
1305
  const client = this.#client(message.playerPath.client.bundleIdentifier, message.playerPath.client.displayName);
1306
+ const oldActiveId = client.activePlayer?.identifier;
768
1307
  client.getOrCreatePlayer(message.playerPath.player.identifier, message.playerPath.player.displayName);
769
1308
  client.setActivePlayer(message.playerPath.player.identifier);
1309
+ if (oldActiveId !== message.playerPath.player.identifier) this.#emitActivePlayerChanged();
770
1310
  }
771
1311
  this.emit("setNowPlayingPlayer", message);
772
1312
  this.#emitNowPlayingChangedIfNeeded();
773
1313
  }
1314
+ /**
1315
+ * Handles comprehensive state updates. Processes playback state, now-playing info,
1316
+ * supported commands, and playback queue in a single message.
1317
+ * Emits granular events for each changed aspect.
1318
+ *
1319
+ * @param message - The set state message.
1320
+ */
774
1321
  onSetState(message) {
775
- const bundleIdentifier = message.playerPath.client.bundleIdentifier;
1322
+ const bundleIdentifier = message.playerPath?.client?.bundleIdentifier;
1323
+ if (!bundleIdentifier) return;
776
1324
  const client = this.#client(bundleIdentifier, message.displayName);
777
1325
  const playerIdentifier = message.playerPath?.player?.identifier || "MediaRemote-DefaultPlayer";
778
1326
  const player = client.getOrCreatePlayer(playerIdentifier, message.playerPath?.player?.displayName);
1327
+ const isActiveClient = bundleIdentifier === this.#nowPlayingClientBundleIdentifier;
1328
+ if (message.playbackState) {
1329
+ const oldState = player.playbackState;
1330
+ player.setPlaybackState(message.playbackState, message.playbackStateTimestamp);
1331
+ if (isActiveClient && oldState !== player.playbackState) this.emit("playbackStateChanged", client, player, oldState, player.playbackState);
1332
+ }
779
1333
  if (message.nowPlayingInfo) player.setNowPlayingInfo(message.nowPlayingInfo);
780
- if (message.playbackState) player.setPlaybackState(message.playbackState, message.playbackStateTimestamp);
781
- if (message.supportedCommands) player.setSupportedCommands(message.supportedCommands.supportedCommands);
782
- if (message.playbackQueue) player.setPlaybackQueue(message.playbackQueue);
1334
+ if (message.supportedCommands) {
1335
+ player.setSupportedCommands(message.supportedCommands.supportedCommands);
1336
+ if (isActiveClient) this.emit("supportedCommandsChanged", client, player, player.supportedCommands);
1337
+ }
1338
+ if (message.playbackQueue) {
1339
+ player.setPlaybackQueue(message.playbackQueue);
1340
+ if (isActiveClient) this.emit("playbackQueueChanged", client, player);
1341
+ }
783
1342
  this.emit("setState", message);
784
- if (bundleIdentifier === this.#nowPlayingClientBundleIdentifier) this.#emitNowPlayingChangedIfNeeded();
1343
+ if (isActiveClient) this.#emitNowPlayingChangedIfNeeded();
785
1344
  }
1345
+ /**
1346
+ * Handles content item updates (metadata, artwork, lyrics changes for existing items).
1347
+ *
1348
+ * @param message - The update content item message.
1349
+ */
786
1350
  onUpdateContentItem(message) {
787
- const bundleIdentifier = message.playerPath.client.bundleIdentifier;
788
- const client = this.#client(bundleIdentifier, message.playerPath.client.displayName);
1351
+ const bundleIdentifier = message.playerPath?.client?.bundleIdentifier;
1352
+ if (!bundleIdentifier) return;
1353
+ const client = this.#client(bundleIdentifier, message.playerPath?.client?.displayName ?? "");
789
1354
  const playerIdentifier = message.playerPath?.player?.identifier || "MediaRemote-DefaultPlayer";
790
1355
  const player = client.getOrCreatePlayer(playerIdentifier, message.playerPath?.player?.displayName);
791
1356
  for (const item of message.contentItems) player.updateContentItem(item);
792
1357
  this.emit("updateContentItem", message);
793
1358
  if (bundleIdentifier === this.#nowPlayingClientBundleIdentifier) this.#emitNowPlayingChangedIfNeeded();
794
1359
  }
1360
+ /**
1361
+ * Handles artwork updates for content items. Emits 'artworkChanged' if a client and player are active.
1362
+ *
1363
+ * @param message - The update content item artwork message.
1364
+ */
795
1365
  onUpdateContentItemArtwork(message) {
796
1366
  this.emit("updateContentItemArtwork", message);
797
- }
1367
+ const client = this.nowPlayingClient;
1368
+ const player = client?.activePlayer;
1369
+ if (client && player) this.emit("artworkChanged", client, player);
1370
+ }
1371
+ /**
1372
+ * Handles player registration or update. Creates the player if it does not exist.
1373
+ *
1374
+ * @param message - The update player message.
1375
+ */
798
1376
  onUpdatePlayer(message) {
799
1377
  if (message.playerPath?.client?.bundleIdentifier && message.playerPath?.player?.identifier) this.#client(message.playerPath.client.bundleIdentifier, message.playerPath.client.displayName).getOrCreatePlayer(message.playerPath.player.identifier, message.playerPath.player.displayName);
800
1378
  this.emit("updatePlayer", message);
801
1379
  }
1380
+ /**
1381
+ * Handles player removal. Removes the player from its client and emits
1382
+ * active player changed events if the removed player was active.
1383
+ *
1384
+ * @param message - The remove player message.
1385
+ */
802
1386
  onRemovePlayer(message) {
803
1387
  if (message.playerPath?.client?.bundleIdentifier && message.playerPath?.player?.identifier) {
804
1388
  const client = this.#clients[message.playerPath.client.bundleIdentifier];
805
1389
  if (client) client.removePlayer(message.playerPath.player.identifier);
806
1390
  }
807
1391
  this.emit("removePlayer", message);
808
- if (message.playerPath?.client?.bundleIdentifier === this.#nowPlayingClientBundleIdentifier) this.#emitNowPlayingChangedIfNeeded();
1392
+ if (message.playerPath?.client?.bundleIdentifier === this.#nowPlayingClientBundleIdentifier) {
1393
+ this.#emitActivePlayerChanged();
1394
+ this.#emitNowPlayingChangedIfNeeded();
1395
+ }
809
1396
  }
1397
+ /**
1398
+ * Handles client (app) registration or display name update.
1399
+ *
1400
+ * @param message - The update client message.
1401
+ */
810
1402
  onUpdateClient(message) {
1403
+ if (!message.client?.bundleIdentifier) return;
811
1404
  this.#client(message.client.bundleIdentifier, message.client.displayName);
1405
+ this.emit("updateClient", message);
812
1406
  this.emit("clients", this.#clients);
813
1407
  }
1408
+ /**
1409
+ * Handles output device list updates. Prefers cluster-aware devices when available.
1410
+ *
1411
+ * @param message - The update output device message.
1412
+ */
814
1413
  onUpdateOutputDevice(message) {
815
1414
  this.#outputDevices = message.clusterAwareOutputDevices?.length > 0 ? message.clusterAwareOutputDevices : message.outputDevices;
816
1415
  this.emit("updateOutputDevice", message);
817
1416
  }
1417
+ /**
1418
+ * Handles volume control availability changes.
1419
+ *
1420
+ * @param message - The volume control availability message.
1421
+ */
818
1422
  onVolumeControlAvailability(message) {
819
1423
  this.#volumeAvailable = message.volumeControlAvailable;
820
1424
  this.#volumeCapabilities = message.volumeCapabilities;
821
1425
  this.emit("volumeControlAvailability", message.volumeControlAvailable, message.volumeCapabilities);
822
1426
  }
1427
+ /**
1428
+ * Handles volume capabilities changes (e.g. device gains or loses absolute volume support).
1429
+ *
1430
+ * @param message - The volume capabilities change message.
1431
+ */
823
1432
  onVolumeControlCapabilitiesDidChange(message) {
1433
+ if (!message.capabilities) return;
824
1434
  this.#volumeAvailable = message.capabilities.volumeControlAvailable;
825
1435
  this.#volumeCapabilities = message.capabilities.volumeCapabilities;
826
1436
  this.emit("volumeControlCapabilitiesDidChange", message.capabilities.volumeControlAvailable, message.capabilities.volumeCapabilities);
827
1437
  }
1438
+ /**
1439
+ * Handles volume level changes.
1440
+ *
1441
+ * @param message - The volume change message.
1442
+ */
828
1443
  onVolumeDidChange(message) {
829
1444
  this.#volume = message.volume;
830
1445
  this.emit("volumeDidChange", message.volume);
831
1446
  }
832
- #updateOutputDeviceUID(message) {
1447
+ /**
1448
+ * Handles mute state changes.
1449
+ *
1450
+ * @param message - The volume muted change message.
1451
+ */
1452
+ onVolumeMutedDidChange(message) {
1453
+ this.#volumeMuted = message.isMuted;
1454
+ this.emit("volumeMutedDidChange", this.#volumeMuted);
1455
+ }
1456
+ /**
1457
+ * Extracts output device UID and cluster information from a device info message.
1458
+ *
1459
+ * @param message - The device info message.
1460
+ */
1461
+ #updateDeviceInfo(message) {
833
1462
  this.#outputDeviceUID = message.clusterID || message.deviceUID || message.uniqueIdentifier || null;
834
1463
  this.#clusterID = message.clusterID || null;
835
1464
  this.#clusterType = message.clusterType ?? 0;
836
1465
  this.#isClusterAware = message.isClusterAware ?? false;
837
- }
1466
+ this.#isClusterLeader = message.isClusterLeader ?? false;
1467
+ }
1468
+ /**
1469
+ * Gets or creates a Client for the given bundle identifier.
1470
+ * Updates the display name if the client already exists.
1471
+ *
1472
+ * @param bundleIdentifier - The app's bundle identifier.
1473
+ * @param displayName - The app's display name.
1474
+ * @returns The existing or newly created Client.
1475
+ */
838
1476
  #client(bundleIdentifier, displayName) {
839
1477
  if (bundleIdentifier in this.#clients) {
840
1478
  const client = this.#clients[bundleIdentifier];
@@ -847,6 +1485,11 @@ var state_default = class extends EventEmitter {
847
1485
  return client;
848
1486
  }
849
1487
  }
1488
+ /**
1489
+ * Creates a snapshot of the current now-playing state for change detection.
1490
+ *
1491
+ * @returns A NowPlayingSnapshot of the current state.
1492
+ */
850
1493
  #createNowPlayingSnapshot() {
851
1494
  const client = this.nowPlayingClient;
852
1495
  const player = client?.activePlayer ?? null;
@@ -859,6 +1502,7 @@ var state_default = class extends EventEmitter {
859
1502
  album: player?.album ?? "",
860
1503
  genre: player?.genre ?? "",
861
1504
  duration: player?.duration ?? 0,
1505
+ playbackRate: player?.playbackRate ?? 0,
862
1506
  shuffleMode: player?.shuffleMode ?? Proto.ShuffleMode_Enum.Unknown,
863
1507
  repeatMode: player?.repeatMode ?? Proto.RepeatMode_Enum.Unknown,
864
1508
  mediaType: player?.mediaType ?? Proto.ContentItemMetadata_MediaType.UnknownMediaType,
@@ -868,9 +1512,17 @@ var state_default = class extends EventEmitter {
868
1512
  contentIdentifier: player?.contentIdentifier ?? "",
869
1513
  artworkId: player?.artworkId ?? null,
870
1514
  hasArtworkUrl: player?.artworkUrl() != null,
871
- hasArtworkData: player?.currentItemArtwork != null
1515
+ hasArtworkData: player?.currentItemArtwork != null,
1516
+ isAlwaysLive: player?.nowPlayingInfo?.isAlwaysLive ?? false,
1517
+ isAdvertisement: player?.nowPlayingInfo?.isAdvertisement ?? false
872
1518
  };
873
1519
  }
1520
+ /** Emits the 'activePlayerChanged' event with the current client and player. */
1521
+ #emitActivePlayerChanged() {
1522
+ const client = this.nowPlayingClient;
1523
+ this.emit("activePlayerChanged", client, client?.activePlayer ?? null);
1524
+ }
1525
+ /** Emits 'nowPlayingChanged' only if the now-playing snapshot has actually changed. */
874
1526
  #emitNowPlayingChangedIfNeeded() {
875
1527
  const snapshot = this.#createNowPlayingSnapshot();
876
1528
  const previous = this.#nowPlayingSnapshot;
@@ -879,25 +1531,51 @@ var state_default = class extends EventEmitter {
879
1531
  const client = this.nowPlayingClient;
880
1532
  this.emit("nowPlayingChanged", client, client?.activePlayer ?? null);
881
1533
  }
1534
+ /**
1535
+ * Compares two NowPlayingSnapshot instances field-by-field for equality.
1536
+ *
1537
+ * @param a - First snapshot.
1538
+ * @param b - Second snapshot.
1539
+ * @returns True if all fields are equal.
1540
+ */
882
1541
  #snapshotsEqual(a, b) {
883
- return a.bundleIdentifier === b.bundleIdentifier && a.playerIdentifier === b.playerIdentifier && a.playbackState === b.playbackState && a.title === b.title && a.artist === b.artist && a.album === b.album && a.genre === b.genre && a.duration === b.duration && a.shuffleMode === b.shuffleMode && a.repeatMode === b.repeatMode && a.mediaType === b.mediaType && a.seriesName === b.seriesName && a.seasonNumber === b.seasonNumber && a.episodeNumber === b.episodeNumber && a.contentIdentifier === b.contentIdentifier && a.artworkId === b.artworkId && a.hasArtworkUrl === b.hasArtworkUrl && a.hasArtworkData === b.hasArtworkData;
1542
+ return a.bundleIdentifier === b.bundleIdentifier && a.playerIdentifier === b.playerIdentifier && a.playbackState === b.playbackState && a.title === b.title && a.artist === b.artist && a.album === b.album && a.genre === b.genre && a.duration === b.duration && a.playbackRate === b.playbackRate && a.shuffleMode === b.shuffleMode && a.repeatMode === b.repeatMode && a.mediaType === b.mediaType && a.seriesName === b.seriesName && a.seasonNumber === b.seasonNumber && a.episodeNumber === b.episodeNumber && a.contentIdentifier === b.contentIdentifier && a.artworkId === b.artworkId && a.hasArtworkUrl === b.hasArtworkUrl && a.hasArtworkData === b.hasArtworkData && a.isAlwaysLive === b.isAlwaysLive && a.isAdvertisement === b.isAdvertisement;
884
1543
  }
885
1544
  };
886
1545
 
887
1546
  //#endregion
888
1547
  //#region src/airplay/volume.ts
1548
+ /** Volume adjustment step size as a fraction (0.05 = 5%). */
889
1549
  const VOLUME_STEP = .05;
1550
+ /**
1551
+ * Smart volume controller for an AirPlay device.
1552
+ * Automatically chooses between absolute volume (set a specific level) and
1553
+ * relative volume (HID volume up/down keys) based on the device's reported capabilities.
1554
+ */
890
1555
  var volume_default = class {
1556
+ /** @returns The underlying AirPlay Protocol instance. */
891
1557
  get #protocol() {
892
1558
  return this.#device[PROTOCOL];
893
1559
  }
1560
+ /** @returns The AirPlay state for volume and capability information. */
894
1561
  get #state() {
895
1562
  return this.#device.state;
896
1563
  }
897
1564
  #device;
1565
+ /**
1566
+ * Creates a new Volume controller.
1567
+ *
1568
+ * @param device - The AirPlay device to control volume for.
1569
+ */
898
1570
  constructor(device) {
899
1571
  this.#device = device;
900
1572
  }
1573
+ /**
1574
+ * Decreases the volume by one step. Uses absolute volume when available,
1575
+ * falls back to HID relative volume keys otherwise.
1576
+ *
1577
+ * @throws CommandError when volume control is not available.
1578
+ */
901
1579
  async down() {
902
1580
  switch (this.#state.volumeCapabilities) {
903
1581
  case Proto.VolumeCapabilities_Enum.Absolute:
@@ -911,6 +1589,12 @@ var volume_default = class {
911
1589
  default: throw new CommandError("Volume control is not available.");
912
1590
  }
913
1591
  }
1592
+ /**
1593
+ * Increases the volume by one step. Uses absolute volume when available,
1594
+ * falls back to HID relative volume keys otherwise.
1595
+ *
1596
+ * @throws CommandError when volume control is not available.
1597
+ */
914
1598
  async up() {
915
1599
  switch (this.#state.volumeCapabilities) {
916
1600
  case Proto.VolumeCapabilities_Enum.Absolute:
@@ -924,12 +1608,24 @@ var volume_default = class {
924
1608
  default: throw new CommandError("Volume control is not available.");
925
1609
  }
926
1610
  }
1611
+ /**
1612
+ * Fetches the current volume level from the device.
1613
+ *
1614
+ * @returns The volume level as a float between 0.0 and 1.0.
1615
+ * @throws CommandError when no output device is active or the request fails.
1616
+ */
927
1617
  async get() {
928
1618
  if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
929
1619
  const response = await this.#protocol.dataStream.exchange(DataStreamMessage.getVolume(this.#state.outputDeviceUID));
930
1620
  if (response.type === Proto.ProtocolMessage_Type.GET_VOLUME_RESULT_MESSAGE) return DataStreamMessage.getExtension(response, Proto.getVolumeResultMessage).volume;
931
1621
  throw new CommandError("Failed to get volume.");
932
1622
  }
1623
+ /**
1624
+ * Sets the volume to an absolute level.
1625
+ *
1626
+ * @param volume - The desired volume level (clamped to 0.0 - 1.0).
1627
+ * @throws CommandError when no output device is active or absolute volume is not supported.
1628
+ */
933
1629
  async set(volume) {
934
1630
  if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
935
1631
  if (![Proto.VolumeCapabilities_Enum.Absolute, Proto.VolumeCapabilities_Enum.Both].includes(this.#state.volumeCapabilities)) throw new CommandError("Absolute volume control is not available.");
@@ -941,43 +1637,78 @@ var volume_default = class {
941
1637
 
942
1638
  //#endregion
943
1639
  //#region src/airplay/device.ts
1640
+ /**
1641
+ * High-level abstraction for an AirPlay device (Apple TV or HomePod).
1642
+ * Manages the full lifecycle: connect, pair/verify, set up control/data/event streams,
1643
+ * and provides access to Remote, State, and Volume controllers.
1644
+ * Supports both transient (PIN-less) and credential-based pairing.
1645
+ */
944
1646
  var device_default = class extends EventEmitter {
1647
+ /** @returns The underlying AirPlay Protocol instance (accessed via symbol for internal use). */
945
1648
  get [PROTOCOL]() {
946
1649
  return this.#protocol;
947
1650
  }
1651
+ /** The mDNS discovery result used to connect to this device. */
948
1652
  get discoveryResult() {
949
1653
  return this.#discoveryResult;
950
1654
  }
1655
+ /** Updates the discovery result, e.g. when the device's address changes. */
951
1656
  set discoveryResult(discoveryResult) {
952
1657
  this.#discoveryResult = discoveryResult;
953
1658
  }
1659
+ /**
1660
+ * Device capabilities derived from the AirPlay feature flags.
1661
+ * Indicates which protocols and features the receiver supports.
1662
+ */
1663
+ get capabilities() {
1664
+ const has = (f) => this.#protocol?.hasReceiverFeature(f) ?? false;
1665
+ return {
1666
+ supportsAudio: has(AirPlayFeature.SupportsAirPlayAudio),
1667
+ supportsBufferedAudio: has(AirPlayFeature.SupportsBufferedAudio),
1668
+ supportsPTP: has(AirPlayFeature.SupportsPTP),
1669
+ supportsRFC2198Redundancy: has(AirPlayFeature.SupportsRFC2198Redundancy),
1670
+ supportsHangdogRemoteControl: has(AirPlayFeature.SupportsHangdogRemoteControl),
1671
+ supportsUnifiedMediaControl: has(AirPlayFeature.SupportsUnifiedMediaControl),
1672
+ supportsTransientPairing: has(AirPlayFeature.SupportsHKPairingAndAccessControl),
1673
+ supportsSystemPairing: has(AirPlayFeature.SupportsSystemPairing),
1674
+ supportsCoreUtilsPairing: has(AirPlayFeature.SupportsCoreUtilsPairingAndEncryption)
1675
+ };
1676
+ }
1677
+ /** Whether the control stream TCP connection is currently active. */
954
1678
  get isConnected() {
955
1679
  return this.#protocol?.controlStream?.isConnected ?? false;
956
1680
  }
1681
+ /** Raw receiver info dictionary from the /info endpoint, or undefined before connect. */
1682
+ get receiverInfo() {
1683
+ return this.#protocol?.receiverInfo;
1684
+ }
1685
+ /** The Remote controller for HID keys, SendCommand, text input, and touch. */
957
1686
  get remote() {
958
1687
  return this.#remote;
959
1688
  }
1689
+ /** The State tracker for now-playing, volume, keyboard, and output device state. */
960
1690
  get state() {
961
1691
  return this.#state;
962
1692
  }
1693
+ /** The Volume controller for absolute and relative volume adjustments. */
963
1694
  get volume() {
964
1695
  return this.#volume;
965
1696
  }
1697
+ /** The shared PTP timing server, if one is assigned for multi-room sync. */
966
1698
  get timingServer() {
967
1699
  return this.#timingServer;
968
1700
  }
1701
+ /** Assigns a PTP timing server for multi-room audio synchronization. */
969
1702
  set timingServer(timingServer) {
970
1703
  this.#timingServer = timingServer;
971
1704
  }
972
- #boundOnClose = () => this.#onClose();
973
- #boundOnError = (err) => this.#onError(err);
974
- #boundOnTimeout = () => this.#onTimeout();
975
1705
  #remote;
976
1706
  #state;
977
1707
  #volume;
978
1708
  #credentials;
979
1709
  #disconnect = false;
980
1710
  #discoveryResult;
1711
+ #identity;
981
1712
  #feedbackInterval;
982
1713
  #keys;
983
1714
  #playUrlProtocol;
@@ -985,26 +1716,45 @@ var device_default = class extends EventEmitter {
985
1716
  #prevEventStream;
986
1717
  #protocol;
987
1718
  #timingServer;
988
- constructor(discoveryResult) {
1719
+ /**
1720
+ * Creates a new AirPlayDevice.
1721
+ *
1722
+ * @param discoveryResult - The mDNS discovery result for the target device.
1723
+ * @param identity - Optional partial device identity to present during pairing.
1724
+ */
1725
+ constructor(discoveryResult, identity) {
989
1726
  super();
990
1727
  this.#discoveryResult = discoveryResult;
1728
+ this.#identity = identity;
991
1729
  this.#remote = new remote_default(this);
992
1730
  this.#state = new state_default(this);
1731
+ this.onClose = this.onClose.bind(this);
1732
+ this.onError = this.onError.bind(this);
1733
+ this.onTimeout = this.onTimeout.bind(this);
993
1734
  this.#volume = new volume_default(this);
994
1735
  }
1736
+ /**
1737
+ * Connects to the AirPlay device, performs pairing/verification,
1738
+ * and sets up all streams (control, data, event). Emits 'connected' on success.
1739
+ * If credentials are set, uses pair-verify; otherwise uses transient pairing.
1740
+ */
995
1741
  async connect() {
996
1742
  if (this.#protocol) {
997
- this.#protocol.controlStream.off("close", this.#boundOnClose);
998
- this.#protocol.controlStream.off("error", this.#boundOnError);
999
- this.#protocol.controlStream.off("timeout", this.#boundOnTimeout);
1743
+ this.#protocol.controlStream.off("close", this.onClose);
1744
+ this.#protocol.controlStream.off("error", this.onError);
1745
+ this.#protocol.controlStream.off("timeout", this.onTimeout);
1746
+ try {
1747
+ this.#protocol.disconnect();
1748
+ } catch {}
1000
1749
  }
1001
1750
  this.#disconnect = false;
1002
1751
  this.#state.clear();
1003
- this.#protocol = new Protocol(this.#discoveryResult);
1004
- this.#protocol.controlStream.on("close", this.#boundOnClose);
1005
- this.#protocol.controlStream.on("error", this.#boundOnError);
1006
- this.#protocol.controlStream.on("timeout", this.#boundOnTimeout);
1752
+ this.#protocol = new Protocol(this.#discoveryResult, this.#identity);
1753
+ this.#protocol.controlStream.on("close", this.onClose);
1754
+ this.#protocol.controlStream.on("error", this.onError);
1755
+ this.#protocol.controlStream.on("timeout", this.onTimeout);
1007
1756
  await this.#protocol.connect();
1757
+ await this.#protocol.fetchInfo();
1008
1758
  if (this.#credentials) this.#keys = await this.#protocol.verify.start(this.#credentials);
1009
1759
  else {
1010
1760
  await this.#protocol.pairing.start();
@@ -1013,6 +1763,7 @@ var device_default = class extends EventEmitter {
1013
1763
  await this.#setup();
1014
1764
  this.emit("connected");
1015
1765
  }
1766
+ /** Gracefully disconnects from the device, clears intervals, and tears down all streams. */
1016
1767
  disconnect() {
1017
1768
  this.#disconnect = true;
1018
1769
  if (this.#feedbackInterval) {
@@ -1022,7 +1773,9 @@ var device_default = class extends EventEmitter {
1022
1773
  this.#cleanupPlayUrl();
1023
1774
  this.#unsubscribe();
1024
1775
  this.#protocol.disconnect();
1776
+ this.emit("disconnected", false);
1025
1777
  }
1778
+ /** Disconnects gracefully, swallowing any errors during cleanup. */
1026
1779
  disconnectSafely() {
1027
1780
  try {
1028
1781
  this.disconnect();
@@ -1030,31 +1783,73 @@ var device_default = class extends EventEmitter {
1030
1783
  this.#protocol?.context?.logger?.warn("[device]", "Error during safe disconnect", err);
1031
1784
  }
1032
1785
  }
1786
+ /**
1787
+ * Enables or disables conversation detection on the output device (HomePod feature).
1788
+ *
1789
+ * @param enabled - Whether to enable conversation detection.
1790
+ * @throws Error when no output device is active.
1791
+ */
1792
+ async setConversationDetectionEnabled(enabled) {
1793
+ const outputDeviceUID = this.#state.outputDeviceUID;
1794
+ if (!outputDeviceUID) throw new Error("No output device active.");
1795
+ await this.#protocol.dataStream.send(DataStreamMessage.setConversationDetectionEnabled(enabled, outputDeviceUID));
1796
+ }
1797
+ /**
1798
+ * Adds devices to the current multi-room output context.
1799
+ *
1800
+ * @param deviceUIDs - UIDs of the devices to add.
1801
+ */
1033
1802
  async addOutputDevices(deviceUIDs) {
1034
1803
  await this.#protocol.dataStream.exchange(DataStreamMessage.modifyOutputContext(deviceUIDs));
1035
1804
  }
1805
+ /**
1806
+ * Removes devices from the current multi-room output context.
1807
+ *
1808
+ * @param deviceUIDs - UIDs of the devices to remove.
1809
+ */
1036
1810
  async removeOutputDevices(deviceUIDs) {
1037
1811
  await this.#protocol.dataStream.exchange(DataStreamMessage.modifyOutputContext([], deviceUIDs));
1038
1812
  }
1813
+ /**
1814
+ * Replaces the entire multi-room output context with the given devices.
1815
+ *
1816
+ * @param deviceUIDs - UIDs of the devices to set as the output context.
1817
+ */
1039
1818
  async setOutputDevices(deviceUIDs) {
1040
1819
  await this.#protocol.dataStream.exchange(DataStreamMessage.modifyOutputContext([], [], deviceUIDs));
1041
1820
  }
1821
+ /**
1822
+ * Plays a URL on the device (the device fetches and plays the content).
1823
+ * Creates a separate Protocol instance to avoid conflicting with the
1824
+ * existing remote control session, following the same approach as pyatv.
1825
+ *
1826
+ * @param url - The media URL to play.
1827
+ * @param position - Start position in seconds (defaults to 0).
1828
+ * @throws Error when not connected.
1829
+ */
1042
1830
  async playUrl(url, position = 0) {
1043
1831
  if (!this.#keys) throw new Error("Not connected. Call connect() first.");
1044
1832
  this.#playUrlProtocol?.disconnect();
1045
- const playProtocol = new Protocol(this.#discoveryResult);
1833
+ const playProtocol = new Protocol(this.#discoveryResult, this.#identity);
1046
1834
  if (this.#timingServer) playProtocol.useTimingServer(this.#timingServer);
1047
- await playProtocol.connect();
1048
- let keys;
1049
- if (this.#credentials) keys = await playProtocol.verify.start(this.#credentials);
1050
- else {
1051
- await playProtocol.pairing.start();
1052
- keys = await playProtocol.pairing.transient();
1835
+ try {
1836
+ await playProtocol.connect();
1837
+ await playProtocol.fetchInfo();
1838
+ let keys;
1839
+ if (this.#credentials) keys = await playProtocol.verify.start(this.#credentials);
1840
+ else {
1841
+ await playProtocol.pairing.start();
1842
+ keys = await playProtocol.pairing.transient();
1843
+ }
1844
+ playProtocol.controlStream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
1845
+ this.#playUrlProtocol = playProtocol;
1846
+ await playProtocol.playUrl(url, keys.sharedSecret, keys.pairingId, position);
1847
+ } catch (err) {
1848
+ if (this.#playUrlProtocol !== playProtocol) playProtocol.disconnect();
1849
+ throw err;
1053
1850
  }
1054
- playProtocol.controlStream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
1055
- this.#playUrlProtocol = playProtocol;
1056
- await playProtocol.playUrl(url, keys.sharedSecret, keys.pairingId, position);
1057
1851
  }
1852
+ /** Waits for the current URL playback to finish, then cleans up the play URL protocol. */
1058
1853
  async waitForPlaybackEnd() {
1059
1854
  if (!this.#playUrlProtocol) return;
1060
1855
  try {
@@ -1063,9 +1858,11 @@ var device_default = class extends EventEmitter {
1063
1858
  this.#cleanupPlayUrl();
1064
1859
  }
1065
1860
  }
1861
+ /** Stops the current URL playback and cleans up the dedicated play URL protocol. */
1066
1862
  stopPlayUrl() {
1067
1863
  this.#cleanupPlayUrl();
1068
1864
  }
1865
+ /** Stops, disconnects, and discards the dedicated play URL protocol instance. */
1069
1866
  #cleanupPlayUrl() {
1070
1867
  if (this.#playUrlProtocol) {
1071
1868
  this.#playUrlProtocol.stopPlayUrl();
@@ -1073,18 +1870,41 @@ var device_default = class extends EventEmitter {
1073
1870
  this.#playUrlProtocol = void 0;
1074
1871
  }
1075
1872
  }
1873
+ /**
1874
+ * Streams audio from a source to the device via RAOP/RTP.
1875
+ *
1876
+ * @param source - The audio source to stream (e.g. MP3, WAV, URL, live).
1877
+ */
1076
1878
  async streamAudio(source) {
1077
1879
  await this.#protocol.setupAudioStream(source);
1078
1880
  }
1881
+ /**
1882
+ * Requests the playback queue from the device.
1883
+ *
1884
+ * @param length - Maximum number of queue items to retrieve.
1885
+ */
1079
1886
  async requestPlaybackQueue(length) {
1080
1887
  await this.#protocol.dataStream.exchange(DataStreamMessage.playbackQueueRequest(0, length));
1081
1888
  }
1889
+ /**
1890
+ * Sends a raw MRP command to the device via the DataStream.
1891
+ *
1892
+ * @param command - The command to send.
1893
+ * @param options - Optional command options.
1894
+ */
1082
1895
  async sendCommand(command, options) {
1083
1896
  await this.#protocol.dataStream.exchange(DataStreamMessage.sendCommand(command, options));
1084
1897
  }
1898
+ /**
1899
+ * Sets the pairing credentials for pair-verify authentication.
1900
+ * Must be called before connect() if credential-based pairing is desired.
1901
+ *
1902
+ * @param credentials - The accessory credentials obtained from pair-setup.
1903
+ */
1085
1904
  setCredentials(credentials) {
1086
1905
  this.#credentials = credentials;
1087
1906
  }
1907
+ /** Sends a periodic feedback request to keep the AirPlay session alive. */
1088
1908
  async #feedback() {
1089
1909
  try {
1090
1910
  await this.#protocol.feedback();
@@ -1092,43 +1912,55 @@ var device_default = class extends EventEmitter {
1092
1912
  this.#protocol.context.logger.error("Feedback error", err);
1093
1913
  }
1094
1914
  }
1095
- #onClose() {
1096
- this.#protocol.context.logger.net("#onClose() called on airplay device.");
1097
- if (!this.#disconnect) {
1098
- this.disconnectSafely();
1099
- this.emit("disconnected", true);
1100
- } else this.emit("disconnected", false);
1101
- }
1102
- #onError(err) {
1915
+ /** Handles the control stream close event. Emits 'disconnected' with unexpected=true if not intentional. */
1916
+ onClose() {
1917
+ this.#protocol.context.logger.net("onClose() called on airplay device.");
1918
+ if (this.#disconnect) return;
1919
+ this.#disconnect = true;
1920
+ this.disconnectSafely();
1921
+ this.emit("disconnected", true);
1922
+ }
1923
+ /**
1924
+ * Handles stream error events by logging them.
1925
+ *
1926
+ * @param err - The error that occurred.
1927
+ */
1928
+ onError(err) {
1103
1929
  this.#protocol.context.logger.error("AirPlay error", err);
1104
1930
  }
1105
- #onTimeout() {
1931
+ /** Handles stream timeout events by destroying the control stream. */
1932
+ onTimeout() {
1106
1933
  this.#protocol.context.logger.error("AirPlay timeout");
1107
1934
  this.#protocol.controlStream.destroy();
1108
1935
  }
1936
+ /**
1937
+ * Sets up encryption, event/data streams, feedback interval, and initial state subscriptions.
1938
+ * Called after successful pairing/verification.
1939
+ */
1109
1940
  async #setup() {
1110
1941
  const keys = this.#keys;
1111
1942
  this.#protocol.controlStream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
1112
1943
  this.#unsubscribe();
1113
1944
  if (this.#timingServer) this.#protocol.useTimingServer(this.#timingServer);
1114
1945
  try {
1115
- this.#prevDataStream?.off("error", this.#boundOnError);
1116
- this.#prevDataStream?.off("timeout", this.#boundOnTimeout);
1117
- this.#prevEventStream?.off("error", this.#boundOnError);
1118
- this.#prevEventStream?.off("timeout", this.#boundOnTimeout);
1946
+ this.#prevDataStream?.off("error", this.onError);
1947
+ this.#prevDataStream?.off("timeout", this.onTimeout);
1948
+ this.#prevEventStream?.off("error", this.onError);
1949
+ this.#prevEventStream?.off("timeout", this.onTimeout);
1119
1950
  await this.#protocol.setupEventStream(keys.sharedSecret, keys.pairingId);
1120
1951
  await this.#protocol.setupDataStream(keys.sharedSecret, () => this.#subscribe());
1121
- this.#protocol.dataStream.on("error", this.#boundOnError);
1122
- this.#protocol.dataStream.on("timeout", this.#boundOnTimeout);
1123
- this.#protocol.eventStream.on("error", this.#boundOnError);
1124
- this.#protocol.eventStream.on("timeout", this.#boundOnTimeout);
1952
+ this.#protocol.dataStream.on("error", this.onError);
1953
+ this.#protocol.dataStream.on("timeout", this.onTimeout);
1954
+ this.#protocol.eventStream.on("error", this.onError);
1955
+ this.#protocol.eventStream.on("timeout", this.onTimeout);
1125
1956
  this.#prevDataStream = this.#protocol.dataStream;
1126
1957
  this.#prevEventStream = this.#protocol.eventStream;
1127
1958
  if (this.#feedbackInterval) clearInterval(this.#feedbackInterval);
1128
1959
  this.#feedbackInterval = setInterval(async () => await this.#feedback(), FEEDBACK_INTERVAL);
1129
- await this.#protocol.dataStream.exchange(DataStreamMessage.deviceInfo(keys.pairingId));
1960
+ await this.#protocol.dataStream.exchange(DataStreamMessage.deviceInfo(keys.pairingId, this.#protocol.context.identity));
1130
1961
  await this.#protocol.dataStream.exchange(DataStreamMessage.setConnectionState());
1131
1962
  await this.#protocol.dataStream.exchange(DataStreamMessage.clientUpdatesConfig(true, true, true, true, true, true));
1963
+ await this.#protocol.dataStream.exchange(DataStreamMessage.getState());
1132
1964
  this.#protocol.context.logger.info("Protocol ready.");
1133
1965
  } catch (err) {
1134
1966
  if (this.#feedbackInterval) {
@@ -1140,9 +1972,11 @@ var device_default = class extends EventEmitter {
1140
1972
  throw err;
1141
1973
  }
1142
1974
  }
1975
+ /** Subscribes the state tracker to DataStream events. */
1143
1976
  #subscribe() {
1144
1977
  this.#state[STATE_SUBSCRIBE_SYMBOL]();
1145
1978
  }
1979
+ /** Unsubscribes the state tracker from DataStream events. */
1146
1980
  #unsubscribe() {
1147
1981
  try {
1148
1982
  this.#state[STATE_UNSUBSCRIBE_SYMBOL]();
@@ -1154,140 +1988,640 @@ var device_default = class extends EventEmitter {
1154
1988
 
1155
1989
  //#endregion
1156
1990
  //#region src/companion-link/const.ts
1991
+ /** Symbol used to access the underlying Companion Link Protocol instance from a CompanionLinkDevice. */
1157
1992
  const PROTOCOL$1 = Symbol("com.basmilius.companion-link:protocol");
1158
1993
 
1994
+ //#endregion
1995
+ //#region src/companion-link/state.ts
1996
+ /** Default text input state when no text input session is active. */
1997
+ const DEFAULT_TEXT_INPUT = {
1998
+ isActive: false,
1999
+ documentText: "",
2000
+ isSecure: false,
2001
+ keyboardType: 0,
2002
+ autocorrection: false,
2003
+ autocapitalization: false
2004
+ };
2005
+ /**
2006
+ * Tracks the state of a Companion Link device: attention state, media controls,
2007
+ * now-playing info, supported actions, text input, and volume availability.
2008
+ * Subscribes to protocol stream events and emits typed state change events.
2009
+ */
2010
+ var CompanionLinkState = class extends EventEmitter {
2011
+ /** Current attention state of the device (active, idle, screensaver, etc.). */
2012
+ get attentionState() {
2013
+ return this.#attentionState;
2014
+ }
2015
+ /** Parsed media capabilities indicating which controls are available. */
2016
+ get mediaCapabilities() {
2017
+ return this.#mediaCapabilities;
2018
+ }
2019
+ /** Raw media control flags bitmask from the device. */
2020
+ get mediaControlFlags() {
2021
+ return this.#mediaControlFlags;
2022
+ }
2023
+ /** Current now-playing info as a key-value dictionary, or null. */
2024
+ get nowPlayingInfo() {
2025
+ return this.#nowPlayingInfo;
2026
+ }
2027
+ /** Currently supported actions dictionary, or null. */
2028
+ get supportedActions() {
2029
+ return this.#supportedActions;
2030
+ }
2031
+ /** Current text input session state. */
2032
+ get textInputState() {
2033
+ return this.#textInputState;
2034
+ }
2035
+ /** Whether volume control is currently available via the Companion Link protocol. */
2036
+ get volumeAvailable() {
2037
+ return this.#volumeAvailable;
2038
+ }
2039
+ #protocol;
2040
+ #attentionState = "unknown";
2041
+ #mediaCapabilities = parseMediaControlFlags(0);
2042
+ #mediaControlFlags = 0;
2043
+ #nowPlayingInfo = null;
2044
+ #supportedActions = null;
2045
+ #textInputState = { ...DEFAULT_TEXT_INPUT };
2046
+ #volumeAvailable = false;
2047
+ /**
2048
+ * Creates a new CompanionLinkState tracker.
2049
+ *
2050
+ * @param protocol - The Companion Link protocol instance to observe.
2051
+ */
2052
+ constructor(protocol) {
2053
+ super();
2054
+ this.#protocol = protocol;
2055
+ this.onMediaControl = this.onMediaControl.bind(this);
2056
+ this.onSystemStatus = this.onSystemStatus.bind(this);
2057
+ this.onTVSystemStatus = this.onTVSystemStatus.bind(this);
2058
+ this.onNowPlayingInfo = this.onNowPlayingInfo.bind(this);
2059
+ this.onSupportedActions = this.onSupportedActions.bind(this);
2060
+ this.onTextInputStarted = this.onTextInputStarted.bind(this);
2061
+ this.onTextInputStopped = this.onTextInputStopped.bind(this);
2062
+ }
2063
+ /** Subscribes to protocol stream events and registers interests for push notifications. */
2064
+ subscribe() {
2065
+ const stream = this.#protocol.stream;
2066
+ stream.on("_iMC", this.onMediaControl);
2067
+ stream.on("SystemStatus", this.onSystemStatus);
2068
+ stream.on("TVSystemStatus", this.onTVSystemStatus);
2069
+ stream.on("NowPlayingInfo", this.onNowPlayingInfo);
2070
+ stream.on("SupportedActions", this.onSupportedActions);
2071
+ stream.on("_tiStarted", this.onTextInputStarted);
2072
+ stream.on("_tiStopped", this.onTextInputStopped);
2073
+ this.#protocol.registerInterests(["_iMC"]);
2074
+ this.#protocol.registerInterests(["SystemStatus"]);
2075
+ this.#protocol.registerInterests(["TVSystemStatus"]);
2076
+ this.#protocol.registerInterests(["NowPlayingInfo"]);
2077
+ this.#protocol.registerInterests(["SupportedActions"]);
2078
+ }
2079
+ /** Unsubscribes from protocol stream events and deregisters interests. */
2080
+ unsubscribe() {
2081
+ const stream = this.#protocol.stream;
2082
+ if (!stream.isConnected) return;
2083
+ stream.off("_iMC", this.onMediaControl);
2084
+ stream.off("SystemStatus", this.onSystemStatus);
2085
+ stream.off("TVSystemStatus", this.onTVSystemStatus);
2086
+ stream.off("NowPlayingInfo", this.onNowPlayingInfo);
2087
+ stream.off("SupportedActions", this.onSupportedActions);
2088
+ stream.off("_tiStarted", this.onTextInputStarted);
2089
+ stream.off("_tiStopped", this.onTextInputStopped);
2090
+ try {
2091
+ this.#protocol.deregisterInterests(["_iMC"]);
2092
+ this.#protocol.deregisterInterests(["SystemStatus"]);
2093
+ this.#protocol.deregisterInterests(["TVSystemStatus"]);
2094
+ this.#protocol.deregisterInterests(["NowPlayingInfo"]);
2095
+ this.#protocol.deregisterInterests(["SupportedActions"]);
2096
+ } catch {}
2097
+ }
2098
+ /** Fetches the initial attention state and media control status from the device. */
2099
+ async fetchInitialState() {
2100
+ try {
2101
+ const state = await this.#protocol.getAttentionState();
2102
+ this.#attentionState = state;
2103
+ this.emit("attentionStateChanged", state);
2104
+ } catch (err) {
2105
+ this.#protocol.context.logger.warn("[cl-state]", "Failed to fetch initial attention state", err);
2106
+ }
2107
+ try {
2108
+ await this.#protocol.fetchMediaControlStatus();
2109
+ } catch (err) {
2110
+ this.#protocol.context.logger.warn("[cl-state]", "Failed to fetch media control status", err);
2111
+ }
2112
+ }
2113
+ /** Resets all state to initial/default values. */
2114
+ clear() {
2115
+ this.#attentionState = "unknown";
2116
+ this.#mediaCapabilities = parseMediaControlFlags(0);
2117
+ this.#mediaControlFlags = 0;
2118
+ this.#nowPlayingInfo = null;
2119
+ this.#supportedActions = null;
2120
+ this.#textInputState = { ...DEFAULT_TEXT_INPUT };
2121
+ this.#volumeAvailable = false;
2122
+ }
2123
+ /**
2124
+ * Handles media control flag updates (_iMC). Parses the flags bitmask and
2125
+ * emits events if capabilities or volume availability changed.
2126
+ *
2127
+ * @param data - The raw media control payload.
2128
+ */
2129
+ onMediaControl(data) {
2130
+ try {
2131
+ const payload = data;
2132
+ const flags = Number(payload?._mcF ?? 0);
2133
+ if (flags !== this.#mediaControlFlags) {
2134
+ this.#mediaControlFlags = flags;
2135
+ this.#mediaCapabilities = parseMediaControlFlags(flags);
2136
+ const wasVolumeAvailable = this.#volumeAvailable;
2137
+ this.#volumeAvailable = this.#mediaCapabilities.volume;
2138
+ this.emit("mediaControlFlagsChanged", flags, this.#mediaCapabilities);
2139
+ if (wasVolumeAvailable !== this.#volumeAvailable) this.emit("volumeAvailabilityChanged", this.#volumeAvailable);
2140
+ }
2141
+ } catch (err) {
2142
+ this.#protocol.context.logger.error("[cl-state]", "_iMC parse error", err);
2143
+ }
2144
+ }
2145
+ /**
2146
+ * Handles SystemStatus events (attention state changes from non-TV devices).
2147
+ *
2148
+ * @param data - The raw system status payload containing a state code.
2149
+ */
2150
+ onSystemStatus(data) {
2151
+ const state = convertAttentionState(data.state);
2152
+ if (state !== this.#attentionState) {
2153
+ this.#attentionState = state;
2154
+ this.emit("attentionStateChanged", state);
2155
+ }
2156
+ }
2157
+ /**
2158
+ * Handles TVSystemStatus events (attention state changes from Apple TV).
2159
+ *
2160
+ * @param data - The raw TV system status payload containing a state code.
2161
+ */
2162
+ onTVSystemStatus(data) {
2163
+ const state = convertAttentionState(data.state);
2164
+ if (state !== this.#attentionState) {
2165
+ this.#attentionState = state;
2166
+ this.emit("attentionStateChanged", state);
2167
+ }
2168
+ }
2169
+ /**
2170
+ * Handles NowPlayingInfo updates. Decodes the NSKeyedArchiver plist payload
2171
+ * when present, otherwise uses the raw dictionary.
2172
+ *
2173
+ * @param data - The raw now-playing info payload.
2174
+ */
2175
+ onNowPlayingInfo(data) {
2176
+ try {
2177
+ const payload = data;
2178
+ if (payload?.NowPlayingInfoKey) {
2179
+ const raw = payload.NowPlayingInfoKey;
2180
+ const buf = Buffer.from(raw);
2181
+ const plist = Plist.parse(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
2182
+ const decoded = NSKeyedArchiver.decode(plist);
2183
+ this.#nowPlayingInfo = decoded && typeof decoded === "object" && !Array.isArray(decoded) ? decoded : null;
2184
+ } else this.#nowPlayingInfo = payload ?? null;
2185
+ this.emit("nowPlayingInfoChanged", this.#nowPlayingInfo);
2186
+ } catch (err) {
2187
+ this.#protocol.context.logger.error("[cl-state]", "NowPlayingInfo parse error", err);
2188
+ }
2189
+ }
2190
+ /**
2191
+ * Handles SupportedActions updates from the device.
2192
+ *
2193
+ * @param data - The raw supported actions payload.
2194
+ */
2195
+ onSupportedActions(data) {
2196
+ try {
2197
+ const payload = data;
2198
+ this.#supportedActions = payload ?? null;
2199
+ this.emit("supportedActionsChanged", payload ?? {});
2200
+ } catch (err) {
2201
+ this.#protocol.context.logger.error("[cl-state]", "SupportedActions parse error", err);
2202
+ }
2203
+ }
2204
+ /**
2205
+ * Handles the start of a text input session. Parses the plist payload to extract
2206
+ * document text, security mode, keyboard type, and autocorrection settings.
2207
+ *
2208
+ * @param data - The raw text input started payload.
2209
+ */
2210
+ onTextInputStarted(data) {
2211
+ try {
2212
+ const payload = data;
2213
+ let documentText = "";
2214
+ let isSecure = false;
2215
+ let keyboardType = 0;
2216
+ let autocorrection = false;
2217
+ let autocapitalization = false;
2218
+ if (payload?._tiD) {
2219
+ const plistData = Plist.parse(Buffer.from(payload._tiD).buffer);
2220
+ documentText = plistData._tiDT ?? "";
2221
+ isSecure = plistData._tiSR ?? false;
2222
+ keyboardType = plistData._tiKT ?? 0;
2223
+ autocorrection = plistData._tiAC ?? false;
2224
+ autocapitalization = plistData._tiAP ?? false;
2225
+ }
2226
+ this.#textInputState = {
2227
+ isActive: true,
2228
+ documentText,
2229
+ isSecure,
2230
+ keyboardType,
2231
+ autocorrection,
2232
+ autocapitalization
2233
+ };
2234
+ this.emit("textInputChanged", this.#textInputState);
2235
+ } catch (err) {
2236
+ this.#protocol.context.logger.error("[cl-state]", "Text input started parse error", err);
2237
+ }
2238
+ }
2239
+ /**
2240
+ * Handles the end of a text input session. Resets the text input state to defaults.
2241
+ *
2242
+ * @param _data - The raw text input stopped payload (unused).
2243
+ */
2244
+ onTextInputStopped(_data) {
2245
+ this.#textInputState = { ...DEFAULT_TEXT_INPUT };
2246
+ this.emit("textInputChanged", this.#textInputState);
2247
+ }
2248
+ };
2249
+ /**
2250
+ * Parses a media control flags bitmask into a structured MediaCapabilities object.
2251
+ *
2252
+ * @param flags - The raw bitmask from the _iMC event.
2253
+ * @returns An object indicating which media controls are available.
2254
+ */
2255
+ function parseMediaControlFlags(flags) {
2256
+ return {
2257
+ play: (flags & MediaControlFlag.Play) !== 0,
2258
+ pause: (flags & MediaControlFlag.Pause) !== 0,
2259
+ previousTrack: (flags & MediaControlFlag.PreviousTrack) !== 0,
2260
+ nextTrack: (flags & MediaControlFlag.NextTrack) !== 0,
2261
+ fastForward: (flags & MediaControlFlag.FastForward) !== 0,
2262
+ rewind: (flags & MediaControlFlag.Rewind) !== 0,
2263
+ volume: (flags & MediaControlFlag.Volume) !== 0,
2264
+ skipForward: (flags & MediaControlFlag.SkipForward) !== 0,
2265
+ skipBackward: (flags & MediaControlFlag.SkipBackward) !== 0
2266
+ };
2267
+ }
2268
+
1159
2269
  //#endregion
1160
2270
  //#region src/companion-link/device.ts
2271
+ /**
2272
+ * High-level abstraction for a Companion Link device (Apple TV).
2273
+ * Manages the OPack-based Companion Link protocol lifecycle: connect, pair-verify,
2274
+ * session setup, and provides access to HID buttons, app launching, user accounts,
2275
+ * media control, text input, touch, Siri, and system controls.
2276
+ * Requires credentials (obtained from pair-setup) to connect.
2277
+ */
1161
2278
  var device_default$1 = class extends EventEmitter {
2279
+ /** @returns The underlying Companion Link Protocol instance (accessed via symbol for internal use). */
1162
2280
  get [PROTOCOL$1]() {
1163
2281
  return this.#protocol;
1164
2282
  }
2283
+ /** The mDNS discovery result used to connect to this device. */
1165
2284
  get discoveryResult() {
1166
2285
  return this.#discoveryResult;
1167
2286
  }
2287
+ /** Updates the discovery result, e.g. when the device's address changes. */
1168
2288
  set discoveryResult(discoveryResult) {
1169
2289
  this.#discoveryResult = discoveryResult;
1170
2290
  }
2291
+ /** Whether the Companion Link stream is currently connected. */
1171
2292
  get isConnected() {
1172
2293
  return this.#protocol?.stream?.isConnected ?? false;
1173
2294
  }
2295
+ /** The state tracker for attention, media controls, now-playing, and text input. */
2296
+ get state() {
2297
+ return this.#state;
2298
+ }
2299
+ /** Current text input session state (convenience accessor). */
2300
+ get textInputState() {
2301
+ return this.#state.textInputState;
2302
+ }
1174
2303
  #credentials;
1175
2304
  #disconnect = false;
1176
2305
  #discoveryResult;
1177
2306
  #heartbeatInterval;
1178
2307
  #keys;
1179
2308
  #protocol;
1180
- get textInputState() {
1181
- return this.#textInputState;
1182
- }
1183
- #boundOnClose = async () => this.#onClose();
1184
- #boundOnError = async (err) => this.#onError(err);
1185
- #boundOnTimeout = async () => this.#onTimeout();
1186
- #onMediaControl = (data) => {
1187
- this.emit("mediaControl", data);
1188
- };
1189
- #textInputState = {
1190
- isActive: false,
1191
- documentText: "",
1192
- isSecure: false,
1193
- keyboardType: 0,
1194
- autocorrection: false,
1195
- autocapitalization: false
1196
- };
2309
+ #state;
2310
+ /**
2311
+ * Creates a new CompanionLinkDevice.
2312
+ *
2313
+ * @param discoveryResult - The mDNS discovery result for the target device.
2314
+ */
1197
2315
  constructor(discoveryResult) {
1198
2316
  super();
1199
2317
  this.#discoveryResult = discoveryResult;
1200
- this.onSystemStatus = this.onSystemStatus.bind(this);
1201
- this.onTVSystemStatus = this.onTVSystemStatus.bind(this);
1202
- }
2318
+ this.onClose = this.onClose.bind(this);
2319
+ this.onError = this.onError.bind(this);
2320
+ this.onTimeout = this.onTimeout.bind(this);
2321
+ }
2322
+ /**
2323
+ * Connects to the Companion Link device, performs pair-verify, and sets up
2324
+ * all protocol sessions (system info, TVRC, touch, text input).
2325
+ * Emits 'connected' on success.
2326
+ *
2327
+ * @throws CredentialsError when no credentials are set.
2328
+ */
1203
2329
  async connect() {
1204
2330
  if (!this.#credentials) throw new CredentialsError("Credentials are required to connect to a Companion Link device.");
1205
2331
  if (this.#protocol) {
1206
- this.#protocol.stream.off("close", this.#boundOnClose);
1207
- this.#protocol.stream.off("error", this.#boundOnError);
1208
- this.#protocol.stream.off("timeout", this.#boundOnTimeout);
2332
+ this.#protocol.stream.off("close", this.onClose);
2333
+ this.#protocol.stream.off("error", this.onError);
2334
+ this.#protocol.stream.off("timeout", this.onTimeout);
1209
2335
  }
1210
2336
  this.#disconnect = false;
1211
2337
  this.#protocol = new Protocol$1(this.#discoveryResult);
1212
- this.#protocol.stream.on("close", this.#boundOnClose);
1213
- this.#protocol.stream.on("error", this.#boundOnError);
1214
- this.#protocol.stream.on("timeout", this.#boundOnTimeout);
2338
+ this.#protocol.stream.on("close", this.onClose);
2339
+ this.#protocol.stream.on("error", this.onError);
2340
+ this.#protocol.stream.on("timeout", this.onTimeout);
1215
2341
  await this.#protocol.connect();
1216
2342
  this.#keys = await this.#protocol.verify.start(this.#credentials);
1217
2343
  await this.#setup();
1218
2344
  this.emit("connected");
1219
2345
  }
2346
+ /** Gracefully disconnects from the device, clears heartbeat interval, and unsubscribes from events. */
1220
2347
  async disconnect() {
1221
2348
  this.#disconnect = true;
1222
2349
  if (this.#heartbeatInterval) {
1223
2350
  clearInterval(this.#heartbeatInterval);
1224
2351
  this.#heartbeatInterval = void 0;
1225
2352
  }
1226
- await this.#unsubscribe();
2353
+ this.#state?.unsubscribe();
1227
2354
  await this.#protocol.disconnect();
1228
2355
  }
2356
+ /** Disconnects gracefully, swallowing any errors during cleanup. */
1229
2357
  async disconnectSafely() {
1230
2358
  try {
1231
2359
  await this.disconnect();
1232
- } catch (_) {}
2360
+ } catch {}
1233
2361
  }
2362
+ /**
2363
+ * Sets the pairing credentials required for pair-verify authentication.
2364
+ * Must be called before connect().
2365
+ *
2366
+ * @param credentials - The accessory credentials from pair-setup.
2367
+ */
1234
2368
  async setCredentials(credentials) {
1235
2369
  this.#credentials = credentials;
1236
2370
  }
2371
+ /**
2372
+ * Fetches the current attention state of the device (active, idle, screensaver, etc.).
2373
+ *
2374
+ * @returns The current attention state.
2375
+ */
1237
2376
  async getAttentionState() {
1238
2377
  return await this.#protocol.getAttentionState();
1239
2378
  }
2379
+ /**
2380
+ * Fetches the list of apps that can be launched on the device.
2381
+ *
2382
+ * @returns Array of launchable app descriptors.
2383
+ */
1240
2384
  async getLaunchableApps() {
1241
2385
  return await this.#protocol.getLaunchableApps();
1242
2386
  }
2387
+ /**
2388
+ * Fetches the list of user accounts configured on the device.
2389
+ *
2390
+ * @returns Array of user account descriptors.
2391
+ */
1243
2392
  async getUserAccounts() {
1244
2393
  return await this.#protocol.getUserAccounts();
1245
2394
  }
2395
+ /**
2396
+ * Fetches the current now-playing information from the device.
2397
+ *
2398
+ * @returns The now-playing info payload.
2399
+ */
2400
+ async fetchNowPlayingInfo() {
2401
+ return await this.#protocol.fetchNowPlayingInfo();
2402
+ }
2403
+ /**
2404
+ * Fetches the currently supported actions from the device.
2405
+ *
2406
+ * @returns The supported actions payload.
2407
+ */
2408
+ async fetchSupportedActions() {
2409
+ return await this.#protocol.fetchSupportedActions();
2410
+ }
2411
+ /**
2412
+ * Fetches the current media control status (available controls bitmask).
2413
+ *
2414
+ * @returns The media control status payload.
2415
+ */
2416
+ async fetchMediaControlStatus() {
2417
+ return await this.#protocol.fetchMediaControlStatus();
2418
+ }
2419
+ /**
2420
+ * Launches an app on the device by its bundle identifier.
2421
+ *
2422
+ * @param bundleId - The bundle identifier of the app to launch.
2423
+ */
1246
2424
  async launchApp(bundleId) {
1247
2425
  await this.#protocol.launchApp(bundleId);
1248
2426
  }
2427
+ /**
2428
+ * Opens a URL on the device (universal link or app-specific URL scheme).
2429
+ *
2430
+ * @param url - The URL to open.
2431
+ */
1249
2432
  async launchUrl(url) {
1250
2433
  await this.#protocol.launchUrl(url);
1251
2434
  }
2435
+ /**
2436
+ * Sends a media control command (play, pause, next, etc.) via the Companion Link protocol.
2437
+ *
2438
+ * @param command - The media control command key.
2439
+ * @param content - Optional additional content for the command.
2440
+ */
1252
2441
  async mediaControlCommand(command, content) {
1253
2442
  await this.#protocol.mediaControlCommand(command, content);
1254
2443
  }
2444
+ /**
2445
+ * Sends a HID button press via the Companion Link protocol.
2446
+ *
2447
+ * @param command - The HID command key (e.g. 'up', 'select', 'menu').
2448
+ * @param type - Optional press type (short, long, double).
2449
+ * @param holdDelayMs - Optional hold duration in milliseconds for long presses.
2450
+ */
1255
2451
  async pressButton(command, type, holdDelayMs) {
1256
2452
  await this.#protocol.pressButton(command, type, holdDelayMs);
1257
2453
  }
2454
+ /**
2455
+ * Switches to a different user account on the device.
2456
+ *
2457
+ * @param accountId - The ID of the user account to switch to.
2458
+ */
1258
2459
  async switchUserAccount(accountId) {
1259
2460
  await this.#protocol.switchUserAccount(accountId);
1260
2461
  }
2462
+ /**
2463
+ * Sets the text input field to the given text, replacing any existing content.
2464
+ *
2465
+ * @param text - The text to set.
2466
+ */
1261
2467
  async textSet(text) {
1262
2468
  await this.#protocol.textInputCommand(text, true);
1263
2469
  }
2470
+ /**
2471
+ * Appends text to the current text input field content.
2472
+ *
2473
+ * @param text - The text to append.
2474
+ */
1264
2475
  async textAppend(text) {
1265
2476
  await this.#protocol.textInputCommand(text, false);
1266
2477
  }
2478
+ /** Clears the text input field. */
1267
2479
  async textClear() {
1268
2480
  await this.#protocol.textInputCommand("", true);
1269
2481
  }
1270
- async #heartbeat() {
1271
- try {
1272
- this.#protocol.noOp();
1273
- } catch (err) {
1274
- this.#protocol.context.logger.error("Heartbeat error", err);
2482
+ /**
2483
+ * Sends a raw touch event to the device.
2484
+ *
2485
+ * @param finger - Finger index (0-based).
2486
+ * @param phase - Touch phase (0 = Began, 1 = Moved, 2 = Ended).
2487
+ * @param x - Horizontal position.
2488
+ * @param y - Vertical position.
2489
+ */
2490
+ async sendTouchEvent(finger, phase, x, y) {
2491
+ await this.#protocol.sendTouchEvent(finger, phase, x, y);
2492
+ }
2493
+ /**
2494
+ * Simulates a tap at the given coordinates.
2495
+ *
2496
+ * @param x - Horizontal position (defaults to center 500).
2497
+ * @param y - Vertical position (defaults to center 500).
2498
+ */
2499
+ async tap(x = 500, y = 500) {
2500
+ await this.sendTouchEvent(0, 0, x, y);
2501
+ await waitFor(50);
2502
+ await this.sendTouchEvent(0, 2, x, y);
2503
+ }
2504
+ /**
2505
+ * Simulates a swipe gesture in the given direction.
2506
+ *
2507
+ * @param direction - Swipe direction.
2508
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
2509
+ */
2510
+ async swipe(direction, duration = 200) {
2511
+ const [startX, startY, endX, endY] = {
2512
+ up: [
2513
+ 500,
2514
+ 700,
2515
+ 500,
2516
+ 300
2517
+ ],
2518
+ down: [
2519
+ 500,
2520
+ 300,
2521
+ 500,
2522
+ 700
2523
+ ],
2524
+ left: [
2525
+ 700,
2526
+ 500,
2527
+ 300,
2528
+ 500
2529
+ ],
2530
+ right: [
2531
+ 300,
2532
+ 500,
2533
+ 700,
2534
+ 500
2535
+ ]
2536
+ }[direction];
2537
+ const steps = Math.max(4, Math.floor(duration / 50));
2538
+ const deltaX = (endX - startX) / steps;
2539
+ const deltaY = (endY - startY) / steps;
2540
+ const stepDuration = duration / steps;
2541
+ await this.sendTouchEvent(0, 0, startX, startY);
2542
+ for (let i = 1; i < steps; i++) {
2543
+ await waitFor(stepDuration);
2544
+ await this.sendTouchEvent(0, 1, Math.round(startX + deltaX * i), Math.round(startY + deltaY * i));
1275
2545
  }
1276
- }
1277
- async #onClose() {
1278
- this.#protocol.context.logger.net("#onClose() called on companion link device.");
1279
- if (!this.#disconnect) {
1280
- await this.disconnectSafely();
1281
- this.emit("disconnected", true);
1282
- } else this.emit("disconnected", false);
1283
- }
1284
- async #onError(err) {
1285
- this.#protocol.context.logger.error("Companion Link error", err);
1286
- }
1287
- async #onTimeout() {
1288
- this.#protocol.context.logger.error("Companion Link timeout");
1289
- await this.#protocol.stream.destroy();
1290
- }
2546
+ await waitFor(stepDuration);
2547
+ await this.sendTouchEvent(0, 2, endX, endY);
2548
+ }
2549
+ /** Toggles closed captions on the device. */
2550
+ async toggleCaptions() {
2551
+ await this.#protocol.toggleCaptions();
2552
+ }
2553
+ /**
2554
+ * Toggles the system appearance between light and dark mode.
2555
+ *
2556
+ * @param light - True for light mode, false for dark mode.
2557
+ */
2558
+ async toggleSystemAppearance(light) {
2559
+ await this.#protocol.toggleSystemAppearance(light);
2560
+ }
2561
+ /**
2562
+ * Enables or disables the "Reduce Loud Sounds" setting.
2563
+ *
2564
+ * @param enabled - Whether to enable the setting.
2565
+ */
2566
+ async toggleReduceLoudSounds(enabled) {
2567
+ await this.#protocol.toggleReduceLoudSounds(enabled);
2568
+ }
2569
+ /**
2570
+ * Enables or disables finding mode (Find My integration).
2571
+ *
2572
+ * @param enabled - Whether to enable finding mode.
2573
+ */
2574
+ async toggleFindingMode(enabled) {
2575
+ await this.#protocol.toggleFindingMode(enabled);
2576
+ }
2577
+ /**
2578
+ * Fetches the "Up Next" queue from the device.
2579
+ *
2580
+ * @param paginationToken - Optional token for paginated results.
2581
+ * @returns The Up Next queue payload.
2582
+ */
2583
+ async fetchUpNext(paginationToken) {
2584
+ return await this.#protocol.fetchUpNext(paginationToken);
2585
+ }
2586
+ /**
2587
+ * Adds an item to the "Up Next" queue.
2588
+ *
2589
+ * @param identifier - Content item identifier.
2590
+ * @param kind - Content kind descriptor.
2591
+ */
2592
+ async addToUpNext(identifier, kind) {
2593
+ await this.#protocol.addToUpNext(identifier, kind);
2594
+ }
2595
+ /**
2596
+ * Removes an item from the "Up Next" queue.
2597
+ *
2598
+ * @param identifier - Content item identifier.
2599
+ * @param kind - Content kind descriptor.
2600
+ */
2601
+ async removeFromUpNext(identifier, kind) {
2602
+ await this.#protocol.removeFromUpNext(identifier, kind);
2603
+ }
2604
+ /**
2605
+ * Marks a content item as watched.
2606
+ *
2607
+ * @param identifier - Content item identifier.
2608
+ * @param kind - Content kind descriptor.
2609
+ */
2610
+ async markAsWatched(identifier, kind) {
2611
+ await this.#protocol.markAsWatched(identifier, kind);
2612
+ }
2613
+ /** Starts a Siri session on the device. */
2614
+ async siriStart() {
2615
+ await this.#protocol.siriStart();
2616
+ }
2617
+ /** Stops the active Siri session on the device. */
2618
+ async siriStop() {
2619
+ await this.#protocol.siriStop();
2620
+ }
2621
+ /**
2622
+ * Sets up encryption, protocol sessions (system info, TVRC, touch, text input),
2623
+ * heartbeat interval, and state tracking. Called after successful pair-verify.
2624
+ */
1291
2625
  async #setup() {
1292
2626
  const keys = this.#keys;
1293
2627
  this.#protocol.stream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
@@ -1298,158 +2632,144 @@ var device_default$1 = class extends EventEmitter {
1298
2632
  await this.#protocol.touchStart();
1299
2633
  await this.#protocol.tiStart();
1300
2634
  this.#heartbeatInterval = setInterval(() => {
1301
- this.#heartbeat().catch((err) => {
2635
+ try {
2636
+ this.#protocol.noOp();
2637
+ } catch (err) {
1302
2638
  this.#protocol.context.logger.error("Heartbeat failed", err);
1303
- });
2639
+ }
1304
2640
  }, 15e3);
1305
- await this.#subscribe();
2641
+ if (this.#state) this.#state.removeAllListeners();
2642
+ this.#state = new CompanionLinkState(this.#protocol);
2643
+ this.#state.on("attentionStateChanged", (s) => this.emit("attentionStateChanged", s));
2644
+ this.#state.on("mediaControlFlagsChanged", (f, c) => this.emit("mediaControlFlagsChanged", f, c));
2645
+ this.#state.on("nowPlayingInfoChanged", (i) => this.emit("nowPlayingInfoChanged", i));
2646
+ this.#state.on("supportedActionsChanged", (a) => this.emit("supportedActionsChanged", a));
2647
+ this.#state.on("textInputChanged", (s) => this.emit("textInputChanged", s));
2648
+ this.#state.on("volumeAvailabilityChanged", (a) => this.emit("volumeAvailabilityChanged", a));
2649
+ this.#state.subscribe();
2650
+ await this.#state.fetchInitialState();
1306
2651
  } catch (err) {
1307
2652
  clearInterval(this.#heartbeatInterval);
1308
2653
  this.#heartbeatInterval = void 0;
1309
2654
  throw err;
1310
2655
  }
1311
2656
  }
1312
- async #subscribe() {
1313
- this.#protocol.stream.on("_iMC", this.#onMediaControl);
1314
- this.#protocol.stream.on("SystemStatus", this.onSystemStatus);
1315
- this.#protocol.stream.on("TVSystemStatus", this.onTVSystemStatus);
1316
- this.#protocol.stream.on("_tiStarted", this.#onTextInputStarted);
1317
- this.#protocol.stream.on("_tiStopped", this.#onTextInputStopped);
1318
- this.#protocol.registerInterests([
1319
- "_iMC",
1320
- "SystemStatus",
1321
- "TVSystemStatus"
1322
- ]);
1323
- const state = await this.getAttentionState();
1324
- this.emit("power", state);
1325
- }
1326
- async #unsubscribe() {
1327
- this.#protocol.stream.off("_iMC", this.#onMediaControl);
1328
- this.#protocol.stream.off("SystemStatus", this.onSystemStatus);
1329
- this.#protocol.stream.off("TVSystemStatus", this.onTVSystemStatus);
1330
- this.#protocol.stream.off("_tiStarted", this.#onTextInputStarted);
1331
- this.#protocol.stream.off("_tiStopped", this.#onTextInputStopped);
1332
- try {
1333
- this.#protocol.deregisterInterests([
1334
- "_iMC",
1335
- "SystemStatus",
1336
- "TVSystemStatus"
1337
- ]);
1338
- } catch (_) {}
1339
- }
1340
- #onTextInputStarted = async (data) => {
1341
- try {
1342
- const payload = data;
1343
- let documentText = "";
1344
- let isSecure = false;
1345
- let keyboardType = 0;
1346
- let autocorrection = false;
1347
- let autocapitalization = false;
1348
- if (payload?._tiD) {
1349
- const plistData = Plist.parse(Buffer.from(payload._tiD).buffer);
1350
- documentText = plistData._tiDT ?? "";
1351
- isSecure = plistData._tiSR ?? false;
1352
- keyboardType = plistData._tiKT ?? 0;
1353
- autocorrection = plistData._tiAC ?? false;
1354
- autocapitalization = plistData._tiAP ?? false;
1355
- }
1356
- this.#textInputState = {
1357
- isActive: true,
1358
- documentText,
1359
- isSecure,
1360
- keyboardType,
1361
- autocorrection,
1362
- autocapitalization
1363
- };
1364
- this.emit("textInput", this.#textInputState);
1365
- } catch (err) {
1366
- this.#protocol.context.logger.error("Text input started parse error", err);
1367
- }
1368
- };
1369
- #onTextInputStopped = async (_data) => {
1370
- this.#textInputState = {
1371
- isActive: false,
1372
- documentText: "",
1373
- isSecure: false,
1374
- keyboardType: 0,
1375
- autocorrection: false,
1376
- autocapitalization: false
1377
- };
1378
- this.emit("textInput", this.#textInputState);
1379
- };
1380
- async onSystemStatus(data) {
1381
- this.#protocol.context.logger.info("System Status", data);
1382
- this.emit("power", convertAttentionState(data.state));
2657
+ /** Handles the stream close event. Emits 'disconnected' with unexpected=true if not intentional. */
2658
+ onClose() {
2659
+ this.#protocol.context.logger.net("onClose() called on companion link device.");
2660
+ if (!this.#disconnect) {
2661
+ this.disconnectSafely();
2662
+ this.emit("disconnected", true);
2663
+ } else this.emit("disconnected", false);
1383
2664
  }
1384
- async onTVSystemStatus(data) {
1385
- this.#protocol.context.logger.info("TV System Status", data);
1386
- this.emit("power", convertAttentionState(data.state));
2665
+ /**
2666
+ * Handles stream error events by logging them.
2667
+ *
2668
+ * @param err - The error that occurred.
2669
+ */
2670
+ onError(err) {
2671
+ this.#protocol.context.logger.error("Companion Link error", err);
2672
+ }
2673
+ /** Handles stream timeout events by destroying the stream. */
2674
+ onTimeout() {
2675
+ this.#protocol.context.logger.error("Companion Link timeout");
2676
+ this.#protocol.stream.destroy();
1387
2677
  }
1388
2678
  };
1389
2679
 
1390
2680
  //#endregion
1391
2681
  //#region src/model/apple-tv.ts
2682
+ /**
2683
+ * High-level Apple TV device model combining AirPlay (remote control, media, streaming)
2684
+ * and Companion Link (apps, accounts, power, text input) protocols.
2685
+ * Provides a unified interface for controlling an Apple TV.
2686
+ */
1392
2687
  var apple_tv_default = class extends EventEmitter {
2688
+ /** The underlying AirPlay device for direct protocol access. */
1393
2689
  get airplay() {
1394
2690
  return this.#airplay;
1395
2691
  }
2692
+ /** The underlying Companion Link device for direct protocol access. */
1396
2693
  get companionLink() {
1397
2694
  return this.#companionLink;
1398
2695
  }
2696
+ /** AirPlay remote controller for HID keys and commands. */
1399
2697
  get remote() {
1400
2698
  return this.#airplay.remote;
1401
2699
  }
2700
+ /** AirPlay state tracker for now-playing, volume, and keyboard. */
1402
2701
  get state() {
1403
2702
  return this.#airplay.state;
1404
2703
  }
2704
+ /** AirPlay volume controller. */
1405
2705
  get volumeControl() {
1406
2706
  return this.#airplay.volume;
1407
2707
  }
2708
+ /** Bundle identifier of the currently playing app, or null. */
1408
2709
  get bundleIdentifier() {
1409
2710
  return this.#nowPlayingClient?.bundleIdentifier ?? null;
1410
2711
  }
2712
+ /** Display name of the currently playing app, or null. */
1411
2713
  get displayName() {
1412
2714
  return this.#nowPlayingClient?.displayName ?? null;
1413
2715
  }
2716
+ /** Whether both AirPlay and Companion Link are connected. */
1414
2717
  get isConnected() {
1415
2718
  return this.#airplay.isConnected && this.#companionLink.isConnected;
1416
2719
  }
2720
+ /** Whether the active player is currently playing. */
1417
2721
  get isPlaying() {
1418
2722
  return this.#nowPlayingClient?.isPlaying ?? false;
1419
2723
  }
2724
+ /** Current track title. */
1420
2725
  get title() {
1421
2726
  return this.#nowPlayingClient?.title ?? "";
1422
2727
  }
2728
+ /** Current track artist. */
1423
2729
  get artist() {
1424
2730
  return this.#nowPlayingClient?.artist ?? "";
1425
2731
  }
2732
+ /** Current track album. */
1426
2733
  get album() {
1427
2734
  return this.#nowPlayingClient?.album ?? "";
1428
2735
  }
2736
+ /** Duration of the current track in seconds. */
1429
2737
  get duration() {
1430
2738
  return this.#nowPlayingClient?.duration ?? 0;
1431
2739
  }
2740
+ /** Extrapolated elapsed time in seconds. */
1432
2741
  get elapsedTime() {
1433
2742
  return this.#nowPlayingClient?.elapsedTime ?? 0;
1434
2743
  }
2744
+ /** Current playback queue from the active player. */
1435
2745
  get playbackQueue() {
1436
2746
  return this.#nowPlayingClient?.playbackQueue ?? null;
1437
2747
  }
2748
+ /** Current playback state. */
1438
2749
  get playbackState() {
1439
2750
  return this.#nowPlayingClient?.playbackState ?? AirPlay.Proto.PlaybackState_Enum.Unknown;
1440
2751
  }
2752
+ /** Timestamp of the last playback state update. */
1441
2753
  get playbackStateTimestamp() {
1442
2754
  return this.#nowPlayingClient?.playbackStateTimestamp ?? -1;
1443
2755
  }
2756
+ /** Current volume level (0.0 - 1.0). */
1444
2757
  get volume() {
1445
2758
  return this.#airplay.state.volume ?? 0;
1446
2759
  }
2760
+ /** @returns The currently active now-playing client, or null. */
1447
2761
  get #nowPlayingClient() {
1448
2762
  return this.#airplay.state.nowPlayingClient;
1449
2763
  }
1450
2764
  #airplay;
1451
2765
  #companionLink;
1452
2766
  #disconnect = false;
2767
+ /**
2768
+ * Creates a new AppleTV instance.
2769
+ *
2770
+ * @param airplayDiscoveryResult - The mDNS discovery result for the AirPlay service.
2771
+ * @param companionLinkDiscoveryResult - The mDNS discovery result for the Companion Link service.
2772
+ */
1453
2773
  constructor(airplayDiscoveryResult, companionLinkDiscoveryResult) {
1454
2774
  super();
1455
2775
  this.#airplay = new device_default(airplayDiscoveryResult);
@@ -1458,9 +2778,16 @@ var apple_tv_default = class extends EventEmitter {
1458
2778
  this.#airplay.on("disconnected", (unexpected) => this.#onDisconnected(unexpected));
1459
2779
  this.#companionLink.on("connected", () => this.#onConnected());
1460
2780
  this.#companionLink.on("disconnected", (unexpected) => this.#onDisconnected(unexpected));
1461
- this.#companionLink.on("power", (state) => this.emit("power", state));
1462
- this.#companionLink.on("textInput", (state) => this.emit("textInput", state));
1463
- }
2781
+ this.#companionLink.on("attentionStateChanged", (state) => this.emit("power", state));
2782
+ this.#companionLink.on("textInputChanged", (state) => this.emit("textInput", state));
2783
+ }
2784
+ /**
2785
+ * Connects both AirPlay and Companion Link protocols.
2786
+ * If Companion Link fails, AirPlay is disconnected to maintain consistency.
2787
+ *
2788
+ * @param airplayCredentials - Credentials for the AirPlay service.
2789
+ * @param companionLinkCredentials - Optional separate credentials for Companion Link (defaults to AirPlay credentials).
2790
+ */
1464
2791
  async connect(airplayCredentials, companionLinkCredentials) {
1465
2792
  this.#airplay.setCredentials(airplayCredentials);
1466
2793
  await this.#companionLink.setCredentials(companionLinkCredentials ?? airplayCredentials);
@@ -1473,81 +2800,148 @@ var apple_tv_default = class extends EventEmitter {
1473
2800
  }
1474
2801
  this.#disconnect = false;
1475
2802
  }
2803
+ /** Disconnects both AirPlay and Companion Link protocols. */
1476
2804
  async disconnect() {
1477
2805
  await this.#airplay.disconnect();
1478
2806
  await this.#companionLink.disconnect();
1479
2807
  }
2808
+ /** Puts the Apple TV to sleep via a suspend HID key press. */
1480
2809
  async turnOff() {
1481
2810
  await this.#airplay.remote.suspend();
1482
2811
  }
2812
+ /** Wakes the Apple TV from sleep via a wake HID key press. */
1483
2813
  async turnOn() {
1484
2814
  await this.#airplay.remote.wake();
1485
2815
  }
2816
+ /** Sends a Pause command. */
1486
2817
  async pause() {
1487
2818
  await this.#airplay.sendCommand(AirPlay.Proto.Command.Pause);
1488
2819
  }
2820
+ /** Sends a TogglePlayPause command. */
1489
2821
  async playPause() {
1490
2822
  await this.#airplay.sendCommand(AirPlay.Proto.Command.TogglePlayPause);
1491
2823
  }
2824
+ /** Sends a Play command. */
1492
2825
  async play() {
1493
2826
  await this.#airplay.sendCommand(AirPlay.Proto.Command.Play);
1494
2827
  }
2828
+ /** Sends a Stop command. */
1495
2829
  async stop() {
1496
2830
  await this.#airplay.sendCommand(AirPlay.Proto.Command.Stop);
1497
2831
  }
2832
+ /** Sends a NextInContext command (next track/episode). */
1498
2833
  async next() {
1499
2834
  await this.#airplay.sendCommand(AirPlay.Proto.Command.NextInContext);
1500
2835
  }
2836
+ /** Sends a PreviousInContext command (previous track/episode). */
1501
2837
  async previous() {
1502
2838
  await this.#airplay.sendCommand(AirPlay.Proto.Command.PreviousInContext);
1503
2839
  }
2840
+ /** Decreases volume via HID volume down key. */
1504
2841
  async volumeDown() {
1505
2842
  await this.#airplay.remote.volumeDown();
1506
2843
  }
2844
+ /** Toggles mute via HID mute key. */
1507
2845
  async volumeMute() {
1508
2846
  await this.#airplay.remote.mute();
1509
2847
  }
2848
+ /** Increases volume via HID volume up key. */
1510
2849
  async volumeUp() {
1511
2850
  await this.#airplay.remote.volumeUp();
1512
2851
  }
2852
+ /**
2853
+ * Fetches the current attention state via Companion Link.
2854
+ *
2855
+ * @returns The current attention state.
2856
+ */
1513
2857
  async getAttentionState() {
1514
2858
  return await this.#companionLink.getAttentionState();
1515
2859
  }
2860
+ /**
2861
+ * Fetches the list of launchable apps via Companion Link.
2862
+ *
2863
+ * @returns Array of launchable app descriptors.
2864
+ */
1516
2865
  async getLaunchableApps() {
1517
2866
  return await this.#companionLink.getLaunchableApps();
1518
2867
  }
2868
+ /**
2869
+ * Fetches user accounts configured on the device via Companion Link.
2870
+ *
2871
+ * @returns Array of user account descriptors.
2872
+ */
1519
2873
  async getUserAccounts() {
1520
2874
  return await this.#companionLink.getUserAccounts();
1521
2875
  }
2876
+ /**
2877
+ * Launches an app via Companion Link.
2878
+ *
2879
+ * @param bundleId - The bundle identifier of the app to launch.
2880
+ */
1522
2881
  async launchApp(bundleId) {
1523
2882
  await this.#companionLink.launchApp(bundleId);
1524
2883
  }
2884
+ /**
2885
+ * Switches user account via Companion Link.
2886
+ *
2887
+ * @param accountId - The ID of the user account to switch to.
2888
+ */
1525
2889
  async switchUserAccount(accountId) {
1526
2890
  await this.#companionLink.switchUserAccount(accountId);
1527
2891
  }
2892
+ /**
2893
+ * Sets the text input field to the given text via Companion Link.
2894
+ *
2895
+ * @param text - The text to set.
2896
+ */
1528
2897
  async textSet(text) {
1529
2898
  await this.#companionLink.textSet(text);
1530
2899
  }
2900
+ /**
2901
+ * Appends text to the text input field via Companion Link.
2902
+ *
2903
+ * @param text - The text to append.
2904
+ */
1531
2905
  async textAppend(text) {
1532
2906
  await this.#companionLink.textAppend(text);
1533
2907
  }
2908
+ /** Clears the text input field via Companion Link. */
1534
2909
  async textClear() {
1535
2910
  await this.#companionLink.textClear();
1536
2911
  }
2912
+ /**
2913
+ * Gets the CommandInfo for a specific command from the active player.
2914
+ *
2915
+ * @param command - The command to look up.
2916
+ * @returns The command info, or null if no client is active or command not found.
2917
+ */
1537
2918
  async getCommandInfo(command) {
1538
2919
  const client = this.#airplay.state.nowPlayingClient;
1539
2920
  if (!client) return null;
1540
2921
  return client.supportedCommands.find((c) => c.command === command) ?? null;
1541
2922
  }
2923
+ /**
2924
+ * Checks whether a command is supported and enabled by the active player.
2925
+ *
2926
+ * @param command - The command to check.
2927
+ * @returns True if supported and enabled, false otherwise.
2928
+ */
1542
2929
  async isCommandSupported(command) {
1543
2930
  const client = this.#airplay.state.nowPlayingClient;
1544
2931
  if (!client) return false;
1545
2932
  return client.isCommandSupported(command);
1546
2933
  }
2934
+ /** Emits 'connected' when both AirPlay and Companion Link are connected. */
1547
2935
  async #onConnected() {
1548
2936
  if (!this.#airplay.isConnected || !this.#companionLink.isConnected) return;
1549
2937
  this.emit("connected");
1550
2938
  }
2939
+ /**
2940
+ * Handles disconnection from either protocol. Disconnects both sides
2941
+ * and emits 'disconnected'. Only fires once per disconnect cycle.
2942
+ *
2943
+ * @param unexpected - Whether the disconnection was unexpected.
2944
+ */
1551
2945
  async #onDisconnected(unexpected) {
1552
2946
  if (this.#disconnect) return;
1553
2947
  this.#disconnect = true;
@@ -1558,119 +2952,187 @@ var apple_tv_default = class extends EventEmitter {
1558
2952
 
1559
2953
  //#endregion
1560
2954
  //#region src/model/homepod-base.ts
2955
+ /**
2956
+ * Abstract base class for HomePod models (HomePod and HomePod Mini).
2957
+ * Uses AirPlay only (no Companion Link). Supports transient pairing, media control,
2958
+ * URL playback, and audio streaming.
2959
+ */
1561
2960
  var homepod_base_default = class extends EventEmitter {
2961
+ /** The underlying AirPlay device for direct protocol access. */
1562
2962
  get airplay() {
1563
2963
  return this.#airplay;
1564
2964
  }
2965
+ /** AirPlay remote controller for HID keys and commands. */
1565
2966
  get remote() {
1566
2967
  return this.#airplay.remote;
1567
2968
  }
2969
+ /** AirPlay state tracker for now-playing and volume. */
1568
2970
  get state() {
1569
2971
  return this.#airplay.state;
1570
2972
  }
2973
+ /** AirPlay volume controller. */
1571
2974
  get volumeControl() {
1572
2975
  return this.#airplay.volume;
1573
2976
  }
2977
+ /** Bundle identifier of the currently playing app, or null. */
1574
2978
  get bundleIdentifier() {
1575
2979
  return this.#nowPlayingClient?.bundleIdentifier ?? null;
1576
2980
  }
2981
+ /** Display name of the currently playing app, or null. */
1577
2982
  get displayName() {
1578
2983
  return this.#nowPlayingClient?.displayName ?? null;
1579
2984
  }
2985
+ /** Whether the AirPlay connection is active. */
1580
2986
  get isConnected() {
1581
2987
  return this.#airplay.isConnected;
1582
2988
  }
2989
+ /** Whether the active player is currently playing. */
1583
2990
  get isPlaying() {
1584
2991
  return this.#nowPlayingClient?.isPlaying ?? false;
1585
2992
  }
2993
+ /** Current track title. */
1586
2994
  get title() {
1587
2995
  return this.#nowPlayingClient?.title ?? "";
1588
2996
  }
2997
+ /** Current track artist. */
1589
2998
  get artist() {
1590
2999
  return this.#nowPlayingClient?.artist ?? "";
1591
3000
  }
3001
+ /** Current track album. */
1592
3002
  get album() {
1593
3003
  return this.#nowPlayingClient?.album ?? "";
1594
3004
  }
3005
+ /** Duration of the current track in seconds. */
1595
3006
  get duration() {
1596
3007
  return this.#nowPlayingClient?.duration ?? 0;
1597
3008
  }
3009
+ /** Extrapolated elapsed time in seconds. */
1598
3010
  get elapsedTime() {
1599
3011
  return this.#nowPlayingClient?.elapsedTime ?? 0;
1600
3012
  }
3013
+ /** Current playback queue from the active player. */
1601
3014
  get playbackQueue() {
1602
3015
  return this.#nowPlayingClient?.playbackQueue ?? null;
1603
3016
  }
3017
+ /** Current playback state. */
1604
3018
  get playbackState() {
1605
3019
  return this.#nowPlayingClient?.playbackState ?? AirPlay.Proto.PlaybackState_Enum.Unknown;
1606
3020
  }
3021
+ /** Timestamp of the last playback state update. */
1607
3022
  get playbackStateTimestamp() {
1608
3023
  return this.#nowPlayingClient?.playbackStateTimestamp ?? -1;
1609
3024
  }
3025
+ /** Current volume level (0.0 - 1.0). */
1610
3026
  get volume() {
1611
3027
  return this.#airplay.state.volume ?? 0;
1612
3028
  }
3029
+ /** @returns The currently active now-playing client, or null. */
1613
3030
  get #nowPlayingClient() {
1614
3031
  return this.#airplay.state.nowPlayingClient;
1615
3032
  }
1616
3033
  #airplay;
1617
3034
  #disconnect = false;
3035
+ /**
3036
+ * Creates a new HomePod base instance.
3037
+ *
3038
+ * @param discoveryResult - The mDNS discovery result for the AirPlay service.
3039
+ */
1618
3040
  constructor(discoveryResult) {
1619
3041
  super();
1620
3042
  this.#airplay = new device_default(discoveryResult);
1621
3043
  this.#airplay.on("connected", () => this.#onConnected());
1622
3044
  this.#airplay.on("disconnected", (unexpected) => this.#onDisconnected(unexpected));
1623
3045
  }
3046
+ /** Connects to the HomePod via AirPlay (transient pairing). */
1624
3047
  async connect() {
1625
3048
  await this.#airplay.connect();
1626
3049
  this.#disconnect = false;
1627
3050
  }
3051
+ /** Disconnects from the HomePod. */
1628
3052
  async disconnect() {
1629
3053
  await this.#airplay.disconnect();
1630
3054
  }
3055
+ /** Sends a Pause command. */
1631
3056
  async pause() {
1632
3057
  await this.#airplay.sendCommand(AirPlay.Proto.Command.Pause);
1633
3058
  }
3059
+ /** Sends a TogglePlayPause command. */
1634
3060
  async playPause() {
1635
3061
  await this.#airplay.sendCommand(AirPlay.Proto.Command.TogglePlayPause);
1636
3062
  }
3063
+ /** Sends a Play command. */
1637
3064
  async play() {
1638
3065
  await this.#airplay.sendCommand(AirPlay.Proto.Command.Play);
1639
3066
  }
3067
+ /** Sends a Stop command. */
1640
3068
  async stop() {
1641
3069
  await this.#airplay.sendCommand(AirPlay.Proto.Command.Stop);
1642
3070
  }
3071
+ /** Sends a NextInContext command (next track). */
1643
3072
  async next() {
1644
3073
  await this.#airplay.sendCommand(AirPlay.Proto.Command.NextInContext);
1645
3074
  }
3075
+ /** Sends a PreviousInContext command (previous track). */
1646
3076
  async previous() {
1647
3077
  await this.#airplay.sendCommand(AirPlay.Proto.Command.PreviousInContext);
1648
3078
  }
3079
+ /**
3080
+ * Plays a URL on the HomePod (the device fetches and plays the content).
3081
+ *
3082
+ * @param url - The media URL to play.
3083
+ * @param position - Start position in seconds (defaults to 0).
3084
+ */
1649
3085
  async playUrl(url, position = 0) {
1650
3086
  await this.#airplay.playUrl(url, position);
1651
3087
  }
3088
+ /** Waits for the current URL playback to finish. */
1652
3089
  async waitForPlaybackEnd() {
1653
3090
  await this.#airplay.waitForPlaybackEnd();
1654
3091
  }
3092
+ /** Stops the current URL playback and cleans up. */
1655
3093
  stopPlayUrl() {
1656
3094
  this.#airplay.stopPlayUrl();
1657
3095
  }
3096
+ /**
3097
+ * Streams audio from a source to the HomePod via RAOP/RTP.
3098
+ *
3099
+ * @param source - The audio source to stream.
3100
+ */
1658
3101
  async streamAudio(source) {
1659
3102
  await this.#airplay.streamAudio(source);
1660
3103
  }
3104
+ /**
3105
+ * Gets the CommandInfo for a specific command from the active player.
3106
+ *
3107
+ * @param command - The command to look up.
3108
+ * @returns The command info, or null if no client is active or command not found.
3109
+ */
1661
3110
  async getCommandInfo(command) {
1662
3111
  const client = this.#airplay.state.nowPlayingClient;
1663
3112
  if (!client) return null;
1664
3113
  return client.supportedCommands.find((c) => c.command === command) ?? null;
1665
3114
  }
3115
+ /**
3116
+ * Checks whether a command is supported and enabled by the active player.
3117
+ *
3118
+ * @param command - The command to check.
3119
+ * @returns True if supported and enabled, false otherwise.
3120
+ */
1666
3121
  async isCommandSupported(command) {
1667
3122
  const client = this.#airplay.state.nowPlayingClient;
1668
3123
  if (!client) return false;
1669
3124
  return client.isCommandSupported(command);
1670
3125
  }
3126
+ /** Emits 'connected' when the AirPlay connection is established. */
1671
3127
  async #onConnected() {
1672
3128
  this.emit("connected");
1673
3129
  }
3130
+ /**
3131
+ * Handles disconnection. Disconnects and emits 'disconnected'.
3132
+ * Only fires once per disconnect cycle.
3133
+ *
3134
+ * @param unexpected - Whether the disconnection was unexpected.
3135
+ */
1674
3136
  async #onDisconnected(unexpected) {
1675
3137
  if (this.#disconnect) return;
1676
3138
  this.#disconnect = true;
@@ -1681,10 +3143,18 @@ var homepod_base_default = class extends EventEmitter {
1681
3143
 
1682
3144
  //#endregion
1683
3145
  //#region src/model/homepod.ts
3146
+ /**
3147
+ * HomePod device model. AirPlay only (no Companion Link).
3148
+ * Supports media control, URL playback, audio streaming, and volume control.
3149
+ */
1684
3150
  var homepod_default = class extends homepod_base_default {};
1685
3151
 
1686
3152
  //#endregion
1687
3153
  //#region src/model/homepod-mini.ts
3154
+ /**
3155
+ * HomePod Mini device model. AirPlay only (no Companion Link).
3156
+ * Identical feature set to HomePod, distinguished for device model identification.
3157
+ */
1688
3158
  var homepod_mini_default = class extends homepod_base_default {};
1689
3159
 
1690
3160
  //#endregion