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.
@@ -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 identificationLabelSchema = zod_1.z.object({
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: zod_1.z
88
- .object({
89
- defaultCaptureSizeMeters: zod_1.z.number().positive().default(120),
90
- captureSizeExtensionMeters: zod_1.z.number().positive().default(80),
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
- firstRow: 'registration',
207
- secondRow: 'blank',
208
- thirdRow: 'blank',
209
- fourthRow: 'blank',
210
- opacity: 30,
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
+ });
@@ -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 (err) {
93
+ catch {
94
94
  await this._postUrlEncoded('/axis-cgi/opticssetup.cgi', {
95
95
  autofocus: 'perform',
96
96
  source: '1',
package/esm/helpers.js CHANGED
@@ -10,7 +10,7 @@ export const jsonParseCameraParam = (param, paramName) => {
10
10
  try {
11
11
  return JSON.parse(decodeURIComponent(param.replaceAll('\\', '')));
12
12
  }
13
- catch (e) {
13
+ catch {
14
14
  throw new JsonParseError(paramName, param);
15
15
  }
16
16
  }
@@ -1,5 +1,5 @@
1
1
  import { Digest } from './Digest';
2
- import { Agent, fetch as undiciFetch, Request as UndiciRequest, } from 'undici';
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 req = new UndiciRequest(url, { body: postData, method: options.method, headers: options.headers });
33
- const res = await undiciFetch(req, { signal: AbortSignal.timeout(options.timeout), dispatcher: this.agent });
34
- if (!res.ok) {
35
- this.invalidateAuthorization();
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 identificationLabelSchema = z.object({
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: z
85
- .object({
86
- defaultCaptureSizeMeters: z.number().positive().default(120),
87
- captureSizeExtensionMeters: z.number().positive().default(80),
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
- firstRow: 'registration',
204
- secondRow: 'blank',
205
- thirdRow: 'blank',
206
- fourthRow: 'blank',
207
- opacity: 30,
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.33-dfd8bbb",
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.0",
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": "^28.0.0",
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": "^6.8.0",
26
- "@typescript-eslint/parser": "^6.8.0",
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": "^3.0.0",
31
- "jest": "^28.1.3",
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": "^28.0.0",
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/*",
@@ -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>;