@basmilius/apple-devices 0.9.18 → 0.9.19

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.d.mts CHANGED
@@ -2,8 +2,8 @@
2
2
  import * as AirPlay from "@basmilius/apple-airplay";
3
3
  import { Proto, Protocol } from "@basmilius/apple-airplay";
4
4
  import { EventEmitter } from "node:events";
5
- import { AccessoryCredentials, AudioSource, DiscoveryResult, TimingServer } from "@basmilius/apple-common";
6
- import { AttentionState, ButtonPressType, HidCommandKey, LaunchableApp, MediaControlCommandKey, Protocol as Protocol$1, UserAccount } from "@basmilius/apple-companion-link";
5
+ import { AccessoryCredentials, AudioSource, CommandError, DiscoveryResult, TimingServer } from "@basmilius/apple-common";
6
+ import { AttentionState, ButtonPressType, HidCommandKey, LaunchableApp, MediaControlCommandKey, Protocol as Protocol$1, TextInputState, UserAccount } from "@basmilius/apple-companion-link";
7
7
 
8
8
  //#region src/airplay/player.d.ts
9
9
  declare class Player {
@@ -99,7 +99,7 @@ declare const STATE_SUBSCRIBE_SYMBOL: unique symbol;
99
99
  declare const STATE_UNSUBSCRIBE_SYMBOL: unique symbol;
100
100
  //#endregion
101
101
  //#region src/airplay/remote.d.ts
102
- declare class SendCommandError extends Error {
102
+ declare class SendCommandError extends CommandError {
103
103
  readonly sendError: Proto.SendError_Enum;
104
104
  readonly handlerReturnStatus: Proto.HandlerReturnStatus_Enum;
105
105
  constructor(sendError: Proto.SendError_Enum, handlerReturnStatus: Proto.HandlerReturnStatus_Enum);
@@ -119,11 +119,15 @@ declare class export_default$1 {
119
119
  play(): Promise<void>;
120
120
  pause(): Promise<void>;
121
121
  playPause(): Promise<void>;
122
+ stop(): Promise<void>;
122
123
  next(): Promise<void>;
123
124
  previous(): Promise<void>;
124
125
  volumeUp(): Promise<void>;
125
126
  volumeDown(): Promise<void>;
126
127
  mute(): Promise<void>;
128
+ topMenu(): Promise<void>;
129
+ channelUp(): Promise<void>;
130
+ channelDown(): Promise<void>;
127
131
  commandPlay(): Promise<void>;
128
132
  commandPause(): Promise<void>;
129
133
  commandTogglePlayPause(): Promise<void>;
@@ -148,6 +152,10 @@ declare class export_default$1 {
148
152
  commandDislikeTrack(): Promise<void>;
149
153
  commandBookmarkTrack(): Promise<void>;
150
154
  commandAddNowPlayingItemToLibrary(): Promise<void>;
155
+ textSet(text: string): Promise<void>;
156
+ textAppend(text: string): Promise<void>;
157
+ textClear(): Promise<void>;
158
+ getKeyboardSession(): Promise<void>;
151
159
  tap(x: number, y: number, finger?: number): Promise<void>;
152
160
  swipeUp(duration?: number): Promise<void>;
153
161
  swipeDown(duration?: number): Promise<void>;
@@ -163,6 +171,7 @@ type EventMap$4 = {
163
171
  readonly clients: [Record<string, Client>];
164
172
  readonly deviceInfo: [Proto.DeviceInfoMessage];
165
173
  readonly deviceInfoUpdate: [Proto.DeviceInfoMessage];
174
+ readonly keyboard: [Proto.KeyboardMessage];
166
175
  readonly nowPlayingChanged: [client: Client | null, player: Player | null];
167
176
  readonly originClientProperties: [Proto.OriginClientPropertiesMessage];
168
177
  readonly playerClientProperties: [Proto.PlayerClientPropertiesMessage];
@@ -186,9 +195,15 @@ type EventMap$4 = {
186
195
  declare class export_default$2 extends EventEmitter<EventMap$4> {
187
196
  #private;
188
197
  get clients(): Record<string, Client>;
198
+ get isKeyboardActive(): boolean;
199
+ get keyboardAttributes(): Proto.TextEditingAttributes | null;
200
+ get keyboardState(): Proto.KeyboardState_Enum;
189
201
  get nowPlayingClient(): Client | null;
190
202
  get outputDeviceUID(): string | null;
191
203
  get outputDevices(): Proto.AVOutputDeviceDescriptor[];
204
+ get clusterID(): string | null;
205
+ get clusterType(): number;
206
+ get isClusterAware(): boolean;
192
207
  get volume(): number;
193
208
  get volumeAvailable(): boolean;
194
209
  get volumeCapabilities(): Proto.VolumeCapabilities_Enum;
@@ -196,6 +211,7 @@ declare class export_default$2 extends EventEmitter<EventMap$4> {
196
211
  [STATE_SUBSCRIBE_SYMBOL](): void;
197
212
  [STATE_UNSUBSCRIBE_SYMBOL](): void;
198
213
  clear(): void;
214
+ onKeyboard(message: Proto.KeyboardMessage): void;
199
215
  onDeviceInfo(message: Proto.DeviceInfoMessage): void;
200
216
  onDeviceInfoUpdate(message: Proto.DeviceInfoMessage): void;
201
217
  onOriginClientProperties(message: Proto.OriginClientPropertiesMessage): void;
@@ -251,6 +267,9 @@ declare class export_default extends EventEmitter<EventMap$3> {
251
267
  addOutputDevices(deviceUIDs: string[]): Promise<void>;
252
268
  removeOutputDevices(deviceUIDs: string[]): Promise<void>;
253
269
  setOutputDevices(deviceUIDs: string[]): Promise<void>;
270
+ playUrl(url: string, position?: number): Promise<void>;
271
+ waitForPlaybackEnd(): Promise<void>;
272
+ stopPlayUrl(): void;
254
273
  streamAudio(source: AudioSource): Promise<void>;
255
274
  requestPlaybackQueue(length: number): Promise<void>;
256
275
  sendCommand(command: Proto.Command, options?: Proto.CommandOptions): Promise<void>;
@@ -265,6 +284,7 @@ type EventMap$2 = {
265
284
  connected: [];
266
285
  disconnected: [unexpected: boolean];
267
286
  power: [AttentionState];
287
+ textInput: [TextInputState];
268
288
  };
269
289
  declare class export_default$5 extends EventEmitter<EventMap$2> {
270
290
  #private;
@@ -272,6 +292,7 @@ declare class export_default$5 extends EventEmitter<EventMap$2> {
272
292
  get discoveryResult(): DiscoveryResult;
273
293
  set discoveryResult(discoveryResult: DiscoveryResult);
274
294
  get isConnected(): boolean;
295
+ get textInputState(): TextInputState;
275
296
  constructor(discoveryResult: DiscoveryResult);
276
297
  connect(): Promise<void>;
277
298
  disconnect(): Promise<void>;
@@ -285,6 +306,9 @@ declare class export_default$5 extends EventEmitter<EventMap$2> {
285
306
  mediaControlCommand(command: MediaControlCommandKey, content?: object): Promise<void>;
286
307
  pressButton(command: HidCommandKey, type?: ButtonPressType, holdDelayMs?: number): Promise<void>;
287
308
  switchUserAccount(accountId: string): Promise<void>;
309
+ textSet(text: string): Promise<void>;
310
+ textAppend(text: string): Promise<void>;
311
+ textClear(): Promise<void>;
288
312
  onSystemStatus(data: {
289
313
  readonly state: number;
290
314
  }): Promise<void>;
@@ -298,6 +322,7 @@ type EventMap$1 = {
298
322
  connected: [];
299
323
  disconnected: [unexpected: boolean];
300
324
  power: [AttentionState];
325
+ textInput: [TextInputState];
301
326
  };
302
327
  declare class export_default$4 extends EventEmitter<EventMap$1> {
303
328
  #private;
@@ -338,6 +363,9 @@ declare class export_default$4 extends EventEmitter<EventMap$1> {
338
363
  getUserAccounts(): Promise<UserAccount[]>;
339
364
  launchApp(bundleId: string): Promise<void>;
340
365
  switchUserAccount(accountId: string): Promise<void>;
366
+ textSet(text: string): Promise<void>;
367
+ textAppend(text: string): Promise<void>;
368
+ textClear(): Promise<void>;
341
369
  getCommandInfo(command: AirPlay.Proto.Command): Promise<AirPlay.Proto.CommandInfo | null>;
342
370
  isCommandSupported(command: AirPlay.Proto.Command): Promise<boolean>;
343
371
  }
@@ -375,6 +403,9 @@ declare abstract class export_default$8 extends EventEmitter<EventMap> {
375
403
  stop(): Promise<void>;
376
404
  next(): Promise<void>;
377
405
  previous(): Promise<void>;
406
+ playUrl(url: string, position?: number): Promise<void>;
407
+ waitForPlaybackEnd(): Promise<void>;
408
+ stopPlayUrl(): void;
378
409
  streamAudio(source: AudioSource): Promise<void>;
379
410
  getCommandInfo(command: AirPlay.Proto.Command): Promise<AirPlay.Proto.CommandInfo | null>;
380
411
  isCommandSupported(command: AirPlay.Proto.Command): Promise<boolean>;
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as AirPlay from "@basmilius/apple-airplay";
2
2
  import { DataStreamMessage, Proto, Protocol } from "@basmilius/apple-airplay";
3
3
  import { EventEmitter } from "node:events";
4
- import { waitFor } from "@basmilius/apple-common";
4
+ import { CommandError, CredentialsError, waitFor } from "@basmilius/apple-common";
5
5
  import { Protocol as Protocol$1, convertAttentionState } from "@basmilius/apple-companion-link";
6
+ import { Plist } from "@basmilius/apple-encoding";
6
7
 
7
8
  //#region src/airplay/player.ts
8
9
  const COCOA_EPOCH_OFFSET = 978307200;
@@ -334,7 +335,7 @@ const STATE_UNSUBSCRIBE_SYMBOL = Symbol("com.basmilius.airplay:unsubscribe");
334
335
 
335
336
  //#endregion
336
337
  //#region src/airplay/remote.ts
337
- var SendCommandError = class extends Error {
338
+ var SendCommandError = class extends CommandError {
338
339
  sendError;
339
340
  handlerReturnStatus;
340
341
  constructor(sendError, handlerReturnStatus) {
@@ -392,6 +393,9 @@ var remote_default = class {
392
393
  if (this.#device.state.nowPlayingClient?.isPlaying) await this.pause();
393
394
  else await this.play();
394
395
  }
396
+ async stop() {
397
+ await this.pressAndRelease(12, 183);
398
+ }
395
399
  async next() {
396
400
  await this.pressAndRelease(12, 181);
397
401
  }
@@ -407,6 +411,15 @@ var remote_default = class {
407
411
  async mute() {
408
412
  await this.pressAndRelease(12, 226);
409
413
  }
414
+ async topMenu() {
415
+ await this.pressAndRelease(12, 96);
416
+ }
417
+ async channelUp() {
418
+ await this.pressAndRelease(12, 156);
419
+ }
420
+ async channelDown() {
421
+ await this.pressAndRelease(12, 157);
422
+ }
410
423
  async commandPlay() {
411
424
  await this.#sendCommand(Proto.Command.Play);
412
425
  }
@@ -479,6 +492,18 @@ var remote_default = class {
479
492
  async commandAddNowPlayingItemToLibrary() {
480
493
  await this.#sendCommand(Proto.Command.AddNowPlayingItemToLibrary);
481
494
  }
495
+ async textSet(text) {
496
+ await this.#dataStream.send(DataStreamMessage.textInput(text, Proto.ActionType_Enum.Set));
497
+ }
498
+ async textAppend(text) {
499
+ await this.#dataStream.send(DataStreamMessage.textInput(text, Proto.ActionType_Enum.Insert));
500
+ }
501
+ async textClear() {
502
+ await this.#dataStream.send(DataStreamMessage.textInput("", Proto.ActionType_Enum.ClearAction));
503
+ }
504
+ async getKeyboardSession() {
505
+ await this.#dataStream.send(DataStreamMessage.getKeyboardSession());
506
+ }
482
507
  async tap(x, y, finger = 1) {
483
508
  await this.#sendTouch(x, y, 1, finger);
484
509
  await waitFor(50);
@@ -554,6 +579,15 @@ var state_default = class extends EventEmitter {
554
579
  get clients() {
555
580
  return this.#clients;
556
581
  }
582
+ get isKeyboardActive() {
583
+ return this.#keyboardState === Proto.KeyboardState_Enum.DidBeginEditing || this.#keyboardState === Proto.KeyboardState_Enum.Editing || this.#keyboardState === Proto.KeyboardState_Enum.TextDidChange;
584
+ }
585
+ get keyboardAttributes() {
586
+ return this.#keyboardAttributes;
587
+ }
588
+ get keyboardState() {
589
+ return this.#keyboardState;
590
+ }
557
591
  get nowPlayingClient() {
558
592
  return this.#nowPlayingClientBundleIdentifier ? this.#clients[this.#nowPlayingClientBundleIdentifier] ?? null : null;
559
593
  }
@@ -563,6 +597,15 @@ var state_default = class extends EventEmitter {
563
597
  get outputDevices() {
564
598
  return this.#outputDevices;
565
599
  }
600
+ get clusterID() {
601
+ return this.#clusterID;
602
+ }
603
+ get clusterType() {
604
+ return this.#clusterType;
605
+ }
606
+ get isClusterAware() {
607
+ return this.#isClusterAware;
608
+ }
566
609
  get volume() {
567
610
  return this.#volume;
568
611
  }
@@ -574,10 +617,15 @@ var state_default = class extends EventEmitter {
574
617
  }
575
618
  #device;
576
619
  #clients;
620
+ #keyboardAttributes;
621
+ #keyboardState;
577
622
  #nowPlayingClientBundleIdentifier;
578
623
  #nowPlayingSnapshot;
579
624
  #outputDeviceUID;
580
625
  #outputDevices = [];
626
+ #clusterID;
627
+ #clusterType;
628
+ #isClusterAware;
581
629
  #volume;
582
630
  #volumeAvailable;
583
631
  #volumeCapabilities;
@@ -585,6 +633,7 @@ var state_default = class extends EventEmitter {
585
633
  super();
586
634
  this.#device = device;
587
635
  this.clear();
636
+ this.onKeyboard = this.onKeyboard.bind(this);
588
637
  this.onDeviceInfo = this.onDeviceInfo.bind(this);
589
638
  this.onDeviceInfoUpdate = this.onDeviceInfoUpdate.bind(this);
590
639
  this.onOriginClientProperties = this.onOriginClientProperties.bind(this);
@@ -607,6 +656,7 @@ var state_default = class extends EventEmitter {
607
656
  this.onVolumeDidChange = this.onVolumeDidChange.bind(this);
608
657
  }
609
658
  [STATE_SUBSCRIBE_SYMBOL]() {
659
+ this.#dataStream.on("keyboard", this.onKeyboard);
610
660
  this.#dataStream.on("deviceInfo", this.onDeviceInfo);
611
661
  this.#dataStream.on("deviceInfoUpdate", this.onDeviceInfoUpdate);
612
662
  this.#dataStream.on("originClientProperties", this.onOriginClientProperties);
@@ -631,6 +681,7 @@ var state_default = class extends EventEmitter {
631
681
  [STATE_UNSUBSCRIBE_SYMBOL]() {
632
682
  const dataStream = this.#dataStream;
633
683
  if (!dataStream) return;
684
+ dataStream.off("keyboard", this.onKeyboard);
634
685
  dataStream.off("deviceInfo", this.onDeviceInfo);
635
686
  dataStream.off("deviceInfoUpdate", this.onDeviceInfoUpdate);
636
687
  dataStream.off("originClientProperties", this.onOriginClientProperties);
@@ -654,14 +705,24 @@ var state_default = class extends EventEmitter {
654
705
  }
655
706
  clear() {
656
707
  this.#clients = {};
708
+ this.#keyboardAttributes = null;
709
+ this.#keyboardState = Proto.KeyboardState_Enum.Unknown;
657
710
  this.#nowPlayingClientBundleIdentifier = null;
658
711
  this.#nowPlayingSnapshot = null;
659
712
  this.#outputDeviceUID = null;
660
713
  this.#outputDevices = [];
714
+ this.#clusterID = null;
715
+ this.#clusterType = 0;
716
+ this.#isClusterAware = false;
661
717
  this.#volume = 0;
662
718
  this.#volumeAvailable = false;
663
719
  this.#volumeCapabilities = Proto.VolumeCapabilities_Enum.None;
664
720
  }
721
+ onKeyboard(message) {
722
+ this.#keyboardState = message.state;
723
+ this.#keyboardAttributes = message.attributes ?? null;
724
+ this.emit("keyboard", message);
725
+ }
665
726
  onDeviceInfo(message) {
666
727
  this.#updateOutputDeviceUID(message);
667
728
  this.emit("deviceInfo", message);
@@ -751,7 +812,7 @@ var state_default = class extends EventEmitter {
751
812
  this.emit("clients", this.#clients);
752
813
  }
753
814
  onUpdateOutputDevice(message) {
754
- this.#outputDevices = message.outputDevices;
815
+ this.#outputDevices = message.clusterAwareOutputDevices?.length > 0 ? message.clusterAwareOutputDevices : message.outputDevices;
755
816
  this.emit("updateOutputDevice", message);
756
817
  }
757
818
  onVolumeControlAvailability(message) {
@@ -770,6 +831,9 @@ var state_default = class extends EventEmitter {
770
831
  }
771
832
  #updateOutputDeviceUID(message) {
772
833
  this.#outputDeviceUID = message.clusterID || message.deviceUID || message.uniqueIdentifier || null;
834
+ this.#clusterID = message.clusterID || null;
835
+ this.#clusterType = message.clusterType ?? 0;
836
+ this.#isClusterAware = message.isClusterAware ?? false;
773
837
  }
774
838
  #client(bundleIdentifier, displayName) {
775
839
  if (bundleIdentifier in this.#clients) {
@@ -844,31 +908,31 @@ var volume_default = class {
844
908
  case Proto.VolumeCapabilities_Enum.Relative:
845
909
  await this.#device.remote.volumeDown();
846
910
  break;
847
- default: throw new Error("Volume control is not available.");
911
+ default: throw new CommandError("Volume control is not available.");
848
912
  }
849
913
  }
850
914
  async up() {
851
915
  switch (this.#state.volumeCapabilities) {
852
916
  case Proto.VolumeCapabilities_Enum.Absolute:
853
917
  case Proto.VolumeCapabilities_Enum.Both:
854
- const newVolume = Math.max(0, this.#state.volume + VOLUME_STEP);
918
+ const newVolume = Math.min(1, Math.max(0, this.#state.volume + VOLUME_STEP));
855
919
  await this.set(newVolume);
856
920
  break;
857
921
  case Proto.VolumeCapabilities_Enum.Relative:
858
922
  await this.#device.remote.volumeUp();
859
923
  break;
860
- default: throw new Error("Volume control is not available.");
924
+ default: throw new CommandError("Volume control is not available.");
861
925
  }
862
926
  }
863
927
  async get() {
864
- if (!this.#state.outputDeviceUID) throw new Error("No output device active.");
928
+ if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
865
929
  const response = await this.#protocol.dataStream.exchange(DataStreamMessage.getVolume(this.#state.outputDeviceUID));
866
930
  if (response.type === Proto.ProtocolMessage_Type.GET_VOLUME_RESULT_MESSAGE) return DataStreamMessage.getExtension(response, Proto.getVolumeResultMessage).volume;
867
- throw new Error("Failed to get volume.");
931
+ throw new CommandError("Failed to get volume.");
868
932
  }
869
933
  async set(volume) {
870
- if (!this.#state.outputDeviceUID) throw new Error("No output device active.");
871
- if (![Proto.VolumeCapabilities_Enum.Absolute, Proto.VolumeCapabilities_Enum.Both].includes(this.#state.volumeCapabilities)) throw new Error("Absolute volume control is not available.");
934
+ if (!this.#state.outputDeviceUID) throw new CommandError("No output device active.");
935
+ if (![Proto.VolumeCapabilities_Enum.Absolute, Proto.VolumeCapabilities_Enum.Both].includes(this.#state.volumeCapabilities)) throw new CommandError("Absolute volume control is not available.");
872
936
  volume = Math.min(1, Math.max(0, volume));
873
937
  this.#protocol.context.logger.info(`Setting volume to ${volume} for device ${this.#state.outputDeviceUID}`);
874
938
  await this.#protocol.dataStream.exchange(DataStreamMessage.setVolume(this.#state.outputDeviceUID, volume));
@@ -905,6 +969,9 @@ var device_default = class extends EventEmitter {
905
969
  set timingServer(timingServer) {
906
970
  this.#timingServer = timingServer;
907
971
  }
972
+ #boundOnClose = () => this.#onClose();
973
+ #boundOnError = (err) => this.#onError(err);
974
+ #boundOnTimeout = () => this.#onTimeout();
908
975
  #remote;
909
976
  #state;
910
977
  #volume;
@@ -913,6 +980,9 @@ var device_default = class extends EventEmitter {
913
980
  #discoveryResult;
914
981
  #feedbackInterval;
915
982
  #keys;
983
+ #playUrlProtocol;
984
+ #prevDataStream;
985
+ #prevEventStream;
916
986
  #protocol;
917
987
  #timingServer;
918
988
  constructor(discoveryResult) {
@@ -923,12 +993,17 @@ var device_default = class extends EventEmitter {
923
993
  this.#volume = new volume_default(this);
924
994
  }
925
995
  async connect() {
996
+ if (this.#protocol) {
997
+ this.#protocol.controlStream.off("close", this.#boundOnClose);
998
+ this.#protocol.controlStream.off("error", this.#boundOnError);
999
+ this.#protocol.controlStream.off("timeout", this.#boundOnTimeout);
1000
+ }
926
1001
  this.#disconnect = false;
927
1002
  this.#state.clear();
928
1003
  this.#protocol = new Protocol(this.#discoveryResult);
929
- this.#protocol.controlStream.on("close", this.#onClose.bind(this));
930
- this.#protocol.controlStream.on("error", this.#onError.bind(this));
931
- this.#protocol.controlStream.on("timeout", this.#onTimeout.bind(this));
1004
+ this.#protocol.controlStream.on("close", this.#boundOnClose);
1005
+ this.#protocol.controlStream.on("error", this.#boundOnError);
1006
+ this.#protocol.controlStream.on("timeout", this.#boundOnTimeout);
932
1007
  await this.#protocol.connect();
933
1008
  if (this.#credentials) this.#keys = await this.#protocol.verify.start(this.#credentials);
934
1009
  else {
@@ -944,6 +1019,7 @@ var device_default = class extends EventEmitter {
944
1019
  clearInterval(this.#feedbackInterval);
945
1020
  this.#feedbackInterval = void 0;
946
1021
  }
1022
+ this.#cleanupPlayUrl();
947
1023
  this.#unsubscribe();
948
1024
  this.#protocol.disconnect();
949
1025
  }
@@ -963,6 +1039,40 @@ var device_default = class extends EventEmitter {
963
1039
  async setOutputDevices(deviceUIDs) {
964
1040
  await this.#protocol.dataStream.exchange(DataStreamMessage.modifyOutputContext([], [], deviceUIDs));
965
1041
  }
1042
+ async playUrl(url, position = 0) {
1043
+ if (!this.#keys) throw new Error("Not connected. Call connect() first.");
1044
+ this.#playUrlProtocol?.disconnect();
1045
+ const playProtocol = new Protocol(this.#discoveryResult);
1046
+ if (this.#timingServer) playProtocol.useTimingServer(this.#timingServer);
1047
+ await playProtocol.connect();
1048
+ let keys;
1049
+ if (this.#credentials) keys = await playProtocol.verify.start(this.#credentials);
1050
+ else {
1051
+ await playProtocol.pairing.start();
1052
+ keys = await playProtocol.pairing.transient();
1053
+ }
1054
+ playProtocol.controlStream.enableEncryption(keys.accessoryToControllerKey, keys.controllerToAccessoryKey);
1055
+ this.#playUrlProtocol = playProtocol;
1056
+ await playProtocol.playUrl(url, keys.sharedSecret, keys.pairingId, position);
1057
+ }
1058
+ async waitForPlaybackEnd() {
1059
+ if (!this.#playUrlProtocol) return;
1060
+ try {
1061
+ await this.#playUrlProtocol.waitForPlaybackEnd();
1062
+ } finally {
1063
+ this.#cleanupPlayUrl();
1064
+ }
1065
+ }
1066
+ stopPlayUrl() {
1067
+ this.#cleanupPlayUrl();
1068
+ }
1069
+ #cleanupPlayUrl() {
1070
+ if (this.#playUrlProtocol) {
1071
+ this.#playUrlProtocol.stopPlayUrl();
1072
+ this.#playUrlProtocol.disconnect();
1073
+ this.#playUrlProtocol = void 0;
1074
+ }
1075
+ }
966
1076
  async streamAudio(source) {
967
1077
  await this.#protocol.setupAudioStream(source);
968
1078
  }
@@ -1002,17 +1112,23 @@ var device_default = class extends EventEmitter {
1002
1112
  this.#unsubscribe();
1003
1113
  if (this.#timingServer) this.#protocol.useTimingServer(this.#timingServer);
1004
1114
  try {
1115
+ this.#prevDataStream?.off("error", this.#boundOnError);
1116
+ this.#prevDataStream?.off("timeout", this.#boundOnTimeout);
1117
+ this.#prevEventStream?.off("error", this.#boundOnError);
1118
+ this.#prevEventStream?.off("timeout", this.#boundOnTimeout);
1005
1119
  await this.#protocol.setupEventStream(keys.sharedSecret, keys.pairingId);
1006
1120
  await this.#protocol.setupDataStream(keys.sharedSecret, () => this.#subscribe());
1007
- this.#protocol.dataStream.on("error", this.#onError.bind(this));
1008
- this.#protocol.dataStream.on("timeout", this.#onTimeout.bind(this));
1009
- this.#protocol.eventStream.on("error", this.#onError.bind(this));
1010
- this.#protocol.eventStream.on("timeout", this.#onTimeout.bind(this));
1121
+ this.#protocol.dataStream.on("error", this.#boundOnError);
1122
+ this.#protocol.dataStream.on("timeout", this.#boundOnTimeout);
1123
+ this.#protocol.eventStream.on("error", this.#boundOnError);
1124
+ this.#protocol.eventStream.on("timeout", this.#boundOnTimeout);
1125
+ this.#prevDataStream = this.#protocol.dataStream;
1126
+ this.#prevEventStream = this.#protocol.eventStream;
1011
1127
  if (this.#feedbackInterval) clearInterval(this.#feedbackInterval);
1012
1128
  this.#feedbackInterval = setInterval(async () => await this.#feedback(), FEEDBACK_INTERVAL);
1013
1129
  await this.#protocol.dataStream.exchange(DataStreamMessage.deviceInfo(keys.pairingId));
1014
1130
  await this.#protocol.dataStream.exchange(DataStreamMessage.setConnectionState());
1015
- await this.#protocol.dataStream.exchange(DataStreamMessage.clientUpdatesConfig());
1131
+ await this.#protocol.dataStream.exchange(DataStreamMessage.clientUpdatesConfig(true, true, true, true, true, true));
1016
1132
  this.#protocol.context.logger.info("Protocol ready.");
1017
1133
  } catch (err) {
1018
1134
  if (this.#feedbackInterval) {
@@ -1061,6 +1177,23 @@ var device_default$1 = class extends EventEmitter {
1061
1177
  #heartbeatInterval;
1062
1178
  #keys;
1063
1179
  #protocol;
1180
+ get textInputState() {
1181
+ return this.#textInputState;
1182
+ }
1183
+ #boundOnClose = async () => this.#onClose();
1184
+ #boundOnError = async (err) => this.#onError(err);
1185
+ #boundOnTimeout = async () => this.#onTimeout();
1186
+ #onMediaControl = (data) => {
1187
+ this.emit("mediaControl", data);
1188
+ };
1189
+ #textInputState = {
1190
+ isActive: false,
1191
+ documentText: "",
1192
+ isSecure: false,
1193
+ keyboardType: 0,
1194
+ autocorrection: false,
1195
+ autocapitalization: false
1196
+ };
1064
1197
  constructor(discoveryResult) {
1065
1198
  super();
1066
1199
  this.#discoveryResult = discoveryResult;
@@ -1068,12 +1201,17 @@ var device_default$1 = class extends EventEmitter {
1068
1201
  this.onTVSystemStatus = this.onTVSystemStatus.bind(this);
1069
1202
  }
1070
1203
  async connect() {
1071
- if (!this.#credentials) throw new Error("Credentials are required to connect to a Companion Link device.");
1204
+ if (!this.#credentials) throw new CredentialsError("Credentials are required to connect to a Companion Link device.");
1205
+ if (this.#protocol) {
1206
+ this.#protocol.stream.off("close", this.#boundOnClose);
1207
+ this.#protocol.stream.off("error", this.#boundOnError);
1208
+ this.#protocol.stream.off("timeout", this.#boundOnTimeout);
1209
+ }
1072
1210
  this.#disconnect = false;
1073
1211
  this.#protocol = new Protocol$1(this.#discoveryResult);
1074
- this.#protocol.stream.on("close", async () => this.#onClose());
1075
- this.#protocol.stream.on("error", async (err) => this.#onError(err));
1076
- this.#protocol.stream.on("timeout", async () => this.#onTimeout());
1212
+ this.#protocol.stream.on("close", this.#boundOnClose);
1213
+ this.#protocol.stream.on("error", this.#boundOnError);
1214
+ this.#protocol.stream.on("timeout", this.#boundOnTimeout);
1077
1215
  await this.#protocol.connect();
1078
1216
  this.#keys = await this.#protocol.verify.start(this.#credentials);
1079
1217
  await this.#setup();
@@ -1120,6 +1258,15 @@ var device_default$1 = class extends EventEmitter {
1120
1258
  async switchUserAccount(accountId) {
1121
1259
  await this.#protocol.switchUserAccount(accountId);
1122
1260
  }
1261
+ async textSet(text) {
1262
+ await this.#protocol.textInputCommand(text, true);
1263
+ }
1264
+ async textAppend(text) {
1265
+ await this.#protocol.textInputCommand(text, false);
1266
+ }
1267
+ async textClear() {
1268
+ await this.#protocol.textInputCommand("", true);
1269
+ }
1123
1270
  async #heartbeat() {
1124
1271
  try {
1125
1272
  this.#protocol.noOp();
@@ -1150,10 +1297,11 @@ var device_default$1 = class extends EventEmitter {
1150
1297
  await this.#protocol.tvrcSessionStart();
1151
1298
  await this.#protocol.touchStart();
1152
1299
  await this.#protocol.tiStart();
1153
- await this.#protocol.subscribe("_iMC", (data) => {
1154
- this.emit("mediaControl", data);
1155
- });
1156
- this.#heartbeatInterval = setInterval(async () => await this.#heartbeat(), 15e3);
1300
+ this.#heartbeatInterval = setInterval(() => {
1301
+ this.#heartbeat().catch((err) => {
1302
+ this.#protocol.context.logger.error("Heartbeat failed", err);
1303
+ });
1304
+ }, 15e3);
1157
1305
  await this.#subscribe();
1158
1306
  } catch (err) {
1159
1307
  clearInterval(this.#heartbeatInterval);
@@ -1162,17 +1310,73 @@ var device_default$1 = class extends EventEmitter {
1162
1310
  }
1163
1311
  }
1164
1312
  async #subscribe() {
1165
- await this.#protocol.subscribe("SystemStatus", this.onSystemStatus);
1166
- await this.#protocol.subscribe("TVSystemStatus", this.onTVSystemStatus);
1313
+ this.#protocol.stream.on("_iMC", this.#onMediaControl);
1314
+ this.#protocol.stream.on("SystemStatus", this.onSystemStatus);
1315
+ this.#protocol.stream.on("TVSystemStatus", this.onTVSystemStatus);
1316
+ this.#protocol.stream.on("_tiStarted", this.#onTextInputStarted);
1317
+ this.#protocol.stream.on("_tiStopped", this.#onTextInputStopped);
1318
+ this.#protocol.registerInterests([
1319
+ "_iMC",
1320
+ "SystemStatus",
1321
+ "TVSystemStatus"
1322
+ ]);
1167
1323
  const state = await this.getAttentionState();
1168
1324
  this.emit("power", state);
1169
1325
  }
1170
1326
  async #unsubscribe() {
1327
+ this.#protocol.stream.off("_iMC", this.#onMediaControl);
1328
+ this.#protocol.stream.off("SystemStatus", this.onSystemStatus);
1329
+ this.#protocol.stream.off("TVSystemStatus", this.onTVSystemStatus);
1330
+ this.#protocol.stream.off("_tiStarted", this.#onTextInputStarted);
1331
+ this.#protocol.stream.off("_tiStopped", this.#onTextInputStopped);
1171
1332
  try {
1172
- await this.#protocol.unsubscribe("SystemStatus", this.onSystemStatus);
1173
- await this.#protocol.unsubscribe("TVSystemStatus", this.onTVSystemStatus);
1333
+ this.#protocol.deregisterInterests([
1334
+ "_iMC",
1335
+ "SystemStatus",
1336
+ "TVSystemStatus"
1337
+ ]);
1174
1338
  } catch (_) {}
1175
1339
  }
1340
+ #onTextInputStarted = async (data) => {
1341
+ try {
1342
+ const payload = data;
1343
+ let documentText = "";
1344
+ let isSecure = false;
1345
+ let keyboardType = 0;
1346
+ let autocorrection = false;
1347
+ let autocapitalization = false;
1348
+ if (payload?._tiD) {
1349
+ const plistData = Plist.parse(Buffer.from(payload._tiD).buffer);
1350
+ documentText = plistData._tiDT ?? "";
1351
+ isSecure = plistData._tiSR ?? false;
1352
+ keyboardType = plistData._tiKT ?? 0;
1353
+ autocorrection = plistData._tiAC ?? false;
1354
+ autocapitalization = plistData._tiAP ?? false;
1355
+ }
1356
+ this.#textInputState = {
1357
+ isActive: true,
1358
+ documentText,
1359
+ isSecure,
1360
+ keyboardType,
1361
+ autocorrection,
1362
+ autocapitalization
1363
+ };
1364
+ this.emit("textInput", this.#textInputState);
1365
+ } catch (err) {
1366
+ this.#protocol.context.logger.error("Text input started parse error", err);
1367
+ }
1368
+ };
1369
+ #onTextInputStopped = async (_data) => {
1370
+ this.#textInputState = {
1371
+ isActive: false,
1372
+ documentText: "",
1373
+ isSecure: false,
1374
+ keyboardType: 0,
1375
+ autocorrection: false,
1376
+ autocapitalization: false
1377
+ };
1378
+ this.emit("textInput", this.#textInputState);
1379
+ };
1176
1380
  async onSystemStatus(data) {
1177
1381
  this.#protocol.context.logger.info("System Status", data);
1178
1382
  this.emit("power", convertAttentionState(data.state));
@@ -1255,12 +1459,18 @@ var apple_tv_default = class extends EventEmitter {
1255
1459
  this.#companionLink.on("connected", () => this.#onConnected());
1256
1460
  this.#companionLink.on("disconnected", (unexpected) => this.#onDisconnected(unexpected));
1257
1461
  this.#companionLink.on("power", (state) => this.emit("power", state));
1462
+ this.#companionLink.on("textInput", (state) => this.emit("textInput", state));
1258
1463
  }
1259
1464
  async connect(airplayCredentials, companionLinkCredentials) {
1260
1465
  this.#airplay.setCredentials(airplayCredentials);
1261
1466
  await this.#companionLink.setCredentials(companionLinkCredentials ?? airplayCredentials);
1262
1467
  await this.#airplay.connect();
1263
- await this.#companionLink.connect();
1468
+ try {
1469
+ await this.#companionLink.connect();
1470
+ } catch (err) {
1471
+ this.#airplay.disconnectSafely();
1472
+ throw err;
1473
+ }
1264
1474
  this.#disconnect = false;
1265
1475
  }
1266
1476
  async disconnect() {
@@ -1315,6 +1525,15 @@ var apple_tv_default = class extends EventEmitter {
1315
1525
  async switchUserAccount(accountId) {
1316
1526
  await this.#companionLink.switchUserAccount(accountId);
1317
1527
  }
1528
+ async textSet(text) {
1529
+ await this.#companionLink.textSet(text);
1530
+ }
1531
+ async textAppend(text) {
1532
+ await this.#companionLink.textAppend(text);
1533
+ }
1534
+ async textClear() {
1535
+ await this.#companionLink.textClear();
1536
+ }
1318
1537
  async getCommandInfo(command) {
1319
1538
  const client = this.#airplay.state.nowPlayingClient;
1320
1539
  if (!client) return null;
@@ -1427,6 +1646,15 @@ var homepod_base_default = class extends EventEmitter {
1427
1646
  async previous() {
1428
1647
  await this.#airplay.sendCommand(AirPlay.Proto.Command.PreviousInContext);
1429
1648
  }
1649
+ async playUrl(url, position = 0) {
1650
+ await this.#airplay.playUrl(url, position);
1651
+ }
1652
+ async waitForPlaybackEnd() {
1653
+ await this.#airplay.waitForPlaybackEnd();
1654
+ }
1655
+ stopPlayUrl() {
1656
+ this.#airplay.stopPlayUrl();
1657
+ }
1430
1658
  async streamAudio(source) {
1431
1659
  await this.#airplay.streamAudio(source);
1432
1660
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@basmilius/apple-devices",
3
3
  "description": "Exposes various Apple devices to connect with either AirPlay or Companion Link.",
4
- "version": "0.9.18",
4
+ "version": "0.9.19",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -49,10 +49,10 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@basmilius/apple-airplay": "0.9.18",
53
- "@basmilius/apple-common": "0.9.18",
54
- "@basmilius/apple-companion-link": "0.9.18",
55
- "@basmilius/apple-encoding": "0.9.18"
52
+ "@basmilius/apple-airplay": "0.9.19",
53
+ "@basmilius/apple-common": "0.9.19",
54
+ "@basmilius/apple-companion-link": "0.9.19",
55
+ "@basmilius/apple-encoding": "0.9.19"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/bun": "^1.3.11",