camstreamerlib 4.0.35-bc56d1e → 4.0.36-05711a0

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.
@@ -87,6 +87,7 @@ export declare class PlaneTrackerAPI<Client extends IClient<TResponse, any>> ext
87
87
  widget: {
88
88
  enabled: boolean;
89
89
  scale: number;
90
+ units: "rotate" | "metric" | "imperial";
90
91
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
91
92
  posX: number;
92
93
  posY: number;
@@ -100,6 +101,8 @@ export declare class PlaneTrackerAPI<Client extends IClient<TResponse, any>> ext
100
101
  posY: number;
101
102
  showWeather: boolean;
102
103
  weatherLocationKey: string;
104
+ windSpeedUnits: "rotate" | "metric" | "imperial";
105
+ temperatureUnits: "rotate" | "metric" | "imperial";
103
106
  };
104
107
  fr24FlightInfoSource: {
105
108
  enabled: boolean;
package/cjs/VapixAPI.js CHANGED
@@ -565,17 +565,21 @@ class VapixAPI extends BasicAPI_1.BasicAPI {
565
565
  allowBooleanAttributes: true,
566
566
  });
567
567
  const result = parser.parse(xml);
568
- let apps = result.reply.application ?? [];
569
- if (!Array.isArray(apps)) {
570
- apps = [apps];
571
- }
572
- const appList = apps.map((app) => {
573
- return {
574
- ...app,
575
- appId: VapixAPI_1.ALL_APP_IDS.find((id) => id.toLowerCase() === app.Name.toLowerCase()) ?? null,
576
- };
577
- });
578
- return VapixAPI_1.applicationListSchema.parse(appList);
568
+ const rawApps = result.reply.application ?? [];
569
+ const apps = Array.isArray(rawApps) ? rawApps : [rawApps];
570
+ const appList = [];
571
+ for (const app of apps) {
572
+ const parsedApp = VapixAPI_1.applicationSchema.safeParse(app);
573
+ if (!parsedApp.success) {
574
+ console.debug('Skipping invalid application entry:', app, parsedApp.error.issues);
575
+ continue;
576
+ }
577
+ appList.push({
578
+ ...parsedApp.data,
579
+ appId: VapixAPI_1.ALL_APP_IDS.find((id) => id.toLowerCase() === parsedApp.data.Name.toLowerCase()) ?? null,
580
+ });
581
+ }
582
+ return appList;
579
583
  }
580
584
  async startApplication(applicationId, options) {
581
585
  const agent = this.getClient(options?.proxyParams);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const VapixAPI_1 = require("./VapixAPI");
4
+ const globals_1 = require("@jest/globals");
5
+ const createApi = (xml) => {
6
+ const client = {
7
+ get: () => Promise.resolve(new Response(xml)),
8
+ post: () => Promise.reject(new Error('unused')),
9
+ };
10
+ return new VapixAPI_1.VapixAPI(client);
11
+ };
12
+ (0, globals_1.describe)('VapixAPI', () => {
13
+ (0, globals_1.describe)('getApplicationList', () => {
14
+ (0, globals_1.afterEach)(() => {
15
+ globals_1.jest.restoreAllMocks();
16
+ });
17
+ (0, globals_1.test)('skips the nameless entry of the live camera list', async () => {
18
+ const debugSpy = globals_1.jest.spyOn(console, 'debug').mockImplementation(() => { });
19
+ const api = createApi(`<reply result="ok">
20
+ <application Name="camstreamer" NiceName="CamStreamer App" Vendor="CamStreamer" Version="5.0-15" ApplicationID="413135" License="None" Status="Running" ConfigurationPage="local/camstreamer/settings.html" />
21
+ <application Name="" License="None" Status="Stopped" />
22
+ <application Name="vmd" NiceName="AXIS Video Motion Detection" Vendor="Axis Communications" Version="4.4-5" ApplicationID="143440" License="None" Status="Stopped" ConfigurationPage="local/vmd/config.html" VendorHomePage="http://www.axis.com" LicenseName="Proprietary" />
23
+ </reply>`);
24
+ await (0, globals_1.expect)(api.getApplicationList()).resolves.toEqual([
25
+ {
26
+ Name: 'camstreamer',
27
+ NiceName: 'CamStreamer App',
28
+ Vendor: 'CamStreamer',
29
+ Version: '5.0-15',
30
+ ApplicationID: '413135',
31
+ License: 'None',
32
+ Status: 'Running',
33
+ ConfigurationPage: 'local/camstreamer/settings.html',
34
+ appId: 'CamStreamer',
35
+ },
36
+ {
37
+ Name: 'vmd',
38
+ NiceName: 'AXIS Video Motion Detection',
39
+ Vendor: 'Axis Communications',
40
+ Version: '4.4-5',
41
+ ApplicationID: '143440',
42
+ License: 'None',
43
+ Status: 'Stopped',
44
+ ConfigurationPage: 'local/vmd/config.html',
45
+ VendorHomePage: 'http://www.axis.com',
46
+ LicenseName: 'Proprietary',
47
+ appId: null,
48
+ },
49
+ ]);
50
+ (0, globals_1.expect)(debugSpy).toHaveBeenCalledTimes(1);
51
+ (0, globals_1.expect)(debugSpy.mock.calls[0]).toContainEqual({ Name: '', License: 'None', Status: 'Stopped' });
52
+ });
53
+ globals_1.test.each([
54
+ {
55
+ name: 'an entry without a name',
56
+ xml: '<reply result="ok"><application Name="vmd" NiceName="AXIS Video Motion Detection" Vendor="Axis Communications" Version="4.4-5" License="None" Status="Stopped" /><application License="None" Status="Stopped" /></reply>',
57
+ expected: [
58
+ {
59
+ Name: 'vmd',
60
+ NiceName: 'AXIS Video Motion Detection',
61
+ Vendor: 'Axis Communications',
62
+ Version: '4.4-5',
63
+ License: 'None',
64
+ Status: 'Stopped',
65
+ appId: null,
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ name: 'a named entry missing informational fields',
71
+ xml: '<reply result="ok"><application Name="vmd" NiceName="AXIS Video Motion Detection" Vendor="Axis Communications" Version="4.4-5" License="None" Status="Stopped" /><application Name="broken" License="None" Status="Stopped" /></reply>',
72
+ expected: [
73
+ {
74
+ Name: 'vmd',
75
+ NiceName: 'AXIS Video Motion Detection',
76
+ Vendor: 'Axis Communications',
77
+ Version: '4.4-5',
78
+ License: 'None',
79
+ Status: 'Stopped',
80
+ appId: null,
81
+ },
82
+ ],
83
+ },
84
+ ])('skips invalid entries individually: $name', async ({ xml, expected }) => {
85
+ const debugSpy = globals_1.jest.spyOn(console, 'debug').mockImplementation(() => { });
86
+ await (0, globals_1.expect)(createApi(xml).getApplicationList()).resolves.toEqual(expected);
87
+ (0, globals_1.expect)(debugSpy).toHaveBeenCalledTimes(1);
88
+ });
89
+ globals_1.test.each([
90
+ {
91
+ name: 'drops attributes outside the schema',
92
+ xml: '<reply result="ok"><application Name="camoverlay" NiceName="CamOverlay App" Vendor="CamStreamer" Version="3.0-1" License="None" Status="Running" Unexpected="value" /></reply>',
93
+ expected: [
94
+ {
95
+ Name: 'camoverlay',
96
+ NiceName: 'CamOverlay App',
97
+ Vendor: 'CamStreamer',
98
+ Version: '3.0-1',
99
+ License: 'None',
100
+ Status: 'Running',
101
+ appId: 'CamOverlay',
102
+ },
103
+ ],
104
+ },
105
+ {
106
+ name: 'maps an application outside the core CamStreamer apps to its appId',
107
+ xml: '<reply result="ok"><application Name="sporttracker" NiceName="SportTracker App" Vendor="CamStreamer" Version="1.0-1" License="None" Status="Running" /></reply>',
108
+ expected: [
109
+ {
110
+ Name: 'sporttracker',
111
+ NiceName: 'SportTracker App',
112
+ Vendor: 'CamStreamer',
113
+ Version: '1.0-1',
114
+ License: 'None',
115
+ Status: 'Running',
116
+ appId: 'SportTracker',
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ name: 'maps a capitalised application name to its appId',
122
+ xml: '<reply result="ok"><application Name="CamOverlay" NiceName="CamOverlay App" Vendor="CamStreamer" Version="3.0-1" License="None" Status="Running" /></reply>',
123
+ expected: [
124
+ {
125
+ Name: 'CamOverlay',
126
+ NiceName: 'CamOverlay App',
127
+ Vendor: 'CamStreamer',
128
+ Version: '3.0-1',
129
+ License: 'None',
130
+ Status: 'Running',
131
+ appId: 'CamOverlay',
132
+ },
133
+ ],
134
+ },
135
+ {
136
+ name: 'returns an empty list when the camera has no applications',
137
+ xml: '<reply result="ok"></reply>',
138
+ expected: [],
139
+ },
140
+ ])('returns every valid entry with its appId and logs nothing: $name', async ({ xml, expected }) => {
141
+ const debugSpy = globals_1.jest.spyOn(console, 'debug').mockImplementation(() => { });
142
+ await (0, globals_1.expect)(createApi(xml).getApplicationList()).resolves.toEqual(expected);
143
+ (0, globals_1.expect)(debugSpy).not.toHaveBeenCalled();
144
+ });
145
+ });
146
+ });
@@ -54,6 +54,8 @@ export declare const widgetSchema: z.ZodObject<{
54
54
  posY: number;
55
55
  enabled?: boolean | undefined;
56
56
  }>;
57
+ declare const widgetUnitsSchema: z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>;
58
+ export type TWidgetUnits = z.infer<typeof widgetUnitsSchema>;
57
59
  declare const labelOptionsSchema: z.ZodUnion<[z.ZodLiteral<"blank">, z.ZodLiteral<"registration">, z.ZodLiteral<"call_sign">, z.ZodLiteral<"flight_number">, z.ZodLiteral<"icao">]>;
58
60
  export type TLabelOption = z.infer<typeof labelOptionsSchema>;
59
61
  declare const droneLabelOptionsSchema: z.ZodUnion<[z.ZodLiteral<"blank">, z.ZodLiteral<"remote_id">]>;
@@ -313,9 +315,12 @@ export declare const cameraSettingsSchema: z.ZodObject<{
313
315
  posX: z.ZodNumber;
314
316
  posY: z.ZodNumber;
315
317
  scale: z.ZodNumber;
318
+ } & {
319
+ units: z.ZodDefault<z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>>;
316
320
  }, "strip", z.ZodTypeAny, {
317
321
  enabled: boolean;
318
322
  scale: number;
323
+ units: "rotate" | "metric" | "imperial";
319
324
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
320
325
  posX: number;
321
326
  posY: number;
@@ -325,6 +330,7 @@ export declare const cameraSettingsSchema: z.ZodObject<{
325
330
  posX: number;
326
331
  posY: number;
327
332
  enabled?: boolean | undefined;
333
+ units?: "rotate" | "metric" | "imperial" | undefined;
328
334
  }>>;
329
335
  airportWidget: z.ZodDefault<z.ZodObject<{
330
336
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -336,6 +342,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
336
342
  showWeather: z.ZodDefault<z.ZodBoolean>;
337
343
  weatherLocationKey: z.ZodDefault<z.ZodString>;
338
344
  weatherLocationName: z.ZodDefault<z.ZodString>;
345
+ windSpeedUnits: z.ZodDefault<z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>>;
346
+ temperatureUnits: z.ZodDefault<z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>>;
339
347
  }, "strip", z.ZodTypeAny, {
340
348
  enabled: boolean;
341
349
  scale: number;
@@ -345,6 +353,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
345
353
  posY: number;
346
354
  showWeather: boolean;
347
355
  weatherLocationKey: string;
356
+ windSpeedUnits: "rotate" | "metric" | "imperial";
357
+ temperatureUnits: "rotate" | "metric" | "imperial";
348
358
  }, {
349
359
  scale: number;
350
360
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
@@ -354,6 +364,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
354
364
  weatherLocationName?: string | undefined;
355
365
  showWeather?: boolean | undefined;
356
366
  weatherLocationKey?: string | undefined;
367
+ windSpeedUnits?: "rotate" | "metric" | "imperial" | undefined;
368
+ temperatureUnits?: "rotate" | "metric" | "imperial" | undefined;
357
369
  }>>;
358
370
  fr24FlightInfoSource: z.ZodDefault<z.ZodObject<{
359
371
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -614,6 +626,7 @@ export declare const cameraSettingsSchema: z.ZodObject<{
614
626
  widget: {
615
627
  enabled: boolean;
616
628
  scale: number;
629
+ units: "rotate" | "metric" | "imperial";
617
630
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
618
631
  posX: number;
619
632
  posY: number;
@@ -627,6 +640,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
627
640
  posY: number;
628
641
  showWeather: boolean;
629
642
  weatherLocationKey: string;
643
+ windSpeedUnits: "rotate" | "metric" | "imperial";
644
+ temperatureUnits: "rotate" | "metric" | "imperial";
630
645
  };
631
646
  fr24FlightInfoSource: {
632
647
  enabled: boolean;
@@ -802,6 +817,7 @@ export declare const cameraSettingsSchema: z.ZodObject<{
802
817
  posX: number;
803
818
  posY: number;
804
819
  enabled?: boolean | undefined;
820
+ units?: "rotate" | "metric" | "imperial" | undefined;
805
821
  } | undefined;
806
822
  airportWidget?: {
807
823
  scale: number;
@@ -812,6 +828,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
812
828
  weatherLocationName?: string | undefined;
813
829
  showWeather?: boolean | undefined;
814
830
  weatherLocationKey?: string | undefined;
831
+ windSpeedUnits?: "rotate" | "metric" | "imperial" | undefined;
832
+ temperatureUnits?: "rotate" | "metric" | "imperial" | undefined;
815
833
  } | undefined;
816
834
  fr24FlightInfoSource?: {
817
835
  enabled?: boolean | undefined;
@@ -28,6 +28,7 @@ exports.widgetSchema = zod_1.z.object({
28
28
  posY: zod_1.z.number().nonnegative(),
29
29
  scale: zod_1.z.number().positive(),
30
30
  });
31
+ const widgetUnitsSchema = zod_1.z.union([zod_1.z.literal('metric'), zod_1.z.literal('imperial'), zod_1.z.literal('rotate')]);
31
32
  const labelOptionsSchema = zod_1.z.union([
32
33
  zod_1.z.literal('blank'),
33
34
  zod_1.z.literal('registration'),
@@ -189,18 +190,25 @@ exports.cameraSettingsSchema = zod_1.z.object({
189
190
  displaySystemInfo: zod_1.z.boolean().optional(),
190
191
  })
191
192
  .optional(),
192
- widget: exports.widgetSchema.default({
193
+ widget: exports.widgetSchema
194
+ .extend({
195
+ units: widgetUnitsSchema.default('rotate'),
196
+ })
197
+ .default({
193
198
  enabled: true,
194
199
  coord: 'top_right',
195
200
  posX: 10,
196
201
  posY: 10,
197
202
  scale: 100,
203
+ units: 'rotate',
198
204
  }),
199
205
  airportWidget: exports.widgetSchema
200
206
  .extend({
201
207
  showWeather: zod_1.z.boolean().default(false),
202
208
  weatherLocationKey: zod_1.z.string().default(''),
203
209
  weatherLocationName: zod_1.z.string().default(''),
210
+ windSpeedUnits: widgetUnitsSchema.default('rotate'),
211
+ temperatureUnits: widgetUnitsSchema.default('rotate'),
204
212
  })
205
213
  .default({
206
214
  enabled: true,
@@ -211,6 +219,8 @@ exports.cameraSettingsSchema = zod_1.z.object({
211
219
  showWeather: false,
212
220
  weatherLocationKey: '',
213
221
  weatherLocationName: '',
222
+ windSpeedUnits: 'rotate',
223
+ temperatureUnits: 'rotate',
214
224
  }),
215
225
  fr24FlightInfoSource: zod_1.z
216
226
  .object({
@@ -88,3 +88,30 @@ const DEFAULT_CAPTURE_CONFIG = { defaultCaptureSizeMeters: 120, captureSizeExten
88
88
  (0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.safeParse({ cameraConfig }).success).toBe(false);
89
89
  });
90
90
  });
91
+ (0, globals_1.describe)('cameraSettingsSchema widget units', () => {
92
+ const WIDGET = { coord: 'top_left', posX: 10, posY: 10, scale: 100 };
93
+ (0, globals_1.test)('defaults all widget units to rotate', () => {
94
+ const settings = PlaneTrackerAPI_1.cameraSettingsSchema.parse({});
95
+ (0, globals_1.expect)(settings.widget.units).toBe('rotate');
96
+ (0, globals_1.expect)(settings.airportWidget.windSpeedUnits).toBe('rotate');
97
+ (0, globals_1.expect)(settings.airportWidget.temperatureUnits).toBe('rotate');
98
+ });
99
+ (0, globals_1.test)('defaults missing units of saved widgets to rotate', () => {
100
+ const settings = PlaneTrackerAPI_1.cameraSettingsSchema.parse({ widget: WIDGET, airportWidget: WIDGET });
101
+ (0, globals_1.expect)(settings.widget.units).toBe('rotate');
102
+ (0, globals_1.expect)(settings.airportWidget.windSpeedUnits).toBe('rotate');
103
+ (0, globals_1.expect)(settings.airportWidget.temperatureUnits).toBe('rotate');
104
+ });
105
+ (0, globals_1.test)('parses independent widget units', () => {
106
+ const settings = PlaneTrackerAPI_1.cameraSettingsSchema.parse({
107
+ widget: { ...WIDGET, units: 'imperial' },
108
+ airportWidget: { ...WIDGET, windSpeedUnits: 'rotate', temperatureUnits: 'metric' },
109
+ });
110
+ (0, globals_1.expect)(settings.widget.units).toBe('imperial');
111
+ (0, globals_1.expect)(settings.airportWidget.windSpeedUnits).toBe('rotate');
112
+ (0, globals_1.expect)(settings.airportWidget.temperatureUnits).toBe('metric');
113
+ });
114
+ (0, globals_1.test)('rejects an unknown widget unit', () => {
115
+ (0, globals_1.expect)(PlaneTrackerAPI_1.cameraSettingsSchema.safeParse({ widget: { ...WIDGET, units: 'kelvin' } }).success).toBe(false);
116
+ });
117
+ });
@@ -54,15 +54,15 @@ declare const cswEventsDataSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
54
54
  }, "strip", z.ZodTypeAny, {
55
55
  type: "StreamSwitchVideoError";
56
56
  playlist_name: string;
57
- playlist_active_stream: number;
58
57
  info: string;
58
+ playlist_active_stream: number;
59
59
  stream_name?: string | undefined;
60
60
  clip_name?: string | undefined;
61
61
  }, {
62
62
  type: "StreamSwitchVideoError";
63
63
  playlist_name: string;
64
- playlist_active_stream: number;
65
64
  info: string;
65
+ playlist_active_stream: number;
66
66
  stream_name?: string | undefined;
67
67
  clip_name?: string | undefined;
68
68
  }>, z.ZodObject<{
@@ -207,15 +207,15 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
207
207
  }, "strip", z.ZodTypeAny, {
208
208
  type: "StreamSwitchVideoError";
209
209
  playlist_name: string;
210
- playlist_active_stream: number;
211
210
  info: string;
211
+ playlist_active_stream: number;
212
212
  stream_name?: string | undefined;
213
213
  clip_name?: string | undefined;
214
214
  }, {
215
215
  type: "StreamSwitchVideoError";
216
216
  playlist_name: string;
217
- playlist_active_stream: number;
218
217
  info: string;
218
+ playlist_active_stream: number;
219
219
  stream_name?: string | undefined;
220
220
  clip_name?: string | undefined;
221
221
  }>, z.ZodObject<{
@@ -323,8 +323,8 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
323
323
  } | {
324
324
  type: "StreamSwitchVideoError";
325
325
  playlist_name: string;
326
- playlist_active_stream: number;
327
326
  info: string;
327
+ playlist_active_stream: number;
328
328
  stream_name?: string | undefined;
329
329
  clip_name?: string | undefined;
330
330
  } | {
@@ -376,8 +376,8 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
376
376
  } | {
377
377
  type: "StreamSwitchVideoError";
378
378
  playlist_name: string;
379
- playlist_active_stream: number;
380
379
  info: string;
380
+ playlist_active_stream: number;
381
381
  stream_name?: string | undefined;
382
382
  clip_name?: string | undefined;
383
383
  } | {
@@ -464,15 +464,15 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
464
464
  }, "strip", z.ZodTypeAny, {
465
465
  type: "StreamSwitchVideoError";
466
466
  playlist_name: string;
467
- playlist_active_stream: number;
468
467
  info: string;
468
+ playlist_active_stream: number;
469
469
  stream_name?: string | undefined;
470
470
  clip_name?: string | undefined;
471
471
  }, {
472
472
  type: "StreamSwitchVideoError";
473
473
  playlist_name: string;
474
- playlist_active_stream: number;
475
474
  info: string;
475
+ playlist_active_stream: number;
476
476
  stream_name?: string | undefined;
477
477
  clip_name?: string | undefined;
478
478
  }>, z.ZodObject<{
package/esm/VapixAPI.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { arrayToUrl, isNullish, paramToUrl } from './internal/utils';
2
- import { sdCardWatchedStatuses, maxFpsResponseSchema, dateTimeinfoSchema, audioDeviceRequestSchema, audioSampleRatesResponseSchema, timeZoneSchema, getPortsResponseSchema, guardTourSchema, ptzOverviewSchema, cameraPTZItemDataSchema, applicationListSchema, sdCardInfoSchema, ALL_APP_IDS, allDateTimeInfoSchema, } from './types/VapixAPI';
2
+ import { sdCardWatchedStatuses, maxFpsResponseSchema, dateTimeinfoSchema, audioDeviceRequestSchema, audioSampleRatesResponseSchema, applicationSchema, timeZoneSchema, getPortsResponseSchema, guardTourSchema, ptzOverviewSchema, cameraPTZItemDataSchema, sdCardInfoSchema, ALL_APP_IDS, allDateTimeInfoSchema, } from './types/VapixAPI';
3
3
  import { ApplicationAPIError, MaxFPSError, NoDeviceInfoError, PtzNotSupportedError, ErrorWithResponse, SDCardActionError, SDCardJobError, SettingParameterError, TimezoneFetchError, } from './errors/errors';
4
4
  import { z } from 'zod';
5
5
  import { XMLParser } from 'fast-xml-parser';
@@ -562,17 +562,21 @@ export class VapixAPI extends BasicAPI {
562
562
  allowBooleanAttributes: true,
563
563
  });
564
564
  const result = parser.parse(xml);
565
- let apps = result.reply.application ?? [];
566
- if (!Array.isArray(apps)) {
567
- apps = [apps];
568
- }
569
- const appList = apps.map((app) => {
570
- return {
571
- ...app,
572
- appId: ALL_APP_IDS.find((id) => id.toLowerCase() === app.Name.toLowerCase()) ?? null,
573
- };
574
- });
575
- return applicationListSchema.parse(appList);
565
+ const rawApps = result.reply.application ?? [];
566
+ const apps = Array.isArray(rawApps) ? rawApps : [rawApps];
567
+ const appList = [];
568
+ for (const app of apps) {
569
+ const parsedApp = applicationSchema.safeParse(app);
570
+ if (!parsedApp.success) {
571
+ console.debug('Skipping invalid application entry:', app, parsedApp.error.issues);
572
+ continue;
573
+ }
574
+ appList.push({
575
+ ...parsedApp.data,
576
+ appId: ALL_APP_IDS.find((id) => id.toLowerCase() === parsedApp.data.Name.toLowerCase()) ?? null,
577
+ });
578
+ }
579
+ return appList;
576
580
  }
577
581
  async startApplication(applicationId, options) {
578
582
  const agent = this.getClient(options?.proxyParams);
@@ -0,0 +1,144 @@
1
+ import { VapixAPI } from './VapixAPI';
2
+ import { describe, test, expect, jest, afterEach } from '@jest/globals';
3
+ const createApi = (xml) => {
4
+ const client = {
5
+ get: () => Promise.resolve(new Response(xml)),
6
+ post: () => Promise.reject(new Error('unused')),
7
+ };
8
+ return new VapixAPI(client);
9
+ };
10
+ describe('VapixAPI', () => {
11
+ describe('getApplicationList', () => {
12
+ afterEach(() => {
13
+ jest.restoreAllMocks();
14
+ });
15
+ test('skips the nameless entry of the live camera list', async () => {
16
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
17
+ const api = createApi(`<reply result="ok">
18
+ <application Name="camstreamer" NiceName="CamStreamer App" Vendor="CamStreamer" Version="5.0-15" ApplicationID="413135" License="None" Status="Running" ConfigurationPage="local/camstreamer/settings.html" />
19
+ <application Name="" License="None" Status="Stopped" />
20
+ <application Name="vmd" NiceName="AXIS Video Motion Detection" Vendor="Axis Communications" Version="4.4-5" ApplicationID="143440" License="None" Status="Stopped" ConfigurationPage="local/vmd/config.html" VendorHomePage="http://www.axis.com" LicenseName="Proprietary" />
21
+ </reply>`);
22
+ await expect(api.getApplicationList()).resolves.toEqual([
23
+ {
24
+ Name: 'camstreamer',
25
+ NiceName: 'CamStreamer App',
26
+ Vendor: 'CamStreamer',
27
+ Version: '5.0-15',
28
+ ApplicationID: '413135',
29
+ License: 'None',
30
+ Status: 'Running',
31
+ ConfigurationPage: 'local/camstreamer/settings.html',
32
+ appId: 'CamStreamer',
33
+ },
34
+ {
35
+ Name: 'vmd',
36
+ NiceName: 'AXIS Video Motion Detection',
37
+ Vendor: 'Axis Communications',
38
+ Version: '4.4-5',
39
+ ApplicationID: '143440',
40
+ License: 'None',
41
+ Status: 'Stopped',
42
+ ConfigurationPage: 'local/vmd/config.html',
43
+ VendorHomePage: 'http://www.axis.com',
44
+ LicenseName: 'Proprietary',
45
+ appId: null,
46
+ },
47
+ ]);
48
+ expect(debugSpy).toHaveBeenCalledTimes(1);
49
+ expect(debugSpy.mock.calls[0]).toContainEqual({ Name: '', License: 'None', Status: 'Stopped' });
50
+ });
51
+ test.each([
52
+ {
53
+ name: 'an entry without a name',
54
+ xml: '<reply result="ok"><application Name="vmd" NiceName="AXIS Video Motion Detection" Vendor="Axis Communications" Version="4.4-5" License="None" Status="Stopped" /><application License="None" Status="Stopped" /></reply>',
55
+ expected: [
56
+ {
57
+ Name: 'vmd',
58
+ NiceName: 'AXIS Video Motion Detection',
59
+ Vendor: 'Axis Communications',
60
+ Version: '4.4-5',
61
+ License: 'None',
62
+ Status: 'Stopped',
63
+ appId: null,
64
+ },
65
+ ],
66
+ },
67
+ {
68
+ name: 'a named entry missing informational fields',
69
+ xml: '<reply result="ok"><application Name="vmd" NiceName="AXIS Video Motion Detection" Vendor="Axis Communications" Version="4.4-5" License="None" Status="Stopped" /><application Name="broken" License="None" Status="Stopped" /></reply>',
70
+ expected: [
71
+ {
72
+ Name: 'vmd',
73
+ NiceName: 'AXIS Video Motion Detection',
74
+ Vendor: 'Axis Communications',
75
+ Version: '4.4-5',
76
+ License: 'None',
77
+ Status: 'Stopped',
78
+ appId: null,
79
+ },
80
+ ],
81
+ },
82
+ ])('skips invalid entries individually: $name', async ({ xml, expected }) => {
83
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
84
+ await expect(createApi(xml).getApplicationList()).resolves.toEqual(expected);
85
+ expect(debugSpy).toHaveBeenCalledTimes(1);
86
+ });
87
+ test.each([
88
+ {
89
+ name: 'drops attributes outside the schema',
90
+ xml: '<reply result="ok"><application Name="camoverlay" NiceName="CamOverlay App" Vendor="CamStreamer" Version="3.0-1" License="None" Status="Running" Unexpected="value" /></reply>',
91
+ expected: [
92
+ {
93
+ Name: 'camoverlay',
94
+ NiceName: 'CamOverlay App',
95
+ Vendor: 'CamStreamer',
96
+ Version: '3.0-1',
97
+ License: 'None',
98
+ Status: 'Running',
99
+ appId: 'CamOverlay',
100
+ },
101
+ ],
102
+ },
103
+ {
104
+ name: 'maps an application outside the core CamStreamer apps to its appId',
105
+ xml: '<reply result="ok"><application Name="sporttracker" NiceName="SportTracker App" Vendor="CamStreamer" Version="1.0-1" License="None" Status="Running" /></reply>',
106
+ expected: [
107
+ {
108
+ Name: 'sporttracker',
109
+ NiceName: 'SportTracker App',
110
+ Vendor: 'CamStreamer',
111
+ Version: '1.0-1',
112
+ License: 'None',
113
+ Status: 'Running',
114
+ appId: 'SportTracker',
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ name: 'maps a capitalised application name to its appId',
120
+ xml: '<reply result="ok"><application Name="CamOverlay" NiceName="CamOverlay App" Vendor="CamStreamer" Version="3.0-1" License="None" Status="Running" /></reply>',
121
+ expected: [
122
+ {
123
+ Name: 'CamOverlay',
124
+ NiceName: 'CamOverlay App',
125
+ Vendor: 'CamStreamer',
126
+ Version: '3.0-1',
127
+ License: 'None',
128
+ Status: 'Running',
129
+ appId: 'CamOverlay',
130
+ },
131
+ ],
132
+ },
133
+ {
134
+ name: 'returns an empty list when the camera has no applications',
135
+ xml: '<reply result="ok"></reply>',
136
+ expected: [],
137
+ },
138
+ ])('returns every valid entry with its appId and logs nothing: $name', async ({ xml, expected }) => {
139
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
140
+ await expect(createApi(xml).getApplicationList()).resolves.toEqual(expected);
141
+ expect(debugSpy).not.toHaveBeenCalled();
142
+ });
143
+ });
144
+ });
@@ -25,6 +25,7 @@ export const widgetSchema = z.object({
25
25
  posY: z.number().nonnegative(),
26
26
  scale: z.number().positive(),
27
27
  });
28
+ const widgetUnitsSchema = z.union([z.literal('metric'), z.literal('imperial'), z.literal('rotate')]);
28
29
  const labelOptionsSchema = z.union([
29
30
  z.literal('blank'),
30
31
  z.literal('registration'),
@@ -186,18 +187,25 @@ export const cameraSettingsSchema = z.object({
186
187
  displaySystemInfo: z.boolean().optional(),
187
188
  })
188
189
  .optional(),
189
- widget: widgetSchema.default({
190
+ widget: widgetSchema
191
+ .extend({
192
+ units: widgetUnitsSchema.default('rotate'),
193
+ })
194
+ .default({
190
195
  enabled: true,
191
196
  coord: 'top_right',
192
197
  posX: 10,
193
198
  posY: 10,
194
199
  scale: 100,
200
+ units: 'rotate',
195
201
  }),
196
202
  airportWidget: widgetSchema
197
203
  .extend({
198
204
  showWeather: z.boolean().default(false),
199
205
  weatherLocationKey: z.string().default(''),
200
206
  weatherLocationName: z.string().default(''),
207
+ windSpeedUnits: widgetUnitsSchema.default('rotate'),
208
+ temperatureUnits: widgetUnitsSchema.default('rotate'),
201
209
  })
202
210
  .default({
203
211
  enabled: true,
@@ -208,6 +216,8 @@ export const cameraSettingsSchema = z.object({
208
216
  showWeather: false,
209
217
  weatherLocationKey: '',
210
218
  weatherLocationName: '',
219
+ windSpeedUnits: 'rotate',
220
+ temperatureUnits: 'rotate',
211
221
  }),
212
222
  fr24FlightInfoSource: z
213
223
  .object({
@@ -86,3 +86,30 @@ describe('cameraSettingsSchema cameraConfig', () => {
86
86
  expect(cameraSettingsSchema.safeParse({ cameraConfig }).success).toBe(false);
87
87
  });
88
88
  });
89
+ describe('cameraSettingsSchema widget units', () => {
90
+ const WIDGET = { coord: 'top_left', posX: 10, posY: 10, scale: 100 };
91
+ test('defaults all widget units to rotate', () => {
92
+ const settings = cameraSettingsSchema.parse({});
93
+ expect(settings.widget.units).toBe('rotate');
94
+ expect(settings.airportWidget.windSpeedUnits).toBe('rotate');
95
+ expect(settings.airportWidget.temperatureUnits).toBe('rotate');
96
+ });
97
+ test('defaults missing units of saved widgets to rotate', () => {
98
+ const settings = cameraSettingsSchema.parse({ widget: WIDGET, airportWidget: WIDGET });
99
+ expect(settings.widget.units).toBe('rotate');
100
+ expect(settings.airportWidget.windSpeedUnits).toBe('rotate');
101
+ expect(settings.airportWidget.temperatureUnits).toBe('rotate');
102
+ });
103
+ test('parses independent widget units', () => {
104
+ const settings = cameraSettingsSchema.parse({
105
+ widget: { ...WIDGET, units: 'imperial' },
106
+ airportWidget: { ...WIDGET, windSpeedUnits: 'rotate', temperatureUnits: 'metric' },
107
+ });
108
+ expect(settings.widget.units).toBe('imperial');
109
+ expect(settings.airportWidget.windSpeedUnits).toBe('rotate');
110
+ expect(settings.airportWidget.temperatureUnits).toBe('metric');
111
+ });
112
+ test('rejects an unknown widget unit', () => {
113
+ expect(cameraSettingsSchema.safeParse({ widget: { ...WIDGET, units: 'kelvin' } }).success).toBe(false);
114
+ });
115
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstreamerlib",
3
- "version": "4.0.35-bc56d1e",
3
+ "version": "4.0.36-05711a0",
4
4
  "description": "Helper library for CamStreamer ACAP applications.",
5
5
  "prettier": "@camstreamer/prettier-config",
6
6
  "engine": {
@@ -87,6 +87,7 @@ export declare class PlaneTrackerAPI<Client extends IClient<TResponse, any>> ext
87
87
  widget: {
88
88
  enabled: boolean;
89
89
  scale: number;
90
+ units: "rotate" | "metric" | "imperial";
90
91
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
91
92
  posX: number;
92
93
  posY: number;
@@ -100,6 +101,8 @@ export declare class PlaneTrackerAPI<Client extends IClient<TResponse, any>> ext
100
101
  posY: number;
101
102
  showWeather: boolean;
102
103
  weatherLocationKey: string;
104
+ windSpeedUnits: "rotate" | "metric" | "imperial";
105
+ temperatureUnits: "rotate" | "metric" | "imperial";
103
106
  };
104
107
  fr24FlightInfoSource: {
105
108
  enabled: boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -54,6 +54,8 @@ export declare const widgetSchema: z.ZodObject<{
54
54
  posY: number;
55
55
  enabled?: boolean | undefined;
56
56
  }>;
57
+ declare const widgetUnitsSchema: z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>;
58
+ export type TWidgetUnits = z.infer<typeof widgetUnitsSchema>;
57
59
  declare const labelOptionsSchema: z.ZodUnion<[z.ZodLiteral<"blank">, z.ZodLiteral<"registration">, z.ZodLiteral<"call_sign">, z.ZodLiteral<"flight_number">, z.ZodLiteral<"icao">]>;
58
60
  export type TLabelOption = z.infer<typeof labelOptionsSchema>;
59
61
  declare const droneLabelOptionsSchema: z.ZodUnion<[z.ZodLiteral<"blank">, z.ZodLiteral<"remote_id">]>;
@@ -313,9 +315,12 @@ export declare const cameraSettingsSchema: z.ZodObject<{
313
315
  posX: z.ZodNumber;
314
316
  posY: z.ZodNumber;
315
317
  scale: z.ZodNumber;
318
+ } & {
319
+ units: z.ZodDefault<z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>>;
316
320
  }, "strip", z.ZodTypeAny, {
317
321
  enabled: boolean;
318
322
  scale: number;
323
+ units: "rotate" | "metric" | "imperial";
319
324
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
320
325
  posX: number;
321
326
  posY: number;
@@ -325,6 +330,7 @@ export declare const cameraSettingsSchema: z.ZodObject<{
325
330
  posX: number;
326
331
  posY: number;
327
332
  enabled?: boolean | undefined;
333
+ units?: "rotate" | "metric" | "imperial" | undefined;
328
334
  }>>;
329
335
  airportWidget: z.ZodDefault<z.ZodObject<{
330
336
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -336,6 +342,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
336
342
  showWeather: z.ZodDefault<z.ZodBoolean>;
337
343
  weatherLocationKey: z.ZodDefault<z.ZodString>;
338
344
  weatherLocationName: z.ZodDefault<z.ZodString>;
345
+ windSpeedUnits: z.ZodDefault<z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>>;
346
+ temperatureUnits: z.ZodDefault<z.ZodUnion<[z.ZodLiteral<"metric">, z.ZodLiteral<"imperial">, z.ZodLiteral<"rotate">]>>;
339
347
  }, "strip", z.ZodTypeAny, {
340
348
  enabled: boolean;
341
349
  scale: number;
@@ -345,6 +353,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
345
353
  posY: number;
346
354
  showWeather: boolean;
347
355
  weatherLocationKey: string;
356
+ windSpeedUnits: "rotate" | "metric" | "imperial";
357
+ temperatureUnits: "rotate" | "metric" | "imperial";
348
358
  }, {
349
359
  scale: number;
350
360
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
@@ -354,6 +364,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
354
364
  weatherLocationName?: string | undefined;
355
365
  showWeather?: boolean | undefined;
356
366
  weatherLocationKey?: string | undefined;
367
+ windSpeedUnits?: "rotate" | "metric" | "imperial" | undefined;
368
+ temperatureUnits?: "rotate" | "metric" | "imperial" | undefined;
357
369
  }>>;
358
370
  fr24FlightInfoSource: z.ZodDefault<z.ZodObject<{
359
371
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -614,6 +626,7 @@ export declare const cameraSettingsSchema: z.ZodObject<{
614
626
  widget: {
615
627
  enabled: boolean;
616
628
  scale: number;
629
+ units: "rotate" | "metric" | "imperial";
617
630
  coord: "top_left" | "top_right" | "bottom_left" | "bottom_right";
618
631
  posX: number;
619
632
  posY: number;
@@ -627,6 +640,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
627
640
  posY: number;
628
641
  showWeather: boolean;
629
642
  weatherLocationKey: string;
643
+ windSpeedUnits: "rotate" | "metric" | "imperial";
644
+ temperatureUnits: "rotate" | "metric" | "imperial";
630
645
  };
631
646
  fr24FlightInfoSource: {
632
647
  enabled: boolean;
@@ -802,6 +817,7 @@ export declare const cameraSettingsSchema: z.ZodObject<{
802
817
  posX: number;
803
818
  posY: number;
804
819
  enabled?: boolean | undefined;
820
+ units?: "rotate" | "metric" | "imperial" | undefined;
805
821
  } | undefined;
806
822
  airportWidget?: {
807
823
  scale: number;
@@ -812,6 +828,8 @@ export declare const cameraSettingsSchema: z.ZodObject<{
812
828
  weatherLocationName?: string | undefined;
813
829
  showWeather?: boolean | undefined;
814
830
  weatherLocationKey?: string | undefined;
831
+ windSpeedUnits?: "rotate" | "metric" | "imperial" | undefined;
832
+ temperatureUnits?: "rotate" | "metric" | "imperial" | undefined;
815
833
  } | undefined;
816
834
  fr24FlightInfoSource?: {
817
835
  enabled?: boolean | undefined;
@@ -54,15 +54,15 @@ declare const cswEventsDataSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
54
54
  }, "strip", z.ZodTypeAny, {
55
55
  type: "StreamSwitchVideoError";
56
56
  playlist_name: string;
57
- playlist_active_stream: number;
58
57
  info: string;
58
+ playlist_active_stream: number;
59
59
  stream_name?: string | undefined;
60
60
  clip_name?: string | undefined;
61
61
  }, {
62
62
  type: "StreamSwitchVideoError";
63
63
  playlist_name: string;
64
- playlist_active_stream: number;
65
64
  info: string;
65
+ playlist_active_stream: number;
66
66
  stream_name?: string | undefined;
67
67
  clip_name?: string | undefined;
68
68
  }>, z.ZodObject<{
@@ -207,15 +207,15 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
207
207
  }, "strip", z.ZodTypeAny, {
208
208
  type: "StreamSwitchVideoError";
209
209
  playlist_name: string;
210
- playlist_active_stream: number;
211
210
  info: string;
211
+ playlist_active_stream: number;
212
212
  stream_name?: string | undefined;
213
213
  clip_name?: string | undefined;
214
214
  }, {
215
215
  type: "StreamSwitchVideoError";
216
216
  playlist_name: string;
217
- playlist_active_stream: number;
218
217
  info: string;
218
+ playlist_active_stream: number;
219
219
  stream_name?: string | undefined;
220
220
  clip_name?: string | undefined;
221
221
  }>, z.ZodObject<{
@@ -323,8 +323,8 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
323
323
  } | {
324
324
  type: "StreamSwitchVideoError";
325
325
  playlist_name: string;
326
- playlist_active_stream: number;
327
326
  info: string;
327
+ playlist_active_stream: number;
328
328
  stream_name?: string | undefined;
329
329
  clip_name?: string | undefined;
330
330
  } | {
@@ -376,8 +376,8 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
376
376
  } | {
377
377
  type: "StreamSwitchVideoError";
378
378
  playlist_name: string;
379
- playlist_active_stream: number;
380
379
  info: string;
380
+ playlist_active_stream: number;
381
381
  stream_name?: string | undefined;
382
382
  clip_name?: string | undefined;
383
383
  } | {
@@ -464,15 +464,15 @@ export declare const cswEventsSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
464
464
  }, "strip", z.ZodTypeAny, {
465
465
  type: "StreamSwitchVideoError";
466
466
  playlist_name: string;
467
- playlist_active_stream: number;
468
467
  info: string;
468
+ playlist_active_stream: number;
469
469
  stream_name?: string | undefined;
470
470
  clip_name?: string | undefined;
471
471
  }, {
472
472
  type: "StreamSwitchVideoError";
473
473
  playlist_name: string;
474
- playlist_active_stream: number;
475
474
  info: string;
475
+ playlist_active_stream: number;
476
476
  stream_name?: string | undefined;
477
477
  clip_name?: string | undefined;
478
478
  }>, z.ZodObject<{