camstreamerlib 1.3.1 → 1.6.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/.github/workflows/github-ci.yml +46 -0
- package/jest.config.js +11 -0
- package/package.json +20 -3
- package/src/CamOverlayAPI.ts +370 -0
- package/src/CamStreamerAPI.ts +58 -0
- package/src/CamSwitcherAPI.ts +130 -0
- package/src/CameraVapix.ts +398 -0
- package/src/Digest.ts +41 -0
- package/src/HTTPRequest.ts +97 -0
- package/{HttpServer.js → src/HttpServer.ts} +43 -34
- package/src/RtspClient.ts +343 -0
- package/test/Digest.test.ts +13 -0
- package/tsconfig.json +11 -0
- package/CamOverlayAPI.js +0 -365
- package/CamStreamerAPI.js +0 -77
- package/CamSwitcherAPI.js +0 -128
- package/CameraVapix.js +0 -258
- package/Digest.js +0 -41
- package/HTTPRequest.js +0 -78
- package/RtspClient.js +0 -327
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# This is a basic workflow to help you get started with Actions
|
|
2
|
+
|
|
3
|
+
name: CI
|
|
4
|
+
|
|
5
|
+
# Controls when the workflow will run
|
|
6
|
+
on:
|
|
7
|
+
# Triggers the workflow on push or pull request events but only for the main branch
|
|
8
|
+
push:
|
|
9
|
+
branches: [ master ]
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [ master ]
|
|
12
|
+
|
|
13
|
+
# Allows you to run this workflow manually from the Actions tab
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
|
17
|
+
jobs:
|
|
18
|
+
# This workflow contains a single job called "build"
|
|
19
|
+
build:
|
|
20
|
+
# The type of runner that the job will run on
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
|
|
23
|
+
# Steps represent a sequence of tasks that will be executed as part of the job
|
|
24
|
+
steps:
|
|
25
|
+
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
|
26
|
+
- uses: actions/checkout@v2
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-node@v2
|
|
29
|
+
with:
|
|
30
|
+
node-version: '16.x'
|
|
31
|
+
registry-url: 'https://registry.npmjs.org'
|
|
32
|
+
|
|
33
|
+
# Runs a single command using the runners shell
|
|
34
|
+
- name: Init
|
|
35
|
+
run: |
|
|
36
|
+
npm install --production=false
|
|
37
|
+
# Runs
|
|
38
|
+
- name: Transpilation
|
|
39
|
+
run: npm run build
|
|
40
|
+
# Runs
|
|
41
|
+
- name: Automatic test (using Jest library)
|
|
42
|
+
run: npm run test
|
|
43
|
+
# Runs a set of commands using the runners shell
|
|
44
|
+
- name: Prettier Check
|
|
45
|
+
run: npm run pretty:check
|
|
46
|
+
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
testMatch: ['**/?(*.)+(test).[jt]s?(x)'],
|
|
6
|
+
moduleDirectories: ['node_modules', 'src', 'test'],
|
|
7
|
+
transform: {
|
|
8
|
+
'^.+\\.ts?$': 'ts-jest',
|
|
9
|
+
},
|
|
10
|
+
transformIgnorePatterns: ['/node_modules/'],
|
|
11
|
+
};
|
package/package.json
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camstreamerlib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Helper library for CamStreamer ACAP applications.",
|
|
5
|
+
"prettier": "@camstreamer/prettier-config",
|
|
5
6
|
"main": "CameraVapix.js",
|
|
6
7
|
"dependencies": {
|
|
7
8
|
"crypto": "^1.0.1",
|
|
8
9
|
"eventemitter2": "^5.0.1",
|
|
9
10
|
"prettify-xml": "^1.2.0",
|
|
11
|
+
"typescript": "^4.7.4",
|
|
10
12
|
"ws": "^7.4.2",
|
|
11
13
|
"xml2js": "^0.4.19"
|
|
12
14
|
},
|
|
13
|
-
"devDependencies": {
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@camstreamer/prettier-config": "^2.0.4",
|
|
17
|
+
"@types/jest": "^28.0.0",
|
|
18
|
+
"@types/node": "^18.0.6",
|
|
19
|
+
"ts-jest": "^28.0.0",
|
|
20
|
+
"ts-node": "^10.7.0",
|
|
21
|
+
"jest": "^28.1.3",
|
|
22
|
+
"npm-run-all": "^4.1.5",
|
|
23
|
+
"prettier": "^2.7.1",
|
|
24
|
+
"rimraf": "^3.0.2"
|
|
25
|
+
},
|
|
14
26
|
"scripts": {
|
|
15
|
-
"
|
|
27
|
+
"clean": "rimraf dist/*",
|
|
28
|
+
"build": "npm-run-all clean tsc",
|
|
29
|
+
"tsc": "tsc",
|
|
30
|
+
"pretty": "prettier --write \"./src/*.{ts,tsx}\"",
|
|
31
|
+
"pretty:check": "prettier --check \"./src/*.{ts,tsx}\"",
|
|
32
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
16
33
|
},
|
|
17
34
|
"repository": {
|
|
18
35
|
"type": "git",
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import * as WebSocket from 'ws';
|
|
2
|
+
import * as EventEmitter from 'events';
|
|
3
|
+
|
|
4
|
+
import { Digest } from './Digest';
|
|
5
|
+
import { httpRequest } from './HTTPRequest';
|
|
6
|
+
|
|
7
|
+
export type CamOverlayOptions = {
|
|
8
|
+
protocol: string;
|
|
9
|
+
ip: string;
|
|
10
|
+
port: number;
|
|
11
|
+
auth: string;
|
|
12
|
+
serviceName: string;
|
|
13
|
+
serviceID: number;
|
|
14
|
+
camera: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Field = {
|
|
18
|
+
field_name: string;
|
|
19
|
+
text: string;
|
|
20
|
+
color?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Service = {
|
|
24
|
+
id: number;
|
|
25
|
+
enabled: number;
|
|
26
|
+
schedule: string;
|
|
27
|
+
name: string;
|
|
28
|
+
identifier: string;
|
|
29
|
+
camera: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ServiceJson = {
|
|
33
|
+
services: Service[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type AsyncMessage = {
|
|
37
|
+
resolve;
|
|
38
|
+
reject;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class CamOverlayAPI extends EventEmitter {
|
|
42
|
+
private protocol: string;
|
|
43
|
+
private ip: string;
|
|
44
|
+
private port: number;
|
|
45
|
+
private auth: string;
|
|
46
|
+
private serviceName: string;
|
|
47
|
+
private serviceID: number;
|
|
48
|
+
private camera: number;
|
|
49
|
+
private callId: number;
|
|
50
|
+
private sendMessages: Record<number, AsyncMessage>;
|
|
51
|
+
|
|
52
|
+
private ws: WebSocket = null;
|
|
53
|
+
|
|
54
|
+
constructor(options?: CamOverlayOptions) {
|
|
55
|
+
super();
|
|
56
|
+
|
|
57
|
+
this.protocol = options?.protocol ?? 'ws';
|
|
58
|
+
this.ip = options?.ip ?? '127.0.0.1';
|
|
59
|
+
this.port = options?.port ?? this.protocol == 'ws' ? 80 : 443;
|
|
60
|
+
this.auth = options?.auth ?? '';
|
|
61
|
+
this.serviceName = options?.serviceName ?? '';
|
|
62
|
+
this.serviceID = options?.serviceID ?? -1; // If service is already created you can skip creation step by filling this parameter
|
|
63
|
+
this.camera = options?.camera ?? 0;
|
|
64
|
+
|
|
65
|
+
this.callId = 0;
|
|
66
|
+
this.sendMessages = {};
|
|
67
|
+
|
|
68
|
+
EventEmitter.call(this);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async connect() {
|
|
72
|
+
if (this.serviceID != -1) {
|
|
73
|
+
await this.openWebsocket();
|
|
74
|
+
} else {
|
|
75
|
+
try {
|
|
76
|
+
let id = await this.createService();
|
|
77
|
+
this.serviceID = id;
|
|
78
|
+
await this.openWebsocket();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.reportErr(err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async createService() {
|
|
86
|
+
const options = {
|
|
87
|
+
host: this.ip,
|
|
88
|
+
port: this.port,
|
|
89
|
+
path: '/local/camoverlay/api/services.cgi?action=get',
|
|
90
|
+
auth: this.auth,
|
|
91
|
+
};
|
|
92
|
+
const response = (await httpRequest(options)) as string;
|
|
93
|
+
let servicesJson: ServiceJson;
|
|
94
|
+
try {
|
|
95
|
+
servicesJson = JSON.parse(response);
|
|
96
|
+
servicesJson.services ??= [];
|
|
97
|
+
} catch {
|
|
98
|
+
servicesJson = { services: [] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Find service
|
|
102
|
+
let service: Service = null;
|
|
103
|
+
let maxID = -1;
|
|
104
|
+
let servicesArr = servicesJson.services;
|
|
105
|
+
for (let s of servicesArr) {
|
|
106
|
+
if (s.id > maxID) {
|
|
107
|
+
maxID = s.id;
|
|
108
|
+
}
|
|
109
|
+
if (s.identifier == this.serviceName && s.name == 'scripter') {
|
|
110
|
+
service = s;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (service != null) {
|
|
116
|
+
if (service.enabled == 1) {
|
|
117
|
+
// Check and update service parameters if necessary
|
|
118
|
+
if (service.camera == undefined || service.camera != this.camera) {
|
|
119
|
+
service.camera = this.camera;
|
|
120
|
+
await this.updateServices(servicesJson);
|
|
121
|
+
return service.id as number;
|
|
122
|
+
} else {
|
|
123
|
+
return service.id as number;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error('CamOverlay service is not enabled');
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// Create new service
|
|
130
|
+
let newServiceID = maxID + 1;
|
|
131
|
+
service = {
|
|
132
|
+
id: newServiceID,
|
|
133
|
+
enabled: 1,
|
|
134
|
+
schedule: '',
|
|
135
|
+
name: 'scripter',
|
|
136
|
+
identifier: this.serviceName,
|
|
137
|
+
camera: this.camera,
|
|
138
|
+
};
|
|
139
|
+
servicesJson.services.push(service);
|
|
140
|
+
await this.updateServices(servicesJson);
|
|
141
|
+
return newServiceID;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async updateServices(servicesJson: ServiceJson) {
|
|
146
|
+
const options = {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
host: this.ip,
|
|
149
|
+
port: this.port,
|
|
150
|
+
path: '/local/camoverlay/api/services.cgi?action=set',
|
|
151
|
+
auth: this.auth,
|
|
152
|
+
};
|
|
153
|
+
await httpRequest(options, JSON.stringify(servicesJson));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
openWebsocket(digestHeader?: string) {
|
|
157
|
+
let promise = new Promise<void>((resolve, reject) => {
|
|
158
|
+
let userPass = this.auth.split(':');
|
|
159
|
+
let addr = `${this.protocol}://${this.ip}:${this.port}/local/camoverlay/ws`;
|
|
160
|
+
|
|
161
|
+
let options = {
|
|
162
|
+
auth: this.auth,
|
|
163
|
+
headers: {},
|
|
164
|
+
};
|
|
165
|
+
if (digestHeader != undefined) {
|
|
166
|
+
options.headers['Authorization'] = Digest.getAuthHeader(
|
|
167
|
+
userPass[0],
|
|
168
|
+
userPass[1],
|
|
169
|
+
'GET',
|
|
170
|
+
'/local/camoverlay/ws',
|
|
171
|
+
digestHeader
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.ws = new WebSocket(addr, 'cairo-api', options);
|
|
176
|
+
this.ws.on('open', () => {
|
|
177
|
+
this.reportMsg('Websocket opened');
|
|
178
|
+
resolve();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
this.ws.on('message', (data: string) => {
|
|
182
|
+
let dataJSON = JSON.parse(data);
|
|
183
|
+
if (dataJSON.hasOwnProperty('call_id') && dataJSON['call_id'] in this.sendMessages) {
|
|
184
|
+
this.sendMessages[dataJSON['call_id']].resolve(dataJSON);
|
|
185
|
+
delete this.sendMessages[dataJSON['call_id']];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (dataJSON.hasOwnProperty('error')) {
|
|
189
|
+
let error = new Error(JSON.stringify(data));
|
|
190
|
+
this.reportErr(error);
|
|
191
|
+
} else {
|
|
192
|
+
this.reportMsg(data);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.ws.on('unexpected-response', async (req, res) => {
|
|
197
|
+
if (res.statusCode == 401 && res.headers['www-authenticate'] != undefined)
|
|
198
|
+
this.openWebsocket(res.headers['www-authenticate']).then(resolve, reject);
|
|
199
|
+
else {
|
|
200
|
+
reject('Error: status code: ' + res.statusCode + ', ' + res.data);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.ws.on('error', (error: Error) => {
|
|
205
|
+
this.reportErr(error);
|
|
206
|
+
reject(error);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this.ws.on('close', () => {
|
|
210
|
+
this.reportClose();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
return promise;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
cairo(command: string, ...params) {
|
|
217
|
+
return this.sendMessage({ command: command, params: params });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
writeText(...params) {
|
|
221
|
+
return this.sendMessage({ command: 'write_text', params: params });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
uploadImageData(imgBuffer: Buffer) {
|
|
225
|
+
return this.sendMessage({ command: 'upload_image_data', params: [imgBuffer.toString('base64')] });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
uploadFontData(fontBuffer: Buffer) {
|
|
229
|
+
return this.sendMessage({ command: 'upload_font_data', params: [fontBuffer.toString('base64')] });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
showCairoImage(cairoImage, posX: number, posY: number) {
|
|
233
|
+
return this.sendMessage({ command: 'show_cairo_image', params: [this.serviceID, cairoImage, posX, posY] });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
removeImage() {
|
|
237
|
+
return this.sendMessage({ command: 'remove_image', params: [this.serviceID] });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
showCairoImageAbsolute(cairoImage, posX: number, posY: number, width: number, height: number) {
|
|
241
|
+
return this.sendMessage({
|
|
242
|
+
command: 'show_cairo_image',
|
|
243
|
+
params: [this.serviceID, cairoImage, -1.0 + (2.0 / width) * posX, -1.0 + (2.0 / height) * posY],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
sendMessage(msgJson) {
|
|
248
|
+
let promise = new Promise((resolve, reject) => {
|
|
249
|
+
try {
|
|
250
|
+
this.sendMessages[this.callId] = { resolve, reject };
|
|
251
|
+
msgJson['call_id'] = this.callId++;
|
|
252
|
+
this.ws.send(JSON.stringify(msgJson));
|
|
253
|
+
} catch (err) {
|
|
254
|
+
this.reportErr(new Error(`Send message error: ${err}`));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return promise;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
reportMsg(msg: string) {
|
|
261
|
+
this.emit('msg', msg);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
reportErr(err: Error) {
|
|
265
|
+
this.ws?.terminate();
|
|
266
|
+
this.emit('error', err);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
reportClose() {
|
|
270
|
+
this.emit('close');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
updateCGText(fields: Field[]) {
|
|
274
|
+
let field_specs = '';
|
|
275
|
+
|
|
276
|
+
for (let field of fields) {
|
|
277
|
+
const name = field.field_name;
|
|
278
|
+
field_specs += `&${name}=${field.text}`;
|
|
279
|
+
if (field.color != undefined) {
|
|
280
|
+
field_specs += `&${name}_color=${field.color}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return this.promiseCGUpdate('update_text', field_specs);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
coorinates =
|
|
289
|
+
left
|
|
290
|
+
right
|
|
291
|
+
top
|
|
292
|
+
bottom
|
|
293
|
+
top_left
|
|
294
|
+
center
|
|
295
|
+
...
|
|
296
|
+
*/
|
|
297
|
+
private formCoordinates(coordinates: string, x: number, y: number) {
|
|
298
|
+
return coordinates != '' ? `&coord_system=${coordinates}&pos_x=${x}&pos_y=${y}` : '';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
updateCGImage(path: string, coordinates = '', x = 0, y = 0) {
|
|
302
|
+
const coord = this.formCoordinates(coordinates, x, y);
|
|
303
|
+
const update = `&image=${path}`;
|
|
304
|
+
return this.promiseCGUpdate('update_image', update + coord);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
updateCGImagePos(coordinates = '', x = 0, y = 0) {
|
|
308
|
+
const coord = this.formCoordinates(coordinates, x, y);
|
|
309
|
+
return this.promiseCGUpdate('update_image', coord);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async promiseCGUpdate(action: string, params: string) {
|
|
313
|
+
const path = encodeURI(
|
|
314
|
+
`/local/camoverlay/api/customGraphics.cgi?action=${action}&service_id=${this.serviceID}${params}`
|
|
315
|
+
);
|
|
316
|
+
const options = {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
host: this.ip,
|
|
319
|
+
port: this.port,
|
|
320
|
+
path: path,
|
|
321
|
+
auth: this.auth,
|
|
322
|
+
};
|
|
323
|
+
await httpRequest(options, '');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async updateInfoticker(text: string) {
|
|
327
|
+
const path = `/local/camoverlay/api/infoticker.cgi?service_id=${this.serviceID}&text=${text}`;
|
|
328
|
+
|
|
329
|
+
const options = {
|
|
330
|
+
method: 'GET',
|
|
331
|
+
host: this.ip,
|
|
332
|
+
port: this.port,
|
|
333
|
+
path: path,
|
|
334
|
+
auth: this.auth,
|
|
335
|
+
};
|
|
336
|
+
await httpRequest(options, '');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async setEnabled(enabled: boolean) {
|
|
340
|
+
const value = enabled ? 1 : 0;
|
|
341
|
+
const path = encodeURI(`/local/camoverlay/api/enabled.cgi?id${this.serviceID}=${value}`);
|
|
342
|
+
const options = {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
host: this.ip,
|
|
345
|
+
port: this.port,
|
|
346
|
+
path: path,
|
|
347
|
+
auth: this.auth,
|
|
348
|
+
};
|
|
349
|
+
await httpRequest(options, '');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async isEnabled() {
|
|
353
|
+
const options = {
|
|
354
|
+
method: 'GET',
|
|
355
|
+
host: this.ip,
|
|
356
|
+
port: this.port,
|
|
357
|
+
path: '/local/camoverlay/api/services.cgi?action=get',
|
|
358
|
+
auth: this.auth,
|
|
359
|
+
};
|
|
360
|
+
const response = (await httpRequest(options, '')) as string;
|
|
361
|
+
const data: ServiceJson = JSON.parse(response);
|
|
362
|
+
|
|
363
|
+
for (let service of data.services) {
|
|
364
|
+
if (service.id == this.serviceID) {
|
|
365
|
+
return service.enabled == 1;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw new Error('Service not found.');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { httpRequest } from './HTTPRequest';
|
|
2
|
+
import { HttpRequestOptions } from './HTTPRequest';
|
|
3
|
+
|
|
4
|
+
export type CamStreamerAPIOptions = {
|
|
5
|
+
protocol: string;
|
|
6
|
+
ip: string;
|
|
7
|
+
port: number;
|
|
8
|
+
auth: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class CamStreamerAPI {
|
|
12
|
+
private protocol: string;
|
|
13
|
+
private ip: string;
|
|
14
|
+
private port: number;
|
|
15
|
+
private auth: string;
|
|
16
|
+
|
|
17
|
+
constructor(options?: CamStreamerAPIOptions) {
|
|
18
|
+
this.protocol = options?.protocol ?? 'http';
|
|
19
|
+
this.ip = options?.ip ?? '127.0.0.1';
|
|
20
|
+
this.port = options?.port ?? this.protocol == 'http' ? 80 : 443;
|
|
21
|
+
this.auth = options?.auth ?? '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getStreamList() {
|
|
25
|
+
const streamListRes = await this.get('/local/camstreamer/stream/list.cgi');
|
|
26
|
+
return streamListRes.data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getStreamParameter(streamID: string, paramName: string) {
|
|
30
|
+
const stream = await this.get(`/local/camstreamer/stream/get.cgi?stream_id=${streamID}`);
|
|
31
|
+
return stream.data[paramName];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async setStreamParameter(streamID: string, paramName: string, value: string) {
|
|
35
|
+
return await this.get(`/local/camstreamer/stream/set.cgi?stream_id=${streamID}&${paramName}=${value}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async isStreaming(streamID: string) {
|
|
39
|
+
const response = await this.get(`/local/camstreamer/get_streamstat.cgi?stream_id=${streamID}`);
|
|
40
|
+
return response.data.is_streaming;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async get(path: string) {
|
|
44
|
+
const options = this.getBaseConnectionParams();
|
|
45
|
+
options.path = encodeURI(path);
|
|
46
|
+
const data = (await httpRequest(options)) as string;
|
|
47
|
+
return JSON.parse(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private getBaseConnectionParams(): HttpRequestOptions {
|
|
51
|
+
return {
|
|
52
|
+
protocol: this.protocol + ':',
|
|
53
|
+
host: this.ip,
|
|
54
|
+
port: this.port,
|
|
55
|
+
auth: this.auth,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as WebSocket from 'ws';
|
|
2
|
+
import * as EventEmitter from 'events';
|
|
3
|
+
|
|
4
|
+
import { httpRequest } from './HTTPRequest';
|
|
5
|
+
import { HttpRequestOptions } from './HTTPRequest';
|
|
6
|
+
|
|
7
|
+
export type CamSwitcherAPIOptions = {
|
|
8
|
+
ip?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
auth?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class CamSwitcherAPI extends EventEmitter {
|
|
14
|
+
private ip: string;
|
|
15
|
+
private port: number;
|
|
16
|
+
private auth: string;
|
|
17
|
+
|
|
18
|
+
private ws: WebSocket;
|
|
19
|
+
private pingTimer: NodeJS.Timer;
|
|
20
|
+
|
|
21
|
+
constructor(options: CamSwitcherAPIOptions) {
|
|
22
|
+
super();
|
|
23
|
+
this.ip = options?.ip ?? '127.0.0.1';
|
|
24
|
+
this.port = options?.port ?? 80;
|
|
25
|
+
this.auth = options?.auth ?? '';
|
|
26
|
+
EventEmitter.call(this);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Connect for Websocket events
|
|
30
|
+
async connect() {
|
|
31
|
+
try {
|
|
32
|
+
const token = await this.get('/local/camswitcher/ws_authorization.cgi');
|
|
33
|
+
this.ws = new WebSocket(`ws://${this.ip}:${this.port}/local/camswitcher/events`, 'events');
|
|
34
|
+
this.pingTimer = null;
|
|
35
|
+
|
|
36
|
+
this.ws.on('open', () => {
|
|
37
|
+
this.ws.send(JSON.stringify({ authorization: token }));
|
|
38
|
+
this.ws.isAlive = true;
|
|
39
|
+
this.pingTimer = setInterval(() => {
|
|
40
|
+
if (this.ws.isAlive === false) {
|
|
41
|
+
return this.ws.terminate();
|
|
42
|
+
}
|
|
43
|
+
this.ws.isAlive = false;
|
|
44
|
+
this.ws.ping();
|
|
45
|
+
}, 30000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.ws.on('pong', () => {
|
|
49
|
+
this.ws.isAlive = true;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this.ws.on('message', (data: string) => {
|
|
53
|
+
try {
|
|
54
|
+
const parsedData: object = JSON.parse(data);
|
|
55
|
+
this.emit('event', parsedData);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.log(err);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.ws.on('close', () => {
|
|
62
|
+
clearInterval(this.pingTimer);
|
|
63
|
+
this.emit('event_connection_close');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.ws.on('error', (err: Error) => {
|
|
67
|
+
this.emit('event_connection_error', err);
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
this.emit('event_connection_error', err as Error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getPlaylistList() {
|
|
75
|
+
return this.get('/local/camswitcher/playlists.cgi?action=get');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getClipList() {
|
|
79
|
+
return this.get('/local/camswitcher/clips.cgi?action=get');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
playlistSwitch(playlistName: string) {
|
|
83
|
+
return this.get(`/local/camswitcher/playlist_switch.cgi?playlist_name=${playlistName}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
playlistQueueList() {
|
|
87
|
+
return this.get('/local/camswitcher/playlist_queue_list.cgi');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
playlistQueueClear() {
|
|
91
|
+
return this.get('/local/camswitcher/playlist_queue_clear.cgi');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
playlistQueuePush(playlistName: string) {
|
|
95
|
+
return this.get(`/local/camswitcher/playlist_queue_push.cgi?playlist_name=${playlistName}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
playlistQueuePlayNext() {
|
|
99
|
+
return this.get('/local/camswitcher/playlist_queue_play_next.cgi');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getOutputInfo() {
|
|
103
|
+
return this.get('/local/camswitcher/output_info.cgi');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async get(path: string) {
|
|
107
|
+
const options = this.getBaseConnectionParams();
|
|
108
|
+
options.path = encodeURI(path);
|
|
109
|
+
const data = (await httpRequest(options)) as string;
|
|
110
|
+
try {
|
|
111
|
+
const response = JSON.parse(data);
|
|
112
|
+
if (response.status == 200) {
|
|
113
|
+
return response.data as object;
|
|
114
|
+
} else {
|
|
115
|
+
throw new Error(`Request (${path}) error, response: ${JSON.stringify(response)}`);
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new Error(`Request (${path}) error: ${err}, msg: ${data}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private getBaseConnectionParams(): HttpRequestOptions {
|
|
123
|
+
return {
|
|
124
|
+
protocol: 'http:',
|
|
125
|
+
host: this.ip,
|
|
126
|
+
port: this.port,
|
|
127
|
+
auth: this.auth,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|