UNPKG

camstreamerlib

Version:

Helper library for CamStreamer ACAP applications.

553 lines (552 loc) 22.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VapixAPI = void 0; const utils_1 = require("./internal/utils"); const VapixAPI_1 = require("./types/VapixAPI"); const errors_1 = require("./errors/errors"); const ProxyClient_1 = require("./internal/ProxyClient"); const zod_1 = require("zod"); const fast_xml_parser_1 = require("fast-xml-parser"); class VapixAPI { client; CustomFormData; constructor(client, CustomFormData = FormData) { this.client = client; this.CustomFormData = CustomFormData; } getClient(proxyParams) { return proxyParams ? new ProxyClient_1.ProxyClient(this.client, proxyParams) : this.client; } async postUrlEncoded(path, parameters, headers, options) { const data = (0, utils_1.paramToUrl)(parameters); const head = { ...headers, 'Content-Type': 'application/x-www-form-urlencoded' }; const agent = this.getClient(options?.proxyParams); const res = await agent.post({ path, data, headers: head, timeout: options?.timeout }); if (!res.ok) { throw new Error(await (0, utils_1.responseStringify)(res)); } return res; } async postJson(path, jsonData, headers, options) { const data = JSON.stringify(jsonData); const head = { ...headers, 'Content-Type': 'application/json' }; const agent = this.getClient(options?.proxyParams); const res = await agent.post({ path, data, headers: head, timeout: options?.timeout }); if (!res.ok) { throw new Error(await (0, utils_1.responseStringify)(res)); } return res; } async getCameraImage(parameters, options) { const agent = this.getClient(options?.proxyParams); return (await agent.get({ path: '/axis-cgi/jpg/image.cgi', parameters, timeout: options?.timeout, })); } async getEventDeclarations(options) { const data = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">' + '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' + 'xmlns:xsd="http://www.w3.org/2001/XMLSchema">' + '<GetEventInstances xmlns="http://www.axis.com/vapix/ws/event1"/>' + '</s:Body>' + '</s:Envelope>'; const agent = this.getClient(options?.proxyParams); const res = await agent.post({ path: '/vapix/services', data, headers: { 'Content-Type': 'application/soap+xml' }, }); if (!res.ok) { throw new Error(await (0, utils_1.responseStringify)(res)); } return await res.text(); } async getSupportedAudioSampleRate(options) { const path = '/axis-cgi/audio/streamingcapabilities.cgi'; const jsonData = { apiVersion: '1.0', method: 'list' }; const res = await this.postJson(path, jsonData, undefined, options); const encoders = VapixAPI_1.audioSampleRatesResponseSchema.parse(await res.json()).data.encoders; const data = encoders.aac ?? encoders.AAC ?? []; return data.map((item) => { return { sampleRate: item.sample_rate, bitRates: item.bit_rates, }; }); } async performAutofocus(options) { try { const data = { apiVersion: '1', method: 'performAutofocus', params: { optics: [ { opticsId: '0', }, ], }, }; await this.postJson('/axis-cgi/opticscontrol.cgi', data, undefined, options); } catch (err) { await this.postUrlEncoded('/axis-cgi/opticssetup.cgi', { autofocus: 'perform', source: '1', }, undefined, options); } } async checkSDCard(options) { const res = await this.postUrlEncoded('/axis-cgi/disks/list.cgi', { diskid: 'SD_DISK', }, undefined, options); const xmlText = await res.text(); const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', allowBooleanAttributes: true, }); const result = parser.parse(xmlText); const data = result.root.disks.disk; return VapixAPI_1.sdCardInfoSchema.parse({ totalSize: parseInt(data.totalsize), freeSize: parseInt(data.freesize), status: VapixAPI_1.sdCardWatchedStatuses.includes(data.status) ? data.status : 'disconnected', }); } mountSDCard(options) { return this._doSDCardMountAction('MOUNT', options); } unmountSDCard(options) { return this._doSDCardMountAction('UNMOUNT', options); } async _doSDCardMountAction(action, options) { const res = await this.postUrlEncoded('/axis-cgi/disks/mount.cgi', { action: action, diskid: 'SD_DISK', }, undefined, options); const textXml = await res.text(); const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', allowBooleanAttributes: true, }); const result = parser.parse(textXml); const job = result.root.job; if (job.result !== 'OK') { throw new errors_1.SDCardActionError(action, await (0, utils_1.responseStringify)(res)); } return Number(job.jobid); } async fetchSDCardJobProgress(jobId, options) { const res = await this.postUrlEncoded('/disks/job.cgi', { jobid: String(jobId), diskid: 'SD_DISK', }, undefined, options); const textXml = await res.text(); const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', allowBooleanAttributes: true, }); const job = parser.parse(textXml).root.job; if (job.result !== 'OK') { throw new errors_1.SDCardJobError(); } return Number(job.progress); } downloadCameraReport(options) { return this.postUrlEncoded('/axis-cgi/serverreport.cgi', { mode: 'text' }, undefined, options); } getSystemLog(options) { return this.postUrlEncoded('/axis-cgi/admin/systemlog.cgi', undefined, undefined, options); } async getMaxFps(channel, options) { const data = { apiVersion: '1.0', method: 'getCaptureModes' }; const res = await this.postJson('/axis-cgi/capturemode.cgi', data, undefined, options); const response = VapixAPI_1.maxFpsResponseSchema.parse(await res.json()); const channels = response.data; if (channels === undefined) { throw new errors_1.MaxFPSError('MALFORMED_REPLY'); } const channelData = channels.find((x) => x.channel === channel); if (channelData === undefined) { throw new errors_1.MaxFPSError('CHANNEL_NOT_FOUND'); } const captureModes = channelData.captureMode; const captureMode = captureModes.find((x) => x.enabled === true); if (captureMode === undefined) { throw new errors_1.MaxFPSError('CAPTURE_MODE_NOT_FOUND'); } if ((0, utils_1.isNullish)(captureMode.maxFPS)) { throw new errors_1.MaxFPSError('FPS_NOT_SPECIFIED'); } return zod_1.z.number().parse(captureMode.maxFPS); } async getTimezone(options) { try { const agent = this.getClient(options?.proxyParams); const resV2 = await agent.get({ path: '/config/rest/time/v2/timeZone', timeout: options?.timeout }); if (!resV2.ok) { throw new Error(await (0, utils_1.responseStringify)(resV2)); } const json = await resV2.json(); const data = VapixAPI_1.timeZoneSchema.parse(json); if (data.status === 'error') { throw new errors_1.TimezoneFetchError(data.error.message); } return data.data.activeTimeZone; } catch (error) { console.warn('Failed to fetch time zone data from time API v2:', error instanceof Error ? error.message : JSON.stringify(error)); console.warn('Falling back to deprecated time API v1'); } const data = await this.getDateTimeInfo(options); if (data.data.timeZone === undefined) { throw new errors_1.TimezoneNotSetupError(); } return zod_1.z.string().parse(data.data.timeZone); } async getDateTimeInfo(options) { const data = { apiVersion: '1.0', method: 'getDateTimeInfo' }; const res = await this.postJson('/axis-cgi/time.cgi', data, undefined, options); return VapixAPI_1.dateTimeinfoSchema.parse(await res.json()); } async getDevicesSettings(options) { const data = { apiVersion: '1.0', method: 'getDevicesSettings' }; const res = await this.postJson('/axis-cgi/audiodevicecontrol.cgi', data, undefined, options); const result = VapixAPI_1.audioDeviceRequestSchema.parse(await res.json()); return result.data.devices.map((device) => ({ ...device, inputs: (device.inputs || []).sort((a, b) => a.id.localeCompare(b.id)), outputs: (device.outputs || []).sort((a, b) => a.id.localeCompare(b.id)), })); } async fetchRemoteDeviceInfo(payload, options) { const res = await this.postJson('/axis-cgi/basicdeviceinfo.cgi', payload, undefined, options); const json = await res.json(); if ((0, utils_1.isNullish)(json.data)) { throw new errors_1.NoDeviceInfoError(); } return json.data; } async getHeaders(options) { const data = { apiVersion: '1.0', method: 'list' }; const res = await this.postJson('/axis-cgi/customhttpheader.cgi', data, undefined, options); return zod_1.z.object({ data: zod_1.z.record(zod_1.z.string()) }).parse(await res.json()).data; } async setHeaders(headers, options) { const data = { apiVersion: '1.0', method: 'set', params: headers }; return this.postJson('/axis-cgi/customhttpheader.cgi', data, undefined, options); } async getParameter(paramNames, options) { const response = await this.postUrlEncoded('/axis-cgi/param.cgi', { action: 'list', group: (0, utils_1.arrayToUrl)(paramNames), }, undefined, options); return VapixAPI.parseParameters(await response.text()); } async setParameter(params, options) { const res = await this.postUrlEncoded('/axis-cgi/param.cgi', { ...params, action: 'update', }, undefined, options); const responseText = await res.text(); if (responseText.startsWith('# Error')) { throw new errors_1.SettingParameterError(responseText); } } async getGuardTourList(options) { const gTourList = new Array(); const response = await this.getParameter('GuardTour', options); for (let i = 0; i < 20; i++) { const gTourBaseName = 'GuardTour.G' + i; if (gTourBaseName + '.CamNbr' in response) { const gTour = { id: gTourBaseName, camNbr: response[gTourBaseName + '.CamNbr'], name: response[gTourBaseName + '.Name'] ?? 'Guard Tour ' + (i + 1), randomEnabled: response[gTourBaseName + '.RandomEnabled'], running: response[gTourBaseName + '.Running'] ?? 'no', timeBetweenSequences: response[gTourBaseName + '.TimeBetweenSequences'], tour: [], }; for (let j = 0; j < 100; j++) { const tourBaseName = 'GuardTour.G' + i + '.Tour.T' + j; if (tourBaseName + '.MoveSpeed' in response) { const tour = { moveSpeed: response[tourBaseName + '.MoveSpeed'], position: response[tourBaseName + '.Position'], presetNbr: response[tourBaseName + '.PresetNbr'], waitTime: response[tourBaseName + '.WaitTime'], waitTimeViewType: response[tourBaseName + '.WaitTimeViewType'], }; gTour.tour.push(tour); } } gTourList.push(gTour); } else { break; } } return VapixAPI_1.guardTourSchema.parse(gTourList); } setGuardTourEnabled(guardTourId, enable, options) { const params = {}; params[guardTourId + '.Running'] = enable ? 'yes' : 'no'; return this.setParameter(params, options); } async getPTZPresetList(channel, options) { const res = await this.postUrlEncoded('/axis-cgi/com/ptz.cgi', { query: 'presetposcam', camera: channel, }, undefined, options); const text = await res.text(); const lines = text.split(/[\r\n]/); const positions = []; for (const line of lines) { if (line.indexOf('presetposno') !== -1) { const delimiterPos = line.indexOf('='); if (delimiterPos !== -1) { const value = line.substring(delimiterPos + 1); positions.push(value); } } } return zod_1.z.array(zod_1.z.string()).parse(positions); } async listPTZ(camera, options) { const url = `/axis-cgi/com/ptz.cgi`; const response = await this.postUrlEncoded(url, { camera, query: 'presetposcamdata', format: 'json', }, undefined, options); const text = await response.text(); if (text === '') { throw new errors_1.PtzNotSupportedError(); } return VapixAPI.parseCameraPtzResponse(text)[camera] ?? []; } async listPtzVideoSourceOverview(options) { const response = await this.postUrlEncoded('/axis-cgi/com/ptz.cgi', { query: 'presetposall', format: 'json', }, undefined, options); const text = await response.text(); if (text === '') { throw new errors_1.PtzNotSupportedError(); } const data = VapixAPI.parseCameraPtzResponse(text); const res = {}; Object.keys(data) .map(Number) .forEach((camera) => { const item = data[camera]; if (item !== undefined) { res[camera - 1] = item.map(({ data: itemData, ...d }) => d); } }); return VapixAPI_1.ptzOverviewSchema.parse(res); } goToPreset(channel, presetName, options) { return this.postUrlEncoded('/axis-cgi/com/ptz.cgi', { camera: channel.toString(), gotoserverpresetname: presetName, }, undefined, options); } async getPtzPosition(camera, options) { const res = await this.postUrlEncoded('/axis-cgi/com/ptz.cgi', { query: 'position', camera: camera.toString(), }, undefined, options); const params = VapixAPI.parseParameters(await res.text()); return VapixAPI_1.cameraPTZItemDataSchema.parse({ pan: Number(params.pan), tilt: Number(params.tilt), zoom: Number(params.zoom), }); } async getPorts(options) { const res = await this.postJson('/axis-cgi/io/portmanagement.cgi', { apiVersion: '1.0', context: '', method: 'getPorts', }, undefined, options); const portResponseParsed = VapixAPI_1.getPortsResponseSchema.parse(await res.json()); return portResponseParsed.data.items; } async setPorts(ports, options) { await this.postJson('/axis-cgi/io/portmanagement.cgi', { apiVersion: '1.0', context: '', method: 'setPorts', params: { ports }, }, undefined, options); } async setPortStateSequence(port, sequence, options) { await this.postJson('/axis-cgi/io/portmanagement.cgi', { apiVersion: '1.0', context: '', method: 'setStateSequence', params: { port, sequence }, }, undefined, options); } async getApplicationList(options) { const agent = this.getClient(options?.proxyParams); const res = await agent.get({ path: '/axis-cgi/applications/list.cgi', timeout: options?.timeout }); const xml = await res.text(); const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', allowBooleanAttributes: true, }); const result = parser.parse(xml); let apps = result.reply.application ?? []; if (!Array.isArray(apps)) { apps = [apps]; } const appList = apps.map((app) => { return { ...app, appId: VapixAPI_1.APP_IDS.find((id) => id.toLowerCase() === app.Name.toLowerCase()) ?? null, }; }); return VapixAPI_1.applicationListSchema.parse(appList); } async startApplication(applicationId, options) { const agent = this.getClient(options?.proxyParams); const res = await agent.get({ path: '/axis-cgi/applications/control.cgi', parameters: { package: applicationId.toLowerCase(), action: 'start', }, timeout: options?.timeout, }); const text = (await res.text()).trim().toLowerCase(); if (text !== 'ok' && !(text.startsWith('error:') && text.substring(7) === '6')) { throw new errors_1.ApplicationAPIError('START', await (0, utils_1.responseStringify)(res)); } } async restartApplication(applicationId, options) { const agent = this.getClient(options?.proxyParams); const res = await agent.get({ path: '/axis-cgi/applications/control.cgi', parameters: { package: applicationId.toLowerCase(), action: 'restart', }, timeout: options?.timeout, }); const text = (await res.text()).trim().toLowerCase(); if (text !== 'ok') { throw new errors_1.ApplicationAPIError('RESTART', await (0, utils_1.responseStringify)(res)); } } async stopApplication(applicationId, options) { const agent = this.getClient(options?.proxyParams); const res = await agent.get({ path: '/axis-cgi/applications/control.cgi', parameters: { package: applicationId.toLowerCase(), action: 'stop', }, timeout: options?.timeout, }); const text = (await res.text()).trim().toLowerCase(); if (text !== 'ok' && !(text.startsWith('error:') && text.substring(7) === '6')) { throw new errors_1.ApplicationAPIError('STOP', await (0, utils_1.responseStringify)(res)); } } async installApplication(data, fileName, options) { const formData = new this.CustomFormData(); formData.append('packfil', data, fileName); const agent = this.getClient(options?.proxyParams); const res = await agent.post({ path: '/axis-cgi/applications/upload.cgi', data: formData, headers: { contentType: 'application/octet-stream', }, timeout: options?.timeout ?? 120000, }); if (!res.ok) { throw new Error(await (0, utils_1.responseStringify)(res)); } const text = await res.text(); if (text.length > 5) { throw new errors_1.ApplicationAPIError('INSTALL', text); } } static parseParameters = (response) => { const params = {}; const lines = response.split(/[\r\n]/); for (const line of lines) { if (line.length === 0 || line.substring(0, 7) === '# Error') { continue; } const delimiterPos = line.indexOf('='); if (delimiterPos !== -1) { const paramName = line.substring(0, delimiterPos).replace('root.', ''); const paramValue = line.substring(delimiterPos + 1); params[paramName] = paramValue; } } return params; }; static parseCameraPtzResponse = (response) => { const json = JSON.parse(response); const parsed = {}; Object.keys(json).forEach((key) => { if (!key.startsWith('Camera ')) { return; } const camera = Number(key.replace('Camera ', '')); if (json[key].presets !== undefined) { parsed[camera] = VapixAPI.parsePtz(json[key].presets); } }); return parsed; }; static parsePtz = (parsed) => { const res = []; parsed.forEach((value) => { const delimiterPos = value.indexOf('='); if (delimiterPos === -1) { return; } if (!value.startsWith('presetposno')) { return; } const id = Number(value.substring(11, delimiterPos)); if (Number.isNaN(id)) { return; } const data = value.substring(delimiterPos + 1).split(':'); const getValue = (valueName) => { for (const d of data) { const p = d.split('='); if (p[0] === valueName) { return Number(p[1]); } } return 0; }; res.push({ id, name: data[0] ?? 'Preset ' + id, data: { pan: getValue('pan'), tilt: getValue('tilt'), zoom: getValue('zoom'), }, }); }); return res; }; } exports.VapixAPI = VapixAPI;