camstreamerlib
Version:
Helper library for CamStreamer ACAP applications.
365 lines (364 loc) • 15.8 kB
JavaScript
import { z } from 'zod';
import { AddNewClipError, JsonParseError, ParameterNotFoundError, ErrorWithResponse } from './errors/errors';
import { isClip, isNullish } from './internal/utils';
import { storageInfoListSchema, outputInfoSchema, audioPushInfoSchema, clipListSchema, playlistQueueSchema, streamSaveLoadSchema, clipSaveLoadSchema, playlistSaveLoadSchema, trackerSaveLoadSchema, secondaryAudioSettingsSchema, globalAudioSettingsSchema, } from './types/CamSwitcherAPI';
import { networkCameraListSchema, } from './types/common';
import { VapixAPI } from './VapixAPI';
import { isFirmwareVersionAtLeast } from './internal/versionCompare';
import { FIRMWARE_WITH_BITRATE_MODES_SUPPORT } from './internal/constants';
import { ProxyClient } from './internal/ProxyClient';
const BASE_PATH = '/local/camswitcher/api';
export class CamSwitcherAPI {
client;
CustomFormData;
vapixAgent;
constructor(client, CustomFormData = FormData) {
this.client = client;
this.CustomFormData = CustomFormData;
this.vapixAgent = new VapixAPI(client);
}
static getProxyPath = () => `${BASE_PATH}/proxy.cgi`;
static getWsEventsPath = () => `/local/camswitcher/events`;
static getClipPreviewPath = (clipId, storage) => `${BASE_PATH}/clip_preview.cgi?clip_name=${clipId}&storage=${storage}`;
getClient(proxyParams) {
return proxyParams ? new ProxyClient(this.client, proxyParams) : this.client;
}
async checkCameraTime(options) {
const res = await this._getJson(`${BASE_PATH}/camera_time.cgi`, undefined, options);
return z.boolean().parse(res.data);
}
async getNetworkCameraList(options) {
const res = await this._getJson(`${BASE_PATH}/network_camera_list.cgi`, undefined, options);
return networkCameraListSchema.parse(res.data);
}
async generateSilence(sampleRate, channels, options) {
const agent = this.getClient(options?.proxyParams);
await agent.get({
path: `${BASE_PATH}/generate_silence.cgi`,
parameters: {
sample_rate: sampleRate.toString(),
channels,
},
timeout: options?.timeout,
});
}
async getMaxFps(source, options) {
const res = await this._getJson(`${BASE_PATH}/get_max_framerate.cgi`, {
video_source: source,
}, options);
return z.number().parse(res.data);
}
async getStorageInfo(options) {
const res = await this._getJson(`${BASE_PATH}/get_storage.cgi`, undefined, options);
return storageInfoListSchema.parse(res.data);
}
async wsAuthorization(options) {
const res = await this._getJson(`${BASE_PATH}/ws_authorization.cgi`, undefined, options);
return z.string().parse(res.data);
}
async getOutputInfo(options) {
const res = await this._getJson(`${BASE_PATH}/output_info.cgi`, undefined, options);
return outputInfoSchema.parse(res.data);
}
async getAudioPushInfo(options) {
const res = await this._getJson(`${BASE_PATH}/audio_push_info.cgi`, undefined, options);
return audioPushInfoSchema.parse(res.data);
}
async getStreamSaveList(options) {
const res = await this._getJson(`${BASE_PATH}/streams.cgi`, { action: 'get' }, options);
return streamSaveLoadSchema.parse(res.data);
}
async getClipSaveList(options) {
const res = await this._getJson(`${BASE_PATH}/clips.cgi`, { action: 'get' }, options);
return clipSaveLoadSchema.parse(res.data);
}
async getPlaylistSaveList(options) {
const res = await this._getJson(`${BASE_PATH}/playlists.cgi`, { action: 'get' }, options);
return playlistSaveLoadSchema.parse(res.data);
}
async getTrackerSaveList(options) {
const res = await this._getJson(`${BASE_PATH}/trackers.cgi`, { action: 'get' }, options);
return trackerSaveLoadSchema.parse(res.data);
}
async setStreamSaveList(data, options) {
await this._post(`${BASE_PATH}/streams.cgi`, JSON.stringify(data), { action: 'set' }, options);
}
async setClipSaveList(data, options) {
await this._post(`${BASE_PATH}/clips.cgi`, JSON.stringify(data), { action: 'set' }, options);
}
async setPlaylistSaveList(data, options) {
await this._post(`${BASE_PATH}/playlists.cgi`, JSON.stringify(data), { action: 'set' }, options);
}
async setTrackerSaveList(data, options) {
await this._post(`${BASE_PATH}/trackers.cgi`, JSON.stringify(data), { action: 'set' }, options);
}
async playlistSwitch(playlistName, options) {
await this._getJson(`${BASE_PATH}/playlist_switch.cgi`, { playlist_name: playlistName }, options);
}
async playlistQueuePush(playlistName, options) {
await this._getJson(`${BASE_PATH}/playlist_queue_push.cgi`, { playlist_name: playlistName }, options);
}
async playlistQueueClear(options) {
await this._getJson(`${BASE_PATH}/playlist_queue_clear.cgi`, undefined, options);
}
async playlistQueueList(options) {
const res = await this._getJson(`${BASE_PATH}/playlist_queue_list.cgi`, undefined, options);
return playlistQueueSchema.parse(res.data).playlistQueueList;
}
async playlistQueuePlayNext(options) {
await this._getJson(`${BASE_PATH}/playlist_queue_play_next.cgi`, undefined, options);
}
async addNewClip(file, clipType, storage, clipId, fileName, options) {
const path = `${BASE_PATH}/clip_upload.cgi`;
const formData = new this.CustomFormData();
formData.append('clip_name', clipId);
formData.append('clip_type', clipType);
formData.append('file', file, fileName);
const agent = this.getClient(options?.proxyParams);
const res = await agent.post({
path,
data: formData,
parameters: {
storage: storage,
},
timeout: options?.timeout,
});
const output = (await res.json());
if (output.status !== 200) {
throw new AddNewClipError(output.message);
}
}
async removeClip(clipId, storage, options) {
await this._getJson(`${BASE_PATH}/clip_remove.cgi`, { clip_name: clipId, storage }, options);
}
async getClipList(options) {
const res = await this._getJson(`${BASE_PATH}/clip_list.cgi`, undefined, options);
return clipListSchema.parse(res.data).clip_list;
}
setCamSwitchOptions(data, cameraFWVersion, options) {
const bitrateVapixParams = parseBitrateOptionsToBitrateVapixParams(cameraFWVersion, data.bitrateMode, data);
const saveData = {
video: {
resolution: data.resolution,
h264Profile: data.h264Profile,
fps: data.fps,
compression: data.compression,
govLength: data.govLength,
videoClipQuality: data.maximumBitRate,
bitrateVapixParams: bitrateVapixParams,
},
audio: {
sampleRate: data.audioSampleRate,
channelCount: data.audioChannelCount,
},
keyboard: data.keyboard,
};
return this.setParamFromCameraJSON(CSW_PARAM_NAMES.SETTINGS, saveData, options);
}
setGlobalAudioSettings(settings, options) {
let acceptedType = 'NONE';
if (settings.type === 'source' && settings.source) {
if (isClip(settings.source)) {
acceptedType = 'CLIP';
}
else {
acceptedType = 'STREAM';
}
}
const data = {
type: acceptedType,
stream_name: settings.source,
clip_name: settings.source,
storage: settings.storage,
};
return this.setParamFromCameraJSON(CSW_PARAM_NAMES.MASTER_AUDIO, data, options);
}
setSecondaryAudioSettings(settings, options) {
const data = {
type: settings.type,
stream_name: settings.streamName ?? '',
clip_name: settings.clipName ?? '',
storage: settings.storage,
secondary_audio_level: settings.secondaryAudioLevel,
master_audio_level: settings.masterAudioLevel,
};
return this.setParamFromCameraJSON(CSW_PARAM_NAMES.SECONDARY_AUDIO, data, options);
}
setDefaultPlaylist(playlistId, options) {
const value = JSON.stringify({ default_playlist_id: playlistId });
return this.vapixAgent.setParameter({
[CSW_PARAM_NAMES.DEFAULT_PLAYLIST]: value,
}, options);
}
setPermanentRtspUrlToken(token, options) {
return this.vapixAgent.setParameter({ [CSW_PARAM_NAMES.RTSP_TOKEN]: token }, options);
}
async getCamSwitchOptions(options) {
const saveData = await this.getParamFromCameraAndJSONParse(CSW_PARAM_NAMES.SETTINGS, options);
if (isNullish(saveData.video)) {
return saveData;
}
if (!isNullish(saveData.video?.bitrateVapixParams)) {
const bitrateOptions = parseVapixParamsToBitrateOptions(saveData.video.bitrateVapixParams);
saveData.video.bitrateMode = bitrateOptions.bitrateMode;
saveData.video.maximumBitRate = bitrateOptions.maximumBitRate;
saveData.video.retentionTime = bitrateOptions.retentionTime;
saveData.video.bitRateLimit = bitrateOptions.bitRateLimit;
}
if (!isNullish(saveData.video?.bitrateLimit)) {
saveData.video.maximumBitRate = saveData.video.bitrateLimit;
saveData.video.bitrateMode = 'MBR';
}
if (!isNullish(saveData.video?.videoClipQuality)) {
saveData.video.maximumBitRate = saveData.video.videoClipQuality;
}
return {
...saveData.video,
audioSampleRate: saveData.audio.sampleRate,
audioChannelCount: saveData.audio.channelCount,
keyboard: saveData.keyboard,
};
}
async getGlobalAudioSettings(options) {
const settings = {
type: 'fromSource',
source: 'fromSource',
};
const res = await this.getParamFromCameraAndJSONParse(CSW_PARAM_NAMES.MASTER_AUDIO, options);
if (res.type === 'STREAM') {
settings.type = 'source';
settings.source = res.stream_name;
}
else if (res.type === 'CLIP') {
settings.type = 'source';
settings.source = res.clip_name;
settings.storage = res.storage;
}
return globalAudioSettingsSchema.parse(settings);
}
async getSecondaryAudioSettings(options) {
const res = await this.getParamFromCameraAndJSONParse(CSW_PARAM_NAMES.SECONDARY_AUDIO, options);
const settings = {
type: res.type ?? 'NONE',
streamName: res.stream_name,
clipName: res.clip_name,
storage: res.storage ?? 'SD_DISK',
secondaryAudioLevel: res.secondary_audio_level ?? 1,
masterAudioLevel: res.master_audio_level ?? 1,
};
return secondaryAudioSettingsSchema.parse(settings);
}
async getPermanentRtspUrlToken(options) {
const paramName = CSW_PARAM_NAMES.RTSP_TOKEN;
const res = await this.vapixAgent.getParameter([paramName], options);
return z.string().parse(res[paramName] ?? '');
}
async _getJson(path, parameters, options) {
const agent = this.getClient(options?.proxyParams);
const res = await agent.get({ path, parameters, timeout: options?.timeout });
if (res.ok) {
return await res.json();
}
else {
throw new ErrorWithResponse(res);
}
}
async _post(path, data, parameters, options, headers) {
const agent = this.getClient(options?.proxyParams);
const res = await agent.post({ path, data, parameters, timeout: options?.timeout, headers });
if (res.ok) {
return await res.json();
}
else {
throw new ErrorWithResponse(res);
}
}
setParamFromCameraJSON(paramName, data, options) {
const params = {};
params[paramName] = JSON.stringify(data);
return this.vapixAgent.setParameter(params, options);
}
async getParamFromCameraAndJSONParse(paramName, options) {
const data = await this.vapixAgent.getParameter([paramName], options);
if (data[paramName] !== undefined) {
try {
if (data[paramName] === '') {
return {};
}
else {
return JSON.parse(data[paramName] + '');
}
}
catch {
throw new JsonParseError(paramName, data[paramName]);
}
}
throw new ParameterNotFoundError(paramName);
}
}
const CSW_PARAM_NAMES = {
SETTINGS: 'Camswitcher.Settings',
MASTER_AUDIO: 'Camswitcher.MasterAudio',
SECONDARY_AUDIO: 'Camswitcher.SecondaryAudio',
RTSP_TOKEN: 'Camswitcher.RTSPAccessToken',
DEFAULT_PLAYLIST: 'Camswitcher.DefaultPlaylist',
};
const parseBitrateOptionsToBitrateVapixParams = (firmWareVersion, bitrateMode, cameraOptions) => {
if (!isFirmwareVersionAtLeast(firmWareVersion, FIRMWARE_WITH_BITRATE_MODES_SUPPORT)) {
return `videomaxbitrate=${cameraOptions.maximumBitRate}`;
}
if (bitrateMode === undefined) {
return '';
}
const data = {
VBR: 'videobitratemode=vbr',
MBR: `videobitratemode=mbr&videomaxbitrate=${cameraOptions.maximumBitRate}`,
ABR: `videobitratemode=abr&videoabrtargetbitrate=${cameraOptions.maximumBitRate}&videoabrretentiontime=${cameraOptions.retentionTime}&videoabrmaxbitrate=${cameraOptions.bitRateLimit}`,
};
return data[bitrateMode];
};
const parseVapixParamsToBitrateOptions = (bitrateVapixParams) => {
const params = {};
const searchParams = new URLSearchParams(bitrateVapixParams);
searchParams.forEach((value, key) => {
params[key] = value;
});
const bitrateMode = params['videobitratemode'] !== undefined ? params['videobitratemode'].toUpperCase() : undefined;
const hasLowerFw = bitrateMode === undefined && params['videomaxbitrate'] !== undefined;
if (hasLowerFw) {
const maximumBitRate = parseInt(params['videomaxbitrate'] ?? '0', 10);
return {
bitrateMode: 'MBR',
maximumBitRate: maximumBitRate,
retentionTime: 1,
bitRateLimit: Math.floor(maximumBitRate * 1.1),
};
}
if (bitrateMode === 'ABR') {
const maximumBitRate = parseInt(params['videoabrtargetbitrate'] ?? '0', 10);
const retentionTime = parseInt(params['videoabrretentiontime'] ?? '0', 10);
const bitRateLimit = parseInt(params['videoabrmaxbitrate'] ?? '0', 10);
return {
bitrateMode,
maximumBitRate,
retentionTime,
bitRateLimit,
};
}
else if (bitrateMode === 'MBR') {
const maximumBitRate = params['videomaxbitrate'] !== undefined ? parseInt(params['videomaxbitrate'], 10) : null;
const oldMaximumBitrateParamValue = parseInt(params['videombrmaxbitrate'] ?? '0', 10);
return {
bitrateMode: bitrateMode,
maximumBitRate: maximumBitRate ?? oldMaximumBitrateParamValue,
retentionTime: 1,
bitRateLimit: Math.floor(maximumBitRate ?? oldMaximumBitrateParamValue * 1.1),
};
}
return {
bitrateMode: bitrateMode,
retentionTime: 1,
maximumBitRate: 0,
bitRateLimit: 0,
};
};