@basmilius/apple-companion-link 0.0.1
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/CODEOWNERS +1 -0
- package/LICENSE +21 -0
- package/dist/cli.d.ts +3 -0
- package/dist/const.d.ts +5 -0
- package/dist/crypto/chacha20.d.ts +7 -0
- package/dist/crypto/curve25519.d.ts +7 -0
- package/dist/crypto/hkdf.d.ts +8 -0
- package/dist/crypto/index.d.ts +3 -0
- package/dist/discovery/discovery.d.ts +10 -0
- package/dist/discovery/index.d.ts +1 -0
- package/dist/encoding/index.d.ts +3 -0
- package/dist/encoding/opack.d.ts +10 -0
- package/dist/encoding/plist.d.ts +2 -0
- package/dist/encoding/tlv8.d.ts +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +24 -0
- package/dist/protocol/api/companionLink.d.ts +71 -0
- package/dist/protocol/companionLink.d.ts +14 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/pairing/companionLink.d.ts +24 -0
- package/dist/protocol/verify/companionLink.d.ts +18 -0
- package/dist/socket/base.d.ts +7 -0
- package/dist/socket/companionLink.d.ts +40 -0
- package/dist/socket/index.d.ts +2 -0
- package/package.json +56 -0
- package/src/cli.ts +19 -0
- package/src/const.ts +7 -0
- package/src/crypto/chacha20.ts +95 -0
- package/src/crypto/curve25519.ts +21 -0
- package/src/crypto/hkdf.ts +13 -0
- package/src/crypto/index.ts +13 -0
- package/src/discovery/discovery.ts +52 -0
- package/src/discovery/index.ts +1 -0
- package/src/encoding/index.ts +22 -0
- package/src/encoding/opack.ts +293 -0
- package/src/encoding/plist.ts +2 -0
- package/src/encoding/tlv8.ts +111 -0
- package/src/index.ts +3 -0
- package/src/net/getLocalIP.ts +25 -0
- package/src/net/getMacAddress.ts +25 -0
- package/src/net/index.ts +2 -0
- package/src/protocol/api/companionLink.ts +353 -0
- package/src/protocol/companionLink.ts +41 -0
- package/src/protocol/index.ts +1 -0
- package/src/protocol/pairing/companionLink.ts +286 -0
- package/src/protocol/verify/companionLink.ts +185 -0
- package/src/socket/base.ts +21 -0
- package/src/socket/companionLink.ts +249 -0
- package/src/socket/index.ts +2 -0
- package/src/test.ts +109 -0
- package/src/transient.ts +64 -0
- package/src/types.d.ts +40 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const Flags = {
|
|
2
|
+
TransientPairing: 0x10
|
|
3
|
+
} as const;
|
|
4
|
+
|
|
5
|
+
export const Method = {
|
|
6
|
+
PairSetup: 0x00,
|
|
7
|
+
PairSetupWithAuth: 0x01,
|
|
8
|
+
PairVerify: 0x02,
|
|
9
|
+
AddPairing: 0x03,
|
|
10
|
+
RemovePairing: 0x04,
|
|
11
|
+
ListPairing: 0x05
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export const State = {
|
|
15
|
+
M1: 0x01,
|
|
16
|
+
M2: 0x02,
|
|
17
|
+
M3: 0x03,
|
|
18
|
+
M4: 0x04,
|
|
19
|
+
M5: 0x05,
|
|
20
|
+
M6: 0x06
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export const Value = {
|
|
24
|
+
Method: 0x00,
|
|
25
|
+
Identifier: 0x01,
|
|
26
|
+
Salt: 0x02,
|
|
27
|
+
PublicKey: 0x03,
|
|
28
|
+
Proof: 0x04,
|
|
29
|
+
EncryptedData: 0x05,
|
|
30
|
+
State: 0x06,
|
|
31
|
+
Error: 0x07,
|
|
32
|
+
BackOff: 0x08,
|
|
33
|
+
Certificate: 0x09,
|
|
34
|
+
Signature: 0x0A,
|
|
35
|
+
Permissions: 0x0B,
|
|
36
|
+
FragmentData: 0x0C,
|
|
37
|
+
FragmentLast: 0x0D,
|
|
38
|
+
|
|
39
|
+
Name: 0x11,
|
|
40
|
+
Flags: 0x13
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
export function bail(data: Map<number, Buffer>): never {
|
|
44
|
+
if (data.has(Value.BackOff)) {
|
|
45
|
+
const buffer = data.get(Value.BackOff);
|
|
46
|
+
const time = buffer.readUintLE(0, buffer.length);
|
|
47
|
+
|
|
48
|
+
throw new Error(`Device is busy, try again in ${time} seconds.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (data.has(Value.Error)) {
|
|
52
|
+
throw new Error(`Device returned an error code: ${data.get(Value.Error).readUint8()}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.error(data);
|
|
56
|
+
|
|
57
|
+
throw new Error('Invalid response');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function encode(entries: [number, number | Buffer][]): Buffer {
|
|
61
|
+
const chunks: number[] = [];
|
|
62
|
+
|
|
63
|
+
for (const [type, valueRaw] of entries) {
|
|
64
|
+
let value: Buffer;
|
|
65
|
+
|
|
66
|
+
if (typeof valueRaw === 'number') {
|
|
67
|
+
value = Buffer.from([valueRaw]);
|
|
68
|
+
} else {
|
|
69
|
+
value = valueRaw;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let offset = 0;
|
|
73
|
+
|
|
74
|
+
do {
|
|
75
|
+
const len = Math.min(value.length - offset, 255);
|
|
76
|
+
chunks.push(type, len);
|
|
77
|
+
|
|
78
|
+
if (len > 0) {
|
|
79
|
+
for (let i = 0; i < len; i++) {
|
|
80
|
+
chunks.push(value[offset + i]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
offset += len;
|
|
85
|
+
} while (offset < value.length);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Buffer.from(chunks);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function decode(buf: Buffer): Map<number, Buffer> {
|
|
92
|
+
const map = new Map<number, Buffer>();
|
|
93
|
+
let i = 0;
|
|
94
|
+
|
|
95
|
+
while (i < buf.length) {
|
|
96
|
+
const type = buf[i++];
|
|
97
|
+
const len = buf[i++];
|
|
98
|
+
|
|
99
|
+
const value = (new Uint8Array(buf)).slice(i, i + len);
|
|
100
|
+
i += len;
|
|
101
|
+
|
|
102
|
+
const existing = map.get(type);
|
|
103
|
+
if (existing) {
|
|
104
|
+
map.set(type, Buffer.concat([existing, value]));
|
|
105
|
+
} else {
|
|
106
|
+
map.set(type, Buffer.from(value));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return map;
|
|
111
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os';
|
|
2
|
+
|
|
3
|
+
export default function (): string {
|
|
4
|
+
const interfaces = networkInterfaces();
|
|
5
|
+
|
|
6
|
+
for (const name of Object.keys(interfaces)) {
|
|
7
|
+
const iface = interfaces[name];
|
|
8
|
+
|
|
9
|
+
if (!iface) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const net of iface) {
|
|
14
|
+
if (net.internal || net.family !== 'IPv4') {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (net.address && net.address !== '127.0.0.1') {
|
|
19
|
+
return net.address;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os';
|
|
2
|
+
|
|
3
|
+
export default function (): string {
|
|
4
|
+
const interfaces = networkInterfaces();
|
|
5
|
+
|
|
6
|
+
for (const name of Object.keys(interfaces)) {
|
|
7
|
+
const iface = interfaces[name];
|
|
8
|
+
|
|
9
|
+
if (!iface) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const net of iface) {
|
|
14
|
+
if (net.internal || net.family !== 'IPv4') {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (net.mac && net.mac !== '00:00:00:00:00:00') {
|
|
19
|
+
return net.mac.toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
package/src/net/index.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
import { debug, waitFor } from '@/cli';
|
|
3
|
+
import { opackFloat, parseBinaryPlist } from '@/encoding';
|
|
4
|
+
import { CompanionLinkFrameType, CompanionLinkMessageType, type CompanionLinkSocket } from '@/socket';
|
|
5
|
+
import type CompanionLink from '../companionLink';
|
|
6
|
+
|
|
7
|
+
export default class Api {
|
|
8
|
+
readonly #protocol: CompanionLink;
|
|
9
|
+
readonly #socket: CompanionLinkSocket;
|
|
10
|
+
|
|
11
|
+
constructor(protocol: CompanionLink, socket: CompanionLinkSocket) {
|
|
12
|
+
this.#protocol = protocol;
|
|
13
|
+
this.#socket = socket;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async fetchMediaControlStatus(): Promise<void> {
|
|
17
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
18
|
+
_i: 'FetchMediaControlStatus',
|
|
19
|
+
_t: CompanionLinkMessageType.Request,
|
|
20
|
+
_c: {}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async fetchNowPlayingInfo(): Promise<void> {
|
|
25
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
26
|
+
_i: 'FetchCurrentNowPlayingInfoEvent',
|
|
27
|
+
_t: CompanionLinkMessageType.Request,
|
|
28
|
+
_c: {}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async fetchSupportedActions(): Promise<void> {
|
|
33
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
34
|
+
_i: 'FetchSupportedActionsEvent',
|
|
35
|
+
_t: CompanionLinkMessageType.Request,
|
|
36
|
+
_c: {}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getAttentionState(): Promise<AttentionState> {
|
|
41
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
42
|
+
_i: 'FetchAttentionState',
|
|
43
|
+
_t: CompanionLinkMessageType.Request,
|
|
44
|
+
_c: {}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const {_c} = objectOrFail<AttentionStateResponse>(payload);
|
|
48
|
+
|
|
49
|
+
switch (_c.state) {
|
|
50
|
+
case 0x01:
|
|
51
|
+
return 'asleep';
|
|
52
|
+
|
|
53
|
+
case 0x02:
|
|
54
|
+
return 'screensaver';
|
|
55
|
+
|
|
56
|
+
case 0x03:
|
|
57
|
+
return 'awake';
|
|
58
|
+
|
|
59
|
+
case 0x04:
|
|
60
|
+
return 'idle';
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
return 'unknown';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getLaunchableApps(): Promise<LaunchableApp[]> {
|
|
68
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
69
|
+
_i: 'FetchLaunchableApplicationssEvent',
|
|
70
|
+
_t: CompanionLinkMessageType.Request,
|
|
71
|
+
_c: {}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const {_c} = objectOrFail<LaunchableAppsResponse>(payload);
|
|
75
|
+
|
|
76
|
+
return Object.entries(_c).map(([bundleId, name]) => ({
|
|
77
|
+
bundleId,
|
|
78
|
+
name
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getSiriRemoteInfo(): Promise<any> {
|
|
83
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
84
|
+
_i: 'FetchSiriRemoteInfo',
|
|
85
|
+
_t: CompanionLinkMessageType.Request,
|
|
86
|
+
_c: {}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return parseBinaryPlist(Buffer.from(payload['_c']['SiriRemoteInfoKey']).buffer);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getUserAccounts(): Promise<UserAccount[]> {
|
|
93
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
94
|
+
_i: 'FetchUserAccountsEvent',
|
|
95
|
+
_t: CompanionLinkMessageType.Request,
|
|
96
|
+
_c: {}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const {_c} = objectOrFail<UserAccountsResponse>(payload);
|
|
100
|
+
|
|
101
|
+
return Object.entries(_c).map(([accountId, name]) => ({
|
|
102
|
+
accountId,
|
|
103
|
+
name
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async hidCommand(command: keyof typeof HidCommand, down = false): Promise<void> {
|
|
108
|
+
await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
109
|
+
_i: '_hidC',
|
|
110
|
+
_t: CompanionLinkMessageType.Request,
|
|
111
|
+
_c: {
|
|
112
|
+
_hBtS: down ? 1 : 2,
|
|
113
|
+
_hidC: HidCommand[command]
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async launchApp(bundleId: string): Promise<void> {
|
|
119
|
+
await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
120
|
+
_i: '_launchApp',
|
|
121
|
+
_t: CompanionLinkMessageType.Request,
|
|
122
|
+
_c: {
|
|
123
|
+
_bundleID: bundleId
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async launchUrl(url: string): Promise<void> {
|
|
129
|
+
await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
130
|
+
_i: '_launchApp',
|
|
131
|
+
_t: CompanionLinkMessageType.Request,
|
|
132
|
+
_c: {
|
|
133
|
+
_urlS: url
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async mediaControlCommand(command: keyof typeof MediaControlCommand, content?: object): Promise<object> {
|
|
139
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
140
|
+
_i: '_mcc',
|
|
141
|
+
_t: CompanionLinkMessageType.Request,
|
|
142
|
+
_c: {
|
|
143
|
+
_mcc: MediaControlCommand[command],
|
|
144
|
+
...(content || {})
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return objectOrFail(payload);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async pressButton(command: keyof typeof HidCommand, type: ButtonPressType = 'SingleTap', holdDelayMs = 500): Promise<void> {
|
|
152
|
+
switch (type) {
|
|
153
|
+
case 'DoubleTap':
|
|
154
|
+
await this.hidCommand(command, true);
|
|
155
|
+
await this.hidCommand(command, false);
|
|
156
|
+
|
|
157
|
+
await this.hidCommand(command, true);
|
|
158
|
+
await this.hidCommand(command, false);
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case 'Hold':
|
|
162
|
+
await this.hidCommand(command, true);
|
|
163
|
+
await waitFor(holdDelayMs);
|
|
164
|
+
await this.hidCommand(command, false);
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'SingleTap':
|
|
168
|
+
await this.hidCommand(command, true);
|
|
169
|
+
await this.hidCommand(command, false);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async switchUserAccount(accountId: string): Promise<void> {
|
|
175
|
+
await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
176
|
+
_i: 'SwitchUserAccountEvent',
|
|
177
|
+
_t: CompanionLinkMessageType.Request,
|
|
178
|
+
_c: {
|
|
179
|
+
SwitchAccountID: accountId
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async _subscribe(event: string, fn: EventListener): Promise<void> {
|
|
185
|
+
this.#socket.addEventListener(event, fn);
|
|
186
|
+
|
|
187
|
+
await this.#socket.send(CompanionLinkFrameType.E_OPACK, {
|
|
188
|
+
_i: '_interest',
|
|
189
|
+
_t: CompanionLinkMessageType.Event,
|
|
190
|
+
_c: {
|
|
191
|
+
_regEvents: [event]
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async _unsubscribe(event: string, fn?: EventListener): Promise<void> {
|
|
197
|
+
if (fn) {
|
|
198
|
+
this.#socket.removeEventListener(event, fn);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await this.#socket.send(CompanionLinkFrameType.E_OPACK, {
|
|
202
|
+
_i: '_interest',
|
|
203
|
+
_t: CompanionLinkMessageType.Event,
|
|
204
|
+
_c: {
|
|
205
|
+
_deregEvents: [event]
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async _sessionStart(): Promise<object> {
|
|
211
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
212
|
+
_i: '_sessionStart',
|
|
213
|
+
_t: CompanionLinkMessageType.Request,
|
|
214
|
+
_c: {
|
|
215
|
+
_srvT: 'com.apple.tvremoteservices',
|
|
216
|
+
_sid: randomInt(0, 2 ** 32 - 1)
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return objectOrFail(payload);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async _systemInfo(pairingId: Buffer): Promise<object> {
|
|
224
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
225
|
+
_i: '_systemInfo',
|
|
226
|
+
_t: CompanionLinkMessageType.Request,
|
|
227
|
+
_c: {
|
|
228
|
+
_bf: 0,
|
|
229
|
+
_cf: 512,
|
|
230
|
+
_clFl: 128,
|
|
231
|
+
_i: 'cafecafecafe',
|
|
232
|
+
_idsID: pairingId.toString(),
|
|
233
|
+
_pubID: 'FF:70:79:61:74:76',
|
|
234
|
+
_sf: 256,
|
|
235
|
+
_sv: '170.18',
|
|
236
|
+
model: 'iPhone10,6',
|
|
237
|
+
name: 'Bas Companion Link'
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return objectOrFail(payload);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async _touchStart(): Promise<object> {
|
|
245
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
246
|
+
_i: '_touchStart',
|
|
247
|
+
_t: CompanionLinkMessageType.Request,
|
|
248
|
+
_c: {
|
|
249
|
+
_height: opackFloat(1000.0),
|
|
250
|
+
_tFl: 0,
|
|
251
|
+
_width: opackFloat(1000.0)
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return objectOrFail(payload);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async _tvrcSessionStart(): Promise<object> {
|
|
259
|
+
const [, payload] = await this.#socket.exchange(CompanionLinkFrameType.E_OPACK, {
|
|
260
|
+
_i: 'TVRCSessionStart',
|
|
261
|
+
_t: CompanionLinkMessageType.Request,
|
|
262
|
+
_btHP: false,
|
|
263
|
+
_inUseProc: 'tvremoted',
|
|
264
|
+
_c: {}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return objectOrFail(payload);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const HidCommand = {
|
|
272
|
+
Up: 1,
|
|
273
|
+
Down: 2,
|
|
274
|
+
Left: 3,
|
|
275
|
+
Right: 4,
|
|
276
|
+
Menu: 5,
|
|
277
|
+
Select: 6,
|
|
278
|
+
Home: 7,
|
|
279
|
+
VolumeUp: 8,
|
|
280
|
+
VolumeDown: 9,
|
|
281
|
+
Siri: 10,
|
|
282
|
+
Screensaver: 11,
|
|
283
|
+
Sleep: 12,
|
|
284
|
+
Wake: 13,
|
|
285
|
+
PlayPause: 14,
|
|
286
|
+
ChannelIncrement: 15,
|
|
287
|
+
ChannelDecrement: 16,
|
|
288
|
+
Guide: 17,
|
|
289
|
+
PageUp: 18,
|
|
290
|
+
PageDown: 19
|
|
291
|
+
} as const;
|
|
292
|
+
|
|
293
|
+
const MediaControlCommand = {
|
|
294
|
+
Play: 1,
|
|
295
|
+
Pause: 2,
|
|
296
|
+
NextTrack: 3,
|
|
297
|
+
PreviousTrack: 4,
|
|
298
|
+
GetVolume: 5,
|
|
299
|
+
SetVolume: 6,
|
|
300
|
+
SkipBy: 7,
|
|
301
|
+
FastForwardBegin: 8,
|
|
302
|
+
FastForwardEnd: 9,
|
|
303
|
+
RewindBegin: 10,
|
|
304
|
+
RewindEnd: 11,
|
|
305
|
+
GetCaptionSettings: 12,
|
|
306
|
+
SetCaptionSettings: 13
|
|
307
|
+
} as const;
|
|
308
|
+
|
|
309
|
+
type ButtonPressType =
|
|
310
|
+
| 'DoubleTap'
|
|
311
|
+
| 'Hold'
|
|
312
|
+
| 'SingleTap';
|
|
313
|
+
|
|
314
|
+
function objectOrFail<T = object>(obj: unknown): T {
|
|
315
|
+
if (typeof obj === 'object') {
|
|
316
|
+
return obj as T;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
debug('Expected an object.', {obj});
|
|
320
|
+
|
|
321
|
+
throw new Error('Expected an object.');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type AttentionState =
|
|
325
|
+
| 'unknown'
|
|
326
|
+
| 'asleep'
|
|
327
|
+
| 'screensaver'
|
|
328
|
+
| 'awake'
|
|
329
|
+
| 'idle';
|
|
330
|
+
|
|
331
|
+
type AttentionStateResponse = {
|
|
332
|
+
readonly _c: {
|
|
333
|
+
readonly state: number;
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
type LaunchableApp = {
|
|
338
|
+
readonly bundleId: string;
|
|
339
|
+
readonly name: string;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
type LaunchableAppsResponse = {
|
|
343
|
+
readonly _c: Record<string, string>;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
type UserAccount = {
|
|
347
|
+
readonly accountId: string;
|
|
348
|
+
readonly name: string;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
type UserAccountsResponse = {
|
|
352
|
+
readonly _c: Record<string, string>;
|
|
353
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Result } from 'node-dns-sd';
|
|
2
|
+
import { CompanionLinkSocket } from '@/socket';
|
|
3
|
+
import Api from './api/companionLink';
|
|
4
|
+
import Pairing from './pairing/companionLink';
|
|
5
|
+
import Verify from './verify/companionLink';
|
|
6
|
+
|
|
7
|
+
export default class CompanionLink {
|
|
8
|
+
get api(): Api {
|
|
9
|
+
return this.#api;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get socket(): CompanionLinkSocket {
|
|
13
|
+
return this.#socket;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get pairing(): Pairing {
|
|
17
|
+
return this.#pairing;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get verify(): Verify {
|
|
21
|
+
return this.#verify;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
readonly #api: Api;
|
|
25
|
+
readonly #device: Result;
|
|
26
|
+
readonly #socket: CompanionLinkSocket;
|
|
27
|
+
readonly #pairing: Pairing;
|
|
28
|
+
readonly #verify: Verify;
|
|
29
|
+
|
|
30
|
+
constructor(device: Result) {
|
|
31
|
+
this.#device = device;
|
|
32
|
+
this.#socket = new CompanionLinkSocket(device.address, device.service.port);
|
|
33
|
+
this.#api = new Api(this, this.#socket);
|
|
34
|
+
this.#pairing = new Pairing(this, this.#socket);
|
|
35
|
+
this.#verify = new Verify(this, this.#socket);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async connect(): Promise<void> {
|
|
39
|
+
await this.#socket.connect();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CompanionLink } from './companionLink';
|