UNPKG

camstreamerlib

Version:

Helper library for CamStreamer ACAP applications.

365 lines (364 loc) 15.8 kB
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, }; };