@basmilius/apple-sdk 0.11.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,4590 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { AirPlayFeatureFlags, CommandError, CredentialsError, Discovery, TimingServer, reporter, waitFor } from "@basmilius/apple-common";
3
+ import { DataStreamMessage, Proto, Proto as Proto$1, Protocol } from "@basmilius/apple-airplay";
4
+ import { MediaControlFlag, Protocol as Protocol$1, convertAttentionState } from "@basmilius/apple-companion-link";
5
+ import { NSKeyedArchiver, Plist } from "@basmilius/apple-encoding";
6
+
7
+ //#region src/configure.ts
8
+ let globalStorage;
9
+ let globalTimingServer;
10
+ /**
11
+ * Configures global settings for the Apple SDK.
12
+ * Should be called once before creating any device instances.
13
+ */
14
+ function configure(config) {
15
+ if (config.storage) globalStorage = config.storage;
16
+ if (config.timingServer) globalTimingServer = config.timingServer;
17
+ if (config.logging) {
18
+ reporter.none();
19
+ for (const group of config.logging) reporter.enable(group);
20
+ }
21
+ }
22
+ /**
23
+ * @internal Returns the globally configured timing server, if any.
24
+ */
25
+ function getGlobalTimingServer() {
26
+ return globalTimingServer;
27
+ }
28
+
29
+ //#endregion
30
+ //#region src/controller/accounts.ts
31
+ /**
32
+ * User account controller for Apple TV devices.
33
+ * Provides user account listing and switching.
34
+ */
35
+ var AccountsController = class {
36
+ #companionLink;
37
+ constructor(companionLink) {
38
+ this.#companionLink = companionLink;
39
+ }
40
+ /**
41
+ * Returns the list of user accounts configured on the device.
42
+ */
43
+ async list() {
44
+ return await this.#companionLink.getUserAccounts();
45
+ }
46
+ /**
47
+ * Switches to a different user account.
48
+ *
49
+ * @param accountId - The account ID to switch to.
50
+ */
51
+ async switch(accountId) {
52
+ await this.#companionLink.switchUserAccount(accountId);
53
+ }
54
+ };
55
+
56
+ //#endregion
57
+ //#region src/controller/apps.ts
58
+ /**
59
+ * App management controller for Apple TV devices.
60
+ * Provides app launching and URL opening.
61
+ */
62
+ var AppsController = class {
63
+ #companionLink;
64
+ constructor(companionLink) {
65
+ this.#companionLink = companionLink;
66
+ }
67
+ /**
68
+ * Returns the list of apps that can be launched on the device.
69
+ */
70
+ async list() {
71
+ return await this.#companionLink.getLaunchableApps();
72
+ }
73
+ /**
74
+ * Launches an app by its bundle identifier.
75
+ *
76
+ * @param bundleId - The bundle identifier (e.g. 'com.netflix.Netflix').
77
+ */
78
+ async launch(bundleId) {
79
+ await this.#companionLink.launchApp(bundleId);
80
+ }
81
+ /**
82
+ * Opens a URL on the device (universal link or app-specific URL scheme).
83
+ *
84
+ * @param url - The URL to open.
85
+ */
86
+ async openUrl(url) {
87
+ await this.#companionLink.launchUrl(url);
88
+ }
89
+ };
90
+
91
+ //#endregion
92
+ //#region src/controller/artwork.ts
93
+ /**
94
+ * Artwork controller for Apple devices.
95
+ * Resolves artwork from all available sources with a 4-tier fallback.
96
+ */
97
+ var ArtworkController = class {
98
+ #airplay;
99
+ constructor(airplay) {
100
+ this.#airplay = airplay;
101
+ }
102
+ /**
103
+ * Gets the current artwork for the active now-playing item.
104
+ *
105
+ * @param width - Desired width in pixels (default: 600).
106
+ * @param height - Desired height in pixels (-1 for proportional).
107
+ * @returns Artwork result with url, data, or both.
108
+ */
109
+ async get(width = 600, height = -1) {
110
+ return await this.#airplay.artwork.get(width, height);
111
+ }
112
+ };
113
+
114
+ //#endregion
115
+ //#region src/controller/keyboard.ts
116
+ /**
117
+ * Text input controller for Apple TV devices.
118
+ * Provides keyboard control when a text field is active on the device.
119
+ */
120
+ var KeyboardController = class {
121
+ #airplay;
122
+ constructor(airplay) {
123
+ this.#airplay = airplay;
124
+ }
125
+ /**
126
+ * Sets the text input field to the given text, replacing any existing content.
127
+ */
128
+ async type(text) {
129
+ await this.#airplay.remote.textSet(text);
130
+ }
131
+ /**
132
+ * Appends text to the current text input field content.
133
+ */
134
+ async append(text) {
135
+ await this.#airplay.remote.textAppend(text);
136
+ }
137
+ /**
138
+ * Clears the text input field.
139
+ */
140
+ async clear() {
141
+ await this.#airplay.remote.textClear();
142
+ }
143
+ /**
144
+ * Fetches the current keyboard session state.
145
+ */
146
+ async getSession() {
147
+ return await this.#airplay.remote.getKeyboardSession();
148
+ }
149
+ };
150
+
151
+ //#endregion
152
+ //#region src/controller/media.ts
153
+ /**
154
+ * Media source controller for Apple devices.
155
+ * Provides URL playback (device fetches and plays) and audio streaming (client sends PCM via RTP).
156
+ */
157
+ var MediaController = class {
158
+ #airplay;
159
+ constructor(airplay) {
160
+ this.#airplay = airplay;
161
+ }
162
+ /**
163
+ * Plays a URL on the device. The device fetches and plays the content.
164
+ * Creates a separate protocol session to avoid conflicting with remote control.
165
+ *
166
+ * @param url - The media URL to play.
167
+ * @param position - Start position in seconds (default: 0).
168
+ */
169
+ async playUrl(url, position = 0) {
170
+ await this.#airplay.playUrl(url, position);
171
+ }
172
+ /**
173
+ * Stops the current URL playback.
174
+ */
175
+ stopPlayUrl() {
176
+ this.#airplay.stopPlayUrl();
177
+ }
178
+ /**
179
+ * Waits for the current URL playback to end naturally.
180
+ */
181
+ async waitForPlaybackEnd() {
182
+ await this.#airplay.waitForPlaybackEnd();
183
+ }
184
+ /**
185
+ * Streams audio from a source to the device via RAOP/RTP.
186
+ * Creates a separate protocol session to avoid conflicting with remote control.
187
+ *
188
+ * @param source - The audio source to stream (MP3, OGG, WAV, PCM, FFmpeg, URL, live).
189
+ */
190
+ async streamAudio(source) {
191
+ await this.#airplay.streamAudio(source);
192
+ }
193
+ /**
194
+ * Stops the current audio stream.
195
+ */
196
+ stopStreamAudio() {
197
+ this.#airplay.stopStreamAudio();
198
+ }
199
+ /**
200
+ * Requests lyrics for the current playback.
201
+ *
202
+ * @param length - Maximum number of lyrics items to retrieve.
203
+ */
204
+ async requestLyrics(length = 10) {
205
+ await this.#airplay.requestPlaybackQueue(length);
206
+ }
207
+ };
208
+
209
+ //#endregion
210
+ //#region src/controller/multiroom.ts
211
+ /**
212
+ * Multi-room / cluster controller for Apple devices.
213
+ * Manages output device groups for multi-room audio.
214
+ */
215
+ var MultiroomController = class {
216
+ #airplay;
217
+ constructor(airplay) {
218
+ this.#airplay = airplay;
219
+ }
220
+ /**
221
+ * The cluster ID if the device is part of a multi-room group, or null.
222
+ */
223
+ get clusterId() {
224
+ return this.#airplay.state.clusterID;
225
+ }
226
+ /**
227
+ * Whether this device is the leader of its multi-room cluster.
228
+ */
229
+ get isLeader() {
230
+ return this.#airplay.state.isClusterLeader;
231
+ }
232
+ /**
233
+ * Whether this device is aware of cluster functionality.
234
+ */
235
+ get isClusterAware() {
236
+ return this.#airplay.state.isClusterAware;
237
+ }
238
+ /**
239
+ * Adds devices to the current multi-room output context.
240
+ *
241
+ * @param deviceUIDs - UIDs of devices to add.
242
+ */
243
+ async addDevice(...deviceUIDs) {
244
+ await this.#airplay.addOutputDevices(deviceUIDs);
245
+ }
246
+ /**
247
+ * Removes devices from the current multi-room output context.
248
+ *
249
+ * @param deviceUIDs - UIDs of devices to remove.
250
+ */
251
+ async removeDevice(...deviceUIDs) {
252
+ await this.#airplay.removeOutputDevices(deviceUIDs);
253
+ }
254
+ /**
255
+ * Replaces the entire multi-room output context.
256
+ *
257
+ * @param deviceUIDs - UIDs of devices to set as the output context.
258
+ */
259
+ async setDevices(...deviceUIDs) {
260
+ await this.#airplay.setOutputDevices(deviceUIDs);
261
+ }
262
+ };
263
+
264
+ //#endregion
265
+ //#region src/controller/playback.ts
266
+ /**
267
+ * Media playback controller for Apple devices.
268
+ * Provides play/pause/next/seek and other media commands via SendCommand (MRP protocol).
269
+ */
270
+ var PlaybackController = class {
271
+ #airplay;
272
+ constructor(airplay) {
273
+ this.#airplay = airplay;
274
+ }
275
+ async play() {
276
+ await this.#airplay.remote.commandPlay();
277
+ }
278
+ async pause() {
279
+ await this.#airplay.remote.commandPause();
280
+ }
281
+ async playPause() {
282
+ await this.#airplay.remote.commandTogglePlayPause();
283
+ }
284
+ async stop() {
285
+ await this.#airplay.remote.commandStop();
286
+ }
287
+ async next() {
288
+ await this.#airplay.remote.commandNextTrack();
289
+ }
290
+ async previous() {
291
+ await this.#airplay.remote.commandPreviousTrack();
292
+ }
293
+ async skipForward(seconds = 15) {
294
+ await this.#airplay.remote.commandSkipForward(seconds);
295
+ }
296
+ async skipBackward(seconds = 15) {
297
+ await this.#airplay.remote.commandSkipBackward(seconds);
298
+ }
299
+ async seekTo(position) {
300
+ await this.#airplay.remote.commandSeekToPosition(position);
301
+ }
302
+ async setShuffleMode(mode) {
303
+ await this.#airplay.remote.commandSetShuffleMode(mode);
304
+ }
305
+ async setRepeatMode(mode) {
306
+ await this.#airplay.remote.commandSetRepeatMode(mode);
307
+ }
308
+ async advanceShuffleMode() {
309
+ await this.#airplay.remote.commandAdvanceShuffleMode();
310
+ }
311
+ async advanceRepeatMode() {
312
+ await this.#airplay.remote.commandAdvanceRepeatMode();
313
+ }
314
+ async setPlaybackRate(rate) {
315
+ await this.#airplay.remote.commandChangePlaybackRate(rate);
316
+ }
317
+ async setSleepTimer(seconds, stopMode = 0) {
318
+ await this.#airplay.remote.commandSetSleepTimer(seconds, stopMode);
319
+ }
320
+ async beginFastForward() {
321
+ await this.#airplay.remote.commandBeginFastForward();
322
+ }
323
+ async endFastForward() {
324
+ await this.#airplay.remote.commandEndFastForward();
325
+ }
326
+ async beginRewind() {
327
+ await this.#airplay.remote.commandBeginRewind();
328
+ }
329
+ async endRewind() {
330
+ await this.#airplay.remote.commandEndRewind();
331
+ }
332
+ async nextChapter() {
333
+ await this.#airplay.remote.commandNextChapter();
334
+ }
335
+ async previousChapter() {
336
+ await this.#airplay.remote.commandPreviousChapter();
337
+ }
338
+ async likeTrack() {
339
+ await this.#airplay.remote.commandLikeTrack();
340
+ }
341
+ async dislikeTrack() {
342
+ await this.#airplay.remote.commandDislikeTrack();
343
+ }
344
+ async bookmarkTrack() {
345
+ await this.#airplay.remote.commandBookmarkTrack();
346
+ }
347
+ async addToLibrary() {
348
+ await this.#airplay.remote.commandAddNowPlayingItemToLibrary();
349
+ }
350
+ /**
351
+ * Requests the playback queue from the device (includes artwork and metadata).
352
+ *
353
+ * @param length - Maximum number of queue items to retrieve (default: 1).
354
+ */
355
+ async requestPlaybackQueue(length = 1) {
356
+ await this.#airplay.requestPlaybackQueue(length);
357
+ }
358
+ /**
359
+ * Checks whether a playback command is currently supported by the active media app.
360
+ */
361
+ isCommandSupported(command) {
362
+ return this.#airplay.state.nowPlayingClient?.isCommandSupported(command) ?? false;
363
+ }
364
+ };
365
+
366
+ //#endregion
367
+ //#region src/controller/power.ts
368
+ /**
369
+ * Power management controller for Apple TV devices.
370
+ * Provides power on/off and attention state queries.
371
+ */
372
+ var PowerController = class {
373
+ #airplay;
374
+ #companionLink;
375
+ constructor(airplay, companionLink) {
376
+ this.#airplay = airplay;
377
+ this.#companionLink = companionLink;
378
+ }
379
+ /**
380
+ * Turns on the device (sends wake HID key).
381
+ */
382
+ async on() {
383
+ await this.#airplay.remote.wake();
384
+ }
385
+ /**
386
+ * Turns off the device (sends suspend HID key).
387
+ */
388
+ async off() {
389
+ await this.#airplay.remote.suspend();
390
+ }
391
+ /**
392
+ * Gets the current attention state of the device.
393
+ *
394
+ * @returns The attention state ('active', 'idle', 'screensaver', etc.).
395
+ */
396
+ async getState() {
397
+ return await this.#companionLink.getAttentionState();
398
+ }
399
+ };
400
+
401
+ //#endregion
402
+ //#region src/controller/remote.ts
403
+ /**
404
+ * Remote controller for Apple devices.
405
+ * Provides all HID-based keys (navigation, media, volume, power),
406
+ * touch/swipe gestures, and low-level HID primitives.
407
+ */
408
+ var RemoteController = class {
409
+ #airplay;
410
+ constructor(airplay) {
411
+ this.#airplay = airplay;
412
+ }
413
+ async up() {
414
+ await this.#airplay.remote.up();
415
+ }
416
+ async down() {
417
+ await this.#airplay.remote.down();
418
+ }
419
+ async left() {
420
+ await this.#airplay.remote.left();
421
+ }
422
+ async right() {
423
+ await this.#airplay.remote.right();
424
+ }
425
+ async select() {
426
+ await this.#airplay.remote.select();
427
+ }
428
+ async menu() {
429
+ await this.#airplay.remote.menu();
430
+ }
431
+ async home() {
432
+ await this.#airplay.remote.home();
433
+ }
434
+ async topMenu() {
435
+ await this.#airplay.remote.topMenu();
436
+ }
437
+ async play() {
438
+ await this.#airplay.remote.play();
439
+ }
440
+ async pause() {
441
+ await this.#airplay.remote.pause();
442
+ }
443
+ async playPause() {
444
+ await this.#airplay.remote.playPause();
445
+ }
446
+ async stop() {
447
+ await this.#airplay.remote.stop();
448
+ }
449
+ async next() {
450
+ await this.#airplay.remote.next();
451
+ }
452
+ async previous() {
453
+ await this.#airplay.remote.previous();
454
+ }
455
+ async channelUp() {
456
+ await this.#airplay.remote.channelUp();
457
+ }
458
+ async channelDown() {
459
+ await this.#airplay.remote.channelDown();
460
+ }
461
+ async volumeUp() {
462
+ await this.#airplay.remote.volumeUp();
463
+ }
464
+ async volumeDown() {
465
+ await this.#airplay.remote.volumeDown();
466
+ }
467
+ async mute() {
468
+ await this.#airplay.remote.mute();
469
+ }
470
+ async wake() {
471
+ await this.#airplay.remote.wake();
472
+ }
473
+ async suspend() {
474
+ await this.#airplay.remote.suspend();
475
+ }
476
+ async tap(x, y, finger = 1) {
477
+ await this.#airplay.remote.tap(x, y, finger);
478
+ }
479
+ async swipe(direction, duration = 200) {
480
+ switch (direction) {
481
+ case "up":
482
+ await this.#airplay.remote.swipeUp(duration);
483
+ break;
484
+ case "down":
485
+ await this.#airplay.remote.swipeDown(duration);
486
+ break;
487
+ case "left":
488
+ await this.#airplay.remote.swipeLeft(duration);
489
+ break;
490
+ case "right":
491
+ await this.#airplay.remote.swipeRight(duration);
492
+ break;
493
+ }
494
+ }
495
+ async pressAndRelease(usePage, usage) {
496
+ await this.#airplay.remote.pressAndRelease(usePage, usage);
497
+ }
498
+ async longPress(usePage, usage, duration = 1e3) {
499
+ await this.#airplay.remote.longPress(usePage, usage, duration);
500
+ }
501
+ async doublePress(usePage, usage) {
502
+ await this.#airplay.remote.doublePress(usePage, usage);
503
+ }
504
+ };
505
+
506
+ //#endregion
507
+ //#region src/controller/state.ts
508
+ /**
509
+ * Now-playing state controller.
510
+ * Provides read-only getters for the current playback state and emits
511
+ * typed events when state changes occur.
512
+ */
513
+ var StateController = class extends EventEmitter {
514
+ #airplay;
515
+ constructor(airplay) {
516
+ super();
517
+ this.#airplay = airplay;
518
+ }
519
+ get #state() {
520
+ return this.#airplay.state;
521
+ }
522
+ get title() {
523
+ return this.#state.nowPlayingClient?.title ?? "";
524
+ }
525
+ get artist() {
526
+ return this.#state.nowPlayingClient?.artist ?? "";
527
+ }
528
+ get album() {
529
+ return this.#state.nowPlayingClient?.album ?? "";
530
+ }
531
+ get genre() {
532
+ return this.#state.nowPlayingClient?.genre ?? "";
533
+ }
534
+ get duration() {
535
+ return this.#state.nowPlayingClient?.duration ?? 0;
536
+ }
537
+ get elapsedTime() {
538
+ return this.#state.nowPlayingClient?.elapsedTime ?? 0;
539
+ }
540
+ get playbackRate() {
541
+ return this.#state.nowPlayingClient?.playbackRate ?? 0;
542
+ }
543
+ get isPlaying() {
544
+ return this.#state.nowPlayingClient?.isPlaying ?? false;
545
+ }
546
+ get playbackState() {
547
+ return this.#state.nowPlayingClient?.playbackState;
548
+ }
549
+ get mediaType() {
550
+ return this.#state.nowPlayingClient?.mediaType;
551
+ }
552
+ get shuffleMode() {
553
+ return this.#state.nowPlayingClient?.shuffleMode;
554
+ }
555
+ get repeatMode() {
556
+ return this.#state.nowPlayingClient?.repeatMode;
557
+ }
558
+ get activeApp() {
559
+ const client = this.#state.nowPlayingClient;
560
+ if (!client) return null;
561
+ return {
562
+ bundleIdentifier: client.bundleIdentifier,
563
+ displayName: client.displayName
564
+ };
565
+ }
566
+ get volume() {
567
+ return this.#state.volume;
568
+ }
569
+ get isMuted() {
570
+ return this.#state.volumeMuted;
571
+ }
572
+ get volumeAvailable() {
573
+ return this.#state.volumeAvailable;
574
+ }
575
+ get isKeyboardActive() {
576
+ return this.#state.keyboardState !== void 0;
577
+ }
578
+ get clusterId() {
579
+ return this.#state.clusterID;
580
+ }
581
+ get isClusterLeader() {
582
+ return this.#state.isClusterLeader;
583
+ }
584
+ get outputDevices() {
585
+ return this.#state.outputDevices;
586
+ }
587
+ get clients() {
588
+ return this.#state.clients;
589
+ }
590
+ get activeClient() {
591
+ return this.#state.nowPlayingClient;
592
+ }
593
+ get activePlayer() {
594
+ return this.#state.nowPlayingClient?.activePlayer ?? null;
595
+ }
596
+ isCommandSupported(command) {
597
+ return this.#state.nowPlayingClient?.isCommandSupported(command) ?? false;
598
+ }
599
+ getCommandInfo(command) {
600
+ return this.#state.nowPlayingClient?.findCommand(command) ?? null;
601
+ }
602
+ /**
603
+ * Subscribes to the underlying AirPlayState events and re-emits them.
604
+ * Called internally by the device after connection is established.
605
+ * @internal
606
+ */
607
+ subscribe() {
608
+ const state = this.#state;
609
+ state.on("nowPlayingChanged", (client, player) => {
610
+ this.emit("nowPlayingChanged", client, player);
611
+ const app = client ? {
612
+ bundleIdentifier: client.bundleIdentifier,
613
+ displayName: client.displayName
614
+ } : null;
615
+ this.emit("activeAppChanged", app?.bundleIdentifier ?? null, app?.displayName ?? null);
616
+ });
617
+ state.on("playbackStateChanged", (client, player, oldState, newState) => {
618
+ this.emit("playbackStateChanged", client, player, oldState, newState);
619
+ });
620
+ state.on("volumeDidChange", (volume) => {
621
+ this.emit("volumeChanged", volume);
622
+ });
623
+ state.on("volumeMutedDidChange", (muted) => {
624
+ this.emit("volumeMutedChanged", muted);
625
+ });
626
+ state.on("artworkChanged", (client, player) => {
627
+ this.emit("artworkChanged", client, player);
628
+ });
629
+ state.on("supportedCommandsChanged", (client, player, commands) => {
630
+ this.emit("supportedCommandsChanged", client, player, commands);
631
+ });
632
+ state.on("clusterChanged", (clusterId, isLeader) => {
633
+ this.emit("clusterChanged", clusterId, isLeader);
634
+ });
635
+ }
636
+ /**
637
+ * Removes all event listeners from this controller.
638
+ * @internal
639
+ */
640
+ unsubscribe() {
641
+ this.removeAllListeners();
642
+ }
643
+ };
644
+
645
+ //#endregion
646
+ //#region src/controller/system.ts
647
+ /**
648
+ * System controller for Apple TV devices.
649
+ * Provides access to captions, appearance, Siri, and Up Next queue.
650
+ */
651
+ var SystemController = class {
652
+ #companionLink;
653
+ constructor(companionLink) {
654
+ this.#companionLink = companionLink;
655
+ }
656
+ /**
657
+ * Toggles closed captions on the device.
658
+ */
659
+ async toggleCaptions() {
660
+ await this.#companionLink.toggleCaptions();
661
+ }
662
+ /**
663
+ * Sets the system appearance.
664
+ *
665
+ * @param mode - 'light' or 'dark'.
666
+ */
667
+ async setAppearance(mode) {
668
+ await this.#companionLink.toggleSystemAppearance(mode === "light");
669
+ }
670
+ /**
671
+ * Enables or disables the "Reduce Loud Sounds" setting.
672
+ */
673
+ async setReduceLoudSounds(enabled) {
674
+ await this.#companionLink.toggleReduceLoudSounds(enabled);
675
+ }
676
+ /**
677
+ * Enables or disables Find My mode.
678
+ */
679
+ async setFindingMode(enabled) {
680
+ await this.#companionLink.toggleFindingMode(enabled);
681
+ }
682
+ /**
683
+ * Starts a Siri session.
684
+ */
685
+ async siriStart() {
686
+ await this.#companionLink.siriStart();
687
+ }
688
+ /**
689
+ * Stops the active Siri session.
690
+ */
691
+ async siriStop() {
692
+ await this.#companionLink.siriStop();
693
+ }
694
+ /**
695
+ * Fetches the Up Next queue.
696
+ *
697
+ * @param paginationToken - Optional token for paginated results.
698
+ */
699
+ async fetchUpNext(paginationToken) {
700
+ return await this.#companionLink.fetchUpNext(paginationToken);
701
+ }
702
+ /**
703
+ * Adds an item to the Up Next queue.
704
+ */
705
+ async addToUpNext(identifier, kind) {
706
+ await this.#companionLink.addToUpNext(identifier, kind);
707
+ }
708
+ /**
709
+ * Removes an item from the Up Next queue.
710
+ */
711
+ async removeFromUpNext(identifier, kind) {
712
+ await this.#companionLink.removeFromUpNext(identifier, kind);
713
+ }
714
+ };
715
+
716
+ //#endregion
717
+ //#region src/controller/volume.ts
718
+ /**
719
+ * Volume controller for Apple devices.
720
+ * Supports absolute volume (set level), relative volume (up/down),
721
+ * muting, fading, and per-device volume in multi-room setups.
722
+ */
723
+ var VolumeController = class {
724
+ #airplay;
725
+ constructor(airplay) {
726
+ this.#airplay = airplay;
727
+ }
728
+ async set(volume) {
729
+ await this.#airplay.volume.set(volume);
730
+ }
731
+ async get() {
732
+ return await this.#airplay.volume.get();
733
+ }
734
+ async up() {
735
+ await this.#airplay.volume.up();
736
+ }
737
+ async down() {
738
+ await this.#airplay.volume.down();
739
+ }
740
+ async mute() {
741
+ await this.#airplay.volume.mute();
742
+ }
743
+ async unmute() {
744
+ await this.#airplay.volume.unmute();
745
+ }
746
+ async toggleMute() {
747
+ await this.#airplay.volume.toggleMute();
748
+ }
749
+ async fade(targetVolume, durationMs) {
750
+ await this.#airplay.volume.fade(targetVolume, durationMs);
751
+ }
752
+ async setForDevice(outputDeviceUID, volume) {
753
+ await this.#airplay.volume.setForDevice(outputDeviceUID, volume);
754
+ }
755
+ async getForDevice(outputDeviceUID) {
756
+ return await this.#airplay.volume.getForDevice(outputDeviceUID);
757
+ }
758
+ async muteDevice(outputDeviceUID) {
759
+ await this.#airplay.volume.muteDevice(outputDeviceUID);
760
+ }
761
+ async unmuteDevice(outputDeviceUID) {
762
+ await this.#airplay.volume.unmuteDevice(outputDeviceUID);
763
+ }
764
+ async adjust(adjustment, outputDeviceUID) {
765
+ await this.#airplay.volume.adjust(adjustment, outputDeviceUID);
766
+ }
767
+ };
768
+
769
+ //#endregion
770
+ //#region src/internal/const.ts
771
+ /**
772
+ * Interval in milliseconds between periodic feedback requests to keep the AirPlay session alive.
773
+ */
774
+ const FEEDBACK_INTERVAL = 2e3;
775
+ /**
776
+ * Symbol used to access the underlying AirPlay Protocol instance from an AirPlayDevice.
777
+ */
778
+ const PROTOCOL = Symbol("com.basmilius.airplay:protocol");
779
+ /**
780
+ * Symbol used to subscribe AirPlayState to DataStream events.
781
+ */
782
+ const STATE_SUBSCRIBE_SYMBOL = Symbol("com.basmilius.airplay:subscribe");
783
+ /**
784
+ * Symbol used to unsubscribe AirPlayState from DataStream events.
785
+ */
786
+ const STATE_UNSUBSCRIBE_SYMBOL = Symbol("com.basmilius.airplay:unsubscribe");
787
+ /**
788
+ * Symbol used to access the underlying Companion Link Protocol instance from a CompanionLinkManager.
789
+ */
790
+ const COMPANION_LINK_PROTOCOL = Symbol("com.basmilius.companion-link:protocol");
791
+
792
+ //#endregion
793
+ //#region src/internal/airplay-artwork.ts
794
+ /**
795
+ * Unified artwork controller for an AirPlay device.
796
+ *
797
+ * Provides a single `get()` method that resolves artwork from all available
798
+ * sources in priority order:
799
+ * 1. URL from now-playing metadata (artworkURL, remoteArtworks, template)
800
+ * 2. Inline binary data from the playback queue (artworkData, dataArtworks)
801
+ * 3. JPEG data from SET_ARTWORK_MESSAGE
802
+ * 4. Fetches the playback queue if artwork is expected but not yet available
803
+ */
804
+ var AirPlayArtwork = class {
805
+ get #protocol() {
806
+ return this.#device[PROTOCOL];
807
+ }
808
+ get #state() {
809
+ return this.#device.state;
810
+ }
811
+ #device;
812
+ #lastIdentifier = null;
813
+ #cached = null;
814
+ constructor(device) {
815
+ this.#device = device;
816
+ }
817
+ /**
818
+ * Gets the current artwork for the active now-playing item.
819
+ *
820
+ * Tries all available sources in priority order and returns a unified result.
821
+ * Results are cached by artwork identifier — subsequent calls for the same
822
+ * track return the cached result without additional requests.
823
+ *
824
+ * @param width - Desired artwork width in pixels (default: 600).
825
+ * @param height - Desired artwork height in pixels (-1 for proportional).
826
+ * @returns The artwork result, or null if no artwork is available.
827
+ */
828
+ async get(width = 600, height = -1) {
829
+ const player = this.#state.nowPlayingClient?.activePlayer;
830
+ if (!player) return null;
831
+ const identifier = player.artworkId;
832
+ if (identifier && identifier === this.#lastIdentifier && this.#cached) return this.#cached;
833
+ const url = player.artworkUrl(width, height);
834
+ if (url) return this.#cache(identifier, {
835
+ url,
836
+ data: null,
837
+ mimeType: guessMimeType(url),
838
+ identifier,
839
+ width,
840
+ height: height < 0 ? 0 : height
841
+ });
842
+ const inlineData = player.currentItemArtwork;
843
+ if (inlineData && inlineData.byteLength > 0) {
844
+ const metadata = player.currentItemMetadata;
845
+ return this.#cache(identifier, {
846
+ url: null,
847
+ data: inlineData,
848
+ mimeType: metadata?.artworkMIMEType || "image/jpeg",
849
+ identifier,
850
+ width: 0,
851
+ height: 0
852
+ });
853
+ }
854
+ const setArtworkData = this.#state.artworkJpegData;
855
+ if (setArtworkData && setArtworkData.byteLength > 0) return this.#cache(identifier, {
856
+ url: null,
857
+ data: setArtworkData,
858
+ mimeType: "image/jpeg",
859
+ identifier,
860
+ width: 0,
861
+ height: 0
862
+ });
863
+ if (identifier) try {
864
+ const location = player.playbackQueue?.location ?? 0;
865
+ await this.#protocol.dataStream.exchange(DataStreamMessage.playbackQueueRequest(location, 1, width, height < 0 ? 400 : height));
866
+ const fetchedData = player.currentItemArtwork;
867
+ if (fetchedData && fetchedData.byteLength > 0) {
868
+ const metadata = player.currentItemMetadata;
869
+ return this.#cache(identifier, {
870
+ url: null,
871
+ data: fetchedData,
872
+ mimeType: metadata?.artworkMIMEType || "image/jpeg",
873
+ identifier,
874
+ width: 0,
875
+ height: 0
876
+ });
877
+ }
878
+ const retryUrl = player.artworkUrl(width, height);
879
+ if (retryUrl) return this.#cache(identifier, {
880
+ url: retryUrl,
881
+ data: null,
882
+ mimeType: guessMimeType(retryUrl),
883
+ identifier,
884
+ width,
885
+ height: height < 0 ? 0 : height
886
+ });
887
+ } catch {}
888
+ this.#lastIdentifier = null;
889
+ this.#cached = null;
890
+ return null;
891
+ }
892
+ /**
893
+ * Clears the cached artwork, forcing a fresh fetch on the next `get()` call.
894
+ */
895
+ clear() {
896
+ this.#lastIdentifier = null;
897
+ this.#cached = null;
898
+ }
899
+ #cache(identifier, result) {
900
+ this.#lastIdentifier = identifier;
901
+ this.#cached = result;
902
+ return result;
903
+ }
904
+ };
905
+ const guessMimeType = (url) => {
906
+ if (url.includes(".png")) return "image/png";
907
+ if (url.includes(".webp")) return "image/webp";
908
+ return "image/jpeg";
909
+ };
910
+
911
+ //#endregion
912
+ //#region src/internal/airplay-player.ts
913
+ /**
914
+ * Offset in seconds between the Cocoa epoch (2001-01-01) and the Unix epoch (1970-01-01).
915
+ */
916
+ const COCOA_EPOCH_OFFSET = 978307200;
917
+ /**
918
+ * Default player identifier used by the Apple TV when no specific player is active.
919
+ */
920
+ const DEFAULT_PLAYER_ID = "MediaRemote-DefaultPlayer";
921
+ /**
922
+ * Converts an artwork URL to a browser-compatible format.
923
+ * Apple's CDN serves HEIC by default, which most browsers can't display.
924
+ * Replacing the extension with .jpg makes the CDN return JPEG instead.
925
+ */
926
+ const convertArtworkUrl = (url) => {
927
+ return url.replace(/\.heic(\b|$)/gi, ".jpg");
928
+ };
929
+ /**
930
+ * Extrapolates the current elapsed time based on a snapshot timestamp and playback rate.
931
+ * Compensates for the time passed since the timestamp was recorded, scaled by the playback rate.
932
+ *
933
+ * @param elapsed - The elapsed time at the moment of the snapshot, in seconds.
934
+ * @param cocoaTimestamp - The Cocoa epoch timestamp when the snapshot was taken.
935
+ * @param rate - The playback rate (e.g. 1.0 for normal speed, 0 for paused).
936
+ * @returns The extrapolated elapsed time in seconds, clamped to a minimum of 0.
937
+ */
938
+ const extrapolateElapsed = (elapsed, cocoaTimestamp, rate) => {
939
+ if (!rate) return elapsed;
940
+ const timestampUnix = cocoaTimestamp + COCOA_EPOCH_OFFSET;
941
+ const delta = (Date.now() / 1e3 - timestampUnix) * rate;
942
+ return Math.max(0, elapsed + delta);
943
+ };
944
+ /**
945
+ * Represents a single media player within an app on the Apple TV.
946
+ * Each app (Client) can have multiple players (e.g. picture-in-picture).
947
+ * Tracks now-playing metadata, playback state, and provides elapsed time extrapolation
948
+ * based on Cocoa timestamps and playback rate.
949
+ */
950
+ var AirPlayPlayer = class {
951
+ /**
952
+ * Unique identifier for this player (e.g. a player path).
953
+ */
954
+ get identifier() {
955
+ return this.#identifier;
956
+ }
957
+ /**
958
+ * Human-readable display name for this player.
959
+ */
960
+ get displayName() {
961
+ return this.#displayName;
962
+ }
963
+ /**
964
+ * Whether this is the default fallback player (MediaRemote-DefaultPlayer).
965
+ */
966
+ get isDefaultPlayer() {
967
+ return this.#identifier === DEFAULT_PLAYER_ID;
968
+ }
969
+ /**
970
+ * Raw now-playing info from the Apple TV, or null if unavailable.
971
+ */
972
+ get nowPlayingInfo() {
973
+ return this.#nowPlayingInfo;
974
+ }
975
+ /**
976
+ * Current playback queue, or null if unavailable.
977
+ */
978
+ get playbackQueue() {
979
+ return this.#playbackQueue;
980
+ }
981
+ /**
982
+ * Effective playback state.
983
+ */
984
+ get playbackState() {
985
+ return this.#playbackState;
986
+ }
987
+ /**
988
+ * Timestamp of the last playback state update, used to discard stale updates.
989
+ */
990
+ get playbackStateTimestamp() {
991
+ return this.#playbackStateTimestamp;
992
+ }
993
+ /**
994
+ * List of commands supported by this player.
995
+ */
996
+ get supportedCommands() {
997
+ return this.#supportedCommands;
998
+ }
999
+ /**
1000
+ * Current track title from NowPlayingInfo or content item metadata.
1001
+ */
1002
+ get title() {
1003
+ return this.#nowPlayingInfo?.title || this.currentItemMetadata?.title || "";
1004
+ }
1005
+ /**
1006
+ * Current track artist from NowPlayingInfo or content item metadata.
1007
+ */
1008
+ get artist() {
1009
+ return this.#nowPlayingInfo?.artist || this.currentItemMetadata?.trackArtistName || "";
1010
+ }
1011
+ /**
1012
+ * Current track album from NowPlayingInfo or content item metadata.
1013
+ */
1014
+ get album() {
1015
+ return this.#nowPlayingInfo?.album || this.currentItemMetadata?.albumName || "";
1016
+ }
1017
+ /**
1018
+ * Genre of the current content item.
1019
+ */
1020
+ get genre() {
1021
+ return this.currentItemMetadata?.genre || "";
1022
+ }
1023
+ /**
1024
+ * Series name for TV show content.
1025
+ */
1026
+ get seriesName() {
1027
+ return this.currentItemMetadata?.seriesName || "";
1028
+ }
1029
+ /**
1030
+ * Season number for TV show content, or 0 if not applicable.
1031
+ */
1032
+ get seasonNumber() {
1033
+ return this.currentItemMetadata?.seasonNumber || 0;
1034
+ }
1035
+ /**
1036
+ * Episode number for TV show content, or 0 if not applicable.
1037
+ */
1038
+ get episodeNumber() {
1039
+ return this.currentItemMetadata?.episodeNumber || 0;
1040
+ }
1041
+ /**
1042
+ * Media type of the current content item (music, video, etc.).
1043
+ */
1044
+ get mediaType() {
1045
+ return this.currentItemMetadata?.mediaType ?? Proto$1.ContentItemMetadata_MediaType.UnknownMediaType;
1046
+ }
1047
+ /**
1048
+ * Unique content identifier for the current item (e.g. iTunes store ID).
1049
+ */
1050
+ get contentIdentifier() {
1051
+ return this.currentItemMetadata?.contentIdentifier || "";
1052
+ }
1053
+ /**
1054
+ * Duration of the current track in seconds, from NowPlayingInfo or metadata.
1055
+ */
1056
+ get duration() {
1057
+ return this.#nowPlayingInfo?.duration || this.currentItemMetadata?.duration || 0;
1058
+ }
1059
+ /**
1060
+ * Current playback rate (1.0 = normal, 0 = paused, 2.0 = double speed).
1061
+ */
1062
+ get playbackRate() {
1063
+ return this.#nowPlayingInfo?.playbackRate ?? this.currentItemMetadata?.playbackRate ?? 0;
1064
+ }
1065
+ /**
1066
+ * Whether the player is currently playing (based on effective playback state).
1067
+ */
1068
+ get isPlaying() {
1069
+ return this.playbackState === Proto$1.PlaybackState_Enum.Playing;
1070
+ }
1071
+ /**
1072
+ * Current shuffle mode, derived from the ChangeShuffleMode command info.
1073
+ */
1074
+ get shuffleMode() {
1075
+ return this.#supportedCommands.find((c) => c.command === Proto$1.Command.ChangeShuffleMode)?.shuffleMode ?? Proto$1.ShuffleMode_Enum.Unknown;
1076
+ }
1077
+ /**
1078
+ * Current repeat mode, derived from the ChangeRepeatMode command info.
1079
+ */
1080
+ get repeatMode() {
1081
+ return this.#supportedCommands.find((c) => c.command === Proto$1.Command.ChangeRepeatMode)?.repeatMode ?? Proto$1.RepeatMode_Enum.Unknown;
1082
+ }
1083
+ /**
1084
+ * Extrapolated elapsed time in seconds. Uses the most recent timestamp
1085
+ * from either NowPlayingInfo or content item metadata, accounting for
1086
+ * playback rate to provide a real-time estimate.
1087
+ */
1088
+ get elapsedTime() {
1089
+ const npi = this.#nowPlayingInfo;
1090
+ const meta = this.currentItemMetadata;
1091
+ const npiValid = npi?.elapsedTime != null && npi.timestamp != null && npi.timestamp !== 0;
1092
+ const metaValid = meta?.elapsedTime != null && meta.elapsedTimeTimestamp != null && meta.elapsedTimeTimestamp !== 0;
1093
+ if (npiValid && metaValid) {
1094
+ if (meta.elapsedTimeTimestamp > npi.timestamp) return extrapolateElapsed(meta.elapsedTime, meta.elapsedTimeTimestamp, meta.playbackRate);
1095
+ return extrapolateElapsed(npi.elapsedTime, npi.timestamp, npi.playbackRate);
1096
+ }
1097
+ if (npiValid) return extrapolateElapsed(npi.elapsedTime, npi.timestamp, npi.playbackRate);
1098
+ if (metaValid) return extrapolateElapsed(meta.elapsedTime, meta.elapsedTimeTimestamp, meta.playbackRate);
1099
+ return npi?.elapsedTime || meta?.elapsedTime || 0;
1100
+ }
1101
+ /**
1102
+ * The currently playing content item from the playback queue, or null.
1103
+ */
1104
+ get currentItem() {
1105
+ if (!this.#playbackQueue || this.#playbackQueue.contentItems.length === 0) return null;
1106
+ return this.#playbackQueue.contentItems[this.#playbackQueue.location] ?? this.#playbackQueue.contentItems[0] ?? null;
1107
+ }
1108
+ /**
1109
+ * Metadata of the current content item, or null if no item is playing.
1110
+ */
1111
+ get currentItemMetadata() {
1112
+ return this.currentItem?.metadata ?? null;
1113
+ }
1114
+ /**
1115
+ * Unique identifier for the current artwork, used for change detection.
1116
+ * Returns null if no artwork evidence exists.
1117
+ */
1118
+ get artworkId() {
1119
+ const metadata = this.currentItemMetadata;
1120
+ if (!metadata) return null;
1121
+ if (!metadata.artworkAvailable && !metadata.artworkURL && !metadata.artworkIdentifier) return null;
1122
+ if (metadata.artworkIdentifier) return metadata.artworkIdentifier;
1123
+ if (metadata.contentIdentifier) return metadata.contentIdentifier;
1124
+ return this.currentItem?.identifier ?? null;
1125
+ }
1126
+ /**
1127
+ * Resolves the best available artwork URL for the current item.
1128
+ * Checks metadata artworkURL, remote artworks, and iTunes template URLs in order.
1129
+ *
1130
+ * @param width - Desired artwork width in pixels (used for template URLs).
1131
+ * @param height - Desired artwork height in pixels (-1 for automatic).
1132
+ * @returns The artwork URL, or null if no artwork URL is available.
1133
+ */
1134
+ artworkUrl(width = 600, height = -1) {
1135
+ const metadata = this.currentItemMetadata;
1136
+ if (metadata?.artworkURL) return convertArtworkUrl(metadata.artworkURL);
1137
+ const item = this.currentItem;
1138
+ if (item?.remoteArtworks.length > 0 && item.remoteArtworks[0].artworkURLString) return convertArtworkUrl(item.remoteArtworks[0].artworkURLString);
1139
+ if (metadata?.artworkIdentifier) try {
1140
+ const url = metadata.artworkIdentifier.replace("{w}", String(width < 1 ? 999999 : width)).replace("{h}", String(height < 1 ? 999999 : height)).replace("{c}", "bb").replace("{f}", "jpg");
1141
+ if (url.startsWith("http://") || url.startsWith("https://")) return url;
1142
+ } catch {}
1143
+ return null;
1144
+ }
1145
+ /**
1146
+ * Raw artwork data (image bytes) for the current item, or null if not embedded.
1147
+ */
1148
+ get currentItemArtwork() {
1149
+ const item = this.currentItem;
1150
+ if (!item) return null;
1151
+ if (item.artworkData?.byteLength > 0) return item.artworkData;
1152
+ if (item.dataArtworks.length > 0 && item.dataArtworks[0].imageData?.byteLength > 0) return item.dataArtworks[0].imageData;
1153
+ return null;
1154
+ }
1155
+ /**
1156
+ * Convenience getter for the artwork URL at default dimensions (600px).
1157
+ */
1158
+ get currentItemArtworkUrl() {
1159
+ return this.artworkUrl();
1160
+ }
1161
+ /**
1162
+ * Lyrics for the current content item, or null if unavailable.
1163
+ */
1164
+ get currentItemLyrics() {
1165
+ return this.currentItem?.lyrics ?? null;
1166
+ }
1167
+ #identifier;
1168
+ #displayName;
1169
+ #nowPlayingInfo = null;
1170
+ #playbackQueue = null;
1171
+ #playbackState;
1172
+ #playbackStateTimestamp = 0;
1173
+ #supportedCommands = [];
1174
+ /**
1175
+ * Creates a new Player instance.
1176
+ *
1177
+ * @param identifier - Unique player identifier.
1178
+ * @param displayName - Human-readable display name.
1179
+ */
1180
+ constructor(identifier, displayName) {
1181
+ this.#identifier = identifier;
1182
+ this.#displayName = displayName;
1183
+ this.#playbackState = Proto$1.PlaybackState_Enum.Unknown;
1184
+ }
1185
+ /**
1186
+ * Finds a command by its command type in the supported commands list.
1187
+ *
1188
+ * @param command - The command to look up.
1189
+ * @returns The command info, or null if not found.
1190
+ */
1191
+ findCommand(command) {
1192
+ return this.#supportedCommands.find((c) => c.command === command) ?? null;
1193
+ }
1194
+ /**
1195
+ * Checks whether a command is supported and enabled.
1196
+ *
1197
+ * @param command - The command to check.
1198
+ * @returns True if the command is in the supported list and enabled.
1199
+ */
1200
+ isCommandSupported(command) {
1201
+ const info = this.findCommand(command);
1202
+ return info != null && info.enabled !== false;
1203
+ }
1204
+ /**
1205
+ * Updates the now-playing info for this player.
1206
+ *
1207
+ * @param nowPlayingInfo - The new now-playing info from the Apple TV.
1208
+ */
1209
+ setNowPlayingInfo(nowPlayingInfo) {
1210
+ this.#nowPlayingInfo = nowPlayingInfo;
1211
+ }
1212
+ /**
1213
+ * Updates the playback queue for this player.
1214
+ *
1215
+ * @param playbackQueue - The new playback queue from the Apple TV.
1216
+ */
1217
+ setPlaybackQueue(playbackQueue) {
1218
+ this.#playbackQueue = playbackQueue;
1219
+ }
1220
+ /**
1221
+ * Updates the playback state. Ignores updates with a timestamp older than the current one
1222
+ * to prevent stale state from overwriting newer data.
1223
+ *
1224
+ * @param playbackState - The new playback state.
1225
+ * @param playbackStateTimestamp - Timestamp of this state update.
1226
+ */
1227
+ setPlaybackState(playbackState, playbackStateTimestamp) {
1228
+ if (playbackStateTimestamp < this.#playbackStateTimestamp) return;
1229
+ this.#playbackState = playbackState;
1230
+ this.#playbackStateTimestamp = playbackStateTimestamp;
1231
+ }
1232
+ /**
1233
+ * Replaces the list of supported commands for this player.
1234
+ *
1235
+ * @param supportedCommands - The new list of supported commands.
1236
+ */
1237
+ setSupportedCommands(supportedCommands) {
1238
+ this.#supportedCommands = supportedCommands;
1239
+ }
1240
+ /**
1241
+ * Merges updated content item data into the existing playback queue.
1242
+ * Updates metadata, artwork, lyrics, and info fields for the matching item.
1243
+ *
1244
+ * @param item - The content item with updated fields.
1245
+ */
1246
+ updateContentItem(item) {
1247
+ if (!this.#playbackQueue) return;
1248
+ const existing = this.#playbackQueue.contentItems.find((i) => i.identifier === item.identifier);
1249
+ if (!existing) return;
1250
+ if (item.metadata != null && existing.metadata != null) {
1251
+ for (const [key, value] of Object.entries(item.metadata)) if (value != null && !key.startsWith("$")) existing.metadata[key] = value;
1252
+ } else if (item.metadata != null) existing.metadata = item.metadata;
1253
+ if (item.artworkData != null) existing.artworkData = item.artworkData;
1254
+ if (item.lyrics != null) existing.lyrics = item.lyrics;
1255
+ if (item.info != null) existing.info = item.info;
1256
+ }
1257
+ };
1258
+
1259
+ //#endregion
1260
+ //#region src/internal/airplay-client.ts
1261
+ /**
1262
+ * Represents a now-playing app (client) on the Apple TV.
1263
+ * Each client is identified by its bundle identifier and can contain multiple players.
1264
+ * Proxies now-playing getters to the active player, merging player-specific and
1265
+ * default supported commands.
1266
+ */
1267
+ var AirPlayClient = class {
1268
+ /**
1269
+ * Bundle identifier of the app (e.g. "com.apple.TVMusic").
1270
+ */
1271
+ get bundleIdentifier() {
1272
+ return this.#bundleIdentifier;
1273
+ }
1274
+ /**
1275
+ * Human-readable display name of the app.
1276
+ */
1277
+ get displayName() {
1278
+ return this.#displayName;
1279
+ }
1280
+ /**
1281
+ * Map of all known players for this client, keyed by player identifier.
1282
+ */
1283
+ get players() {
1284
+ return this.#players;
1285
+ }
1286
+ /**
1287
+ * The currently active player, or null if none is active. Falls back to the default player.
1288
+ */
1289
+ get activePlayer() {
1290
+ return this.#players.get(this.#activePlayerId ?? "MediaRemote-DefaultPlayer") ?? null;
1291
+ }
1292
+ /**
1293
+ * Now-playing info from the active player, or null.
1294
+ */
1295
+ get nowPlayingInfo() {
1296
+ return this.activePlayer?.nowPlayingInfo ?? null;
1297
+ }
1298
+ /**
1299
+ * Playback queue from the active player, or null.
1300
+ */
1301
+ get playbackQueue() {
1302
+ return this.activePlayer?.playbackQueue ?? null;
1303
+ }
1304
+ /**
1305
+ * Playback state from the active player, or Unknown.
1306
+ */
1307
+ get playbackState() {
1308
+ return this.activePlayer?.playbackState ?? Proto$1.PlaybackState_Enum.Unknown;
1309
+ }
1310
+ /**
1311
+ * Timestamp of the last playback state update from the active player.
1312
+ */
1313
+ get playbackStateTimestamp() {
1314
+ return this.activePlayer?.playbackStateTimestamp ?? -1;
1315
+ }
1316
+ /**
1317
+ * Merged list of supported commands from the active player and client defaults.
1318
+ * Player commands take precedence; default commands are appended if not already present.
1319
+ */
1320
+ get supportedCommands() {
1321
+ const playerCommands = this.activePlayer?.supportedCommands ?? [];
1322
+ if (playerCommands.length === 0) return this.#defaultSupportedCommands;
1323
+ if (this.#defaultSupportedCommands.length === 0) return playerCommands;
1324
+ const playerCommandSet = new Set(playerCommands.map((c) => c.command));
1325
+ const merged = [...playerCommands];
1326
+ for (const cmd of this.#defaultSupportedCommands) if (!playerCommandSet.has(cmd.command)) merged.push(cmd);
1327
+ return merged;
1328
+ }
1329
+ /**
1330
+ * Current track title from the active player.
1331
+ */
1332
+ get title() {
1333
+ return this.activePlayer?.title ?? "";
1334
+ }
1335
+ /**
1336
+ * Current track artist from the active player.
1337
+ */
1338
+ get artist() {
1339
+ return this.activePlayer?.artist ?? "";
1340
+ }
1341
+ /**
1342
+ * Current track album from the active player.
1343
+ */
1344
+ get album() {
1345
+ return this.activePlayer?.album ?? "";
1346
+ }
1347
+ /**
1348
+ * Genre of the current content from the active player.
1349
+ */
1350
+ get genre() {
1351
+ return this.activePlayer?.genre ?? "";
1352
+ }
1353
+ /**
1354
+ * Series name for TV show content from the active player.
1355
+ */
1356
+ get seriesName() {
1357
+ return this.activePlayer?.seriesName ?? "";
1358
+ }
1359
+ /**
1360
+ * Season number for TV show content from the active player.
1361
+ */
1362
+ get seasonNumber() {
1363
+ return this.activePlayer?.seasonNumber ?? 0;
1364
+ }
1365
+ /**
1366
+ * Episode number for TV show content from the active player.
1367
+ */
1368
+ get episodeNumber() {
1369
+ return this.activePlayer?.episodeNumber ?? 0;
1370
+ }
1371
+ /**
1372
+ * Media type of the current content from the active player.
1373
+ */
1374
+ get mediaType() {
1375
+ return this.activePlayer?.mediaType ?? Proto$1.ContentItemMetadata_MediaType.UnknownMediaType;
1376
+ }
1377
+ /**
1378
+ * Content identifier of the current item from the active player.
1379
+ */
1380
+ get contentIdentifier() {
1381
+ return this.activePlayer?.contentIdentifier ?? "";
1382
+ }
1383
+ /**
1384
+ * Duration of the current track in seconds from the active player.
1385
+ */
1386
+ get duration() {
1387
+ return this.activePlayer?.duration ?? 0;
1388
+ }
1389
+ /**
1390
+ * Playback rate from the active player (1.0 = normal, 0 = paused).
1391
+ */
1392
+ get playbackRate() {
1393
+ return this.activePlayer?.playbackRate ?? 0;
1394
+ }
1395
+ /**
1396
+ * Whether the active player is currently playing.
1397
+ */
1398
+ get isPlaying() {
1399
+ return this.activePlayer?.isPlaying ?? false;
1400
+ }
1401
+ /**
1402
+ * Current shuffle mode from the active player.
1403
+ */
1404
+ get shuffleMode() {
1405
+ return this.activePlayer?.shuffleMode ?? Proto$1.ShuffleMode_Enum.Unknown;
1406
+ }
1407
+ /**
1408
+ * Current repeat mode from the active player.
1409
+ */
1410
+ get repeatMode() {
1411
+ return this.activePlayer?.repeatMode ?? Proto$1.RepeatMode_Enum.Unknown;
1412
+ }
1413
+ /**
1414
+ * Extrapolated elapsed time in seconds from the active player.
1415
+ */
1416
+ get elapsedTime() {
1417
+ return this.activePlayer?.elapsedTime ?? 0;
1418
+ }
1419
+ /**
1420
+ * Artwork identifier for change detection from the active player.
1421
+ */
1422
+ get artworkId() {
1423
+ return this.activePlayer?.artworkId ?? null;
1424
+ }
1425
+ /**
1426
+ * Resolves the best available artwork URL from the active player.
1427
+ *
1428
+ * @param width - Desired artwork width in pixels.
1429
+ * @param height - Desired artwork height in pixels (-1 for automatic).
1430
+ * @returns The artwork URL, or null if unavailable.
1431
+ */
1432
+ artworkUrl(width = 600, height = -1) {
1433
+ return this.activePlayer?.artworkUrl(width, height) ?? null;
1434
+ }
1435
+ /**
1436
+ * Current content item from the active player's playback queue.
1437
+ */
1438
+ get currentItem() {
1439
+ return this.activePlayer?.currentItem ?? null;
1440
+ }
1441
+ /**
1442
+ * Metadata of the current content item from the active player.
1443
+ */
1444
+ get currentItemMetadata() {
1445
+ return this.activePlayer?.currentItemMetadata ?? null;
1446
+ }
1447
+ /**
1448
+ * Raw artwork data (image bytes) from the active player.
1449
+ */
1450
+ get currentItemArtwork() {
1451
+ return this.activePlayer?.currentItemArtwork ?? null;
1452
+ }
1453
+ /**
1454
+ * Artwork URL at default dimensions from the active player.
1455
+ */
1456
+ get currentItemArtworkUrl() {
1457
+ return this.activePlayer?.currentItemArtworkUrl ?? null;
1458
+ }
1459
+ /**
1460
+ * Lyrics for the current content item from the active player.
1461
+ */
1462
+ get currentItemLyrics() {
1463
+ return this.activePlayer?.currentItemLyrics ?? null;
1464
+ }
1465
+ #bundleIdentifier;
1466
+ #displayName;
1467
+ #players = /* @__PURE__ */ new Map();
1468
+ #activePlayerId = null;
1469
+ #defaultSupportedCommands = [];
1470
+ /**
1471
+ * Creates a new Client instance.
1472
+ *
1473
+ * @param bundleIdentifier - Bundle identifier of the app.
1474
+ * @param displayName - Human-readable app name.
1475
+ */
1476
+ constructor(bundleIdentifier, displayName) {
1477
+ this.#bundleIdentifier = bundleIdentifier;
1478
+ this.#displayName = displayName;
1479
+ }
1480
+ /**
1481
+ * Gets an existing player or creates a new one if it does not exist.
1482
+ *
1483
+ * @param identifier - Unique player identifier.
1484
+ * @param displayName - Human-readable player name (defaults to identifier).
1485
+ * @returns The existing or newly created Player.
1486
+ */
1487
+ getOrCreatePlayer(identifier, displayName) {
1488
+ let player = this.#players.get(identifier);
1489
+ if (!player) {
1490
+ player = new AirPlayPlayer(identifier, displayName ?? identifier);
1491
+ this.#players.set(identifier, player);
1492
+ }
1493
+ return player;
1494
+ }
1495
+ /**
1496
+ * Sets the active player by identifier.
1497
+ *
1498
+ * @param identifier - Identifier of the player to activate.
1499
+ */
1500
+ setActivePlayer(identifier) {
1501
+ this.#activePlayerId = identifier;
1502
+ }
1503
+ /**
1504
+ * Removes a player from this client. If the removed player was active,
1505
+ * the active player is reset to null (falling back to the default player).
1506
+ *
1507
+ * @param identifier - Identifier of the player to remove.
1508
+ */
1509
+ removePlayer(identifier) {
1510
+ this.#players.delete(identifier);
1511
+ if (this.#activePlayerId === identifier) this.#activePlayerId = null;
1512
+ }
1513
+ /**
1514
+ * Sets the default supported commands for this client. These are used as
1515
+ * fallback when the active player has no commands of its own.
1516
+ *
1517
+ * @param supportedCommands - The default command list.
1518
+ */
1519
+ setDefaultSupportedCommands(supportedCommands) {
1520
+ this.#defaultSupportedCommands = supportedCommands;
1521
+ }
1522
+ /**
1523
+ * Finds a command by type, checking the active player first,
1524
+ * then falling back to the default supported commands.
1525
+ *
1526
+ * @param command - The command to look up.
1527
+ * @returns The command info, or null if not found.
1528
+ */
1529
+ findCommand(command) {
1530
+ const playerCmd = this.activePlayer?.findCommand(command) ?? null;
1531
+ if (playerCmd) return playerCmd;
1532
+ return this.#defaultSupportedCommands.find((c) => c.command === command) ?? null;
1533
+ }
1534
+ /**
1535
+ * Checks whether a command is supported and enabled, checking both
1536
+ * the active player and default commands.
1537
+ *
1538
+ * @param command - The command to check.
1539
+ * @returns True if the command is supported and enabled.
1540
+ */
1541
+ isCommandSupported(command) {
1542
+ const info = this.findCommand(command);
1543
+ return info != null && info.enabled !== false;
1544
+ }
1545
+ /**
1546
+ * Updates the display name for this client.
1547
+ *
1548
+ * @param displayName - The new display name.
1549
+ */
1550
+ updateDisplayName(displayName) {
1551
+ this.#displayName = displayName;
1552
+ }
1553
+ };
1554
+
1555
+ //#endregion
1556
+ //#region src/internal/airplay-remote.ts
1557
+ /**
1558
+ * Error thrown when a SendCommand request fails on the Apple TV side.
1559
+ * Contains the specific send error and handler return status for diagnostics.
1560
+ */
1561
+ var SendCommandError = class extends CommandError {
1562
+ /**
1563
+ * The send error reported by the Apple TV.
1564
+ */
1565
+ sendError;
1566
+ /**
1567
+ * The handler return status reported by the Apple TV.
1568
+ */
1569
+ handlerReturnStatus;
1570
+ /**
1571
+ * Creates a new SendCommandError.
1572
+ *
1573
+ * @param sendError - The send error code from the Apple TV.
1574
+ * @param handlerReturnStatus - The handler return status from the Apple TV.
1575
+ */
1576
+ constructor(sendError, handlerReturnStatus) {
1577
+ super(`SendCommand failed: sendError=${Proto$1.SendError_Enum[sendError]}, handlerReturnStatus=${Proto$1.HandlerReturnStatus_Enum[handlerReturnStatus]}`);
1578
+ this.name = "SendCommandError";
1579
+ this.sendError = sendError;
1580
+ this.handlerReturnStatus = handlerReturnStatus;
1581
+ }
1582
+ };
1583
+ /**
1584
+ * Remote control for an AirPlay device.
1585
+ * Provides HID-based navigation and media keys (USB usage pages: Generic Desktop 0x01
1586
+ * and Consumer 0x0c), SendCommand-based media controls, keyboard/text input,
1587
+ * and touch/gesture simulation.
1588
+ */
1589
+ var AirPlayRemote = class {
1590
+ /**
1591
+ * @returns The DataStream for sending HID events and commands.
1592
+ */
1593
+ get #dataStream() {
1594
+ return this.#protocol.dataStream;
1595
+ }
1596
+ /**
1597
+ * @returns The underlying AirPlay Protocol instance.
1598
+ */
1599
+ get #protocol() {
1600
+ return this.#device[PROTOCOL];
1601
+ }
1602
+ #device;
1603
+ /**
1604
+ * Creates a new Remote controller.
1605
+ *
1606
+ * @param device - The AirPlay device to control.
1607
+ */
1608
+ constructor(device) {
1609
+ this.#device = device;
1610
+ }
1611
+ /**
1612
+ * Sends an Up navigation key press (Generic Desktop, usage 0x8C).
1613
+ */
1614
+ async up() {
1615
+ await this.pressAndRelease(1, 140);
1616
+ }
1617
+ /**
1618
+ * Sends a Down navigation key press (Generic Desktop, usage 0x8D).
1619
+ */
1620
+ async down() {
1621
+ await this.pressAndRelease(1, 141);
1622
+ }
1623
+ /**
1624
+ * Sends a Left navigation key press (Generic Desktop, usage 0x8B).
1625
+ */
1626
+ async left() {
1627
+ await this.pressAndRelease(1, 139);
1628
+ }
1629
+ /**
1630
+ * Sends a Right navigation key press (Generic Desktop, usage 0x8A).
1631
+ */
1632
+ async right() {
1633
+ await this.pressAndRelease(1, 138);
1634
+ }
1635
+ /**
1636
+ * Sends a Menu key press (Generic Desktop, usage 0x86).
1637
+ */
1638
+ async menu() {
1639
+ await this.pressAndRelease(1, 134);
1640
+ }
1641
+ /**
1642
+ * Sends a Select/Enter key press (Generic Desktop, usage 0x89).
1643
+ */
1644
+ async select() {
1645
+ await this.pressAndRelease(1, 137);
1646
+ }
1647
+ /**
1648
+ * Sends a Home button press (Consumer, usage 0x40).
1649
+ */
1650
+ async home() {
1651
+ await this.pressAndRelease(12, 64);
1652
+ }
1653
+ /**
1654
+ * Sends a Suspend/Sleep key press to put the device to sleep (Generic Desktop, usage 0x82).
1655
+ */
1656
+ async suspend() {
1657
+ await this.pressAndRelease(1, 130);
1658
+ }
1659
+ /**
1660
+ * Sends a Wake key press to wake the device (Generic Desktop, usage 0x83).
1661
+ */
1662
+ async wake() {
1663
+ await this.pressAndRelease(1, 131);
1664
+ }
1665
+ /**
1666
+ * Sends a Play key press (Consumer, usage 0xB0).
1667
+ */
1668
+ async play() {
1669
+ await this.pressAndRelease(12, 176);
1670
+ }
1671
+ /**
1672
+ * Sends a Pause key press (Consumer, usage 0xB1).
1673
+ */
1674
+ async pause() {
1675
+ await this.pressAndRelease(12, 177);
1676
+ }
1677
+ /**
1678
+ * Toggles play/pause based on the current playback state.
1679
+ */
1680
+ async playPause() {
1681
+ if (this.#device.state.nowPlayingClient?.isPlaying) await this.pause();
1682
+ else await this.play();
1683
+ }
1684
+ /**
1685
+ * Sends a Stop key press (Consumer, usage 0xB7).
1686
+ */
1687
+ async stop() {
1688
+ await this.pressAndRelease(12, 183);
1689
+ }
1690
+ /**
1691
+ * Sends a Next Track key press (Consumer, usage 0xB5).
1692
+ */
1693
+ async next() {
1694
+ await this.pressAndRelease(12, 181);
1695
+ }
1696
+ /**
1697
+ * Sends a Previous Track key press (Consumer, usage 0xB6).
1698
+ */
1699
+ async previous() {
1700
+ await this.pressAndRelease(12, 182);
1701
+ }
1702
+ /**
1703
+ * Sends a Volume Up key press (Consumer, usage 0xE9).
1704
+ */
1705
+ async volumeUp() {
1706
+ await this.pressAndRelease(12, 233);
1707
+ }
1708
+ /**
1709
+ * Sends a Volume Down key press (Consumer, usage 0xEA).
1710
+ */
1711
+ async volumeDown() {
1712
+ await this.pressAndRelease(12, 234);
1713
+ }
1714
+ /**
1715
+ * Sends a Mute key press (Consumer, usage 0xE2).
1716
+ */
1717
+ async mute() {
1718
+ await this.pressAndRelease(12, 226);
1719
+ }
1720
+ /**
1721
+ * Sends a Top Menu key press (Consumer, usage 0x60).
1722
+ */
1723
+ async topMenu() {
1724
+ await this.pressAndRelease(12, 96);
1725
+ }
1726
+ /**
1727
+ * Sends a Channel Up key press (Consumer, usage 0x9C).
1728
+ */
1729
+ async channelUp() {
1730
+ await this.pressAndRelease(12, 156);
1731
+ }
1732
+ /**
1733
+ * Sends a Channel Down key press (Consumer, usage 0x9D).
1734
+ */
1735
+ async channelDown() {
1736
+ await this.pressAndRelease(12, 157);
1737
+ }
1738
+ /**
1739
+ * Sends a Play command via the MRP SendCommand protocol.
1740
+ */
1741
+ async commandPlay() {
1742
+ await this.#sendCommand(Proto$1.Command.Play);
1743
+ }
1744
+ /**
1745
+ * Sends a Pause command via the MRP SendCommand protocol.
1746
+ */
1747
+ async commandPause() {
1748
+ await this.#sendCommand(Proto$1.Command.Pause);
1749
+ }
1750
+ /**
1751
+ * Sends a TogglePlayPause command via the MRP SendCommand protocol.
1752
+ */
1753
+ async commandTogglePlayPause() {
1754
+ await this.#sendCommand(Proto$1.Command.TogglePlayPause);
1755
+ }
1756
+ /**
1757
+ * Sends a Stop command via the MRP SendCommand protocol.
1758
+ */
1759
+ async commandStop() {
1760
+ await this.#sendCommand(Proto$1.Command.Stop);
1761
+ }
1762
+ /**
1763
+ * Sends a NextTrack command via the MRP SendCommand protocol.
1764
+ */
1765
+ async commandNextTrack() {
1766
+ await this.#sendCommand(Proto$1.Command.NextTrack);
1767
+ }
1768
+ /**
1769
+ * Sends a PreviousTrack command via the MRP SendCommand protocol.
1770
+ */
1771
+ async commandPreviousTrack() {
1772
+ await this.#sendCommand(Proto$1.Command.PreviousTrack);
1773
+ }
1774
+ /**
1775
+ * Sends a SkipForward command with a configurable interval.
1776
+ *
1777
+ * @param interval - Seconds to skip forward (defaults to 15).
1778
+ */
1779
+ async commandSkipForward(interval = 15) {
1780
+ await this.#sendCommandRaw(DataStreamMessage.sendCommandWithSkipInterval(Proto$1.Command.SkipForward, interval));
1781
+ }
1782
+ /**
1783
+ * Sends a SkipBackward command with a configurable interval.
1784
+ *
1785
+ * @param interval - Seconds to skip backward (defaults to 15).
1786
+ */
1787
+ async commandSkipBackward(interval = 15) {
1788
+ await this.#sendCommandRaw(DataStreamMessage.sendCommandWithSkipInterval(Proto$1.Command.SkipBackward, interval));
1789
+ }
1790
+ /**
1791
+ * Seeks to an absolute playback position.
1792
+ *
1793
+ * @param position - The target position in seconds.
1794
+ */
1795
+ async commandSeekToPosition(position) {
1796
+ await this.#sendCommandRaw(DataStreamMessage.sendCommandWithPlaybackPosition(Proto$1.Command.SeekToPlaybackPosition, position));
1797
+ }
1798
+ /**
1799
+ * Sets the shuffle mode.
1800
+ *
1801
+ * @param mode - The desired shuffle mode.
1802
+ */
1803
+ async commandSetShuffleMode(mode) {
1804
+ await this.#sendCommandRaw(DataStreamMessage.sendCommandWithShuffleMode(Proto$1.Command.ChangeShuffleMode, mode));
1805
+ }
1806
+ /**
1807
+ * Sets the repeat mode.
1808
+ *
1809
+ * @param mode - The desired repeat mode.
1810
+ */
1811
+ async commandSetRepeatMode(mode) {
1812
+ await this.#sendCommandRaw(DataStreamMessage.sendCommandWithRepeatMode(Proto$1.Command.ChangeRepeatMode, mode));
1813
+ }
1814
+ /**
1815
+ * Changes the playback rate (speed).
1816
+ *
1817
+ * @param rate - The desired playback rate (e.g. 1.0 for normal, 2.0 for double speed).
1818
+ */
1819
+ async commandChangePlaybackRate(rate) {
1820
+ await this.#sendCommandRaw(DataStreamMessage.sendCommandWithPlaybackRate(Proto$1.Command.ChangePlaybackRate, rate));
1821
+ }
1822
+ /**
1823
+ * Cycles the shuffle mode to the next value.
1824
+ */
1825
+ async commandAdvanceShuffleMode() {
1826
+ await this.#sendCommand(Proto$1.Command.AdvanceShuffleMode);
1827
+ }
1828
+ /**
1829
+ * Cycles the repeat mode to the next value.
1830
+ */
1831
+ async commandAdvanceRepeatMode() {
1832
+ await this.#sendCommand(Proto$1.Command.AdvanceRepeatMode);
1833
+ }
1834
+ /**
1835
+ * Begins fast-forwarding playback.
1836
+ */
1837
+ async commandBeginFastForward() {
1838
+ await this.#sendCommand(Proto$1.Command.BeginFastForward);
1839
+ }
1840
+ /**
1841
+ * Ends fast-forwarding playback.
1842
+ */
1843
+ async commandEndFastForward() {
1844
+ await this.#sendCommand(Proto$1.Command.EndFastForward);
1845
+ }
1846
+ /**
1847
+ * Begins rewinding playback.
1848
+ */
1849
+ async commandBeginRewind() {
1850
+ await this.#sendCommand(Proto$1.Command.BeginRewind);
1851
+ }
1852
+ /**
1853
+ * Ends rewinding playback.
1854
+ */
1855
+ async commandEndRewind() {
1856
+ await this.#sendCommand(Proto$1.Command.EndRewind);
1857
+ }
1858
+ /**
1859
+ * Skips to the next chapter.
1860
+ */
1861
+ async commandNextChapter() {
1862
+ await this.#sendCommand(Proto$1.Command.NextChapter);
1863
+ }
1864
+ /**
1865
+ * Skips to the previous chapter.
1866
+ */
1867
+ async commandPreviousChapter() {
1868
+ await this.#sendCommand(Proto$1.Command.PreviousChapter);
1869
+ }
1870
+ /**
1871
+ * Marks the current track as liked.
1872
+ */
1873
+ async commandLikeTrack() {
1874
+ await this.#sendCommand(Proto$1.Command.LikeTrack);
1875
+ }
1876
+ /**
1877
+ * Marks the current track as disliked.
1878
+ */
1879
+ async commandDislikeTrack() {
1880
+ await this.#sendCommand(Proto$1.Command.DislikeTrack);
1881
+ }
1882
+ /**
1883
+ * Bookmarks the current track.
1884
+ */
1885
+ async commandBookmarkTrack() {
1886
+ await this.#sendCommand(Proto$1.Command.BookmarkTrack);
1887
+ }
1888
+ /**
1889
+ * Adds the currently playing item to the user's library.
1890
+ */
1891
+ async commandAddNowPlayingItemToLibrary() {
1892
+ await this.#sendCommand(Proto$1.Command.AddNowPlayingItemToLibrary);
1893
+ }
1894
+ /**
1895
+ * Sets a sleep timer that will stop playback after the specified duration.
1896
+ * The timer works by attaching sleep timer options to a Pause command.
1897
+ *
1898
+ * @param seconds - Timer duration in seconds. Use 0 to cancel an active timer.
1899
+ * @param stopMode - Stop mode: 0 = stop playback, 1 = pause, 2 = end of track, 3 = end of queue.
1900
+ */
1901
+ async commandSetSleepTimer(seconds, stopMode = 0) {
1902
+ await this.#sendCommandRaw(DataStreamMessage.sendCommandWithSleepTimer(seconds, stopMode));
1903
+ }
1904
+ /**
1905
+ * Sets the text input field to the given text, replacing any existing content.
1906
+ *
1907
+ * @param text - The text to set.
1908
+ */
1909
+ async textSet(text) {
1910
+ await this.#dataStream.send(DataStreamMessage.textInput(text, Proto$1.ActionType_Enum.Set));
1911
+ }
1912
+ /**
1913
+ * Appends text to the current text input field content.
1914
+ *
1915
+ * @param text - The text to append.
1916
+ */
1917
+ async textAppend(text) {
1918
+ await this.#dataStream.send(DataStreamMessage.textInput(text, Proto$1.ActionType_Enum.Insert));
1919
+ }
1920
+ /**
1921
+ * Clears the text input field.
1922
+ */
1923
+ async textClear() {
1924
+ await this.#dataStream.send(DataStreamMessage.textInput("", Proto$1.ActionType_Enum.ClearAction));
1925
+ }
1926
+ /**
1927
+ * Requests the current keyboard session state from the Apple TV.
1928
+ */
1929
+ async getKeyboardSession() {
1930
+ await this.#dataStream.send(DataStreamMessage.getKeyboardSession());
1931
+ }
1932
+ /**
1933
+ * Simulates a tap at the given coordinates.
1934
+ *
1935
+ * @param x - Horizontal position in the virtual touch area.
1936
+ * @param y - Vertical position in the virtual touch area.
1937
+ * @param finger - Finger index for multi-touch (defaults to 1).
1938
+ */
1939
+ async tap(x, y, finger = 1) {
1940
+ await this.#sendTouch(x, y, 1, finger);
1941
+ await waitFor(50);
1942
+ await this.#sendTouch(x, y, 4, finger);
1943
+ }
1944
+ /**
1945
+ * Simulates an upward swipe gesture.
1946
+ *
1947
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
1948
+ */
1949
+ async swipeUp(duration = 200) {
1950
+ await this.#swipe(200, 400, 200, 100, duration);
1951
+ }
1952
+ /**
1953
+ * Simulates a downward swipe gesture.
1954
+ *
1955
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
1956
+ */
1957
+ async swipeDown(duration = 200) {
1958
+ await this.#swipe(200, 100, 200, 400, duration);
1959
+ }
1960
+ /**
1961
+ * Simulates a leftward swipe gesture.
1962
+ *
1963
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
1964
+ */
1965
+ async swipeLeft(duration = 200) {
1966
+ await this.#swipe(400, 200, 100, 200, duration);
1967
+ }
1968
+ /**
1969
+ * Simulates a rightward swipe gesture.
1970
+ *
1971
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
1972
+ */
1973
+ async swipeRight(duration = 200) {
1974
+ await this.#swipe(100, 200, 400, 200, duration);
1975
+ }
1976
+ /**
1977
+ * Sends a double press of a HID key (two press-and-release cycles with a 150ms gap).
1978
+ *
1979
+ * @param usePage - USB HID usage page (1 = Generic Desktop, 12 = Consumer).
1980
+ * @param usage - USB HID usage code.
1981
+ */
1982
+ async doublePress(usePage, usage) {
1983
+ await this.pressAndRelease(usePage, usage);
1984
+ await waitFor(150);
1985
+ await this.pressAndRelease(usePage, usage);
1986
+ }
1987
+ /**
1988
+ * Sends a long press of a HID key (hold for a configurable duration).
1989
+ *
1990
+ * @param usePage - USB HID usage page (1 = Generic Desktop, 12 = Consumer).
1991
+ * @param usage - USB HID usage code.
1992
+ * @param duration - Hold duration in milliseconds (defaults to 1000).
1993
+ */
1994
+ async longPress(usePage, usage, duration = 1e3) {
1995
+ await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, true));
1996
+ await waitFor(duration);
1997
+ await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, false));
1998
+ }
1999
+ /**
2000
+ * Sends a single press-and-release of a HID key with a 25ms hold.
2001
+ *
2002
+ * @param usePage - USB HID usage page (1 = Generic Desktop, 12 = Consumer).
2003
+ * @param usage - USB HID usage code.
2004
+ */
2005
+ async pressAndRelease(usePage, usage) {
2006
+ await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, true));
2007
+ await waitFor(25);
2008
+ await this.#dataStream.exchange(DataStreamMessage.sendHIDEvent(usePage, usage, false));
2009
+ }
2010
+ /**
2011
+ * Sends a SendCommand request and checks the result.
2012
+ *
2013
+ * @param command - The command to send.
2014
+ * @param options - Optional command options.
2015
+ * @returns The command result message, or undefined if no result was returned.
2016
+ * @throws SendCommandError when the Apple TV reports a send error.
2017
+ */
2018
+ async #sendCommand(command, options) {
2019
+ const response = await this.#dataStream.exchange(DataStreamMessage.sendCommand(command, options));
2020
+ return this.#checkCommandResult(response);
2021
+ }
2022
+ /**
2023
+ * Sends a pre-built command message and checks the result.
2024
+ *
2025
+ * @param message - The pre-built DataStream message to send.
2026
+ * @returns The command result message, or undefined if no result was returned.
2027
+ * @throws SendCommandError when the Apple TV reports a send error.
2028
+ */
2029
+ async #sendCommandRaw(message) {
2030
+ const response = await this.#dataStream.exchange(message);
2031
+ return this.#checkCommandResult(response);
2032
+ }
2033
+ /**
2034
+ * Validates the response from a SendCommand request and throws on error.
2035
+ *
2036
+ * @param response - The protocol message response.
2037
+ * @returns The decoded result, or undefined if the response has no result extension.
2038
+ * @throws SendCommandError when the result indicates a send error.
2039
+ */
2040
+ #checkCommandResult(response) {
2041
+ let result;
2042
+ try {
2043
+ result = DataStreamMessage.getExtension(response, Proto$1.sendCommandResultMessage);
2044
+ } catch {
2045
+ return;
2046
+ }
2047
+ if (!result) return;
2048
+ if (result.sendError !== Proto$1.SendError_Enum.NoError) throw new SendCommandError(result.sendError, result.handlerReturnStatus);
2049
+ return result;
2050
+ }
2051
+ /**
2052
+ * Sends a virtual touch event at the given coordinates.
2053
+ *
2054
+ * @param x - Horizontal position.
2055
+ * @param y - Vertical position.
2056
+ * @param phase - Touch phase (1 = Began, 2 = Moved, 4 = Ended).
2057
+ * @param finger - Finger index for multi-touch.
2058
+ */
2059
+ async #sendTouch(x, y, phase, finger) {
2060
+ await this.#dataStream.exchange(DataStreamMessage.sendVirtualTouchEvent(x, y, phase, finger));
2061
+ }
2062
+ /**
2063
+ * Performs a swipe gesture by interpolating touch events between start and end coordinates.
2064
+ *
2065
+ * @param startX - Starting horizontal position.
2066
+ * @param startY - Starting vertical position.
2067
+ * @param endX - Ending horizontal position.
2068
+ * @param endY - Ending vertical position.
2069
+ * @param duration - Total swipe duration in milliseconds.
2070
+ */
2071
+ async #swipe(startX, startY, endX, endY, duration) {
2072
+ const steps = Math.max(4, Math.floor(duration / 50));
2073
+ const deltaX = (endX - startX) / steps;
2074
+ const deltaY = (endY - startY) / steps;
2075
+ const stepDuration = duration / steps;
2076
+ await this.#sendTouch(startX, startY, 1, 1);
2077
+ for (let i = 1; i < steps; i++) {
2078
+ await waitFor(stepDuration);
2079
+ await this.#sendTouch(Math.round(startX + deltaX * i), Math.round(startY + deltaY * i), 2, 1);
2080
+ }
2081
+ await waitFor(stepDuration);
2082
+ await this.#sendTouch(endX, endY, 4, 1);
2083
+ }
2084
+ };
2085
+
2086
+ //#endregion
2087
+ //#region src/internal/airplay-state.ts
2088
+ /**
2089
+ * Tracks the complete state of an AirPlay device: clients, players, now-playing,
2090
+ * volume, keyboard, output devices, and cluster info.
2091
+ * Listens to DataStream protocol messages and emits both low-level (1:1 with protocol)
2092
+ * and high-level (deduplicated, resolved) events.
2093
+ */
2094
+ var AirPlayState = class extends EventEmitter {
2095
+ /**
2096
+ * @returns The DataStream for event subscription.
2097
+ */
2098
+ get #dataStream() {
2099
+ return this.#protocol.dataStream;
2100
+ }
2101
+ /**
2102
+ * @returns The underlying AirPlay Protocol instance.
2103
+ */
2104
+ get #protocol() {
2105
+ return this.#device[PROTOCOL];
2106
+ }
2107
+ /**
2108
+ * All known clients (apps) keyed by bundle identifier.
2109
+ */
2110
+ get clients() {
2111
+ return this.#clients;
2112
+ }
2113
+ /**
2114
+ * Whether a keyboard/text input session is currently active on the Apple TV.
2115
+ */
2116
+ get isKeyboardActive() {
2117
+ return this.#keyboardState === Proto$1.KeyboardState_Enum.DidBeginEditing || this.#keyboardState === Proto$1.KeyboardState_Enum.Editing || this.#keyboardState === Proto$1.KeyboardState_Enum.TextDidChange;
2118
+ }
2119
+ /**
2120
+ * Text editing attributes for the active keyboard session, or null.
2121
+ */
2122
+ get keyboardAttributes() {
2123
+ return this.#keyboardAttributes;
2124
+ }
2125
+ /**
2126
+ * Current keyboard state enum value.
2127
+ */
2128
+ get keyboardState() {
2129
+ return this.#keyboardState;
2130
+ }
2131
+ /**
2132
+ * The currently active now-playing client, or null if nothing is playing.
2133
+ */
2134
+ get nowPlayingClient() {
2135
+ return this.#nowPlayingClientBundleIdentifier ? this.#clients[this.#nowPlayingClientBundleIdentifier] ?? null : null;
2136
+ }
2137
+ /**
2138
+ * UID of the primary output device (used for volume control and multi-room).
2139
+ */
2140
+ get outputDeviceUID() {
2141
+ return this.#outputDeviceUID;
2142
+ }
2143
+ /**
2144
+ * List of all output device descriptors in the current AirPlay group.
2145
+ */
2146
+ get outputDevices() {
2147
+ return this.#outputDevices;
2148
+ }
2149
+ /**
2150
+ * Cluster identifier for multi-room groups, or null.
2151
+ */
2152
+ get clusterID() {
2153
+ return this.#clusterID;
2154
+ }
2155
+ /**
2156
+ * Cluster type code (0 if not clustered).
2157
+ */
2158
+ get clusterType() {
2159
+ return this.#clusterType;
2160
+ }
2161
+ /**
2162
+ * Whether this device is aware of multi-room clusters.
2163
+ */
2164
+ get isClusterAware() {
2165
+ return this.#isClusterAware;
2166
+ }
2167
+ /**
2168
+ * Whether this device is the leader of its multi-room cluster.
2169
+ */
2170
+ get isClusterLeader() {
2171
+ return this.#isClusterLeader;
2172
+ }
2173
+ /**
2174
+ * Current playback queue participants (e.g. SharePlay users).
2175
+ */
2176
+ get participants() {
2177
+ return this.#participants;
2178
+ }
2179
+ /**
2180
+ * Raw JPEG artwork data from the last SET_ARTWORK_MESSAGE, or null.
2181
+ */
2182
+ get artworkJpegData() {
2183
+ return this.#artworkJpegData;
2184
+ }
2185
+ /**
2186
+ * Current volume level (0.0 - 1.0).
2187
+ */
2188
+ get volume() {
2189
+ return this.#volume;
2190
+ }
2191
+ /**
2192
+ * Whether volume control is available on this device.
2193
+ */
2194
+ get volumeAvailable() {
2195
+ return this.#volumeAvailable;
2196
+ }
2197
+ /**
2198
+ * Volume capabilities (absolute, relative, both, or none).
2199
+ */
2200
+ get volumeCapabilities() {
2201
+ return this.#volumeCapabilities;
2202
+ }
2203
+ /**
2204
+ * Whether the device is currently muted.
2205
+ */
2206
+ get volumeMuted() {
2207
+ return this.#volumeMuted;
2208
+ }
2209
+ #device;
2210
+ #clients;
2211
+ #keyboardAttributes;
2212
+ #keyboardState;
2213
+ #nowPlayingClientBundleIdentifier;
2214
+ #nowPlayingSnapshot;
2215
+ #outputDeviceUID;
2216
+ #outputDevices = [];
2217
+ #clusterID;
2218
+ #clusterType;
2219
+ #isClusterAware;
2220
+ #isClusterLeader;
2221
+ #participants;
2222
+ #artworkJpegData;
2223
+ #volume;
2224
+ #volumeAvailable;
2225
+ #volumeCapabilities;
2226
+ #volumeMuted;
2227
+ /**
2228
+ * Creates a new AirPlayState tracker.
2229
+ *
2230
+ * @param device - The AirPlay device to track state for.
2231
+ */
2232
+ constructor(device) {
2233
+ super();
2234
+ this.#device = device;
2235
+ this.clear();
2236
+ this.onConfigureConnection = this.onConfigureConnection.bind(this);
2237
+ this.onKeyboard = this.onKeyboard.bind(this);
2238
+ this.onDeviceInfo = this.onDeviceInfo.bind(this);
2239
+ this.onDeviceInfoUpdate = this.onDeviceInfoUpdate.bind(this);
2240
+ this.onOriginClientProperties = this.onOriginClientProperties.bind(this);
2241
+ this.onPlayerClientProperties = this.onPlayerClientProperties.bind(this);
2242
+ this.onRemoveClient = this.onRemoveClient.bind(this);
2243
+ this.onRemovePlayer = this.onRemovePlayer.bind(this);
2244
+ this.onSendCommandResult = this.onSendCommandResult.bind(this);
2245
+ this.onSendLyricsEvent = this.onSendLyricsEvent.bind(this);
2246
+ this.onSetArtwork = this.onSetArtwork.bind(this);
2247
+ this.onSetDefaultSupportedCommands = this.onSetDefaultSupportedCommands.bind(this);
2248
+ this.onSetNowPlayingClient = this.onSetNowPlayingClient.bind(this);
2249
+ this.onSetNowPlayingPlayer = this.onSetNowPlayingPlayer.bind(this);
2250
+ this.onSetState = this.onSetState.bind(this);
2251
+ this.onUpdateClient = this.onUpdateClient.bind(this);
2252
+ this.onUpdateContentItem = this.onUpdateContentItem.bind(this);
2253
+ this.onUpdateContentItemArtwork = this.onUpdateContentItemArtwork.bind(this);
2254
+ this.onUpdatePlayer = this.onUpdatePlayer.bind(this);
2255
+ this.onPlayerClientParticipantsUpdate = this.onPlayerClientParticipantsUpdate.bind(this);
2256
+ this.onUpdateOutputDevice = this.onUpdateOutputDevice.bind(this);
2257
+ this.onVolumeControlAvailability = this.onVolumeControlAvailability.bind(this);
2258
+ this.onVolumeControlCapabilitiesDidChange = this.onVolumeControlCapabilitiesDidChange.bind(this);
2259
+ this.onVolumeDidChange = this.onVolumeDidChange.bind(this);
2260
+ this.onVolumeMutedDidChange = this.onVolumeMutedDidChange.bind(this);
2261
+ }
2262
+ /**
2263
+ * Subscribes to all DataStream events to track device state. Called internally via symbol.
2264
+ */
2265
+ [STATE_SUBSCRIBE_SYMBOL]() {
2266
+ this.#dataStream.on("configureConnection", this.onConfigureConnection);
2267
+ this.#dataStream.on("keyboard", this.onKeyboard);
2268
+ this.#dataStream.on("deviceInfo", this.onDeviceInfo);
2269
+ this.#dataStream.on("deviceInfoUpdate", this.onDeviceInfoUpdate);
2270
+ this.#dataStream.on("originClientProperties", this.onOriginClientProperties);
2271
+ this.#dataStream.on("playerClientProperties", this.onPlayerClientProperties);
2272
+ this.#dataStream.on("removeClient", this.onRemoveClient);
2273
+ this.#dataStream.on("removePlayer", this.onRemovePlayer);
2274
+ this.#dataStream.on("sendCommandResult", this.onSendCommandResult);
2275
+ this.#dataStream.on("sendLyricsEvent", this.onSendLyricsEvent);
2276
+ this.#dataStream.on("setArtwork", this.onSetArtwork);
2277
+ this.#dataStream.on("setDefaultSupportedCommands", this.onSetDefaultSupportedCommands);
2278
+ this.#dataStream.on("setNowPlayingClient", this.onSetNowPlayingClient);
2279
+ this.#dataStream.on("setNowPlayingPlayer", this.onSetNowPlayingPlayer);
2280
+ this.#dataStream.on("setState", this.onSetState);
2281
+ this.#dataStream.on("updateClient", this.onUpdateClient);
2282
+ this.#dataStream.on("updateContentItem", this.onUpdateContentItem);
2283
+ this.#dataStream.on("updateContentItemArtwork", this.onUpdateContentItemArtwork);
2284
+ this.#dataStream.on("updatePlayer", this.onUpdatePlayer);
2285
+ this.#dataStream.on("playerClientParticipantsUpdate", this.onPlayerClientParticipantsUpdate);
2286
+ this.#dataStream.on("updateOutputDevice", this.onUpdateOutputDevice);
2287
+ this.#dataStream.on("volumeControlAvailability", this.onVolumeControlAvailability);
2288
+ this.#dataStream.on("volumeControlCapabilitiesDidChange", this.onVolumeControlCapabilitiesDidChange);
2289
+ this.#dataStream.on("volumeDidChange", this.onVolumeDidChange);
2290
+ this.#dataStream.on("volumeMutedDidChange", this.onVolumeMutedDidChange);
2291
+ }
2292
+ /**
2293
+ * Unsubscribes from all DataStream events. Called internally via symbol.
2294
+ */
2295
+ [STATE_UNSUBSCRIBE_SYMBOL]() {
2296
+ const dataStream = this.#dataStream;
2297
+ if (!dataStream) return;
2298
+ dataStream.off("configureConnection", this.onConfigureConnection);
2299
+ dataStream.off("keyboard", this.onKeyboard);
2300
+ dataStream.off("deviceInfo", this.onDeviceInfo);
2301
+ dataStream.off("deviceInfoUpdate", this.onDeviceInfoUpdate);
2302
+ dataStream.off("originClientProperties", this.onOriginClientProperties);
2303
+ dataStream.off("playerClientProperties", this.onPlayerClientProperties);
2304
+ dataStream.off("removeClient", this.onRemoveClient);
2305
+ dataStream.off("removePlayer", this.onRemovePlayer);
2306
+ dataStream.off("sendCommandResult", this.onSendCommandResult);
2307
+ dataStream.off("sendLyricsEvent", this.onSendLyricsEvent);
2308
+ dataStream.off("setArtwork", this.onSetArtwork);
2309
+ dataStream.off("setDefaultSupportedCommands", this.onSetDefaultSupportedCommands);
2310
+ dataStream.off("setNowPlayingClient", this.onSetNowPlayingClient);
2311
+ dataStream.off("setNowPlayingPlayer", this.onSetNowPlayingPlayer);
2312
+ dataStream.off("setState", this.onSetState);
2313
+ dataStream.off("updateClient", this.onUpdateClient);
2314
+ dataStream.off("updateContentItem", this.onUpdateContentItem);
2315
+ dataStream.off("updateContentItemArtwork", this.onUpdateContentItemArtwork);
2316
+ dataStream.off("updatePlayer", this.onUpdatePlayer);
2317
+ dataStream.off("playerClientParticipantsUpdate", this.onPlayerClientParticipantsUpdate);
2318
+ dataStream.off("updateOutputDevice", this.onUpdateOutputDevice);
2319
+ dataStream.off("volumeControlAvailability", this.onVolumeControlAvailability);
2320
+ dataStream.off("volumeControlCapabilitiesDidChange", this.onVolumeControlCapabilitiesDidChange);
2321
+ dataStream.off("volumeDidChange", this.onVolumeDidChange);
2322
+ dataStream.off("volumeMutedDidChange", this.onVolumeMutedDidChange);
2323
+ }
2324
+ /**
2325
+ * Resets all state to initial/default values. Called on connect and reconnect.
2326
+ */
2327
+ clear() {
2328
+ this.#clients = {};
2329
+ this.#keyboardAttributes = null;
2330
+ this.#keyboardState = Proto$1.KeyboardState_Enum.Unknown;
2331
+ this.#nowPlayingClientBundleIdentifier = null;
2332
+ this.#nowPlayingSnapshot = null;
2333
+ this.#outputDeviceUID = null;
2334
+ this.#outputDevices = [];
2335
+ this.#clusterID = null;
2336
+ this.#clusterType = 0;
2337
+ this.#isClusterAware = false;
2338
+ this.#isClusterLeader = false;
2339
+ this.#participants = [];
2340
+ this.#artworkJpegData = null;
2341
+ this.#volume = 0;
2342
+ this.#volumeAvailable = false;
2343
+ this.#volumeCapabilities = Proto$1.VolumeCapabilities_Enum.None;
2344
+ this.#volumeMuted = false;
2345
+ }
2346
+ /**
2347
+ * Handles a ConfigureConnection message from the Apple TV.
2348
+ *
2349
+ * @param message - The configure connection message.
2350
+ */
2351
+ onConfigureConnection(message) {
2352
+ this.emit("configureConnection", message);
2353
+ }
2354
+ /**
2355
+ * Handles keyboard state changes. Updates internal state and emits 'keyboard'.
2356
+ *
2357
+ * @param message - The keyboard message with state and attributes.
2358
+ */
2359
+ onKeyboard(message) {
2360
+ this.#keyboardState = message.state;
2361
+ this.#keyboardAttributes = message.attributes ?? null;
2362
+ this.emit("keyboard", message);
2363
+ }
2364
+ /**
2365
+ * Handles initial device info. Updates output device UID and cluster info.
2366
+ *
2367
+ * @param message - The device info message.
2368
+ */
2369
+ onDeviceInfo(message) {
2370
+ this.#updateDeviceInfo(message);
2371
+ this.emit("deviceInfo", message);
2372
+ }
2373
+ /**
2374
+ * Handles device info updates (e.g. cluster changes). Updates output device UID and cluster info.
2375
+ *
2376
+ * @param message - The device info update message.
2377
+ */
2378
+ onDeviceInfoUpdate(message) {
2379
+ this.#updateDeviceInfo(message);
2380
+ this.emit("deviceInfoUpdate", message);
2381
+ }
2382
+ /**
2383
+ * Handles origin client properties updates.
2384
+ *
2385
+ * @param message - The origin client properties message.
2386
+ */
2387
+ onOriginClientProperties(message) {
2388
+ this.emit("originClientProperties", message);
2389
+ }
2390
+ /**
2391
+ * Handles player client properties updates.
2392
+ *
2393
+ * @param message - The player client properties message.
2394
+ */
2395
+ onPlayerClientProperties(message) {
2396
+ this.emit("playerClientProperties", message);
2397
+ }
2398
+ /**
2399
+ * Handles removal of a client (app). Clears the now-playing reference if
2400
+ * the removed client was the active one.
2401
+ *
2402
+ * @param message - The remove client message.
2403
+ */
2404
+ onRemoveClient(message) {
2405
+ if (!message.client?.bundleIdentifier) return;
2406
+ if (!(message.client.bundleIdentifier in this.#clients)) return;
2407
+ const wasActive = this.#nowPlayingClientBundleIdentifier === message.client.bundleIdentifier;
2408
+ delete this.#clients[message.client.bundleIdentifier];
2409
+ if (wasActive) this.#nowPlayingClientBundleIdentifier = null;
2410
+ this.emit("removeClient", message);
2411
+ this.emit("clients", this.#clients);
2412
+ if (wasActive) {
2413
+ this.#emitActivePlayerChanged();
2414
+ this.#emitNowPlayingChangedIfNeeded();
2415
+ }
2416
+ }
2417
+ /**
2418
+ * Handles command result notifications from the Apple TV.
2419
+ *
2420
+ * @param message - The send command result message.
2421
+ */
2422
+ onSendCommandResult(message) {
2423
+ this.emit("sendCommandResult", message);
2424
+ }
2425
+ /**
2426
+ * Handles lyrics events (time-synced lyrics updates).
2427
+ *
2428
+ * @param message - The lyrics event message.
2429
+ */
2430
+ onSendLyricsEvent(message) {
2431
+ if (message.event) this.emit("lyricsEvent", message.event, message.playerPath);
2432
+ }
2433
+ /**
2434
+ * Handles artwork set notifications.
2435
+ *
2436
+ * @param message - The set artwork message.
2437
+ */
2438
+ onSetArtwork(message) {
2439
+ if (message.jpegData?.byteLength > 0) this.#artworkJpegData = message.jpegData;
2440
+ this.emit("setArtwork", message);
2441
+ }
2442
+ /**
2443
+ * Handles default supported commands for a client. These serve as fallback
2444
+ * commands when a player has no commands of its own.
2445
+ *
2446
+ * @param message - The set default supported commands message.
2447
+ */
2448
+ onSetDefaultSupportedCommands(message) {
2449
+ if (message.playerPath?.client?.bundleIdentifier && message.supportedCommands) this.#client(message.playerPath.client.bundleIdentifier, message.playerPath.client.displayName).setDefaultSupportedCommands(message.supportedCommands.supportedCommands);
2450
+ this.emit("setDefaultSupportedCommands", message);
2451
+ }
2452
+ /**
2453
+ * Handles the now-playing client changing (e.g. user switches app).
2454
+ * Updates the active client reference and emits change events.
2455
+ *
2456
+ * @param message - The set now-playing client message.
2457
+ */
2458
+ onSetNowPlayingClient(message) {
2459
+ const oldBundleId = this.#nowPlayingClientBundleIdentifier;
2460
+ this.#nowPlayingClientBundleIdentifier = message.client?.bundleIdentifier ?? null;
2461
+ if (message.client?.bundleIdentifier && message.client?.displayName) this.#client(message.client.bundleIdentifier, message.client.displayName);
2462
+ this.emit("setNowPlayingClient", message);
2463
+ if (oldBundleId !== this.#nowPlayingClientBundleIdentifier) this.#emitActivePlayerChanged();
2464
+ this.#emitNowPlayingChangedIfNeeded();
2465
+ }
2466
+ /**
2467
+ * Handles the active player changing within a client (e.g. PiP player becomes active).
2468
+ * Creates the player if needed and sets it as the active player.
2469
+ *
2470
+ * @param message - The set now-playing player message.
2471
+ */
2472
+ onSetNowPlayingPlayer(message) {
2473
+ if (message.playerPath?.client?.bundleIdentifier && message.playerPath?.player?.identifier) {
2474
+ const client = this.#client(message.playerPath.client.bundleIdentifier, message.playerPath.client.displayName);
2475
+ const oldActiveId = client.activePlayer?.identifier;
2476
+ client.getOrCreatePlayer(message.playerPath.player.identifier, message.playerPath.player.displayName);
2477
+ client.setActivePlayer(message.playerPath.player.identifier);
2478
+ if (oldActiveId !== message.playerPath.player.identifier) this.#emitActivePlayerChanged();
2479
+ }
2480
+ this.emit("setNowPlayingPlayer", message);
2481
+ this.#emitNowPlayingChangedIfNeeded();
2482
+ }
2483
+ /**
2484
+ * Handles comprehensive state updates. Processes playback state, now-playing info,
2485
+ * supported commands, and playback queue in a single message.
2486
+ * Emits granular events for each changed aspect.
2487
+ *
2488
+ * @param message - The set state message.
2489
+ */
2490
+ onSetState(message) {
2491
+ const bundleIdentifier = message.playerPath?.client?.bundleIdentifier;
2492
+ if (!bundleIdentifier) return;
2493
+ const client = this.#client(bundleIdentifier, message.displayName);
2494
+ const playerIdentifier = message.playerPath?.player?.identifier || "MediaRemote-DefaultPlayer";
2495
+ const player = client.getOrCreatePlayer(playerIdentifier, message.playerPath?.player?.displayName);
2496
+ const isActiveClient = bundleIdentifier === this.#nowPlayingClientBundleIdentifier;
2497
+ if (message.playbackState) {
2498
+ const oldState = player.playbackState;
2499
+ player.setPlaybackState(message.playbackState, message.playbackStateTimestamp);
2500
+ if (isActiveClient && oldState !== player.playbackState) this.emit("playbackStateChanged", client, player, oldState, player.playbackState);
2501
+ }
2502
+ if (message.nowPlayingInfo) player.setNowPlayingInfo(message.nowPlayingInfo);
2503
+ if (message.supportedCommands) {
2504
+ player.setSupportedCommands(message.supportedCommands.supportedCommands);
2505
+ if (isActiveClient) this.emit("supportedCommandsChanged", client, player, player.supportedCommands);
2506
+ }
2507
+ if (message.playbackQueue) {
2508
+ player.setPlaybackQueue(message.playbackQueue);
2509
+ if (isActiveClient) this.emit("playbackQueueChanged", client, player);
2510
+ }
2511
+ this.emit("setState", message);
2512
+ if (isActiveClient) this.#emitNowPlayingChangedIfNeeded();
2513
+ }
2514
+ /**
2515
+ * Handles content item updates (metadata, artwork, lyrics changes for existing items).
2516
+ *
2517
+ * @param message - The update content item message.
2518
+ */
2519
+ onUpdateContentItem(message) {
2520
+ const bundleIdentifier = message.playerPath?.client?.bundleIdentifier;
2521
+ if (!bundleIdentifier) return;
2522
+ const client = this.#client(bundleIdentifier, message.playerPath?.client?.displayName ?? "");
2523
+ const playerIdentifier = message.playerPath?.player?.identifier || "MediaRemote-DefaultPlayer";
2524
+ const player = client.getOrCreatePlayer(playerIdentifier, message.playerPath?.player?.displayName);
2525
+ for (const item of message.contentItems) player.updateContentItem(item);
2526
+ this.emit("updateContentItem", message);
2527
+ if (bundleIdentifier === this.#nowPlayingClientBundleIdentifier) this.#emitNowPlayingChangedIfNeeded();
2528
+ }
2529
+ /**
2530
+ * Handles artwork updates for content items. Emits 'artworkChanged' if a client and player are active.
2531
+ *
2532
+ * @param message - The update content item artwork message.
2533
+ */
2534
+ onUpdateContentItemArtwork(message) {
2535
+ this.emit("updateContentItemArtwork", message);
2536
+ const client = this.nowPlayingClient;
2537
+ const player = client?.activePlayer;
2538
+ if (client && player) this.emit("artworkChanged", client, player);
2539
+ }
2540
+ /**
2541
+ * Handles player registration or update. Creates the player if it does not exist.
2542
+ *
2543
+ * @param message - The update player message.
2544
+ */
2545
+ onUpdatePlayer(message) {
2546
+ 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);
2547
+ this.emit("updatePlayer", message);
2548
+ }
2549
+ /**
2550
+ * Handles player removal. Removes the player from its client and emits
2551
+ * active player changed events if the removed player was active.
2552
+ *
2553
+ * @param message - The remove player message.
2554
+ */
2555
+ onRemovePlayer(message) {
2556
+ if (message.playerPath?.client?.bundleIdentifier && message.playerPath?.player?.identifier) {
2557
+ const client = this.#clients[message.playerPath.client.bundleIdentifier];
2558
+ if (client) client.removePlayer(message.playerPath.player.identifier);
2559
+ }
2560
+ this.emit("removePlayer", message);
2561
+ if (message.playerPath?.client?.bundleIdentifier === this.#nowPlayingClientBundleIdentifier) {
2562
+ this.#emitActivePlayerChanged();
2563
+ this.#emitNowPlayingChangedIfNeeded();
2564
+ }
2565
+ }
2566
+ /**
2567
+ * Handles client (app) registration or display name update.
2568
+ *
2569
+ * @param message - The update client message.
2570
+ */
2571
+ onUpdateClient(message) {
2572
+ if (!message.client?.bundleIdentifier) return;
2573
+ this.#client(message.client.bundleIdentifier, message.client.displayName);
2574
+ this.emit("updateClient", message);
2575
+ this.emit("clients", this.#clients);
2576
+ }
2577
+ /**
2578
+ * Handles output device list updates. Prefers cluster-aware devices when available.
2579
+ *
2580
+ * @param message - The update output device message.
2581
+ */
2582
+ /**
2583
+ * Handles playback queue participant updates (e.g. SharePlay users).
2584
+ *
2585
+ * @param message - The participants update message.
2586
+ */
2587
+ onPlayerClientParticipantsUpdate(message) {
2588
+ this.#participants = message.participants ?? [];
2589
+ this.emit("playerClientParticipantsUpdate", message);
2590
+ }
2591
+ onUpdateOutputDevice(message) {
2592
+ this.#outputDevices = message.clusterAwareOutputDevices?.length > 0 ? message.clusterAwareOutputDevices : message.outputDevices;
2593
+ this.emit("updateOutputDevice", message);
2594
+ }
2595
+ /**
2596
+ * Handles volume control availability changes.
2597
+ *
2598
+ * @param message - The volume control availability message.
2599
+ */
2600
+ onVolumeControlAvailability(message) {
2601
+ this.#volumeAvailable = message.volumeControlAvailable;
2602
+ this.#volumeCapabilities = message.volumeCapabilities;
2603
+ this.emit("volumeControlAvailability", message.volumeControlAvailable, message.volumeCapabilities);
2604
+ }
2605
+ /**
2606
+ * Handles volume capabilities changes (e.g. device gains or loses absolute volume support).
2607
+ *
2608
+ * @param message - The volume capabilities change message.
2609
+ */
2610
+ onVolumeControlCapabilitiesDidChange(message) {
2611
+ if (!message.capabilities) return;
2612
+ this.#volumeAvailable = message.capabilities.volumeControlAvailable;
2613
+ this.#volumeCapabilities = message.capabilities.volumeCapabilities;
2614
+ this.emit("volumeControlCapabilitiesDidChange", message.capabilities.volumeControlAvailable, message.capabilities.volumeCapabilities);
2615
+ }
2616
+ /**
2617
+ * Handles volume level changes.
2618
+ *
2619
+ * @param message - The volume change message.
2620
+ */
2621
+ onVolumeDidChange(message) {
2622
+ this.#volume = message.volume;
2623
+ this.emit("volumeDidChange", message.volume);
2624
+ }
2625
+ /**
2626
+ * Handles mute state changes.
2627
+ *
2628
+ * @param message - The volume muted change message.
2629
+ */
2630
+ onVolumeMutedDidChange(message) {
2631
+ this.#volumeMuted = message.isMuted;
2632
+ this.emit("volumeMutedDidChange", this.#volumeMuted);
2633
+ }
2634
+ /**
2635
+ * Extracts output device UID and cluster information from a device info message.
2636
+ *
2637
+ * @param message - The device info message.
2638
+ */
2639
+ #updateDeviceInfo(message) {
2640
+ const previousClusterID = this.#clusterID;
2641
+ const previousIsLeader = this.#isClusterLeader;
2642
+ this.#outputDeviceUID = message.clusterID || message.deviceUID || message.uniqueIdentifier || null;
2643
+ this.#clusterID = message.clusterID || null;
2644
+ this.#clusterType = message.clusterType ?? 0;
2645
+ this.#isClusterAware = message.isClusterAware ?? false;
2646
+ this.#isClusterLeader = message.isClusterLeader ?? false;
2647
+ if (this.#clusterID !== previousClusterID || this.#isClusterLeader !== previousIsLeader) this.emit("clusterChanged", this.#clusterID, this.#isClusterLeader);
2648
+ }
2649
+ /**
2650
+ * Gets or creates a Client for the given bundle identifier.
2651
+ * Updates the display name if the client already exists.
2652
+ *
2653
+ * @param bundleIdentifier - The app's bundle identifier.
2654
+ * @param displayName - The app's display name.
2655
+ * @returns The existing or newly created Client.
2656
+ */
2657
+ #client(bundleIdentifier, displayName) {
2658
+ if (bundleIdentifier in this.#clients) {
2659
+ const client = this.#clients[bundleIdentifier];
2660
+ if (displayName) client.updateDisplayName(displayName);
2661
+ return client;
2662
+ } else {
2663
+ const client = new AirPlayClient(bundleIdentifier, displayName);
2664
+ this.#clients[bundleIdentifier] = client;
2665
+ this.emit("clients", this.#clients);
2666
+ return client;
2667
+ }
2668
+ }
2669
+ /**
2670
+ * Creates a snapshot of the current now-playing state for change detection.
2671
+ *
2672
+ * @returns A NowPlayingSnapshot of the current state.
2673
+ */
2674
+ #createNowPlayingSnapshot() {
2675
+ const client = this.nowPlayingClient;
2676
+ const player = client?.activePlayer ?? null;
2677
+ return {
2678
+ bundleIdentifier: client?.bundleIdentifier ?? null,
2679
+ playerIdentifier: player?.identifier ?? null,
2680
+ playbackState: player?.playbackState ?? Proto$1.PlaybackState_Enum.Unknown,
2681
+ title: player?.title ?? "",
2682
+ artist: player?.artist ?? "",
2683
+ album: player?.album ?? "",
2684
+ genre: player?.genre ?? "",
2685
+ duration: player?.duration ?? 0,
2686
+ playbackRate: player?.playbackRate ?? 0,
2687
+ shuffleMode: player?.shuffleMode ?? Proto$1.ShuffleMode_Enum.Unknown,
2688
+ repeatMode: player?.repeatMode ?? Proto$1.RepeatMode_Enum.Unknown,
2689
+ mediaType: player?.mediaType ?? Proto$1.ContentItemMetadata_MediaType.UnknownMediaType,
2690
+ seriesName: player?.seriesName ?? "",
2691
+ seasonNumber: player?.seasonNumber ?? 0,
2692
+ episodeNumber: player?.episodeNumber ?? 0,
2693
+ contentIdentifier: player?.contentIdentifier ?? "",
2694
+ artworkId: player?.artworkId ?? null,
2695
+ hasArtworkUrl: player?.artworkUrl() != null,
2696
+ hasArtworkData: player?.currentItemArtwork != null,
2697
+ isAlwaysLive: player?.nowPlayingInfo?.isAlwaysLive ?? false,
2698
+ isAdvertisement: player?.nowPlayingInfo?.isAdvertisement ?? false
2699
+ };
2700
+ }
2701
+ /**
2702
+ * Emits the 'activePlayerChanged' event with the current client and player.
2703
+ */
2704
+ #emitActivePlayerChanged() {
2705
+ const client = this.nowPlayingClient;
2706
+ this.emit("activePlayerChanged", client, client?.activePlayer ?? null);
2707
+ }
2708
+ /**
2709
+ * Emits 'nowPlayingChanged' only if the now-playing snapshot has actually changed.
2710
+ */
2711
+ #emitNowPlayingChangedIfNeeded() {
2712
+ const snapshot = this.#createNowPlayingSnapshot();
2713
+ const previous = this.#nowPlayingSnapshot;
2714
+ if (previous && this.#snapshotsEqual(previous, snapshot)) return;
2715
+ this.#nowPlayingSnapshot = snapshot;
2716
+ const client = this.nowPlayingClient;
2717
+ this.emit("nowPlayingChanged", client, client?.activePlayer ?? null);
2718
+ }
2719
+ /**
2720
+ * Compares two NowPlayingSnapshot instances field-by-field for equality.
2721
+ *
2722
+ * @param a - First snapshot.
2723
+ * @param b - Second snapshot.
2724
+ * @returns True if all fields are equal.
2725
+ */
2726
+ #snapshotsEqual(a, b) {
2727
+ 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;
2728
+ }
2729
+ };
2730
+
2731
+ //#endregion
2732
+ //#region src/internal/airplay-volume.ts
2733
+ /**
2734
+ * Volume adjustment step size as a fraction (0.05 = 5%).
2735
+ */
2736
+ const VOLUME_STEP = .05;
2737
+ /**
2738
+ * Minimum interval between volume fade steps in milliseconds.
2739
+ */
2740
+ const FADE_STEP_INTERVAL = 50;
2741
+ /**
2742
+ * Smart volume controller for an AirPlay device.
2743
+ * Automatically chooses between absolute volume (set a specific level) and
2744
+ * relative volume (HID volume up/down keys) based on the device's reported capabilities.
2745
+ */
2746
+ var AirPlayVolume = class {
2747
+ /**
2748
+ * @returns The underlying AirPlay Protocol instance.
2749
+ */
2750
+ get #protocol() {
2751
+ return this.#device[PROTOCOL];
2752
+ }
2753
+ /**
2754
+ * @returns The AirPlay state for volume and capability information.
2755
+ */
2756
+ get #state() {
2757
+ return this.#device.state;
2758
+ }
2759
+ #device;
2760
+ /**
2761
+ * Creates a new Volume controller.
2762
+ *
2763
+ * @param device - The AirPlay device to control volume for.
2764
+ */
2765
+ constructor(device) {
2766
+ this.#device = device;
2767
+ }
2768
+ /**
2769
+ * Decreases the volume by one step. Uses absolute volume when available,
2770
+ * falls back to HID relative volume keys otherwise.
2771
+ *
2772
+ * @throws CommandError when volume control is not available.
2773
+ */
2774
+ async down() {
2775
+ switch (this.#state.volumeCapabilities) {
2776
+ case Proto$1.VolumeCapabilities_Enum.Absolute:
2777
+ case Proto$1.VolumeCapabilities_Enum.Both:
2778
+ const newVolume = Math.max(0, this.#state.volume - VOLUME_STEP);
2779
+ await this.set(newVolume);
2780
+ break;
2781
+ case Proto$1.VolumeCapabilities_Enum.Relative:
2782
+ await this.#device.remote.volumeDown();
2783
+ break;
2784
+ default: throw new CommandError("Volume control is not available.");
2785
+ }
2786
+ }
2787
+ /**
2788
+ * Increases the volume by one step. Uses absolute volume when available,
2789
+ * falls back to HID relative volume keys otherwise.
2790
+ *
2791
+ * @throws CommandError when volume control is not available.
2792
+ */
2793
+ async up() {
2794
+ switch (this.#state.volumeCapabilities) {
2795
+ case Proto$1.VolumeCapabilities_Enum.Absolute:
2796
+ case Proto$1.VolumeCapabilities_Enum.Both:
2797
+ const newVolume = Math.min(1, Math.max(0, this.#state.volume + VOLUME_STEP));
2798
+ await this.set(newVolume);
2799
+ break;
2800
+ case Proto$1.VolumeCapabilities_Enum.Relative:
2801
+ await this.#device.remote.volumeUp();
2802
+ break;
2803
+ default: throw new CommandError("Volume control is not available.");
2804
+ }
2805
+ }
2806
+ /**
2807
+ * Fetches the current volume level from the device.
2808
+ *
2809
+ * @returns The volume level as a float between 0.0 and 1.0.
2810
+ * @throws CommandError when no output device is active or the request fails.
2811
+ */
2812
+ async get() {
2813
+ if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
2814
+ const response = await this.#protocol.dataStream.exchange(DataStreamMessage.getVolume(this.#state.outputDeviceUID));
2815
+ if (response.type === Proto$1.ProtocolMessage_Type.GET_VOLUME_RESULT_MESSAGE) return DataStreamMessage.getExtension(response, Proto$1.getVolumeResultMessage).volume;
2816
+ throw new CommandError("Failed to get volume.");
2817
+ }
2818
+ /**
2819
+ * Sets the volume to an absolute level.
2820
+ *
2821
+ * @param volume - The desired volume level (clamped to 0.0 - 1.0).
2822
+ * @throws CommandError when no output device is active or absolute volume is not supported.
2823
+ */
2824
+ async set(volume) {
2825
+ if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
2826
+ if (![Proto$1.VolumeCapabilities_Enum.Absolute, Proto$1.VolumeCapabilities_Enum.Both].includes(this.#state.volumeCapabilities)) throw new CommandError("Absolute volume control is not available.");
2827
+ volume = Math.min(1, Math.max(0, volume));
2828
+ this.#protocol.context.logger.info(`Setting volume to ${volume} for device ${this.#state.outputDeviceUID}`);
2829
+ await this.#protocol.dataStream.exchange(DataStreamMessage.setVolume(this.#state.outputDeviceUID, volume));
2830
+ }
2831
+ /**
2832
+ * Mutes the output device.
2833
+ *
2834
+ * @throws CommandError when no output device is active.
2835
+ */
2836
+ async mute() {
2837
+ if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
2838
+ await this.#protocol.dataStream.exchange(DataStreamMessage.setVolumeMuted(this.#state.outputDeviceUID, true));
2839
+ }
2840
+ /**
2841
+ * Unmutes the output device.
2842
+ *
2843
+ * @throws CommandError when no output device is active.
2844
+ */
2845
+ async unmute() {
2846
+ if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
2847
+ await this.#protocol.dataStream.exchange(DataStreamMessage.setVolumeMuted(this.#state.outputDeviceUID, false));
2848
+ }
2849
+ /**
2850
+ * Toggles the mute state of the output device.
2851
+ *
2852
+ * @throws CommandError when no output device is active.
2853
+ */
2854
+ async toggleMute() {
2855
+ if (this.#state.volumeMuted) await this.unmute();
2856
+ else await this.mute();
2857
+ }
2858
+ /**
2859
+ * Sets the volume for a specific output device in a speaker group.
2860
+ * Use this to control individual speakers when multiple devices are grouped.
2861
+ *
2862
+ * @param outputDeviceUID - The unique identifier of the target output device.
2863
+ * @param volume - The desired volume level (clamped to 0.0 - 1.0).
2864
+ */
2865
+ async setForDevice(outputDeviceUID, volume) {
2866
+ volume = Math.min(1, Math.max(0, volume));
2867
+ this.#protocol.context.logger.info(`Setting volume to ${volume} for output device ${outputDeviceUID}`);
2868
+ await this.#protocol.dataStream.exchange(DataStreamMessage.setVolume(outputDeviceUID, volume));
2869
+ }
2870
+ /**
2871
+ * Fetches the volume for a specific output device in a speaker group.
2872
+ *
2873
+ * @param outputDeviceUID - The unique identifier of the target output device.
2874
+ * @returns The volume level as a float between 0.0 and 1.0.
2875
+ */
2876
+ async getForDevice(outputDeviceUID) {
2877
+ const response = await this.#protocol.dataStream.exchange(DataStreamMessage.getVolume(outputDeviceUID));
2878
+ if (response.type === Proto$1.ProtocolMessage_Type.GET_VOLUME_RESULT_MESSAGE) return DataStreamMessage.getExtension(response, Proto$1.getVolumeResultMessage).volume;
2879
+ throw new CommandError("Failed to get volume for output device.");
2880
+ }
2881
+ /**
2882
+ * Mutes a specific output device in a speaker group.
2883
+ *
2884
+ * @param outputDeviceUID - The unique identifier of the target output device.
2885
+ */
2886
+ async muteDevice(outputDeviceUID) {
2887
+ await this.#protocol.dataStream.exchange(DataStreamMessage.setVolumeMuted(outputDeviceUID, true));
2888
+ }
2889
+ /**
2890
+ * Unmutes a specific output device in a speaker group.
2891
+ *
2892
+ * @param outputDeviceUID - The unique identifier of the target output device.
2893
+ */
2894
+ async unmuteDevice(outputDeviceUID) {
2895
+ await this.#protocol.dataStream.exchange(DataStreamMessage.setVolumeMuted(outputDeviceUID, false));
2896
+ }
2897
+ /**
2898
+ * Adjusts the volume by a relative increment/decrement on a specific output device.
2899
+ * This is the method Apple uses internally in Music.app for volume changes.
2900
+ *
2901
+ * @param adjustment - The volume adjustment to apply (IncrementSmall/Medium/Large, DecrementSmall/Medium/Large).
2902
+ * @param outputDeviceUID - Optional UID of the target output device. Defaults to the active device.
2903
+ */
2904
+ async adjust(adjustment, outputDeviceUID) {
2905
+ const uid = outputDeviceUID ?? this.#state.outputDeviceUID;
2906
+ if (!uid) throw new CommandError("No output device active.");
2907
+ await this.#protocol.dataStream.exchange(DataStreamMessage.adjustVolume(adjustment, uid));
2908
+ }
2909
+ /**
2910
+ * Smoothly fades the volume to a target level over a given duration.
2911
+ * Uses linear interpolation with absolute volume set calls.
2912
+ *
2913
+ * @param targetVolume - The target volume level (0.0 - 1.0).
2914
+ * @param durationMs - The fade duration in milliseconds.
2915
+ * @throws CommandError when absolute volume control is not available.
2916
+ */
2917
+ async fade(targetVolume, durationMs) {
2918
+ if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
2919
+ if (![Proto$1.VolumeCapabilities_Enum.Absolute, Proto$1.VolumeCapabilities_Enum.Both].includes(this.#state.volumeCapabilities)) throw new CommandError("Absolute volume control is not available.");
2920
+ targetVolume = Math.min(1, Math.max(0, targetVolume));
2921
+ const startVolume = this.#state.volume;
2922
+ const steps = Math.max(1, Math.floor(durationMs / FADE_STEP_INTERVAL));
2923
+ const stepDuration = durationMs / steps;
2924
+ const volumeDelta = (targetVolume - startVolume) / steps;
2925
+ for (let i = 1; i <= steps; i++) {
2926
+ const volume = i === steps ? targetVolume : Math.min(1, Math.max(0, startVolume + volumeDelta * i));
2927
+ await this.set(volume);
2928
+ if (i < steps) await waitFor(stepDuration);
2929
+ }
2930
+ }
2931
+ };
2932
+
2933
+ //#endregion
2934
+ //#region src/internal/airplay-manager.ts
2935
+ /**
2936
+ * High-level abstraction for an AirPlay device (Apple TV or HomePod).
2937
+ * Manages the full lifecycle: connect, pair/verify, set up control/data/event streams,
2938
+ * and provides access to Remote, State, and Volume controllers.
2939
+ * Supports both transient (PIN-less) and credential-based pairing.
2940
+ */
2941
+ var AirPlayManager = class extends EventEmitter {
2942
+ /**
2943
+ * @returns The underlying AirPlay Protocol instance (accessed via symbol for internal use).
2944
+ */
2945
+ get [PROTOCOL]() {
2946
+ return this.#protocol;
2947
+ }
2948
+ /**
2949
+ * The mDNS discovery result used to connect to this device.
2950
+ */
2951
+ get discoveryResult() {
2952
+ return this.#discoveryResult;
2953
+ }
2954
+ /**
2955
+ * Updates the discovery result, e.g. when the device's address changes.
2956
+ */
2957
+ set discoveryResult(discoveryResult) {
2958
+ this.#discoveryResult = discoveryResult;
2959
+ }
2960
+ /**
2961
+ * Device capabilities derived from the AirPlay feature flags.
2962
+ * Indicates which protocols and features the receiver supports.
2963
+ */
2964
+ get capabilities() {
2965
+ const has = (f) => this.#protocol?.hasReceiverFeature(f) ?? false;
2966
+ return {
2967
+ supportsAudio: has(AirPlayFeatureFlags.SupportsAirPlayAudio),
2968
+ supportsBufferedAudio: has(AirPlayFeatureFlags.SupportsBufferedAudio),
2969
+ supportsPTP: has(AirPlayFeatureFlags.SupportsPTP),
2970
+ supportsRFC2198Redundancy: has(AirPlayFeatureFlags.SupportsRFC2198Redundancy),
2971
+ supportsHangdogRemoteControl: has(AirPlayFeatureFlags.SupportsHangdogRemoteControl),
2972
+ supportsUnifiedMediaControl: has(AirPlayFeatureFlags.SupportsUnifiedMediaControl),
2973
+ supportsTransientPairing: has(AirPlayFeatureFlags.SupportsHKPairingAndAccessControl),
2974
+ supportsSystemPairing: has(AirPlayFeatureFlags.SupportsSystemPairing),
2975
+ supportsCoreUtilsPairing: has(AirPlayFeatureFlags.SupportsCoreUtilsPairingAndEncryption)
2976
+ };
2977
+ }
2978
+ /**
2979
+ * Whether the control stream TCP connection is currently active.
2980
+ */
2981
+ get isConnected() {
2982
+ return this.#protocol?.controlStream?.isConnected ?? false;
2983
+ }
2984
+ /**
2985
+ * Raw receiver info dictionary from the /info endpoint, or undefined before connect.
2986
+ */
2987
+ get receiverInfo() {
2988
+ return this.#protocol?.receiverInfo;
2989
+ }
2990
+ /**
2991
+ * The Artwork controller for fetching now-playing artwork from all sources.
2992
+ */
2993
+ get artwork() {
2994
+ return this.#artwork;
2995
+ }
2996
+ /**
2997
+ * The Remote controller for HID keys, SendCommand, text input, and touch.
2998
+ */
2999
+ get remote() {
3000
+ return this.#remote;
3001
+ }
3002
+ /**
3003
+ * The State tracker for now-playing, volume, keyboard, and output device state.
3004
+ */
3005
+ get state() {
3006
+ return this.#state;
3007
+ }
3008
+ /**
3009
+ * The Volume controller for absolute and relative volume adjustments.
3010
+ */
3011
+ get volume() {
3012
+ return this.#volume;
3013
+ }
3014
+ /**
3015
+ * The shared PTP timing server, if one is assigned for multi-room sync.
3016
+ */
3017
+ get timingServer() {
3018
+ return this.#timingServer;
3019
+ }
3020
+ /**
3021
+ * Assigns a PTP timing server for multi-room audio synchronization.
3022
+ */
3023
+ set timingServer(timingServer) {
3024
+ this.#timingServer = timingServer;
3025
+ }
3026
+ #artwork;
3027
+ #remote;
3028
+ #state;
3029
+ #volume;
3030
+ #credentials;
3031
+ #disconnect = false;
3032
+ #discoveryResult;
3033
+ #identity;
3034
+ #feedbackInterval;
3035
+ #keys;
3036
+ #lastArtworkId = null;
3037
+ #playUrlProtocol;
3038
+ #prevDataStream;
3039
+ #prevEventStream;
3040
+ #protocol;
3041
+ #streamProtocol;
3042
+ #streamFeedbackInterval;
3043
+ #timingServer;
3044
+ /**
3045
+ * Creates a new AirPlayDevice.
3046
+ *
3047
+ * @param discoveryResult - The mDNS discovery result for the target device.
3048
+ * @param identity - Optional partial device identity to present during pairing.
3049
+ */
3050
+ constructor(discoveryResult, identity) {
3051
+ super();
3052
+ this.#discoveryResult = discoveryResult;
3053
+ this.#identity = identity;
3054
+ this.#artwork = new AirPlayArtwork(this);
3055
+ this.#remote = new AirPlayRemote(this);
3056
+ this.#state = new AirPlayState(this);
3057
+ this.onClose = this.onClose.bind(this);
3058
+ this.onError = this.onError.bind(this);
3059
+ this.onNowPlayingChanged = this.onNowPlayingChanged.bind(this);
3060
+ this.onTimeout = this.onTimeout.bind(this);
3061
+ this.#volume = new AirPlayVolume(this);
3062
+ }
3063
+ /**
3064
+ * Connects to the AirPlay device, performs pairing/verification,
3065
+ * and sets up all streams (control, data, event). Emits 'connected' on success.
3066
+ * If credentials are set, uses pair-verify; otherwise uses transient pairing.
3067
+ */
3068
+ async connect() {
3069
+ if (this.#protocol) {
3070
+ this.#protocol.controlStream.off("close", this.onClose);
3071
+ this.#protocol.controlStream.off("error", this.onError);
3072
+ this.#protocol.controlStream.off("timeout", this.onTimeout);
3073
+ try {
3074
+ this.#protocol.disconnect();
3075
+ } catch {}
3076
+ }
3077
+ this.#disconnect = false;
3078
+ this.#state.clear();
3079
+ this.#protocol = new Protocol(this.#discoveryResult, this.#identity);
3080
+ this.#protocol.controlStream.on("close", this.onClose);
3081
+ this.#protocol.controlStream.on("error", this.onError);
3082
+ this.#protocol.controlStream.on("timeout", this.onTimeout);
3083
+ await this.#protocol.connect();
3084
+ await this.#protocol.fetchInfo();
3085
+ if (this.#credentials) this.#keys = await this.#protocol.verify.start(this.#credentials);
3086
+ else {
3087
+ await this.#protocol.pairing.start();
3088
+ this.#keys = await this.#protocol.pairing.transient();
3089
+ }
3090
+ await this.#setup();
3091
+ this.emit("connected");
3092
+ }
3093
+ /**
3094
+ * Gracefully disconnects from the device, clears intervals, and tears down all streams.
3095
+ */
3096
+ disconnect() {
3097
+ this.#disconnect = true;
3098
+ if (this.#feedbackInterval) {
3099
+ clearInterval(this.#feedbackInterval);
3100
+ this.#feedbackInterval = void 0;
3101
+ }
3102
+ this.#cleanupPlayUrl();
3103
+ this.#cleanupStream();
3104
+ this.#unsubscribe();
3105
+ this.#protocol.disconnect();
3106
+ this.emit("disconnected", false);
3107
+ }
3108
+ /**
3109
+ * Disconnects gracefully, swallowing any errors during cleanup.
3110
+ */
3111
+ disconnectSafely() {
3112
+ try {
3113
+ this.disconnect();
3114
+ } catch (err) {
3115
+ this.#protocol?.context?.logger?.warn("[device]", "Error during safe disconnect", err);
3116
+ }
3117
+ }
3118
+ /**
3119
+ * Enables or disables conversation detection on the output device (HomePod feature).
3120
+ *
3121
+ * @param enabled - Whether to enable conversation detection.
3122
+ * @throws Error when no output device is active.
3123
+ */
3124
+ async setConversationDetectionEnabled(enabled) {
3125
+ const outputDeviceUID = this.#state.outputDeviceUID;
3126
+ if (!outputDeviceUID) throw new Error("No output device active.");
3127
+ await this.#protocol.dataStream.send(DataStreamMessage.setConversationDetectionEnabled(enabled, outputDeviceUID));
3128
+ }
3129
+ /**
3130
+ * Adds devices to the current multi-room output context.
3131
+ *
3132
+ * @param deviceUIDs - UIDs of the devices to add.
3133
+ */
3134
+ async addOutputDevices(deviceUIDs) {
3135
+ await this.#protocol.dataStream.exchange(DataStreamMessage.modifyOutputContext(deviceUIDs));
3136
+ }
3137
+ /**
3138
+ * Removes devices from the current multi-room output context.
3139
+ *
3140
+ * @param deviceUIDs - UIDs of the devices to remove.
3141
+ */
3142
+ async removeOutputDevices(deviceUIDs) {
3143
+ await this.#protocol.dataStream.exchange(DataStreamMessage.modifyOutputContext([], deviceUIDs));
3144
+ }
3145
+ /**
3146
+ * Replaces the entire multi-room output context with the given devices.
3147
+ *
3148
+ * @param deviceUIDs - UIDs of the devices to set as the output context.
3149
+ */
3150
+ async setOutputDevices(deviceUIDs) {
3151
+ await this.#protocol.dataStream.exchange(DataStreamMessage.modifyOutputContext([], [], deviceUIDs));
3152
+ }
3153
+ /**
3154
+ * Plays a URL on the device (the device fetches and plays the content).
3155
+ * Creates a separate Protocol instance to avoid conflicting with the
3156
+ * existing remote control session, following the same approach as pyatv.
3157
+ *
3158
+ * @param url - The media URL to play.
3159
+ * @param position - Start position in seconds (defaults to 0).
3160
+ * @throws Error when not connected.
3161
+ */
3162
+ async playUrl(url, position = 0) {
3163
+ if (!this.#keys) throw new Error("Not connected. Call connect() first.");
3164
+ this.#playUrlProtocol?.disconnect();
3165
+ const playProtocol = new Protocol(this.#discoveryResult, this.#identity);
3166
+ if (this.#timingServer) playProtocol.useTimingServer(this.#timingServer);
3167
+ try {
3168
+ await playProtocol.connect();
3169
+ await playProtocol.fetchInfo();
3170
+ let keys;
3171
+ if (this.#credentials) keys = await playProtocol.verify.start(this.#credentials);
3172
+ else {
3173
+ await playProtocol.pairing.start();
3174
+ keys = await playProtocol.pairing.transient();
3175
+ }
3176
+ playProtocol.controlStream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
3177
+ this.#playUrlProtocol = playProtocol;
3178
+ await playProtocol.playUrl(url, keys.sharedSecret, keys.pairingId, position);
3179
+ } catch (err) {
3180
+ if (this.#playUrlProtocol !== playProtocol) playProtocol.disconnect();
3181
+ throw err;
3182
+ }
3183
+ }
3184
+ /**
3185
+ * Waits for the current URL playback to finish, then cleans up the play URL protocol.
3186
+ */
3187
+ async waitForPlaybackEnd() {
3188
+ if (!this.#playUrlProtocol) return;
3189
+ try {
3190
+ await this.#playUrlProtocol.waitForPlaybackEnd();
3191
+ } finally {
3192
+ this.#cleanupPlayUrl();
3193
+ }
3194
+ }
3195
+ /**
3196
+ * Stops the current URL playback and cleans up the dedicated play URL protocol.
3197
+ */
3198
+ stopPlayUrl() {
3199
+ this.#cleanupPlayUrl();
3200
+ }
3201
+ /**
3202
+ * Stops, disconnects, and discards the dedicated play URL protocol instance.
3203
+ */
3204
+ #cleanupPlayUrl() {
3205
+ if (this.#playUrlProtocol) {
3206
+ this.#playUrlProtocol.stopPlayUrl();
3207
+ this.#playUrlProtocol.disconnect();
3208
+ this.#playUrlProtocol = void 0;
3209
+ }
3210
+ }
3211
+ /**
3212
+ * Streams audio from a source to the device via RAOP/RTP.
3213
+ * Creates a separate Protocol instance to avoid conflicting with the
3214
+ * existing remote control session, following the same approach as playUrl.
3215
+ *
3216
+ * @param source - The audio source to stream (e.g. MP3, WAV, URL, live).
3217
+ */
3218
+ async streamAudio(source) {
3219
+ if (!this.#keys) throw new Error("Not connected. Call connect() first.");
3220
+ this.#cleanupStream();
3221
+ const streamProtocol = new Protocol(this.#discoveryResult, this.#identity);
3222
+ if (this.#timingServer) streamProtocol.useTimingServer(this.#timingServer);
3223
+ try {
3224
+ await streamProtocol.connect();
3225
+ await streamProtocol.fetchInfo();
3226
+ let keys;
3227
+ if (this.#credentials) keys = await streamProtocol.verify.start(this.#credentials);
3228
+ else {
3229
+ await streamProtocol.pairing.start();
3230
+ keys = await streamProtocol.pairing.transient();
3231
+ }
3232
+ streamProtocol.controlStream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
3233
+ this.#streamProtocol = streamProtocol;
3234
+ await streamProtocol.setupEventStreamForAudioStreaming(keys.sharedSecret, keys.pairingId);
3235
+ this.#streamFeedbackInterval = setInterval(async () => {
3236
+ try {
3237
+ await streamProtocol.feedback();
3238
+ } catch {}
3239
+ }, FEEDBACK_INTERVAL);
3240
+ await streamProtocol.setupAudioStream(source);
3241
+ } catch (err) {
3242
+ if (this.#streamProtocol !== streamProtocol) streamProtocol.disconnect();
3243
+ throw err;
3244
+ } finally {
3245
+ this.#cleanupStream();
3246
+ }
3247
+ }
3248
+ /**
3249
+ * Stops the current audio stream and cleans up the dedicated stream protocol.
3250
+ */
3251
+ stopStreamAudio() {
3252
+ this.#cleanupStream();
3253
+ }
3254
+ /**
3255
+ * Stops, disconnects, and discards the dedicated audio stream protocol instance.
3256
+ */
3257
+ #cleanupStream() {
3258
+ if (this.#streamFeedbackInterval) {
3259
+ clearInterval(this.#streamFeedbackInterval);
3260
+ this.#streamFeedbackInterval = void 0;
3261
+ }
3262
+ if (this.#streamProtocol) {
3263
+ this.#streamProtocol.disconnect();
3264
+ this.#streamProtocol = void 0;
3265
+ }
3266
+ }
3267
+ /**
3268
+ * Sets the audio listening mode on the device (HomePod).
3269
+ *
3270
+ * @param mode - Listening mode string (e.g. 'Default', 'Vivid', 'LateNight').
3271
+ */
3272
+ async setListeningMode(mode) {
3273
+ const uid = this.state.outputDeviceUID;
3274
+ if (uid) await this.#protocol.dataStream.send(DataStreamMessage.setListeningMode(mode, uid));
3275
+ }
3276
+ /**
3277
+ * Sets the audio routing mode on the receiver via the control stream.
3278
+ *
3279
+ * @param mode - Audio mode (e.g. 'default', 'moviePlayback', 'spoken').
3280
+ */
3281
+ async setAudioMode(mode) {
3282
+ await this.#protocol.controlStream.setAudioMode(mode);
3283
+ }
3284
+ /**
3285
+ * Triggers an audio fade on the device.
3286
+ *
3287
+ * @param fadeType - The fade type (0 = fade out, 1 = fade in).
3288
+ */
3289
+ async audioFade(fadeType) {
3290
+ await this.#protocol.dataStream.send(DataStreamMessage.audioFade(fadeType));
3291
+ }
3292
+ /**
3293
+ * Wakes the device from sleep via the DataStream.
3294
+ */
3295
+ async wake() {
3296
+ await this.#protocol.dataStream.send(DataStreamMessage.wakeDevice());
3297
+ }
3298
+ /**
3299
+ * Requests the playback queue from the device.
3300
+ *
3301
+ * @param length - Maximum number of queue items to retrieve.
3302
+ */
3303
+ async requestPlaybackQueue(length) {
3304
+ const location = this.#state.nowPlayingClient?.activePlayer?.playbackQueue?.location ?? 0;
3305
+ await this.#protocol.dataStream.exchange(DataStreamMessage.playbackQueueRequest(location, length));
3306
+ }
3307
+ /**
3308
+ * Sends a raw MRP command to the device via the DataStream.
3309
+ *
3310
+ * @param command - The command to send.
3311
+ * @param options - Optional command options.
3312
+ */
3313
+ async sendCommand(command, options) {
3314
+ await this.#protocol.dataStream.exchange(DataStreamMessage.sendCommand(command, options));
3315
+ }
3316
+ /**
3317
+ * Sets the pairing credentials for pair-verify authentication.
3318
+ * Must be called before connect() if credential-based pairing is desired.
3319
+ *
3320
+ * @param credentials - The accessory credentials obtained from pair-setup.
3321
+ */
3322
+ setCredentials(credentials) {
3323
+ this.#credentials = credentials;
3324
+ }
3325
+ /**
3326
+ * Sends a periodic feedback request to keep the AirPlay session alive.
3327
+ */
3328
+ async #feedback() {
3329
+ try {
3330
+ await this.#protocol.feedback();
3331
+ } catch (err) {
3332
+ this.#protocol.context.logger.error("Feedback error", err);
3333
+ }
3334
+ }
3335
+ /**
3336
+ * Handles the control stream close event. Emits 'disconnected' with unexpected=true if not intentional.
3337
+ */
3338
+ onClose() {
3339
+ this.#protocol.context.logger.net("onClose() called on airplay device.");
3340
+ if (this.#disconnect) return;
3341
+ this.#disconnect = true;
3342
+ this.disconnectSafely();
3343
+ this.emit("disconnected", true);
3344
+ }
3345
+ /**
3346
+ * Handles stream error events by logging them.
3347
+ *
3348
+ * @param err - The error that occurred.
3349
+ */
3350
+ onError(err) {
3351
+ this.#protocol.context.logger.error("AirPlay error", err);
3352
+ }
3353
+ /**
3354
+ * Handles now-playing changes to auto-fetch artwork on track changes.
3355
+ */
3356
+ onNowPlayingChanged(_client, player) {
3357
+ const artworkId = player?.artworkId ?? null;
3358
+ if (artworkId !== this.#lastArtworkId) {
3359
+ this.#lastArtworkId = artworkId;
3360
+ this.requestPlaybackQueue(1).catch(() => {});
3361
+ }
3362
+ }
3363
+ /**
3364
+ * Handles stream timeout events by destroying the control stream.
3365
+ */
3366
+ onTimeout() {
3367
+ this.#protocol.context.logger.error("AirPlay timeout");
3368
+ this.#protocol.controlStream.destroy();
3369
+ }
3370
+ /**
3371
+ * Sets up encryption, event/data streams, feedback interval, and initial state subscriptions.
3372
+ * Called after successful pairing/verification.
3373
+ */
3374
+ async #setup() {
3375
+ const keys = this.#keys;
3376
+ this.#protocol.controlStream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
3377
+ this.#unsubscribe();
3378
+ if (this.#timingServer) this.#protocol.useTimingServer(this.#timingServer);
3379
+ try {
3380
+ this.#prevDataStream?.off("error", this.onError);
3381
+ this.#prevDataStream?.off("timeout", this.onTimeout);
3382
+ this.#prevEventStream?.off("error", this.onError);
3383
+ this.#prevEventStream?.off("timeout", this.onTimeout);
3384
+ await this.#protocol.setupEventStream(keys.sharedSecret, keys.pairingId);
3385
+ await this.#protocol.setupDataStream(keys.sharedSecret, () => this.#subscribe());
3386
+ this.#protocol.dataStream.on("error", this.onError);
3387
+ this.#protocol.dataStream.on("timeout", this.onTimeout);
3388
+ this.#protocol.eventStream.on("error", this.onError);
3389
+ this.#protocol.eventStream.on("timeout", this.onTimeout);
3390
+ this.#prevDataStream = this.#protocol.dataStream;
3391
+ this.#prevEventStream = this.#protocol.eventStream;
3392
+ if (this.#feedbackInterval) clearInterval(this.#feedbackInterval);
3393
+ this.#feedbackInterval = setInterval(async () => await this.#feedback(), FEEDBACK_INTERVAL);
3394
+ await this.#protocol.dataStream.exchange(DataStreamMessage.deviceInfo(keys.pairingId, this.#protocol.context.identity));
3395
+ this.#protocol.dataStream.send(DataStreamMessage.setConnectionState());
3396
+ this.#protocol.dataStream.send(DataStreamMessage.clientUpdatesConfig(true, true, true, true));
3397
+ await this.#protocol.dataStream.exchange(DataStreamMessage.getState());
3398
+ this.#lastArtworkId = null;
3399
+ this.#state.on("nowPlayingChanged", this.onNowPlayingChanged);
3400
+ this.#protocol.context.logger.info("Protocol ready.");
3401
+ } catch (err) {
3402
+ if (this.#feedbackInterval) {
3403
+ clearInterval(this.#feedbackInterval);
3404
+ this.#feedbackInterval = void 0;
3405
+ }
3406
+ this.#protocol.context.logger.error("[device]", "Setup failed, cleaning up", err);
3407
+ this.#protocol.disconnect();
3408
+ throw err;
3409
+ }
3410
+ }
3411
+ /**
3412
+ * Subscribes the state tracker to DataStream events.
3413
+ */
3414
+ #subscribe() {
3415
+ this.#state[STATE_SUBSCRIBE_SYMBOL]();
3416
+ }
3417
+ /**
3418
+ * Unsubscribes the state tracker from DataStream events.
3419
+ */
3420
+ #unsubscribe() {
3421
+ try {
3422
+ this.#state.off("nowPlayingChanged", this.onNowPlayingChanged);
3423
+ this.#state[STATE_UNSUBSCRIBE_SYMBOL]();
3424
+ } catch (err) {
3425
+ this.#protocol.context.logger.error("State unsubscribe error", err);
3426
+ }
3427
+ }
3428
+ };
3429
+
3430
+ //#endregion
3431
+ //#region src/internal/companion-link-state.ts
3432
+ /**
3433
+ * Default text input state when no text input session is active.
3434
+ */
3435
+ const DEFAULT_TEXT_INPUT = {
3436
+ isActive: false,
3437
+ documentText: "",
3438
+ isSecure: false,
3439
+ keyboardType: 0,
3440
+ autocorrection: false,
3441
+ autocapitalization: false
3442
+ };
3443
+ /**
3444
+ * Tracks the state of a Companion Link device: attention state, media controls,
3445
+ * now-playing info, supported actions, text input, and volume availability.
3446
+ * Subscribes to protocol stream events and emits typed state change events.
3447
+ */
3448
+ var CompanionLinkState = class extends EventEmitter {
3449
+ /**
3450
+ * Current attention state of the device (active, idle, screensaver, etc.).
3451
+ */
3452
+ get attentionState() {
3453
+ return this.#attentionState;
3454
+ }
3455
+ /**
3456
+ * Parsed media capabilities indicating which controls are available.
3457
+ */
3458
+ get mediaCapabilities() {
3459
+ return this.#mediaCapabilities;
3460
+ }
3461
+ /**
3462
+ * Raw media control flags bitmask from the device.
3463
+ */
3464
+ get mediaControlFlags() {
3465
+ return this.#mediaControlFlags;
3466
+ }
3467
+ /**
3468
+ * Current now-playing info as a key-value dictionary, or null.
3469
+ */
3470
+ get nowPlayingInfo() {
3471
+ return this.#nowPlayingInfo;
3472
+ }
3473
+ /**
3474
+ * Currently supported actions dictionary, or null.
3475
+ */
3476
+ get supportedActions() {
3477
+ return this.#supportedActions;
3478
+ }
3479
+ /**
3480
+ * Current text input session state.
3481
+ */
3482
+ get textInputState() {
3483
+ return this.#textInputState;
3484
+ }
3485
+ /**
3486
+ * Whether volume control is currently available via the Companion Link protocol.
3487
+ */
3488
+ get volumeAvailable() {
3489
+ return this.#volumeAvailable;
3490
+ }
3491
+ #protocol;
3492
+ #attentionState = "unknown";
3493
+ #mediaCapabilities = parseMediaControlFlags(0);
3494
+ #mediaControlFlags = 0;
3495
+ #nowPlayingInfo = null;
3496
+ #supportedActions = null;
3497
+ #textInputState = { ...DEFAULT_TEXT_INPUT };
3498
+ #volumeAvailable = false;
3499
+ /**
3500
+ * Creates a new CompanionLinkState tracker.
3501
+ *
3502
+ * @param protocol - The Companion Link protocol instance to observe.
3503
+ */
3504
+ constructor(protocol) {
3505
+ super();
3506
+ this.#protocol = protocol;
3507
+ this.onMediaControl = this.onMediaControl.bind(this);
3508
+ this.onSystemStatus = this.onSystemStatus.bind(this);
3509
+ this.onTVSystemStatus = this.onTVSystemStatus.bind(this);
3510
+ this.onNowPlayingInfo = this.onNowPlayingInfo.bind(this);
3511
+ this.onSupportedActions = this.onSupportedActions.bind(this);
3512
+ this.onTextInputStarted = this.onTextInputStarted.bind(this);
3513
+ this.onTextInputStopped = this.onTextInputStopped.bind(this);
3514
+ }
3515
+ /**
3516
+ * Subscribes to protocol stream events and registers interests for push notifications.
3517
+ */
3518
+ subscribe() {
3519
+ const stream = this.#protocol.stream;
3520
+ stream.on("_iMC", this.onMediaControl);
3521
+ stream.on("SystemStatus", this.onSystemStatus);
3522
+ stream.on("TVSystemStatus", this.onTVSystemStatus);
3523
+ stream.on("NowPlayingInfo", this.onNowPlayingInfo);
3524
+ stream.on("SupportedActions", this.onSupportedActions);
3525
+ stream.on("_tiStarted", this.onTextInputStarted);
3526
+ stream.on("_tiStopped", this.onTextInputStopped);
3527
+ this.#protocol.registerInterests(["_iMC"]);
3528
+ this.#protocol.registerInterests(["SystemStatus"]);
3529
+ this.#protocol.registerInterests(["TVSystemStatus"]);
3530
+ this.#protocol.registerInterests(["NowPlayingInfo"]);
3531
+ this.#protocol.registerInterests(["SupportedActions"]);
3532
+ }
3533
+ /**
3534
+ * Unsubscribes from protocol stream events and deregisters interests.
3535
+ */
3536
+ unsubscribe() {
3537
+ const stream = this.#protocol.stream;
3538
+ if (!stream.isConnected) return;
3539
+ stream.off("_iMC", this.onMediaControl);
3540
+ stream.off("SystemStatus", this.onSystemStatus);
3541
+ stream.off("TVSystemStatus", this.onTVSystemStatus);
3542
+ stream.off("NowPlayingInfo", this.onNowPlayingInfo);
3543
+ stream.off("SupportedActions", this.onSupportedActions);
3544
+ stream.off("_tiStarted", this.onTextInputStarted);
3545
+ stream.off("_tiStopped", this.onTextInputStopped);
3546
+ try {
3547
+ this.#protocol.deregisterInterests(["_iMC"]);
3548
+ this.#protocol.deregisterInterests(["SystemStatus"]);
3549
+ this.#protocol.deregisterInterests(["TVSystemStatus"]);
3550
+ this.#protocol.deregisterInterests(["NowPlayingInfo"]);
3551
+ this.#protocol.deregisterInterests(["SupportedActions"]);
3552
+ } catch {}
3553
+ }
3554
+ /**
3555
+ * Fetches the initial attention state and media control status from the device.
3556
+ */
3557
+ async fetchInitialState() {
3558
+ try {
3559
+ const state = await this.#protocol.getAttentionState();
3560
+ this.#attentionState = state;
3561
+ this.emit("attentionStateChanged", state);
3562
+ } catch (err) {
3563
+ this.#protocol.context.logger.warn("[cl-state]", "Failed to fetch initial attention state", err);
3564
+ }
3565
+ try {
3566
+ await this.#protocol.fetchMediaControlStatus();
3567
+ } catch (err) {
3568
+ this.#protocol.context.logger.warn("[cl-state]", "Failed to fetch media control status", err);
3569
+ }
3570
+ }
3571
+ /**
3572
+ * Resets all state to initial/default values.
3573
+ */
3574
+ clear() {
3575
+ this.#attentionState = "unknown";
3576
+ this.#mediaCapabilities = parseMediaControlFlags(0);
3577
+ this.#mediaControlFlags = 0;
3578
+ this.#nowPlayingInfo = null;
3579
+ this.#supportedActions = null;
3580
+ this.#textInputState = { ...DEFAULT_TEXT_INPUT };
3581
+ this.#volumeAvailable = false;
3582
+ }
3583
+ /**
3584
+ * Handles media control flag updates (_iMC). Parses the flags bitmask and
3585
+ * emits events if capabilities or volume availability changed.
3586
+ *
3587
+ * @param data - The raw media control payload.
3588
+ */
3589
+ onMediaControl(data) {
3590
+ try {
3591
+ const flags = Number(data?._mcF ?? 0);
3592
+ if (flags !== this.#mediaControlFlags) {
3593
+ this.#mediaControlFlags = flags;
3594
+ this.#mediaCapabilities = parseMediaControlFlags(flags);
3595
+ const wasVolumeAvailable = this.#volumeAvailable;
3596
+ this.#volumeAvailable = this.#mediaCapabilities.volume;
3597
+ this.emit("mediaControlFlagsChanged", flags, this.#mediaCapabilities);
3598
+ if (wasVolumeAvailable !== this.#volumeAvailable) this.emit("volumeAvailabilityChanged", this.#volumeAvailable);
3599
+ }
3600
+ } catch (err) {
3601
+ this.#protocol.context.logger.error("[cl-state]", "_iMC parse error", err);
3602
+ }
3603
+ }
3604
+ /**
3605
+ * Handles SystemStatus events (attention state changes from non-TV devices).
3606
+ *
3607
+ * @param data - The raw system status payload containing a state code.
3608
+ */
3609
+ onSystemStatus(data) {
3610
+ const state = convertAttentionState(data.state);
3611
+ if (state !== this.#attentionState) {
3612
+ this.#attentionState = state;
3613
+ this.emit("attentionStateChanged", state);
3614
+ }
3615
+ }
3616
+ /**
3617
+ * Handles TVSystemStatus events (attention state changes from Apple TV).
3618
+ *
3619
+ * @param data - The raw TV system status payload containing a state code.
3620
+ */
3621
+ onTVSystemStatus(data) {
3622
+ const state = convertAttentionState(data.state);
3623
+ if (state !== this.#attentionState) {
3624
+ this.#attentionState = state;
3625
+ this.emit("attentionStateChanged", state);
3626
+ }
3627
+ }
3628
+ /**
3629
+ * Handles NowPlayingInfo updates. Decodes the NSKeyedArchiver plist payload
3630
+ * when present, otherwise uses the raw dictionary.
3631
+ *
3632
+ * @param data - The raw now-playing info payload.
3633
+ */
3634
+ onNowPlayingInfo(data) {
3635
+ try {
3636
+ const payload = data;
3637
+ if (payload?.NowPlayingInfoKey) {
3638
+ const raw = payload.NowPlayingInfoKey;
3639
+ const buf = Buffer.from(raw);
3640
+ const plist = Plist.parse(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
3641
+ const decoded = NSKeyedArchiver.decode(plist);
3642
+ this.#nowPlayingInfo = decoded && typeof decoded === "object" && !Array.isArray(decoded) ? decoded : null;
3643
+ } else this.#nowPlayingInfo = payload ?? null;
3644
+ this.emit("nowPlayingInfoChanged", this.#nowPlayingInfo);
3645
+ } catch (err) {
3646
+ this.#protocol.context.logger.error("[cl-state]", "NowPlayingInfo parse error", err);
3647
+ }
3648
+ }
3649
+ /**
3650
+ * Handles SupportedActions updates from the device.
3651
+ *
3652
+ * @param data - The raw supported actions payload.
3653
+ */
3654
+ onSupportedActions(data) {
3655
+ try {
3656
+ const payload = data;
3657
+ this.#supportedActions = payload ?? null;
3658
+ this.emit("supportedActionsChanged", payload ?? {});
3659
+ } catch (err) {
3660
+ this.#protocol.context.logger.error("[cl-state]", "SupportedActions parse error", err);
3661
+ }
3662
+ }
3663
+ /**
3664
+ * Handles the start of a text input session. Parses the plist payload to extract
3665
+ * document text, security mode, keyboard type, and autocorrection settings.
3666
+ *
3667
+ * @param data - The raw text input started payload.
3668
+ */
3669
+ onTextInputStarted(data) {
3670
+ try {
3671
+ const payload = data;
3672
+ let documentText = "";
3673
+ let isSecure = false;
3674
+ let keyboardType = 0;
3675
+ let autocorrection = false;
3676
+ let autocapitalization = false;
3677
+ if (payload?._tiD) {
3678
+ const plistData = Plist.parse(Buffer.from(payload._tiD).buffer);
3679
+ documentText = plistData._tiDT ?? "";
3680
+ isSecure = plistData._tiSR ?? false;
3681
+ keyboardType = plistData._tiKT ?? 0;
3682
+ autocorrection = plistData._tiAC ?? false;
3683
+ autocapitalization = plistData._tiAP ?? false;
3684
+ }
3685
+ this.#textInputState = {
3686
+ isActive: true,
3687
+ documentText,
3688
+ isSecure,
3689
+ keyboardType,
3690
+ autocorrection,
3691
+ autocapitalization
3692
+ };
3693
+ this.emit("textInputChanged", this.#textInputState);
3694
+ } catch (err) {
3695
+ this.#protocol.context.logger.error("[cl-state]", "Text input started parse error", err);
3696
+ }
3697
+ }
3698
+ /**
3699
+ * Handles the end of a text input session. Resets the text input state to defaults.
3700
+ *
3701
+ * @param _data - The raw text input stopped payload (unused).
3702
+ */
3703
+ onTextInputStopped(_data) {
3704
+ this.#textInputState = { ...DEFAULT_TEXT_INPUT };
3705
+ this.emit("textInputChanged", this.#textInputState);
3706
+ }
3707
+ };
3708
+ /**
3709
+ * Parses a media control flags bitmask into a structured MediaCapabilities object.
3710
+ *
3711
+ * @param flags - The raw bitmask from the _iMC event.
3712
+ * @returns An object indicating which media controls are available.
3713
+ */
3714
+ function parseMediaControlFlags(flags) {
3715
+ return {
3716
+ play: (flags & MediaControlFlag.Play) !== 0,
3717
+ pause: (flags & MediaControlFlag.Pause) !== 0,
3718
+ previousTrack: (flags & MediaControlFlag.PreviousTrack) !== 0,
3719
+ nextTrack: (flags & MediaControlFlag.NextTrack) !== 0,
3720
+ fastForward: (flags & MediaControlFlag.FastForward) !== 0,
3721
+ rewind: (flags & MediaControlFlag.Rewind) !== 0,
3722
+ volume: (flags & MediaControlFlag.Volume) !== 0,
3723
+ skipForward: (flags & MediaControlFlag.SkipForward) !== 0,
3724
+ skipBackward: (flags & MediaControlFlag.SkipBackward) !== 0
3725
+ };
3726
+ }
3727
+
3728
+ //#endregion
3729
+ //#region src/internal/companion-link-manager.ts
3730
+ /**
3731
+ * High-level abstraction for a Companion Link device (Apple TV).
3732
+ * Manages the OPack-based Companion Link protocol lifecycle: connect, pair-verify,
3733
+ * session setup, and provides access to HID buttons, app launching, user accounts,
3734
+ * media control, text input, touch, Siri, and system controls.
3735
+ * Requires credentials (obtained from pair-setup) to connect.
3736
+ */
3737
+ var CompanionLinkManager = class extends EventEmitter {
3738
+ /**
3739
+ * @returns The underlying Companion Link Protocol instance (accessed via symbol for internal use).
3740
+ */
3741
+ get [COMPANION_LINK_PROTOCOL]() {
3742
+ return this.#protocol;
3743
+ }
3744
+ /**
3745
+ * The mDNS discovery result used to connect to this device.
3746
+ */
3747
+ get discoveryResult() {
3748
+ return this.#discoveryResult;
3749
+ }
3750
+ /**
3751
+ * Updates the discovery result, e.g. when the device's address changes.
3752
+ */
3753
+ set discoveryResult(discoveryResult) {
3754
+ this.#discoveryResult = discoveryResult;
3755
+ }
3756
+ /**
3757
+ * Whether the Companion Link stream is currently connected.
3758
+ */
3759
+ get isConnected() {
3760
+ return this.#protocol?.stream?.isConnected ?? false;
3761
+ }
3762
+ /**
3763
+ * The state tracker for attention, media controls, now-playing, and text input.
3764
+ */
3765
+ get state() {
3766
+ return this.#state;
3767
+ }
3768
+ /**
3769
+ * Current text input session state (convenience accessor).
3770
+ */
3771
+ get textInputState() {
3772
+ return this.#state.textInputState;
3773
+ }
3774
+ #credentials;
3775
+ #disconnect = false;
3776
+ #discoveryResult;
3777
+ #heartbeatInterval;
3778
+ #keys;
3779
+ #protocol;
3780
+ #state;
3781
+ /**
3782
+ * Creates a new CompanionLinkDevice.
3783
+ *
3784
+ * @param discoveryResult - The mDNS discovery result for the target device.
3785
+ */
3786
+ constructor(discoveryResult) {
3787
+ super();
3788
+ this.#discoveryResult = discoveryResult;
3789
+ this.onClose = this.onClose.bind(this);
3790
+ this.onError = this.onError.bind(this);
3791
+ this.onTimeout = this.onTimeout.bind(this);
3792
+ }
3793
+ /**
3794
+ * Connects to the Companion Link device, performs pair-verify, and sets up
3795
+ * all protocol sessions (system info, TVRC, touch, text input).
3796
+ * Emits 'connected' on success.
3797
+ *
3798
+ * @throws CredentialsError when no credentials are set.
3799
+ */
3800
+ async connect() {
3801
+ if (!this.#credentials) throw new CredentialsError("Credentials are required to connect to a Companion Link device.");
3802
+ if (this.#protocol) {
3803
+ this.#protocol.stream.off("close", this.onClose);
3804
+ this.#protocol.stream.off("error", this.onError);
3805
+ this.#protocol.stream.off("timeout", this.onTimeout);
3806
+ }
3807
+ this.#disconnect = false;
3808
+ this.#protocol = new Protocol$1(this.#discoveryResult);
3809
+ this.#protocol.stream.on("close", this.onClose);
3810
+ this.#protocol.stream.on("error", this.onError);
3811
+ this.#protocol.stream.on("timeout", this.onTimeout);
3812
+ await this.#protocol.connect();
3813
+ this.#keys = await this.#protocol.verify.start(this.#credentials);
3814
+ await this.#setup();
3815
+ this.emit("connected");
3816
+ }
3817
+ /**
3818
+ * Gracefully disconnects from the device, clears heartbeat interval, and unsubscribes from events.
3819
+ */
3820
+ async disconnect() {
3821
+ this.#disconnect = true;
3822
+ if (this.#heartbeatInterval) {
3823
+ clearInterval(this.#heartbeatInterval);
3824
+ this.#heartbeatInterval = void 0;
3825
+ }
3826
+ this.#state?.unsubscribe();
3827
+ await this.#protocol.disconnect();
3828
+ }
3829
+ /**
3830
+ * Disconnects gracefully, swallowing any errors during cleanup.
3831
+ */
3832
+ async disconnectSafely() {
3833
+ try {
3834
+ await this.disconnect();
3835
+ } catch {}
3836
+ }
3837
+ /**
3838
+ * Sets the pairing credentials required for pair-verify authentication.
3839
+ * Must be called before connect().
3840
+ *
3841
+ * @param credentials - The accessory credentials from pair-setup.
3842
+ */
3843
+ async setCredentials(credentials) {
3844
+ this.#credentials = credentials;
3845
+ }
3846
+ /**
3847
+ * Fetches the current attention state of the device (active, idle, screensaver, etc.).
3848
+ *
3849
+ * @returns The current attention state.
3850
+ */
3851
+ async getAttentionState() {
3852
+ return await this.#protocol.getAttentionState();
3853
+ }
3854
+ /**
3855
+ * Fetches the list of apps that can be launched on the device.
3856
+ *
3857
+ * @returns Array of launchable app descriptors.
3858
+ */
3859
+ async getLaunchableApps() {
3860
+ return await this.#protocol.getLaunchableApps();
3861
+ }
3862
+ /**
3863
+ * Fetches the list of user accounts configured on the device.
3864
+ *
3865
+ * @returns Array of user account descriptors.
3866
+ */
3867
+ async getUserAccounts() {
3868
+ return await this.#protocol.getUserAccounts();
3869
+ }
3870
+ /**
3871
+ * Fetches the current now-playing information from the device.
3872
+ *
3873
+ * @returns The now-playing info payload.
3874
+ */
3875
+ async fetchNowPlayingInfo() {
3876
+ return await this.#protocol.fetchNowPlayingInfo();
3877
+ }
3878
+ /**
3879
+ * Fetches the currently supported actions from the device.
3880
+ *
3881
+ * @returns The supported actions payload.
3882
+ */
3883
+ async fetchSupportedActions() {
3884
+ return await this.#protocol.fetchSupportedActions();
3885
+ }
3886
+ /**
3887
+ * Fetches the current media control status (available controls bitmask).
3888
+ *
3889
+ * @returns The media control status payload.
3890
+ */
3891
+ async fetchMediaControlStatus() {
3892
+ return await this.#protocol.fetchMediaControlStatus();
3893
+ }
3894
+ /**
3895
+ * Launches an app on the device by its bundle identifier.
3896
+ *
3897
+ * @param bundleId - The bundle identifier of the app to launch.
3898
+ */
3899
+ async launchApp(bundleId) {
3900
+ await this.#protocol.launchApp(bundleId);
3901
+ }
3902
+ /**
3903
+ * Opens a URL on the device (universal link or app-specific URL scheme).
3904
+ *
3905
+ * @param url - The URL to open.
3906
+ */
3907
+ async launchUrl(url) {
3908
+ await this.#protocol.launchUrl(url);
3909
+ }
3910
+ /**
3911
+ * Sends a media control command (play, pause, next, etc.) via the Companion Link protocol.
3912
+ *
3913
+ * @param command - The media control command key.
3914
+ * @param content - Optional additional content for the command.
3915
+ */
3916
+ async mediaControlCommand(command, content) {
3917
+ await this.#protocol.mediaControlCommand(command, content);
3918
+ }
3919
+ /**
3920
+ * Sends a HID button press via the Companion Link protocol.
3921
+ *
3922
+ * @param command - The HID command key (e.g. 'up', 'select', 'menu').
3923
+ * @param type - Optional press type (short, long, double).
3924
+ * @param holdDelayMs - Optional hold duration in milliseconds for long presses.
3925
+ */
3926
+ async pressButton(command, type, holdDelayMs) {
3927
+ await this.#protocol.pressButton(command, type, holdDelayMs);
3928
+ }
3929
+ /**
3930
+ * Switches to a different user account on the device.
3931
+ *
3932
+ * @param accountId - The ID of the user account to switch to.
3933
+ */
3934
+ async switchUserAccount(accountId) {
3935
+ await this.#protocol.switchUserAccount(accountId);
3936
+ }
3937
+ /**
3938
+ * Sets the text input field to the given text, replacing any existing content.
3939
+ *
3940
+ * @param text - The text to set.
3941
+ */
3942
+ async textSet(text) {
3943
+ await this.#protocol.textInputCommand(text, true);
3944
+ }
3945
+ /**
3946
+ * Appends text to the current text input field content.
3947
+ *
3948
+ * @param text - The text to append.
3949
+ */
3950
+ async textAppend(text) {
3951
+ await this.#protocol.textInputCommand(text, false);
3952
+ }
3953
+ /**
3954
+ * Clears the text input field.
3955
+ */
3956
+ async textClear() {
3957
+ await this.#protocol.textInputCommand("", true);
3958
+ }
3959
+ /**
3960
+ * Sends a raw touch event to the device.
3961
+ *
3962
+ * @param finger - Finger index (0-based).
3963
+ * @param phase - Touch phase (0 = Began, 1 = Moved, 2 = Ended).
3964
+ * @param x - Horizontal position.
3965
+ * @param y - Vertical position.
3966
+ */
3967
+ async sendTouchEvent(finger, phase, x, y) {
3968
+ await this.#protocol.sendTouchEvent(finger, phase, x, y);
3969
+ }
3970
+ /**
3971
+ * Simulates a tap at the given coordinates.
3972
+ *
3973
+ * @param x - Horizontal position (defaults to center 500).
3974
+ * @param y - Vertical position (defaults to center 500).
3975
+ */
3976
+ async tap(x = 500, y = 500) {
3977
+ await this.sendTouchEvent(0, 0, x, y);
3978
+ await waitFor(50);
3979
+ await this.sendTouchEvent(0, 2, x, y);
3980
+ }
3981
+ /**
3982
+ * Simulates a swipe gesture in the given direction.
3983
+ *
3984
+ * @param direction - Swipe direction.
3985
+ * @param duration - Swipe duration in milliseconds (defaults to 200).
3986
+ */
3987
+ async swipe(direction, duration = 200) {
3988
+ const [startX, startY, endX, endY] = {
3989
+ up: [
3990
+ 500,
3991
+ 700,
3992
+ 500,
3993
+ 300
3994
+ ],
3995
+ down: [
3996
+ 500,
3997
+ 300,
3998
+ 500,
3999
+ 700
4000
+ ],
4001
+ left: [
4002
+ 700,
4003
+ 500,
4004
+ 300,
4005
+ 500
4006
+ ],
4007
+ right: [
4008
+ 300,
4009
+ 500,
4010
+ 700,
4011
+ 500
4012
+ ]
4013
+ }[direction];
4014
+ const steps = Math.max(4, Math.floor(duration / 50));
4015
+ const deltaX = (endX - startX) / steps;
4016
+ const deltaY = (endY - startY) / steps;
4017
+ const stepDuration = duration / steps;
4018
+ await this.sendTouchEvent(0, 0, startX, startY);
4019
+ for (let i = 1; i < steps; i++) {
4020
+ await waitFor(stepDuration);
4021
+ await this.sendTouchEvent(0, 1, Math.round(startX + deltaX * i), Math.round(startY + deltaY * i));
4022
+ }
4023
+ await waitFor(stepDuration);
4024
+ await this.sendTouchEvent(0, 2, endX, endY);
4025
+ }
4026
+ /**
4027
+ * Toggles closed captions on the device.
4028
+ */
4029
+ async toggleCaptions() {
4030
+ await this.#protocol.toggleCaptions();
4031
+ }
4032
+ /**
4033
+ * Toggles the system appearance between light and dark mode.
4034
+ *
4035
+ * @param light - True for light mode, false for dark mode.
4036
+ */
4037
+ async toggleSystemAppearance(light) {
4038
+ await this.#protocol.toggleSystemAppearance(light);
4039
+ }
4040
+ /**
4041
+ * Enables or disables the "Reduce Loud Sounds" setting.
4042
+ *
4043
+ * @param enabled - Whether to enable the setting.
4044
+ */
4045
+ async toggleReduceLoudSounds(enabled) {
4046
+ await this.#protocol.toggleReduceLoudSounds(enabled);
4047
+ }
4048
+ /**
4049
+ * Enables or disables finding mode (Find My integration).
4050
+ *
4051
+ * @param enabled - Whether to enable finding mode.
4052
+ */
4053
+ async toggleFindingMode(enabled) {
4054
+ await this.#protocol.toggleFindingMode(enabled);
4055
+ }
4056
+ /**
4057
+ * Fetches the "Up Next" queue from the device.
4058
+ *
4059
+ * @param paginationToken - Optional token for paginated results.
4060
+ * @returns The Up Next queue payload.
4061
+ */
4062
+ async fetchUpNext(paginationToken) {
4063
+ return await this.#protocol.fetchUpNext(paginationToken);
4064
+ }
4065
+ /**
4066
+ * Adds an item to the "Up Next" queue.
4067
+ *
4068
+ * @param identifier - Content item identifier.
4069
+ * @param kind - Content kind descriptor.
4070
+ */
4071
+ async addToUpNext(identifier, kind) {
4072
+ await this.#protocol.addToUpNext(identifier, kind);
4073
+ }
4074
+ /**
4075
+ * Removes an item from the "Up Next" queue.
4076
+ *
4077
+ * @param identifier - Content item identifier.
4078
+ * @param kind - Content kind descriptor.
4079
+ */
4080
+ async removeFromUpNext(identifier, kind) {
4081
+ await this.#protocol.removeFromUpNext(identifier, kind);
4082
+ }
4083
+ /**
4084
+ * Marks a content item as watched.
4085
+ *
4086
+ * @param identifier - Content item identifier.
4087
+ * @param kind - Content kind descriptor.
4088
+ */
4089
+ async markAsWatched(identifier, kind) {
4090
+ await this.#protocol.markAsWatched(identifier, kind);
4091
+ }
4092
+ /**
4093
+ * Starts a Siri session on the device.
4094
+ */
4095
+ async siriStart() {
4096
+ await this.#protocol.siriStart();
4097
+ }
4098
+ /**
4099
+ * Stops the active Siri session on the device.
4100
+ */
4101
+ async siriStop() {
4102
+ await this.#protocol.siriStop();
4103
+ }
4104
+ /**
4105
+ * Sets up encryption, protocol sessions (system info, TVRC, touch, text input),
4106
+ * heartbeat interval, and state tracking. Called after successful pair-verify.
4107
+ */
4108
+ async #setup() {
4109
+ const keys = this.#keys;
4110
+ this.#protocol.stream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
4111
+ try {
4112
+ await this.#protocol.systemInfo(this.#credentials.pairingId);
4113
+ await this.#protocol.sessionStart();
4114
+ await this.#protocol.tvrcSessionStart();
4115
+ await this.#protocol.touchStart();
4116
+ await this.#protocol.tiStart();
4117
+ this.#heartbeatInterval = setInterval(() => {
4118
+ try {
4119
+ this.#protocol.noOp();
4120
+ } catch (err) {
4121
+ this.#protocol.context.logger.error("Heartbeat failed", err);
4122
+ }
4123
+ }, 15e3);
4124
+ if (this.#state) this.#state.removeAllListeners();
4125
+ this.#state = new CompanionLinkState(this.#protocol);
4126
+ this.#state.on("attentionStateChanged", (s) => this.emit("attentionStateChanged", s));
4127
+ this.#state.on("mediaControlFlagsChanged", (f, c) => this.emit("mediaControlFlagsChanged", f, c));
4128
+ this.#state.on("nowPlayingInfoChanged", (i) => this.emit("nowPlayingInfoChanged", i));
4129
+ this.#state.on("supportedActionsChanged", (a) => this.emit("supportedActionsChanged", a));
4130
+ this.#state.on("textInputChanged", (s) => this.emit("textInputChanged", s));
4131
+ this.#state.on("volumeAvailabilityChanged", (a) => this.emit("volumeAvailabilityChanged", a));
4132
+ this.#state.subscribe();
4133
+ await this.#state.fetchInitialState();
4134
+ } catch (err) {
4135
+ clearInterval(this.#heartbeatInterval);
4136
+ this.#heartbeatInterval = void 0;
4137
+ throw err;
4138
+ }
4139
+ }
4140
+ /**
4141
+ * Handles the stream close event. Emits 'disconnected' with unexpected=true if not intentional.
4142
+ */
4143
+ onClose() {
4144
+ this.#protocol.context.logger.net("onClose() called on companion link device.");
4145
+ if (!this.#disconnect) {
4146
+ this.disconnectSafely();
4147
+ this.emit("disconnected", true);
4148
+ } else this.emit("disconnected", false);
4149
+ }
4150
+ /**
4151
+ * Handles stream error events by logging them.
4152
+ *
4153
+ * @param err - The error that occurred.
4154
+ */
4155
+ onError(err) {
4156
+ this.#protocol.context.logger.error("Companion Link error", err);
4157
+ }
4158
+ /**
4159
+ * Handles stream timeout events by destroying the stream.
4160
+ */
4161
+ onTimeout() {
4162
+ this.#protocol.context.logger.error("Companion Link timeout");
4163
+ this.#protocol.stream.destroy();
4164
+ }
4165
+ };
4166
+
4167
+ //#endregion
4168
+ //#region src/pairing/pairing-session.ts
4169
+ /**
4170
+ * Step-based pairing session for Apple TV devices.
4171
+ *
4172
+ * Provides a three-phase pairing flow suitable for external UI frameworks
4173
+ * (e.g., Homey's pairing wizard) where PIN entry happens asynchronously:
4174
+ *
4175
+ * ```ts
4176
+ * const session = tv.createPairingSession();
4177
+ * await session.start(); // Connects and triggers PIN dialog on TV
4178
+ * await session.pin('1234'); // Submits PIN, executes M1-M6
4179
+ * const credentials = await session.end(); // Returns credentials, cleans up
4180
+ * ```
4181
+ */
4182
+ var PairingSession = class {
4183
+ #protocol;
4184
+ #credentials;
4185
+ #discoveryResult;
4186
+ #identity;
4187
+ #finished = false;
4188
+ constructor(discoveryResult, identity) {
4189
+ this.#discoveryResult = discoveryResult;
4190
+ this.#identity = identity;
4191
+ }
4192
+ /**
4193
+ * Connects to the device and triggers the PIN dialog.
4194
+ * After this method returns, the Apple TV displays a 4-digit PIN on screen.
4195
+ */
4196
+ async start() {
4197
+ this.#protocol = new Protocol(this.#discoveryResult, this.#identity);
4198
+ await this.#protocol.connect();
4199
+ await this.#protocol.fetchInfo();
4200
+ await this.#protocol.pairing.start();
4201
+ await this.#protocol.pairing.pinStart();
4202
+ }
4203
+ /**
4204
+ * Submits the PIN and executes the M1-M6 key exchange.
4205
+ *
4206
+ * @param code - The 4-digit PIN displayed on the Apple TV screen.
4207
+ */
4208
+ async pin(code) {
4209
+ if (!this.#protocol) throw new Error("PairingSession.start() must be called before pin().");
4210
+ const internal = this.#protocol.pairing.internal;
4211
+ const m1 = await internal.m1();
4212
+ const m2 = await internal.m2(m1, code);
4213
+ const m3 = await internal.m3(m2);
4214
+ const m4 = await internal.m4(m3);
4215
+ const m5 = await internal.m5(m4);
4216
+ const credentials = await internal.m6(m4, m5);
4217
+ if (!credentials) throw new Error("Pairing failed: could not obtain credentials.");
4218
+ this.#credentials = credentials;
4219
+ }
4220
+ /**
4221
+ * Finishes the pairing session, cleans up the protocol connection,
4222
+ * and returns the obtained credentials.
4223
+ *
4224
+ * @returns Long-term credentials for future connections.
4225
+ */
4226
+ async end() {
4227
+ if (!this.#credentials) throw new Error("PairingSession.pin() must be called before end().");
4228
+ const credentials = this.#credentials;
4229
+ this.#cleanup();
4230
+ return credentials;
4231
+ }
4232
+ /**
4233
+ * Aborts the pairing session and cleans up without returning credentials.
4234
+ * Use this when the user cancels pairing.
4235
+ */
4236
+ abort() {
4237
+ this.#cleanup();
4238
+ }
4239
+ #cleanup() {
4240
+ if (this.#finished) return;
4241
+ this.#finished = true;
4242
+ try {
4243
+ this.#protocol?.disconnect();
4244
+ } catch {}
4245
+ }
4246
+ };
4247
+
4248
+ //#endregion
4249
+ //#region src/device/device.ts
4250
+ /**
4251
+ * Abstract base class for all Apple devices.
4252
+ * Provides shared controllers and lifecycle management.
4253
+ */
4254
+ var AbstractDevice = class extends EventEmitter {
4255
+ #airplay;
4256
+ #discoveryResult;
4257
+ #identity;
4258
+ remote;
4259
+ playback;
4260
+ state;
4261
+ volume;
4262
+ artwork;
4263
+ media;
4264
+ multiroom;
4265
+ constructor(options) {
4266
+ super();
4267
+ if (!options.airplay && !options.address) throw new Error("Either `airplay` discovery result or `address` must be provided.");
4268
+ this.#discoveryResult = options.airplay ?? {
4269
+ id: options.address,
4270
+ txt: {},
4271
+ fqdn: options.address,
4272
+ address: options.address,
4273
+ modelName: "",
4274
+ familyName: null,
4275
+ service: {
4276
+ port: 7e3,
4277
+ protocol: "tcp",
4278
+ type: "_airplay._tcp"
4279
+ },
4280
+ packet: null
4281
+ };
4282
+ this.#identity = options.identity;
4283
+ this.#airplay = new AirPlayManager(this.#discoveryResult, this.#identity);
4284
+ const timingServer = options.timingServer ?? getGlobalTimingServer();
4285
+ if (timingServer) this.#airplay.timingServer = timingServer;
4286
+ this.remote = new RemoteController(this.#airplay);
4287
+ this.playback = new PlaybackController(this.#airplay);
4288
+ this.state = new StateController(this.#airplay);
4289
+ this.volume = new VolumeController(this.#airplay);
4290
+ this.artwork = new ArtworkController(this.#airplay);
4291
+ this.media = new MediaController(this.#airplay);
4292
+ this.multiroom = new MultiroomController(this.#airplay);
4293
+ this.#airplay.on("connected", () => this.onAirPlayConnected());
4294
+ this.#airplay.on("disconnected", (unexpected) => this.onAirPlayDisconnected(unexpected));
4295
+ }
4296
+ /**
4297
+ * The unique identifier of the device (from mDNS discovery).
4298
+ */
4299
+ get id() {
4300
+ return this.#discoveryResult.id;
4301
+ }
4302
+ /**
4303
+ * The human-readable name of the device.
4304
+ */
4305
+ get name() {
4306
+ return this.#discoveryResult.familyName ?? this.#discoveryResult.fqdn;
4307
+ }
4308
+ /**
4309
+ * The IP address of the device.
4310
+ */
4311
+ get address() {
4312
+ return this.#discoveryResult.address;
4313
+ }
4314
+ /**
4315
+ * Whether the device is currently connected.
4316
+ */
4317
+ get isConnected() {
4318
+ return this.#airplay.isConnected;
4319
+ }
4320
+ /**
4321
+ * Raw receiver info from the AirPlay /info endpoint.
4322
+ */
4323
+ get receiverInfo() {
4324
+ return this.#airplay.receiverInfo;
4325
+ }
4326
+ /**
4327
+ * AirPlay device capabilities (features supported by the receiver).
4328
+ */
4329
+ get capabilities() {
4330
+ return this.#airplay.capabilities;
4331
+ }
4332
+ /**
4333
+ * Updates the discovery result (e.g. when the device's IP address changes).
4334
+ */
4335
+ set discoveryResult(result) {
4336
+ this.#airplay.discoveryResult = result;
4337
+ }
4338
+ /**
4339
+ * Updates the timing server for multi-room audio sync.
4340
+ */
4341
+ set timingServer(server) {
4342
+ this.#airplay.timingServer = server;
4343
+ }
4344
+ /**
4345
+ * The underlying AirPlay protocol manager.
4346
+ * Use this for low-level protocol access, raw state events, or features
4347
+ * not covered by the high-level controllers.
4348
+ */
4349
+ get airplay() {
4350
+ return this.#airplay;
4351
+ }
4352
+ /**
4353
+ * Creates a step-based pairing session for interactive PIN entry flows.
4354
+ *
4355
+ * ```ts
4356
+ * const session = tv.createPairingSession();
4357
+ * await session.start(); // Connects and triggers PIN dialog
4358
+ * await session.pin('1234'); // Submits PIN, executes M1-M6
4359
+ * const creds = await session.end(); // Returns credentials, cleans up
4360
+ * ```
4361
+ */
4362
+ createPairingSession() {
4363
+ return new PairingSession(this.#discoveryResult, this.#identity);
4364
+ }
4365
+ /**
4366
+ * Pairs with the device using a callback-based PIN flow.
4367
+ * Convenience method that wraps createPairingSession().
4368
+ *
4369
+ * @param options - Pairing options with onPinRequired callback.
4370
+ * @returns Long-term credentials for future connections.
4371
+ */
4372
+ async pair(options) {
4373
+ const session = this.createPairingSession();
4374
+ await session.start();
4375
+ const pin = await options.onPinRequired();
4376
+ await session.pin(pin);
4377
+ return await session.end();
4378
+ }
4379
+ /**
4380
+ * Disconnects from the device.
4381
+ */
4382
+ disconnect() {
4383
+ this.state.unsubscribe();
4384
+ this.#airplay.disconnectSafely();
4385
+ }
4386
+ /**
4387
+ * Called when AirPlay connects successfully. Override for additional setup.
4388
+ */
4389
+ onAirPlayConnected() {
4390
+ this.state.subscribe();
4391
+ }
4392
+ /**
4393
+ * Called when AirPlay disconnects. Override for additional cleanup.
4394
+ */
4395
+ onAirPlayDisconnected(unexpected) {
4396
+ this.state.unsubscribe();
4397
+ }
4398
+ };
4399
+
4400
+ //#endregion
4401
+ //#region src/device/apple-tv.ts
4402
+ /**
4403
+ * High-level Apple TV device combining AirPlay and Companion Link protocols.
4404
+ * Provides remote control, media playback, app launching, keyboard input,
4405
+ * power management, and system settings.
4406
+ *
4407
+ * ```ts
4408
+ * const tv = new AppleTV({ airplay: result, companionLink: clResult });
4409
+ *
4410
+ * // First time — pair
4411
+ * const session = tv.createPairingSession();
4412
+ * await session.start();
4413
+ * await session.pin('1234');
4414
+ * const credentials = await session.end();
4415
+ *
4416
+ * // Connect with credentials
4417
+ * await tv.connect(credentials);
4418
+ * await tv.playback.play();
4419
+ * ```
4420
+ */
4421
+ var AppleTV = class extends AbstractDevice {
4422
+ #companionLink;
4423
+ accounts;
4424
+ apps;
4425
+ keyboard;
4426
+ power;
4427
+ system;
4428
+ constructor(options) {
4429
+ super(options);
4430
+ this.keyboard = new KeyboardController(this.airplay);
4431
+ if (options.companionLink) {
4432
+ this.#companionLink = new CompanionLinkManager(options.companionLink);
4433
+ this.accounts = new AccountsController(this.#companionLink);
4434
+ this.apps = new AppsController(this.#companionLink);
4435
+ this.power = new PowerController(this.airplay, this.#companionLink);
4436
+ this.system = new SystemController(this.#companionLink);
4437
+ this.#companionLink.on("attentionStateChanged", (state) => {
4438
+ this.emit("power", state);
4439
+ });
4440
+ this.#companionLink.on("textInputChanged", (state) => {
4441
+ this.emit("textInput", state);
4442
+ });
4443
+ }
4444
+ }
4445
+ /**
4446
+ * The underlying Companion Link protocol manager, or undefined if no
4447
+ * Companion Link discovery result was provided.
4448
+ * Use this for low-level protocol access or features not covered by controllers.
4449
+ */
4450
+ get companionLink() {
4451
+ return this.#companionLink;
4452
+ }
4453
+ get isConnected() {
4454
+ const airplayConnected = this.airplay.isConnected;
4455
+ const companionLinkConnected = this.#companionLink?.isConnected ?? true;
4456
+ return airplayConnected && companionLinkConnected;
4457
+ }
4458
+ /**
4459
+ * Connects to the Apple TV using AirPlay and (optionally) Companion Link.
4460
+ *
4461
+ * @param credentials - Pairing credentials from pair-setup.
4462
+ */
4463
+ async connect(credentials) {
4464
+ this.airplay.setCredentials(credentials);
4465
+ await this.airplay.connect();
4466
+ if (this.#companionLink) try {
4467
+ await this.#companionLink.setCredentials(credentials);
4468
+ await this.#companionLink.connect();
4469
+ } catch {}
4470
+ }
4471
+ disconnect() {
4472
+ super.disconnect();
4473
+ this.#companionLink?.disconnectSafely();
4474
+ }
4475
+ onAirPlayConnected() {
4476
+ super.onAirPlayConnected();
4477
+ this.emit("connected");
4478
+ }
4479
+ onAirPlayDisconnected(unexpected) {
4480
+ super.onAirPlayDisconnected(unexpected);
4481
+ this.emit("disconnected", unexpected);
4482
+ }
4483
+ };
4484
+
4485
+ //#endregion
4486
+ //#region src/device/homepod.ts
4487
+ /**
4488
+ * High-level HomePod device using AirPlay only (transient pairing).
4489
+ * Provides media playback, URL playback, audio streaming, and volume control.
4490
+ * No credentials needed — transient pairing is handled transparently.
4491
+ *
4492
+ * ```ts
4493
+ * const pod = new HomePod({ airplay: result });
4494
+ * await pod.connect();
4495
+ * await pod.media.playUrl('https://example.com/song.mp3');
4496
+ * ```
4497
+ */
4498
+ var HomePod = class extends AbstractDevice {
4499
+ constructor(options) {
4500
+ super(options);
4501
+ }
4502
+ /**
4503
+ * Connects to the HomePod using transient pairing (no credentials needed).
4504
+ */
4505
+ async connect() {
4506
+ await this.airplay.connect();
4507
+ }
4508
+ onAirPlayConnected() {
4509
+ super.onAirPlayConnected();
4510
+ this.emit("connected");
4511
+ }
4512
+ onAirPlayDisconnected(unexpected) {
4513
+ super.onAirPlayDisconnected(unexpected);
4514
+ this.emit("disconnected", unexpected);
4515
+ }
4516
+ };
4517
+
4518
+ //#endregion
4519
+ //#region src/device/homepod-mini.ts
4520
+ /**
4521
+ * High-level HomePod Mini device.
4522
+ * Functionally identical to HomePod, different device model.
4523
+ */
4524
+ var HomePodMini = class extends HomePod {};
4525
+
4526
+ //#endregion
4527
+ //#region src/discover.ts
4528
+ /**
4529
+ * Discovers Apple devices on the local network via mDNS.
4530
+ * Returns device descriptors that can be passed to createDevice().
4531
+ */
4532
+ async function discover() {
4533
+ const airplayResults = await Discovery.airplay().find();
4534
+ const companionLinkResults = await Discovery.companionLink().find();
4535
+ const byAddress = /* @__PURE__ */ new Map();
4536
+ for (const result of airplayResults) byAddress.set(result.address, {
4537
+ id: result.id,
4538
+ name: result.familyName ?? result.fqdn,
4539
+ address: result.address,
4540
+ modelName: result.modelName,
4541
+ deviceType: detectDeviceType(result.modelName),
4542
+ services: { airplay: result }
4543
+ });
4544
+ for (const result of companionLinkResults) {
4545
+ const existing = byAddress.get(result.address);
4546
+ if (existing) byAddress.set(result.address, {
4547
+ ...existing,
4548
+ services: {
4549
+ ...existing.services,
4550
+ companionLink: result
4551
+ }
4552
+ });
4553
+ else byAddress.set(result.address, {
4554
+ id: result.id,
4555
+ name: result.familyName ?? result.fqdn,
4556
+ address: result.address,
4557
+ modelName: result.modelName,
4558
+ deviceType: detectDeviceType(result.modelName),
4559
+ services: { companionLink: result }
4560
+ });
4561
+ }
4562
+ return Array.from(byAddress.values());
4563
+ }
4564
+ /**
4565
+ * Creates a typed device instance from a discovery result.
4566
+ * Automatically selects AppleTV, HomePod, or HomePodMini based on the model.
4567
+ */
4568
+ function createDevice(discovered) {
4569
+ switch (discovered.deviceType) {
4570
+ case "appletv": return new AppleTV({
4571
+ airplay: discovered.services.airplay,
4572
+ companionLink: discovered.services.companionLink
4573
+ });
4574
+ case "homepod-mini": return new HomePodMini({ airplay: discovered.services.airplay });
4575
+ default: return new HomePod({ airplay: discovered.services.airplay });
4576
+ }
4577
+ }
4578
+ /**
4579
+ * Detects the device type from the model name string.
4580
+ */
4581
+ function detectDeviceType(modelName) {
4582
+ if (!modelName) return "unknown";
4583
+ if (/^AppleTV/i.test(modelName)) return "appletv";
4584
+ if (/AudioAccessory[56]/i.test(modelName)) return "homepod-mini";
4585
+ if (/AudioAccessory/i.test(modelName)) return "homepod";
4586
+ return "unknown";
4587
+ }
4588
+
4589
+ //#endregion
4590
+ export { PROTOCOL as AIRPLAY_PROTOCOL, AbstractDevice, AccountsController, AirPlayArtwork, AirPlayClient, AirPlayManager, AirPlayPlayer, AirPlayRemote, AirPlayState, AirPlayVolume, AppleTV, AppsController, ArtworkController, COMPANION_LINK_PROTOCOL, CompanionLinkManager, CompanionLinkState, HomePod, HomePodMini, KeyboardController, MediaController, MultiroomController, PairingSession, PlaybackController, PowerController, Proto, RemoteController, SendCommandError, StateController, SystemController, TimingServer, VolumeController, configure, createDevice, discover };