-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
249 lines (223 loc) · 7.47 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/**
* Module dependencies.
* @private
*/
const net = require('net')
/**
* A Samsung TV client for older TVs to simulate remote control
*/
class SamsungTVClient {
/**
* TVs response types.
* @type {{granted: Buffer, denied: Buffer, await: Buffer, timeout: Buffer}}
* @public
*/
static get accessResponse () {
return {
granted: Buffer.from([0x64, 0x00, 0x01, 0x00]), // access granted, you can now send key codes and it will be executed by TV
denied: Buffer.from([0x64, 0x00, 0x00, 0x00]), // access denied – user rejected your network remote controller
await: Buffer.from([0x0A, 0x00, 0x02, 0x00, 0x00, 0x00]), // waiting for user to grant or deny access for your app
timeout: Buffer.from([0x65, 0x00]) // timeout or cancelled by user
}
}
/**
* Initialize a new `SamsungTVClient`.
* @param {number} delay Default delay to send messages to TV
* @public
*/
constructor (delay = 0) {
this.socket = new net.Socket()
this.delay = delay
}
/**
* Connect to TV.
* @param {string} ip TVs IP address
* @return {Promise<undefined|Error>} Resolve on connection, reject on invalid IP or error
* @public
*/
connect (ip) {
return new Promise((resolve, reject) => {
if (net.isIP(ip) === 0) reject(new Error('Not a valid IP'))
this.socket.connect(55000, ip, resolve)
this.socket.on('error', reject)
})
}
/**
* Authenticate to TV.
* @param {string} clientIP IP address of the client
* @param {string} clientUID UID of the client
* @param {string} clientName Client name to display
* @return {Promise<string|Error>} Resolve on success, reject on invalid IP, access denied or abort/timeout
* @public
*/
authenticate (clientIP, clientUID, clientName) {
return new Promise((resolve, reject) => {
if (net.isIP(clientIP) === 0) reject(new Error('Not a valid IP'))
this.socket.write(SamsungTVClient.createMessage(SamsungTVClient.createAuthPayload(clientIP, clientUID, clientName)))
this.socket.on('data', data => {
const payload = SamsungTVClient.parseResponse(data)
if (payload.compare(SamsungTVClient.accessResponse.granted) === 0) {
resolve('Success')
} else if (payload.compare(SamsungTVClient.accessResponse.denied) === 0) {
reject(new Error('Access denied'))
} else if (payload.compare(SamsungTVClient.accessResponse.timeout) === 0) {
reject(new Error('Timeout'))
}
})
})
}
/**
* Send a custom message to TV
* @param {Buffer} buffer Buffer to send
* @return {Promise<boolean>} Resolve on send, never rejected
* @public
*/
send (buffer) {
return new Promise(resolve => {
resolve(this.socket.write(buffer))
})
}
/**
* Send a message created by createMessage to TV
* @param {Buffer} message Message to send
* @param {number} delay Delay to send message
* @return {Promise<boolean>} Resolve on send, never rejected
* @public
*/
sendMessage (message, delay = this.delay) {
return new Promise(resolve => {
return this
.send(message)
.then(() => setTimeout(resolve, delay || this.delay))
})
}
/**
* Send a payload wrapped in TVs standard message
* @param {Buffer} payload Payload to send
* @param {number} delay Delay to send message
* @return {Promise<boolean>} Resolve on send, never rejected
* @public
*/
sendMessageByPayload (payload, delay = this.delay) {
return this.sendMessage(SamsungTVClient.createMessage(payload), delay)
}
/**
* Send a key wrapped in TVs standard message
* @param {string} key Key to send
* @param {number} delay Delay to send message
* @return {Promise<boolean>} Resolve on send, never rejected
* @public
*/
sendMessageByKey (key, delay = this.delay) {
return this.sendMessageByPayload(SamsungTVClient.createKeyPayload(key), delay)
}
/**
* Create a message to send to tv with the given payload.
* @param {Buffer} payload Payload to add to send to TV
* @return {Buffer} Message with given payload
* @public
*/
static createMessage (payload) {
const name = 'iphone.iapp.samsung'
const nameSize = name.length
const payloadSize = payload.length
const buffLen = 5 + nameSize + payloadSize // 1 0x00, 4 size bytes
const message = Buffer.alloc(buffLen)
let offset = 1 // First is 0x00
message.writeUInt16LE(nameSize, offset)
offset += 2
message.write(name, offset)
offset += nameSize
message.writeUInt16LE(payloadSize, offset)
offset += 2
message.write(payload.toString(), offset) // offset += payloadSize
return message
}
/**
* Create the payload to authenticate.
* @param {string} ip IP address to add to payload
* @param {string} id ID to add to payload
* @param {string} name Name to add to payload
* @return {Buffer} Payload
* @throws {Error} Invalid IP
* @public
*/
static createAuthPayload (ip, id, name) {
if (net.isIP(ip) === 0) throw new Error('Not a valid IP')
const ipBase64 = SamsungTVClient.toBase64(ip)
const ipSize = ipBase64.length
const idBase64 = SamsungTVClient.toBase64(id)
const idSize = idBase64.length
const nameBase64 = SamsungTVClient.toBase64(name)
const nameSize = nameBase64.length
const buffLen = 8 + ipSize + idSize + nameSize // 2 static bytes, 6 size bytes
const payload = Buffer.alloc(buffLen)
let offset = 0
payload.writeUInt8(0x64, offset)
offset += 2 // Next is 0x00
payload.writeUInt16LE(ipSize, offset)
offset += 2
payload.write(ipBase64, offset)
offset += ipSize
payload.writeUInt16LE(idSize, offset)
offset += 2
payload.write(idBase64, offset)
offset += idSize
payload.writeUInt16LE(nameSize, offset)
offset += 2
payload.write(nameBase64, offset) // offset += nameSize
return payload
}
/**
* Create the payload with the key.
* @param {string} key TV key
* @return {Buffer} Key to wrap in payload
* @public
*/
static createKeyPayload (key) {
const keyBase64 = SamsungTVClient.toBase64(key)
const keySize = keyBase64.length
const buffLen = 5 + keySize // 3x 0x00, key size, key value
const payload = Buffer.alloc(buffLen)
let offset = 3 // First 3 are 0x00
payload.writeUInt16LE(keySize, offset)
offset += 2
payload.write(keyBase64, offset) // offset += keySize
return payload
}
/**
* Parse TVs response.
* @param {Buffer} response Response from TV
* @return {Buffer} Payload
* @public
*/
static parseResponse (response) {
let offset = 1 // ignore first byte
const stringLen = response.readUInt16LE(offset)
offset += 2 + stringLen
const payloadSize = response.readUInt16LE(offset)
offset += 2
return response.slice(offset, payloadSize + offset)
// // Detailed way
// let offset = 0;
// const firstByte = response.readUInt8(offset); offset += 1
// const stringLen = response.readUInt16LE(offset); offset += 2
// const string = response.toString('utf8', offset, stringLen + offset); offset += stringLen
// const payloadSize = response.readUInt16LE(offset); offset += 2
// return response.slice(offset, payloadSize + offset); // offset += payloadSize
}
/**
* Converts a string to base64.
* @param {string} string String to convert
* @return {string} String in base64
* @private
*/
static toBase64 (string) {
return Buffer.from(string).toString('base64')
}
}
/**
* Module exports.
* @public
*/
module.exports = SamsungTVClient