camstreamerlib 4.0.33-dfd8bbb → 4.0.34-be26059
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/cjs/CamOverlayAPI.d.ts +0 -1
- package/cjs/CamOverlayAPI.js +0 -7
- package/cjs/PlaneTrackerAPI.d.ts +42 -9
- package/cjs/VapixAPI.js +1 -1
- package/cjs/helpers.js +1 -1
- package/cjs/node/HttpRequestSender.js +8 -5
- package/cjs/node/HttpRequestSender.test.d.ts +1 -0
- package/cjs/node/HttpRequestSender.test.js +161 -0
- package/cjs/types/PlaneTrackerAPI.d.ts +205 -26
- package/cjs/types/PlaneTrackerAPI.js +59 -15
- package/cjs/types/PlaneTrackerAPI.test.d.ts +1 -0
- package/cjs/types/PlaneTrackerAPI.test.js +90 -0
- package/esm/CamOverlayAPI.js +0 -7
- package/esm/VapixAPI.js +1 -1
- package/esm/helpers.js +1 -1
- package/esm/node/HttpRequestSender.js +9 -6
- package/esm/node/HttpRequestSender.test.js +136 -0
- package/esm/types/PlaneTrackerAPI.js +59 -15
- package/esm/types/PlaneTrackerAPI.test.js +88 -0
- package/package.json +12 -9
- package/types/CamOverlayAPI.d.ts +0 -1
- package/types/PlaneTrackerAPI.d.ts +42 -9
- package/types/node/HttpRequestSender.test.d.ts +1 -0
- package/types/types/PlaneTrackerAPI.d.ts +205 -26
- package/types/types/PlaneTrackerAPI.test.d.ts +1 -0
|
@@ -40,13 +40,59 @@ const labelOptionsSchema = zod_1.z.union([
|
|
|
40
40
|
zod_1.z.literal('flight_number'),
|
|
41
41
|
zod_1.z.literal('icao'),
|
|
42
42
|
]);
|
|
43
|
-
const
|
|
43
|
+
const aircraftIdentificationLabelSchema = zod_1.z.object({
|
|
44
44
|
firstRow: labelOptionsSchema,
|
|
45
45
|
secondRow: labelOptionsSchema,
|
|
46
46
|
thirdRow: labelOptionsSchema,
|
|
47
47
|
fourthRow: labelOptionsSchema,
|
|
48
48
|
opacity: zod_1.z.number().positive(),
|
|
49
49
|
});
|
|
50
|
+
const droneLabelOptionsSchema = zod_1.z.union([zod_1.z.literal('blank'), zod_1.z.literal('remote_id')]);
|
|
51
|
+
const droneIdentificationLabelSchema = zod_1.z.object({
|
|
52
|
+
firstRow: droneLabelOptionsSchema,
|
|
53
|
+
opacity: zod_1.z.number().positive(),
|
|
54
|
+
});
|
|
55
|
+
const DEFAULT_DRONE_IDENTIFICATION_LABEL = {
|
|
56
|
+
firstRow: 'remote_id',
|
|
57
|
+
opacity: 30,
|
|
58
|
+
};
|
|
59
|
+
const identificationLabelSchema = zod_1.z.union([
|
|
60
|
+
zod_1.z.object({
|
|
61
|
+
aircraft: aircraftIdentificationLabelSchema,
|
|
62
|
+
drone: droneIdentificationLabelSchema.default(DEFAULT_DRONE_IDENTIFICATION_LABEL),
|
|
63
|
+
}),
|
|
64
|
+
aircraftIdentificationLabelSchema.transform((aircraft) => ({
|
|
65
|
+
aircraft,
|
|
66
|
+
drone: { ...DEFAULT_DRONE_IDENTIFICATION_LABEL },
|
|
67
|
+
})),
|
|
68
|
+
]);
|
|
69
|
+
const captureConfigSchema = zod_1.z.object({
|
|
70
|
+
defaultCaptureSizeMeters: zod_1.z.number().positive(),
|
|
71
|
+
captureSizeExtensionMeters: zod_1.z.number().positive(),
|
|
72
|
+
});
|
|
73
|
+
const DEFAULT_CAPTURE_CONFIG = {
|
|
74
|
+
defaultCaptureSizeMeters: 120,
|
|
75
|
+
captureSizeExtensionMeters: 80,
|
|
76
|
+
};
|
|
77
|
+
const cameraConfigSchema = zod_1.z.union([
|
|
78
|
+
zod_1.z.object({
|
|
79
|
+
aircraft: captureConfigSchema,
|
|
80
|
+
drone: captureConfigSchema.default(() => ({ ...DEFAULT_CAPTURE_CONFIG })),
|
|
81
|
+
}),
|
|
82
|
+
zod_1.z
|
|
83
|
+
.object({
|
|
84
|
+
defaultCaptureSizeMeters: zod_1.z.number().positive().default(DEFAULT_CAPTURE_CONFIG.defaultCaptureSizeMeters),
|
|
85
|
+
captureSizeExtensionMeters: zod_1.z
|
|
86
|
+
.number()
|
|
87
|
+
.positive()
|
|
88
|
+
.default(DEFAULT_CAPTURE_CONFIG.captureSizeExtensionMeters),
|
|
89
|
+
})
|
|
90
|
+
.strict()
|
|
91
|
+
.transform((captureConfig) => ({
|
|
92
|
+
aircraft: captureConfig,
|
|
93
|
+
drone: { ...captureConfig },
|
|
94
|
+
})),
|
|
95
|
+
]);
|
|
50
96
|
exports.cameraSettingsSchema = zod_1.z.object({
|
|
51
97
|
units: zod_1.z.union([zod_1.z.literal('metric'), zod_1.z.literal('imperial')]).default('imperial'),
|
|
52
98
|
adsbSource: zod_1.z
|
|
@@ -84,15 +130,10 @@ exports.cameraSettingsSchema = zod_1.z.object({
|
|
|
84
130
|
nightSkyCalibrationFocusValue: 9000,
|
|
85
131
|
focusCalibrationPoints: '',
|
|
86
132
|
}),
|
|
87
|
-
cameraConfig:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
})
|
|
92
|
-
.default({
|
|
93
|
-
defaultCaptureSizeMeters: 120,
|
|
94
|
-
captureSizeExtensionMeters: 80,
|
|
95
|
-
}),
|
|
133
|
+
cameraConfig: cameraConfigSchema.default(() => ({
|
|
134
|
+
aircraft: { ...DEFAULT_CAPTURE_CONFIG },
|
|
135
|
+
drone: { ...DEFAULT_CAPTURE_CONFIG },
|
|
136
|
+
})),
|
|
96
137
|
stream: zod_1.z
|
|
97
138
|
.object({
|
|
98
139
|
width: zod_1.z.number().positive(),
|
|
@@ -203,11 +244,14 @@ exports.cameraSettingsSchema = zod_1.z.object({
|
|
|
203
244
|
port: 80,
|
|
204
245
|
}),
|
|
205
246
|
identificationLabel: identificationLabelSchema.default({
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
247
|
+
aircraft: {
|
|
248
|
+
firstRow: 'icao',
|
|
249
|
+
secondRow: 'call_sign',
|
|
250
|
+
thirdRow: 'blank',
|
|
251
|
+
fourthRow: 'blank',
|
|
252
|
+
opacity: 30,
|
|
253
|
+
},
|
|
254
|
+
drone: DEFAULT_DRONE_IDENTIFICATION_LABEL,
|
|
211
255
|
}),
|
|
212
256
|
acs: exports.connectionSchema
|
|
213
257
|
.extend({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const PlaneTrackerAPI_1 = require("./PlaneTrackerAPI");
|
|
4
|
+
const globals_1 = require("@jest/globals");
|
|
5
|
+
const AIRCRAFT_LABEL = {
|
|
6
|
+
firstRow: 'icao',
|
|
7
|
+
secondRow: 'call_sign',
|
|
8
|
+
thirdRow: 'blank',
|
|
9
|
+
fourthRow: 'registration',
|
|
10
|
+
opacity: 50,
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_DRONE_LABEL = { firstRow: 'remote_id', opacity: 30 };
|
|
13
|
+
(0, globals_1.describe)('cameraSettingsSchema identificationLabel', () => {
|
|
14
|
+
(0, globals_1.test)('defaults both aircraft and drone labels', () => {
|
|
15
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({}).identificationLabel).toEqual({
|
|
16
|
+
aircraft: {
|
|
17
|
+
firstRow: 'icao',
|
|
18
|
+
secondRow: 'call_sign',
|
|
19
|
+
thirdRow: 'blank',
|
|
20
|
+
fourthRow: 'blank',
|
|
21
|
+
opacity: 30,
|
|
22
|
+
},
|
|
23
|
+
drone: DEFAULT_DRONE_LABEL,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
(0, globals_1.test)('parses the aircraft and drone label', () => {
|
|
27
|
+
const identificationLabel = { aircraft: AIRCRAFT_LABEL, drone: { firstRow: 'blank', opacity: 70 } };
|
|
28
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({ identificationLabel }).identificationLabel).toEqual(identificationLabel);
|
|
29
|
+
});
|
|
30
|
+
(0, globals_1.test)('defaults a missing drone label', () => {
|
|
31
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({ identificationLabel: { aircraft: AIRCRAFT_LABEL } }).identificationLabel).toEqual({ aircraft: AIRCRAFT_LABEL, drone: DEFAULT_DRONE_LABEL });
|
|
32
|
+
});
|
|
33
|
+
(0, globals_1.test)('converts the legacy flat label to the aircraft label', () => {
|
|
34
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({ identificationLabel: AIRCRAFT_LABEL }).identificationLabel).toEqual({
|
|
35
|
+
aircraft: AIRCRAFT_LABEL,
|
|
36
|
+
drone: DEFAULT_DRONE_LABEL,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
(0, globals_1.test)('returns an independent drone label for each legacy parse', () => {
|
|
40
|
+
const first = PlaneTrackerAPI_1.cameraSettingsSchema.parse({ identificationLabel: AIRCRAFT_LABEL });
|
|
41
|
+
first.identificationLabel.drone.opacity = 90;
|
|
42
|
+
const second = PlaneTrackerAPI_1.cameraSettingsSchema.parse({ identificationLabel: AIRCRAFT_LABEL });
|
|
43
|
+
(0, globals_1.expect)(second.identificationLabel.drone).toEqual(DEFAULT_DRONE_LABEL);
|
|
44
|
+
});
|
|
45
|
+
(0, globals_1.test)('rejects an unknown drone label row', () => {
|
|
46
|
+
const identificationLabel = { aircraft: AIRCRAFT_LABEL, drone: { firstRow: 'icao', opacity: 30 } };
|
|
47
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.safeParse({ identificationLabel }).success).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
const CAPTURE_CONFIG = { defaultCaptureSizeMeters: 50, captureSizeExtensionMeters: 20 };
|
|
51
|
+
const DEFAULT_CAPTURE_CONFIG = { defaultCaptureSizeMeters: 120, captureSizeExtensionMeters: 80 };
|
|
52
|
+
(0, globals_1.describe)('cameraSettingsSchema cameraConfig', () => {
|
|
53
|
+
(0, globals_1.test)('defaults both aircraft and drone capture config', () => {
|
|
54
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({}).cameraConfig).toEqual({
|
|
55
|
+
aircraft: DEFAULT_CAPTURE_CONFIG,
|
|
56
|
+
drone: DEFAULT_CAPTURE_CONFIG,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
(0, globals_1.test)('parses independent aircraft and drone capture config', () => {
|
|
60
|
+
const cameraConfig = {
|
|
61
|
+
aircraft: CAPTURE_CONFIG,
|
|
62
|
+
drone: { defaultCaptureSizeMeters: 5, captureSizeExtensionMeters: 3 },
|
|
63
|
+
};
|
|
64
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({ cameraConfig }).cameraConfig).toEqual(cameraConfig);
|
|
65
|
+
});
|
|
66
|
+
(0, globals_1.test)('defaults a missing drone capture config', () => {
|
|
67
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({ cameraConfig: { aircraft: CAPTURE_CONFIG } }).cameraConfig).toEqual({
|
|
68
|
+
aircraft: CAPTURE_CONFIG,
|
|
69
|
+
drone: DEFAULT_CAPTURE_CONFIG,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
(0, globals_1.test)('copies the legacy flat capture config to both aircraft and drone', () => {
|
|
73
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({ cameraConfig: CAPTURE_CONFIG }).cameraConfig).toEqual({
|
|
74
|
+
aircraft: CAPTURE_CONFIG,
|
|
75
|
+
drone: CAPTURE_CONFIG,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
(0, globals_1.test)('returns independent aircraft and drone objects', () => {
|
|
79
|
+
const legacy = PlaneTrackerAPI_1.cameraSettingsSchema.parse({ cameraConfig: CAPTURE_CONFIG }).cameraConfig;
|
|
80
|
+
legacy.drone.defaultCaptureSizeMeters = 1;
|
|
81
|
+
(0, globals_1.expect)(legacy.aircraft).toEqual(CAPTURE_CONFIG);
|
|
82
|
+
const first = PlaneTrackerAPI_1.cameraSettingsSchema.parse({}).cameraConfig;
|
|
83
|
+
first.drone.defaultCaptureSizeMeters = 1;
|
|
84
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.parse({}).cameraConfig.drone).toEqual(DEFAULT_CAPTURE_CONFIG);
|
|
85
|
+
});
|
|
86
|
+
(0, globals_1.test)('rejects a non-positive drone capture size', () => {
|
|
87
|
+
const cameraConfig = { aircraft: CAPTURE_CONFIG, drone: { ...CAPTURE_CONFIG, defaultCaptureSizeMeters: 0 } };
|
|
88
|
+
(0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.safeParse({ cameraConfig }).success).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/esm/CamOverlayAPI.js
CHANGED
|
@@ -26,13 +26,6 @@ export class CamOverlayAPI extends BasicAPI {
|
|
|
26
26
|
async getMjpegStreamImage(mjpegUrl, options) {
|
|
27
27
|
return await this._getBlob(`${BASE_PATH}/fetch_mjpeg_image.cgi`, { mjpeg_url: decodeURIComponent(mjpegUrl) }, options);
|
|
28
28
|
}
|
|
29
|
-
async isViewerPassValid(pass, options) {
|
|
30
|
-
const res = await this._getJson(`${BASE_PATH}/check_pass.cgi`, { pass }, options);
|
|
31
|
-
if (res.status !== 200) {
|
|
32
|
-
throw new Error(res.message);
|
|
33
|
-
}
|
|
34
|
-
return res.data === '1';
|
|
35
|
-
}
|
|
36
29
|
async listFiles(fileType, options) {
|
|
37
30
|
const res = await this._getJson(`${BASE_PATH}/upload_${fileType}.cgi`, { action: 'list' }, options);
|
|
38
31
|
return getFileListSchema(fileType).parse(res.list);
|
package/esm/VapixAPI.js
CHANGED
|
@@ -90,7 +90,7 @@ export class VapixAPI extends BasicAPI {
|
|
|
90
90
|
};
|
|
91
91
|
await this._postJsonEncoded('/axis-cgi/opticscontrol.cgi', data, undefined, options);
|
|
92
92
|
}
|
|
93
|
-
catch
|
|
93
|
+
catch {
|
|
94
94
|
await this._postUrlEncoded('/axis-cgi/opticssetup.cgi', {
|
|
95
95
|
autofocus: 'perform',
|
|
96
96
|
source: '1',
|
package/esm/helpers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Digest } from './Digest';
|
|
2
|
-
import { Agent, fetch as undiciFetch
|
|
2
|
+
import { Agent, fetch as undiciFetch } from 'undici';
|
|
3
3
|
export class HttpRequestSender {
|
|
4
4
|
agent;
|
|
5
5
|
authData;
|
|
@@ -29,12 +29,15 @@ export class HttpRequestSender {
|
|
|
29
29
|
options.headers ??= {};
|
|
30
30
|
options.headers['Authorization'] = authorization;
|
|
31
31
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const res = await undiciFetch(url, {
|
|
33
|
+
method: options.method,
|
|
34
|
+
headers: options.headers,
|
|
35
|
+
body: postData,
|
|
36
|
+
signal: AbortSignal.timeout(options.timeout),
|
|
37
|
+
dispatcher: this.agent,
|
|
38
|
+
});
|
|
37
39
|
if (res.status === 401) {
|
|
40
|
+
this.invalidateAuthorization();
|
|
38
41
|
const authenticateHeader = res.headers.get('www-authenticate');
|
|
39
42
|
if (authenticateHeader !== null &&
|
|
40
43
|
authenticateHeader.indexOf('Digest') !== -1 &&
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { FormData } from 'undici';
|
|
3
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
4
|
+
import { HttpRequestSender } from './HttpRequestSender';
|
|
5
|
+
const DIGEST_CHALLENGE = 'Digest realm="test", qop="auth", nonce="abc123", opaque="xyz"';
|
|
6
|
+
const okHandler = (_req, res) => {
|
|
7
|
+
res.end('ok');
|
|
8
|
+
};
|
|
9
|
+
const digestHandler = (req, res) => {
|
|
10
|
+
if (req.headers.authorization?.startsWith('Digest ') !== true) {
|
|
11
|
+
res.writeHead(401, { 'WWW-Authenticate': DIGEST_CHALLENGE });
|
|
12
|
+
res.end();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
res.end('ok');
|
|
16
|
+
};
|
|
17
|
+
describe('HttpRequestSender', () => {
|
|
18
|
+
let server;
|
|
19
|
+
let received;
|
|
20
|
+
let handler;
|
|
21
|
+
const options = (overrides = {}) => ({
|
|
22
|
+
protocol: 'http:',
|
|
23
|
+
host: '127.0.0.1',
|
|
24
|
+
port: server.address().port,
|
|
25
|
+
path: '/test.cgi',
|
|
26
|
+
user: 'root',
|
|
27
|
+
pass: 'pass',
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
received = [];
|
|
32
|
+
handler = okHandler;
|
|
33
|
+
server = http.createServer((req, res) => {
|
|
34
|
+
let body = '';
|
|
35
|
+
req.on('data', (chunk) => {
|
|
36
|
+
body += chunk.toString();
|
|
37
|
+
});
|
|
38
|
+
req.on('end', () => {
|
|
39
|
+
received.push({
|
|
40
|
+
method: req.method,
|
|
41
|
+
url: req.url,
|
|
42
|
+
authorization: req.headers.authorization,
|
|
43
|
+
contentType: req.headers['content-type'],
|
|
44
|
+
body,
|
|
45
|
+
});
|
|
46
|
+
handler(req, res);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
await new Promise((resolve) => {
|
|
50
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
server.closeAllConnections();
|
|
55
|
+
await new Promise((resolve) => {
|
|
56
|
+
server.close(() => resolve());
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
test('sends basic auth when the server accepts it', async () => {
|
|
60
|
+
const res = await new HttpRequestSender().sendRequest(options());
|
|
61
|
+
expect(res.status).toBe(200);
|
|
62
|
+
expect(await res.text()).toBe('ok');
|
|
63
|
+
expect(received).toHaveLength(1);
|
|
64
|
+
expect(received[0]?.authorization).toBe(`Basic ${Buffer.from('root:pass').toString('base64')}`);
|
|
65
|
+
});
|
|
66
|
+
test('sends no authorization without credentials', async () => {
|
|
67
|
+
await new HttpRequestSender().sendRequest(options({ user: undefined, pass: undefined }));
|
|
68
|
+
expect(received[0]?.authorization).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
test('retries with digest auth on a digest 401 and reuses it for later requests', async () => {
|
|
71
|
+
handler = digestHandler;
|
|
72
|
+
const sender = new HttpRequestSender();
|
|
73
|
+
const first = await sender.sendRequest(options());
|
|
74
|
+
const second = await sender.sendRequest(options());
|
|
75
|
+
expect(first.status).toBe(200);
|
|
76
|
+
expect(second.status).toBe(200);
|
|
77
|
+
expect(received.map((r) => r.authorization?.split(' ')[0])).toEqual(['Basic', 'Digest', 'Digest']);
|
|
78
|
+
});
|
|
79
|
+
test('keeps digest auth after a non-401 error response', async () => {
|
|
80
|
+
const sender = new HttpRequestSender();
|
|
81
|
+
handler = digestHandler;
|
|
82
|
+
await sender.sendRequest(options());
|
|
83
|
+
handler = (_req, res) => {
|
|
84
|
+
res.writeHead(500);
|
|
85
|
+
res.end();
|
|
86
|
+
};
|
|
87
|
+
await sender.sendRequest(options());
|
|
88
|
+
handler = digestHandler;
|
|
89
|
+
const res = await sender.sendRequest(options());
|
|
90
|
+
expect(res.status).toBe(200);
|
|
91
|
+
expect(received.map((r) => r.authorization?.split(' ')[0])).toEqual(['Basic', 'Digest', 'Digest', 'Digest']);
|
|
92
|
+
});
|
|
93
|
+
test('retries a digest 401 only once', async () => {
|
|
94
|
+
handler = (_req, res) => {
|
|
95
|
+
res.writeHead(401, { 'WWW-Authenticate': DIGEST_CHALLENGE });
|
|
96
|
+
res.end();
|
|
97
|
+
};
|
|
98
|
+
const res = await new HttpRequestSender().sendRequest(options());
|
|
99
|
+
expect(res.status).toBe(401);
|
|
100
|
+
expect(received).toHaveLength(2);
|
|
101
|
+
});
|
|
102
|
+
test('rejects when the response does not arrive within the timeout', async () => {
|
|
103
|
+
handler = () => undefined;
|
|
104
|
+
await expect(new HttpRequestSender().sendRequest(options({ timeout: 200 }))).rejects.toThrow();
|
|
105
|
+
});
|
|
106
|
+
test('sends a url-encoded POST body', async () => {
|
|
107
|
+
const res = await new HttpRequestSender().sendRequest(options({ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }), 'action=settext&text=hello');
|
|
108
|
+
expect(res.ok).toBe(true);
|
|
109
|
+
expect(received[0]).toMatchObject({
|
|
110
|
+
method: 'POST',
|
|
111
|
+
contentType: 'application/x-www-form-urlencoded',
|
|
112
|
+
body: 'action=settext&text=hello',
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
test('sends a JSON POST body', async () => {
|
|
116
|
+
const payload = JSON.stringify({ text: 'hello', index: 1 });
|
|
117
|
+
await new HttpRequestSender().sendRequest(options({ method: 'POST', headers: { 'Content-Type': 'application/json' } }), payload);
|
|
118
|
+
expect(received[0]).toMatchObject({ method: 'POST', contentType: 'application/json', body: payload });
|
|
119
|
+
});
|
|
120
|
+
test('sends a Buffer POST body', async () => {
|
|
121
|
+
await new HttpRequestSender().sendRequest(options({ method: 'POST' }), Buffer.from('binary-data'));
|
|
122
|
+
expect(received[0]?.body).toBe('binary-data');
|
|
123
|
+
});
|
|
124
|
+
test('sends a FormData POST body as multipart', async () => {
|
|
125
|
+
const formData = new FormData();
|
|
126
|
+
formData.append('field', 'value');
|
|
127
|
+
await new HttpRequestSender().sendRequest(options({ method: 'POST' }), formData);
|
|
128
|
+
expect(received[0]?.contentType).toMatch(/^multipart\/form-data; boundary=/);
|
|
129
|
+
expect(received[0]?.body).toContain('value');
|
|
130
|
+
});
|
|
131
|
+
test('resends the POST body on the digest retry', async () => {
|
|
132
|
+
handler = digestHandler;
|
|
133
|
+
await new HttpRequestSender().sendRequest(options({ method: 'POST' }), 'payload');
|
|
134
|
+
expect(received.map((r) => r.body)).toEqual(['payload', 'payload']);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -37,13 +37,59 @@ const labelOptionsSchema = z.union([
|
|
|
37
37
|
z.literal('flight_number'),
|
|
38
38
|
z.literal('icao'),
|
|
39
39
|
]);
|
|
40
|
-
const
|
|
40
|
+
const aircraftIdentificationLabelSchema = z.object({
|
|
41
41
|
firstRow: labelOptionsSchema,
|
|
42
42
|
secondRow: labelOptionsSchema,
|
|
43
43
|
thirdRow: labelOptionsSchema,
|
|
44
44
|
fourthRow: labelOptionsSchema,
|
|
45
45
|
opacity: z.number().positive(),
|
|
46
46
|
});
|
|
47
|
+
const droneLabelOptionsSchema = z.union([z.literal('blank'), z.literal('remote_id')]);
|
|
48
|
+
const droneIdentificationLabelSchema = z.object({
|
|
49
|
+
firstRow: droneLabelOptionsSchema,
|
|
50
|
+
opacity: z.number().positive(),
|
|
51
|
+
});
|
|
52
|
+
const DEFAULT_DRONE_IDENTIFICATION_LABEL = {
|
|
53
|
+
firstRow: 'remote_id',
|
|
54
|
+
opacity: 30,
|
|
55
|
+
};
|
|
56
|
+
const identificationLabelSchema = z.union([
|
|
57
|
+
z.object({
|
|
58
|
+
aircraft: aircraftIdentificationLabelSchema,
|
|
59
|
+
drone: droneIdentificationLabelSchema.default(DEFAULT_DRONE_IDENTIFICATION_LABEL),
|
|
60
|
+
}),
|
|
61
|
+
aircraftIdentificationLabelSchema.transform((aircraft) => ({
|
|
62
|
+
aircraft,
|
|
63
|
+
drone: { ...DEFAULT_DRONE_IDENTIFICATION_LABEL },
|
|
64
|
+
})),
|
|
65
|
+
]);
|
|
66
|
+
const captureConfigSchema = z.object({
|
|
67
|
+
defaultCaptureSizeMeters: z.number().positive(),
|
|
68
|
+
captureSizeExtensionMeters: z.number().positive(),
|
|
69
|
+
});
|
|
70
|
+
const DEFAULT_CAPTURE_CONFIG = {
|
|
71
|
+
defaultCaptureSizeMeters: 120,
|
|
72
|
+
captureSizeExtensionMeters: 80,
|
|
73
|
+
};
|
|
74
|
+
const cameraConfigSchema = z.union([
|
|
75
|
+
z.object({
|
|
76
|
+
aircraft: captureConfigSchema,
|
|
77
|
+
drone: captureConfigSchema.default(() => ({ ...DEFAULT_CAPTURE_CONFIG })),
|
|
78
|
+
}),
|
|
79
|
+
z
|
|
80
|
+
.object({
|
|
81
|
+
defaultCaptureSizeMeters: z.number().positive().default(DEFAULT_CAPTURE_CONFIG.defaultCaptureSizeMeters),
|
|
82
|
+
captureSizeExtensionMeters: z
|
|
83
|
+
.number()
|
|
84
|
+
.positive()
|
|
85
|
+
.default(DEFAULT_CAPTURE_CONFIG.captureSizeExtensionMeters),
|
|
86
|
+
})
|
|
87
|
+
.strict()
|
|
88
|
+
.transform((captureConfig) => ({
|
|
89
|
+
aircraft: captureConfig,
|
|
90
|
+
drone: { ...captureConfig },
|
|
91
|
+
})),
|
|
92
|
+
]);
|
|
47
93
|
export const cameraSettingsSchema = z.object({
|
|
48
94
|
units: z.union([z.literal('metric'), z.literal('imperial')]).default('imperial'),
|
|
49
95
|
adsbSource: z
|
|
@@ -81,15 +127,10 @@ export const cameraSettingsSchema = z.object({
|
|
|
81
127
|
nightSkyCalibrationFocusValue: 9000,
|
|
82
128
|
focusCalibrationPoints: '',
|
|
83
129
|
}),
|
|
84
|
-
cameraConfig:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
89
|
-
.default({
|
|
90
|
-
defaultCaptureSizeMeters: 120,
|
|
91
|
-
captureSizeExtensionMeters: 80,
|
|
92
|
-
}),
|
|
130
|
+
cameraConfig: cameraConfigSchema.default(() => ({
|
|
131
|
+
aircraft: { ...DEFAULT_CAPTURE_CONFIG },
|
|
132
|
+
drone: { ...DEFAULT_CAPTURE_CONFIG },
|
|
133
|
+
})),
|
|
93
134
|
stream: z
|
|
94
135
|
.object({
|
|
95
136
|
width: z.number().positive(),
|
|
@@ -200,11 +241,14 @@ export const cameraSettingsSchema = z.object({
|
|
|
200
241
|
port: 80,
|
|
201
242
|
}),
|
|
202
243
|
identificationLabel: identificationLabelSchema.default({
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
244
|
+
aircraft: {
|
|
245
|
+
firstRow: 'icao',
|
|
246
|
+
secondRow: 'call_sign',
|
|
247
|
+
thirdRow: 'blank',
|
|
248
|
+
fourthRow: 'blank',
|
|
249
|
+
opacity: 30,
|
|
250
|
+
},
|
|
251
|
+
drone: DEFAULT_DRONE_IDENTIFICATION_LABEL,
|
|
208
252
|
}),
|
|
209
253
|
acs: connectionSchema
|
|
210
254
|
.extend({
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { cameraSettingsSchema } from './PlaneTrackerAPI';
|
|
2
|
+
import { describe, test, expect } from '@jest/globals';
|
|
3
|
+
const AIRCRAFT_LABEL = {
|
|
4
|
+
firstRow: 'icao',
|
|
5
|
+
secondRow: 'call_sign',
|
|
6
|
+
thirdRow: 'blank',
|
|
7
|
+
fourthRow: 'registration',
|
|
8
|
+
opacity: 50,
|
|
9
|
+
};
|
|
10
|
+
const DEFAULT_DRONE_LABEL = { firstRow: 'remote_id', opacity: 30 };
|
|
11
|
+
describe('cameraSettingsSchema identificationLabel', () => {
|
|
12
|
+
test('defaults both aircraft and drone labels', () => {
|
|
13
|
+
expect(cameraSettingsSchema.parse({}).identificationLabel).toEqual({
|
|
14
|
+
aircraft: {
|
|
15
|
+
firstRow: 'icao',
|
|
16
|
+
secondRow: 'call_sign',
|
|
17
|
+
thirdRow: 'blank',
|
|
18
|
+
fourthRow: 'blank',
|
|
19
|
+
opacity: 30,
|
|
20
|
+
},
|
|
21
|
+
drone: DEFAULT_DRONE_LABEL,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
test('parses the aircraft and drone label', () => {
|
|
25
|
+
const identificationLabel = { aircraft: AIRCRAFT_LABEL, drone: { firstRow: 'blank', opacity: 70 } };
|
|
26
|
+
expect(cameraSettingsSchema.parse({ identificationLabel }).identificationLabel).toEqual(identificationLabel);
|
|
27
|
+
});
|
|
28
|
+
test('defaults a missing drone label', () => {
|
|
29
|
+
expect(cameraSettingsSchema.parse({ identificationLabel: { aircraft: AIRCRAFT_LABEL } }).identificationLabel).toEqual({ aircraft: AIRCRAFT_LABEL, drone: DEFAULT_DRONE_LABEL });
|
|
30
|
+
});
|
|
31
|
+
test('converts the legacy flat label to the aircraft label', () => {
|
|
32
|
+
expect(cameraSettingsSchema.parse({ identificationLabel: AIRCRAFT_LABEL }).identificationLabel).toEqual({
|
|
33
|
+
aircraft: AIRCRAFT_LABEL,
|
|
34
|
+
drone: DEFAULT_DRONE_LABEL,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
test('returns an independent drone label for each legacy parse', () => {
|
|
38
|
+
const first = cameraSettingsSchema.parse({ identificationLabel: AIRCRAFT_LABEL });
|
|
39
|
+
first.identificationLabel.drone.opacity = 90;
|
|
40
|
+
const second = cameraSettingsSchema.parse({ identificationLabel: AIRCRAFT_LABEL });
|
|
41
|
+
expect(second.identificationLabel.drone).toEqual(DEFAULT_DRONE_LABEL);
|
|
42
|
+
});
|
|
43
|
+
test('rejects an unknown drone label row', () => {
|
|
44
|
+
const identificationLabel = { aircraft: AIRCRAFT_LABEL, drone: { firstRow: 'icao', opacity: 30 } };
|
|
45
|
+
expect(cameraSettingsSchema.safeParse({ identificationLabel }).success).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
const CAPTURE_CONFIG = { defaultCaptureSizeMeters: 50, captureSizeExtensionMeters: 20 };
|
|
49
|
+
const DEFAULT_CAPTURE_CONFIG = { defaultCaptureSizeMeters: 120, captureSizeExtensionMeters: 80 };
|
|
50
|
+
describe('cameraSettingsSchema cameraConfig', () => {
|
|
51
|
+
test('defaults both aircraft and drone capture config', () => {
|
|
52
|
+
expect(cameraSettingsSchema.parse({}).cameraConfig).toEqual({
|
|
53
|
+
aircraft: DEFAULT_CAPTURE_CONFIG,
|
|
54
|
+
drone: DEFAULT_CAPTURE_CONFIG,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
test('parses independent aircraft and drone capture config', () => {
|
|
58
|
+
const cameraConfig = {
|
|
59
|
+
aircraft: CAPTURE_CONFIG,
|
|
60
|
+
drone: { defaultCaptureSizeMeters: 5, captureSizeExtensionMeters: 3 },
|
|
61
|
+
};
|
|
62
|
+
expect(cameraSettingsSchema.parse({ cameraConfig }).cameraConfig).toEqual(cameraConfig);
|
|
63
|
+
});
|
|
64
|
+
test('defaults a missing drone capture config', () => {
|
|
65
|
+
expect(cameraSettingsSchema.parse({ cameraConfig: { aircraft: CAPTURE_CONFIG } }).cameraConfig).toEqual({
|
|
66
|
+
aircraft: CAPTURE_CONFIG,
|
|
67
|
+
drone: DEFAULT_CAPTURE_CONFIG,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
test('copies the legacy flat capture config to both aircraft and drone', () => {
|
|
71
|
+
expect(cameraSettingsSchema.parse({ cameraConfig: CAPTURE_CONFIG }).cameraConfig).toEqual({
|
|
72
|
+
aircraft: CAPTURE_CONFIG,
|
|
73
|
+
drone: CAPTURE_CONFIG,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
test('returns independent aircraft and drone objects', () => {
|
|
77
|
+
const legacy = cameraSettingsSchema.parse({ cameraConfig: CAPTURE_CONFIG }).cameraConfig;
|
|
78
|
+
legacy.drone.defaultCaptureSizeMeters = 1;
|
|
79
|
+
expect(legacy.aircraft).toEqual(CAPTURE_CONFIG);
|
|
80
|
+
const first = cameraSettingsSchema.parse({}).cameraConfig;
|
|
81
|
+
first.drone.defaultCaptureSizeMeters = 1;
|
|
82
|
+
expect(cameraSettingsSchema.parse({}).cameraConfig.drone).toEqual(DEFAULT_CAPTURE_CONFIG);
|
|
83
|
+
});
|
|
84
|
+
test('rejects a non-positive drone capture size', () => {
|
|
85
|
+
const cameraConfig = { aircraft: CAPTURE_CONFIG, drone: { ...CAPTURE_CONFIG, defaultCaptureSizeMeters: 0 } };
|
|
86
|
+
expect(cameraSettingsSchema.safeParse({ cameraConfig }).success).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camstreamerlib",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.34-be26059",
|
|
4
4
|
"description": "Helper library for CamStreamer ACAP applications.",
|
|
5
5
|
"prettier": "@camstreamer/prettier-config",
|
|
6
6
|
"engine": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"eventemitter2": "^5.0.1",
|
|
12
12
|
"fast-xml-parser": "^5.3.6",
|
|
13
13
|
"type-fest": "^4.41.0",
|
|
14
|
-
"undici": "^6.28.
|
|
14
|
+
"undici": "^6.28.1",
|
|
15
15
|
"ws": "^8.18.0",
|
|
16
16
|
"zod": "^3.25.76"
|
|
17
17
|
},
|
|
@@ -19,25 +19,28 @@
|
|
|
19
19
|
"@camstreamer/eslint-config": "^1.0.2",
|
|
20
20
|
"@camstreamer/prettier-config": "^2.0.4",
|
|
21
21
|
"@types/adm-zip": "^0.5.5",
|
|
22
|
-
"@types/jest": "^
|
|
22
|
+
"@types/jest": "^30.0.0",
|
|
23
23
|
"@types/node": "^18.19.127",
|
|
24
24
|
"@types/ws": "^8.5.10",
|
|
25
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
26
|
-
"@typescript-eslint/parser": "^
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^8.71.1",
|
|
26
|
+
"@typescript-eslint/parser": "^8.71.1",
|
|
27
27
|
"esbuild": "^0.28.2",
|
|
28
28
|
"eslint": "^8.51.0",
|
|
29
29
|
"eslint-plugin-deprecation": "^2.0.0",
|
|
30
|
-
"eslint-plugin-unused-imports": "^
|
|
31
|
-
"jest": "^
|
|
30
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
31
|
+
"jest": "^30.5.2",
|
|
32
32
|
"npm-run-all": "^4.1.5",
|
|
33
33
|
"prettier": "^2.7.1",
|
|
34
|
-
"ts-jest": "^
|
|
34
|
+
"ts-jest": "^29.4.14",
|
|
35
35
|
"ts-node": "^10.7.0",
|
|
36
36
|
"typescript": "5.3.3"
|
|
37
37
|
},
|
|
38
38
|
"overrides": {
|
|
39
39
|
"js-yaml": "^4.3.2",
|
|
40
|
-
"browserslist": "^4.28.7"
|
|
40
|
+
"browserslist": "^4.28.7",
|
|
41
|
+
"eslint-plugin-deprecation": {
|
|
42
|
+
"@typescript-eslint/utils": "^8.71.1"
|
|
43
|
+
}
|
|
41
44
|
},
|
|
42
45
|
"scripts": {
|
|
43
46
|
"clean": "rimraf dist/*",
|
package/types/CamOverlayAPI.d.ts
CHANGED
|
@@ -14,7 +14,6 @@ export declare class CamOverlayAPI<Client extends IClient<TResponse, any>> exten
|
|
|
14
14
|
}[]>;
|
|
15
15
|
wsAuthorization(options?: THttpRequestOptions): Promise<string>;
|
|
16
16
|
getMjpegStreamImage(mjpegUrl: string, options?: THttpRequestOptions): Promise<ReturnType<Awaited<ReturnType<Client["get"]>>["blob"]>>;
|
|
17
|
-
isViewerPassValid(pass: string, options?: THttpRequestOptions): Promise<boolean>;
|
|
18
17
|
listFiles<T extends TFileType>(fileType: T, options?: THttpRequestOptions): Promise<TFileList<T>>;
|
|
19
18
|
uploadFile<T extends TFileType>(fileType: T, formData: Parameters<Client['post']>[0]['data'], storage: TFileStorageType<T>, options?: THttpRequestOptions): Promise<void>;
|
|
20
19
|
removeFile<T extends TFileType>(fileType: T, fileParams: TFile<T>, options?: THttpRequestOptions): Promise<void>;
|