camstreamerlib
Version:
Helper library for CamStreamer ACAP applications.
480 lines (479 loc) • 19.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.VapixAPI = void 0;
const prettifyXml = require("prettify-xml");
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;
constructor(client, getProxyUrl) {
this.client = new ProxyClient_1.ProxyClient(client, getProxyUrl);
}
async getUrlEncoded(proxy, path, parameters, headers = {}) {
const data = (0, utils_1.paramToUrl)(parameters);
const head = { ...headers, 'Content-Type': 'application/x-www-form-urlencoded' };
const res = await this.client.post(proxy, path, data, {}, head);
if (!res.ok) {
throw new Error(await (0, utils_1.responseStringify)(res));
}
return res;
}
async postJson(proxy, path, jsonData, headers = {}) {
const data = JSON.stringify(jsonData);
const head = { ...headers, 'Content-Type': 'application/json' };
const res = await this.client.post(proxy, path, data, {}, head);
if (!res.ok) {
throw new Error(await (0, utils_1.responseStringify)(res));
}
return res;
}
async getCameraImage(params, proxy = null) {
return await this.client.get(proxy, '/axis-cgi/jpg/image.cgi', params);
}
async getEventDeclarations(proxy = null) {
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 res = await this.client.post(proxy, '/vapix/services', data, { 'Content-Type': 'application/soap+xml' });
if (!res.ok) {
throw new Error(await (0, utils_1.responseStringify)(res));
}
const declarations = await res.text();
return prettifyXml(declarations);
}
async getSupportedAudioSampleRate(proxy = null) {
const url = '/axis-cgi/audio/streamingcapabilities.cgi';
const jsonData = { apiVersion: '1.0', method: 'list' };
const res = await this.postJson(proxy, url, jsonData);
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(proxy = null) {
try {
const data = {
apiVersion: '1',
method: 'performAutofocus',
params: {
optics: [
{
opticsId: '0',
},
],
},
};
await this.postJson(proxy, '/axis-cgi/opticscontrol.cgi', data);
}
catch (err) {
await this.getUrlEncoded(proxy, '/axis-cgi/opticssetup.cgi', {
autofocus: 'perform',
source: '1',
});
}
}
async checkSDCard(proxy = null) {
const res = await this.getUrlEncoded(proxy, '/axis-cgi/disks/list.cgi', {
diskid: 'SD_DISK',
});
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 {
totalSize: parseInt(data.totalsize),
freeSize: parseInt(data.freesize),
status: VapixAPI_1.sdCardWatchedStatuses.includes(data.status) ? data.status : 'disconnected',
};
}
mountSDCard(proxy = null) {
return this._doSDCardMountAction('MOUNT', proxy);
}
unmountSDCard(proxy = null) {
return this._doSDCardMountAction('UNMOUNT', proxy);
}
async _doSDCardMountAction(action, proxy = null) {
const res = await this.getUrlEncoded(proxy, '/axis-cgi/disks/mount.cgi', {
action: action,
diskid: 'SD_DISK',
});
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, proxy = null) {
const res = await this.getUrlEncoded(proxy, '/disks/job.cgi', {
jobid: String(jobId),
diskid: 'SD_DISK',
});
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(proxy = null) {
return this.getUrlEncoded(proxy, '/axis-cgi/serverreport.cgi', { mode: 'text' });
}
getSystemLog(proxy = null) {
return this.getUrlEncoded(proxy, '/axis-cgi/admin/systemlog.cgi');
}
async getMaxFps(channel, proxy = null) {
const data = { apiVersion: '1.0', method: 'getCaptureModes' };
const res = await this.postJson(proxy, '/axis-cgi/capturemode.cgi', data);
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 captureMode.maxFPS;
}
async getTimezone(proxy = null) {
const data = { apiVersion: '1.0', method: 'getDateTimeInfo' };
const res = await this.postJson(proxy, '/axis-cgi/time.cgi', data);
return (await res.json())?.timeZone ?? 'Europe/Prague';
}
async getDateTimeInfo(proxy = null) {
const data = { apiVersion: '1.0', method: 'getDateTimeInfo' };
const res = await this.postJson(proxy, '/axis-cgi/time.cgi', data);
return VapixAPI_1.dateTimeinfoSchema.parse(await res.json());
}
async getDevicesSettings(proxy = null) {
const data = { apiVersion: '1.0', method: 'getDevicesSettings' };
const res = await this.postJson(proxy, '/axis-cgi/audiodevicecontrol.cgi', data);
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, proxy = null) {
const res = await this.postJson(proxy, '/axis-cgi/basicdeviceinfo.cgi', payload);
const textXml = await res.text();
const parser = new fast_xml_parser_1.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
allowBooleanAttributes: true,
});
const result = parser.parse(textXml);
if ((0, utils_1.isNullish)(result.body.data)) {
throw new errors_1.NoDeviceInfoError();
}
return result.data;
}
async getHeaders(proxy = null) {
const data = { apiVersion: '1.0', method: 'list' };
const res = await this.postJson(proxy, '/axis-cgi/customhttpheader.cgi', data);
return zod_1.z.object({ data: zod_1.z.record(zod_1.z.string()) }).parse(await res.json()).data;
}
async setHeaders(headers, proxy = null) {
const data = { apiVersion: '1.0', method: 'set', params: headers };
return this.postJson(proxy, '/axis-cgi/customhttpheader.cgi', data);
}
async getParameter(paramNames, proxy = null) {
const response = await this.getUrlEncoded(proxy, '/axis-cgi/param.cgi', {
action: 'list',
group: (0, utils_1.arrayToUrl)(paramNames),
});
return parseParameters(await response.text());
}
async setParameter(params, proxy = null) {
const res = await this.getUrlEncoded(proxy, '/axis-cgi/param.cgi', {
...params,
action: 'update',
});
const responseText = await res.text();
if (responseText.startsWith('# Error')) {
throw new Error(responseText);
}
return true;
}
async getGuardTourList(proxy = null) {
const gTourList = new Array();
const response = await this.getParameter('GuardTour', proxy);
for (let i = 0; i < 20; i++) {
const gTourBaseName = 'root.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 = 'root.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 gTourList;
}
setGuardTourEnabled(guardTourID, enable, proxy = null) {
const options = {};
options[guardTourID + '.Running'] = enable ? 'yes' : 'no';
return this.setParameter(options, proxy);
}
async getPTZPresetList(channel, proxy = null) {
const res = await this.getUrlEncoded(proxy, '/axis-cgi/com/ptz.cgi', {
query: 'presetposcam',
camera: channel.toString(),
});
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 positions;
}
async listPTZ(camera, proxy = null) {
const url = `/axis-cgi/com/ptz.cgi`;
const response = await this.getUrlEncoded(proxy, url, {
camera,
query: 'presetposcamdata',
format: 'json',
});
const text = await response.text();
if (text === '') {
throw new errors_1.PtzNotSupportedError();
}
return parseCameraPtzResponse(text)[camera] ?? [];
}
async listPtzVideoSourceOverview(proxy = null) {
const response = await this.getUrlEncoded(proxy, '/axis-cgi/com/ptz.cgi', {
query: 'presetposall',
format: 'json',
});
const text = await response.text();
if (text === '') {
throw new errors_1.PtzNotSupportedError();
}
const data = parseCameraPtzResponse(text);
const res = {};
Object.keys(data)
.map(Number)
.forEach((camera) => {
if (data[camera] !== undefined) {
res[camera - 1] = data[camera]?.map(({ data: itemData, ...d }) => d);
}
});
return res;
}
goToPreset(channel, presetName, proxy = null) {
return this.getUrlEncoded(proxy, '/axis-cgi/com/ptz.cgi', {
camera: channel.toString(),
gotoserverpresetname: presetName,
});
}
async getPtzPosition(camera, proxy = null) {
const res = await this.getUrlEncoded(proxy, '/axis-cgi/com/ptz.cgi', {
query: 'position',
camera: camera.toString(),
});
const params = parseParameters(await res.text());
return {
pan: Number(params.pan),
tilt: Number(params.tilt),
zoom: Number(params.zoom),
};
}
async getInputState(port, proxy = null) {
const response = await (await this.getUrlEncoded(proxy, '/axis-cgi/io/port.cgi', { checkactive: port.toString() })).text();
return response.split('=')[1]?.indexOf('active') === 0;
}
async setOutputState(port, active, proxy = null) {
return this.getUrlEncoded(proxy, '/axis-cgi/io/port.cgi', { action: active ? `${port}:/` : `${port}:\\` });
}
async getApplicationList(proxy = null) {
const res = await this.client.get(proxy, '/axis-cgi/applications/list.cgi');
const xml = await res.text();
const parser = new fast_xml_parser_1.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
allowBooleanAttributes: true,
});
const result = parser.parse(xml);
return result.reply.application.map((app) => {
return {
...app,
appId: VapixAPI_1.APP_IDS.find((id) => id.toLowerCase() === app.Name.toLowerCase()) ?? null,
};
});
}
async startApplication(applicationID, proxy = null) {
const res = await this.client.get(proxy, '/axis-cgi/applications/control.cgi', {
package: applicationID.toLowerCase(),
action: 'start',
});
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, proxy = null) {
const res = await this.client.get(proxy, '/axis-cgi/applications/control.cgi', {
package: applicationID.toLowerCase(),
action: 'restart',
});
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, proxy = null) {
const res = await this.client.get(proxy, '/axis-cgi/applications/control.cgi', {
package: applicationID.toLowerCase(),
action: 'stop',
});
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) {
const formData = new FormData();
formData.append('packfil', data, fileName);
const res = await this.client.post(null, '/axis-cgi/applications/upload.cgi', formData, {}, {
contentType: 'application/octet-stream',
});
if (!res.ok) {
throw new Error(await (0, utils_1.responseStringify)(res));
}
const text = await res.text();
if (text.length > 5) {
throw new Error('installing error: ' + text);
}
}
}
exports.VapixAPI = VapixAPI;
const 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;
};
const 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] = parsePtz(json[key].presets);
}
});
return parsed;
};
const 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;
};