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.
@@ -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.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
- "test": "echo \"Error: no test specified\""
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
+ }