@apocaliss92/scrypted-reolink-hub
Version:
Reolink (HUBs) Plugin for Scrypted
1,126 lines (949 loc) • 33.9 kB
text/typescript
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { PassThrough, Readable } from 'stream';
import { sleep } from "@scrypted/common/src/sleep";
import { PanTiltZoomCommand, VideoClipOptions } from "@scrypted/sdk";
import { VideoSearchResult, VideoSearchType } from '../../scrypted-reolink-videoclips/src/client';
import { getLoginParameters } from '../../scrypted/plugins/reolink/src/probe';
import { HttpFetchOptions } from '../../scrypted/server/src/fetch/http-fetch';
const wakingCoomands = [
"GetWhiteLed",
"GetZoomFocus",
"GetAudioCfg",
"GetPtzGuard",
"GetAutoReply",
"GetPtzTraceSection",
"GetAiCfg",
"GetAiAlarm",
"GetPtzCurPos",
"GetAudioAlarm",
"GetDingDongList",
"GetDingDongCfg",
"DingDongOpt",
"GetPerformance",
"296",
"483",
];
export interface DeviceInputData {
hasBattery: boolean,
hasPirEvents: boolean,
hasFloodlight: boolean,
hasPtz: boolean,
sleeping: boolean,
};
export interface EventsResponse { motion: boolean, objects: string[], entries: any[] };
export interface DeviceInfoResponse {
channelStatus?: string,
ai?: any,
channelInfo?: any,
entries: any[]
};
export interface BatteryInfoResponse { batteryLevel: number, sleeping: boolean, entries: any[] };
export interface DeviceStatusResponse {
floodlightEnabled?: boolean,
pirEnabled?: boolean,
ptzPresets?: any[],
osd?: any[],
entries: any[]
};
export interface Enc {
audio: number;
channel: number;
mainStream: Stream;
subStream: Stream;
}
export interface Stream {
bitRate: number;
frameRate: number;
gop: number;
height: number;
profile: string;
size: string;
vType: string;
width: number;
}
export interface PurpleOsdChannel {
enable: number;
name: string;
pos: string;
}
export interface PurpleOsdTime {
enable: number;
pos: string;
}
export interface InitialOsd {
bgcolor: number;
channel: number;
osdChannel: PurpleOsdChannel;
osdTime: PurpleOsdTime;
watermark: number;
}
export interface Initial {
Osd: InitialOsd;
}
export interface Osd {
cmd: string;
code: number;
initial: Initial;
range: Range;
value: Initial;
}
export interface AIDetectionState {
alarm_state: number;
support: number;
}
type AiKey = 'dog_cat' | 'face' | 'other' | 'package' | 'people';
export type AIState = Partial<Record<AiKey, AIDetectionState>> & {
channel: number;
};
export type SirenResponse = {
rspCode: number;
}
export interface PtzPreset {
id: number;
name: string;
}
export class ReolinkHubClient {
credential: AuthFetchCredentialState;
parameters: Record<string, string>;
tokenLease: number;
loggingIn = false;
loggedIn = false;
rebooting = false;
conmnectionTime = Date.now();
maxSessionsCount = 0;
loginFirstCount = 0;
constructor(public host: string, public username: string, public password: string, public console: Console) {
this.credential = {
username,
password,
};
}
private async request(options: HttpFetchOptions<Readable>, body?: Readable) {
const response = await authHttpFetch({
...options,
rejectUnauthorized: false,
credential: this.credential,
body,
});
return response;
}
private createReadable = (data: any) => {
const pt = new PassThrough();
pt.write(Buffer.from(JSON.stringify(data)));
pt.end();
return pt;
}
async login() {
if (this.parameters && this.tokenLease && this.tokenLease > Date.now()) {
return;
}
if (this.loggingIn) {
return;
}
this.loggingIn = true;
if (!this.tokenLease || !this.parameters) {
this.console.log(`Creating authentication session`);
} else {
this.console.log(`Token expired at ${new Date(this.tokenLease).toISOString()}, renewing`);
}
const { parameters, leaseTimeSeconds } = await getLoginParameters(this.host, this.username, this.password, true);
this.parameters = parameters;
const now = Date.now();
this.tokenLease = now + 1000 * leaseTimeSeconds;
this.loggingIn = false;
this.loggedIn = true;
this.conmnectionTime = now;
}
async checkErrors() {
if (this.rebooting) {
return;
}
if (Date.now() - this.conmnectionTime > 1000 * 60 * 60 || this.loginFirstCount > 5) {
this.console.log('Reconnecting')
await this.reconnect();
} else if (this.maxSessionsCount > 5) {
await this.reboot();
}
}
async requestWithLogin(options: HttpFetchOptions<Readable>, body?: Readable) {
await this.login();
if (!this.parameters) {
return;
}
if (this.rebooting) {
return;
}
const url = options.url as URL;
const params = url.searchParams;
for (const [k, v] of Object.entries(this.parameters)) {
params.set(k, v);
}
const res = await this.request(options, body);
const errors = res?.body?.filter(elem => elem.error).map(elem => elem.error);
if (errors.length) {
for (const error of errors) {
const code = error.rspCode;
if ([-6].includes(code)) {
this.loginFirstCount++;
} else if ([-5].includes(code)) {
this.maxSessionsCount++;
} else {
this.maxSessionsCount = 0;
this.loginFirstCount = 0;
}
}
}
return res;
}
async reboot() {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'Reboot');
this.rebooting = true;
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
// Wait 1 minute, supposed to be ready
setTimeout(() => {
this.rebooting = false;
this.maxSessionsCount = 0;
this.loginFirstCount = 0;
}, 1000 * 60);
return {
value: response?.body?.[0]?.value?.rspCode,
data: response?.body,
};
}
async logout() {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [
{
cmd: "Logout",
},
];
await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
this.tokenLease = undefined;
this.parameters = {};
}
async reconnect() {
await this.logout();
await this.login();
}
async getOsd(channel: number): Promise<Osd> {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [
{
cmd: "GetOsd",
action: 1,
param: { channel }
},
];
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
const error = response?.body?.find(elem => elem.error)?.error;
if (error) {
this.console.error('error during call to getOsd', error);
}
return response?.body?.[0] as Osd;
}
async setOsd(channel: number, osd: Osd) {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [
{
cmd: "SetOsd",
param: {
Osd: {
channel,
osdChannel: osd.value.Osd.osdChannel,
osdTime: osd.value.Osd.osdTime,
}
}
}
];
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
const error = response?.body?.find(elem => elem.error)?.error;
if (error) {
this.console.error('error during call to getOsd', error);
}
}
async getAiState(channel: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetAiState');
params.set('channel', String(channel));
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
return {
value: (response?.body?.[0]?.value || response?.body?.value) as AIState,
data: response?.body,
};
}
printErrors(response: any, action: string, body: any[]) {
const errors = response?.body?.filter(elem => elem.error).map(elem => ({ ...elem.error, cmd: elem.cmd }));
if (errors.length) {
this.console.error(`error during call to ${action}`, JSON.stringify({ errors, body }));
}
}
async getHubInfo() {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [
{
cmd: "GetAbility",
action: 0,
param: { User: { userName: this.username } }
},
{
cmd: "GetDevInfo",
action: 0,
param: {}
}
];
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
this.printErrors(response, 'getHubInfo', body);
const abilities = response.body.find(item => item.cmd === 'GetAbility')?.value;
const hubData = response.body.find(item => item.cmd === 'GetDevInfo')?.value;
return {
abilities,
hubData,
response: response.body
};
}
async jpegSnapshot(channel: number, timeout = 10000) {
const url = new URL(`http://${this.host}/cgi-bin/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'Snap');
params.set('channel', String(channel));
params.set('rs', Date.now().toString());
const response = await this.requestWithLogin({
url,
timeout,
});
return response?.body;
}
async getEncoderConfiguration(channel: number): Promise<Enc> {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetEnc');
params.set('channel', String(channel));
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
return response?.body?.[0]?.value?.Enc;
}
private async ptzOp(channel: number, op: string, speed: number, id?: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'PtzCtrl');
const c1 = this.requestWithLogin({
url,
method: 'POST',
responseType: 'text',
}, this.createReadable([
{
cmd: "PtzCtrl",
param: {
channel,
op,
speed,
timeout: 1,
id
}
},
]));
await sleep(500);
const c2 = this.requestWithLogin({
url,
method: 'POST',
}, this.createReadable([
{
cmd: "PtzCtrl",
param: {
channel,
op: "Stop"
}
},
]));
this.console.log(await c1);
this.console.log(await c2);
}
private async presetOp(channel: number, speed: number, id: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'PtzCtrl');
const c1 = this.requestWithLogin({
url,
method: 'POST',
responseType: 'text',
}, this.createReadable([
{
cmd: "PtzCtrl",
param: {
channel,
op: 'ToPos',
speed,
id
}
},
]));
}
async ptz(channel: number, command: PanTiltZoomCommand) {
// reolink doesnt accept signed values to ptz
// in favor of explicit direction.
// so we need to convert the signed values to abs explicit direction.
if (command.preset && !Number.isNaN(Number(command.preset))) {
await this.presetOp(channel, 1, Number(command.preset));
return;
}
let op = '';
if (command.pan < 0)
op += 'Left';
else if (command.pan > 0)
op += 'Right'
if (command.tilt < 0)
op += 'Down';
else if (command.tilt > 0)
op += 'Up';
if (op) {
await this.ptzOp(channel, op, Math.ceil(Math.abs(command?.pan || command?.tilt || 1) * 10));
}
op = undefined;
if (command.zoom < 0)
op = 'ZoomDec';
else if (command.zoom > 0)
op = 'ZoomInc';
if (op) {
await this.ptzOp(channel, op, Math.ceil(Math.abs(command?.zoom || 1) * 10));
}
}
async getSiren(channel: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [{
cmd: 'GetAudioAlarmV20',
action: 0,
param: { channel }
}];
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
}, this.createReadable(body));
const error = response?.body?.[0]?.error;
if (error) {
this.console.error('error during call to getSiren', JSON.stringify(body), error);
}
return {
enabled: response?.body?.[0]?.value?.Audio?.enable === 1
};
}
async setSiren(channel: number, on: boolean, duration?: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'AudioAlarmPlay');
let alarmMode;
if (duration) {
alarmMode = {
alarm_mode: 'times',
times: duration
};
}
else {
alarmMode = {
alarm_mode: 'manul',
manual_switch: on ? 1 : 0
};
}
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
}, this.createReadable([
{
cmd: "AudioAlarmPlay",
action: 0,
param: {
channel,
...alarmMode
}
},
]));
return {
value: (response?.body?.[0]?.value || response?.body?.value) as SirenResponse,
data: response?.body,
};
}
async setWhiteLedState(channel: number, on?: boolean, brightness?: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const settings: any = { channel };
if (on !== undefined) {
settings.state = on ? 1 : 0;
}
if (brightness !== undefined) {
settings.bright = brightness;
}
const body = [{
cmd: 'SetWhiteLed',
param: { WhiteLed: settings }
}];
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
}, this.createReadable(body));
const error = response?.body?.[0]?.error;
if (error) {
this.console.error('error during call to setWhiteLedState', JSON.stringify(body), error);
}
}
async getStatusInfo(channelsMap: Map<number, DeviceInputData>) {
const url = new URL(`http://${this.host}/api.cgi`);
const chanelIndex: Record<number, { osd?: number, floodlight?: number, pir?: number, presets?: number }> = {};
const body: any[] = [];
channelsMap.forEach(({ hasFloodlight, hasPirEvents, hasPtz, sleeping }, channel) => {
chanelIndex[channel] = {};
if (!sleeping) {
body.push(
{
cmd: "GetOsd",
action: 1,
param: { channel }
}
);
chanelIndex[channel].osd = body.length - 1;
if (hasFloodlight) {
body.push(
{
cmd: 'GetWhiteLed',
action: 0,
param: { channel }
},
);
chanelIndex[channel].floodlight = body.length - 1;
}
if (hasPirEvents) {
body.push(
{
cmd: 'GetPirInfo',
action: 0,
param: { channel }
}
);
chanelIndex[channel].pir = body.length - 1;
}
if (hasPtz) {
body.push(
{
cmd: "GetPtzPreset",
action: 1,
param: {
channel
}
}
);
chanelIndex[channel].presets = body.length - 1;
}
}
});
const channelData: Record<number, DeviceStatusResponse> = {};
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
this.printErrors(response, 'getStatusInfo', body);
channelsMap.forEach(({ hasFloodlight, hasPirEvents, hasPtz }, channel) => {
const { floodlight, pir, presets, osd } = chanelIndex[channel];
channelData[channel] = { entries: [] };
if (osd !== undefined) {
const osdEntry = response?.body?.[osd];
channelData[channel].osd = osdEntry;
channelData[channel].entries.push(osdEntry);
}
if (hasFloodlight && floodlight !== undefined) {
const floodlightEntry = response?.body?.[floodlight];
channelData[channel].floodlightEnabled = floodlightEntry?.value?.WhiteLed?.state === 1;
channelData[channel].entries.push(floodlightEntry);
}
if (hasPirEvents && pir !== undefined) {
const pirEntry = response?.body?.[pir];
channelData[channel].pirEnabled = pirEntry?.value?.pirInfo?.enable === 1
channelData[channel].entries.push(pirEntry);
}
if (hasPtz && presets !== undefined) {
const ptzPresetsEntry = response?.body?.[presets];
channelData[channel].ptzPresets = ptzPresetsEntry?.value?.PtzPreset?.filter(preset => preset.enable === 1);
channelData[channel].entries.push(ptzPresetsEntry);
}
});
return {
deviceStatusData: channelData,
response: response.body,
};
}
async getBatteryInfo(channelsMap: Map<number, DeviceInputData>) {
const url = new URL(`http://${this.host}/api.cgi`);
const chanelIndex: Record<number, number> = {};
const body: any[] = [
{
cmd: "GetChannelstatus",
}
];
const channels: number[] = [];
channelsMap.forEach(({ hasBattery }, channel) => {
if (hasBattery) {
channels.push(channel)
}
});
for (const channel of channels) {
body.push(
{
cmd: "GetBatteryInfo",
action: 0,
param: { channel }
},
);
chanelIndex[channel] = body.length - 1;
}
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
this.printErrors(response, 'getBatteryInfo', body);
const channelData: Record<number, BatteryInfoResponse> = {};
const channelStatusData = response?.body?.[0];
for (const channel of channels) {
const batteryInfoEntry = response?.body?.[chanelIndex[channel]]?.value?.Battery;
const channelStatusEntry = channelStatusData?.value?.status?.find(elem => elem.channel === channel);
channelData[channel] = {
entries: [batteryInfoEntry, channelStatusEntry],
batteryLevel: batteryInfoEntry?.batteryPercent,
sleeping: channelStatusEntry?.sleep === 1,
};
}
return {
batteryInfoData: channelData,
response: response.body,
};
}
async getChannels() {
const url = new URL(`http://${this.host}/api.cgi`);
const channelsBody = [{ cmd: 'GetChannelstatus' }];
const channelsResponse = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(channelsBody));
const channels = channelsResponse.body?.[0]?.value?.status
?.filter(elem => !!elem.uid)
?.map(elem => elem.channel)
return {
channels,
channelsResponse
};
}
async getEvents(channelsMap: Map<number, DeviceInputData>) {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [];
const chanelIndex: Record<number, { events?: number, motion?: number, }> = {};
channelsMap.forEach(({ hasPirEvents }, channel) => {
chanelIndex[channel] = {};
if (hasPirEvents) {
body.push({
cmd: 'GetEvents',
action: 0,
param: { channel }
});
chanelIndex[channel].events = body.length - 1;
} else {
body.push({
cmd: 'GetMdState',
action: 0,
param: { channel }
});
chanelIndex[channel].motion = body.length - 1;
body.push({
cmd: 'GetAiState',
action: 0,
param: { channel }
});
chanelIndex[channel].events = body.length - 1;
}
})
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
if (!response) {
return {};
}
const channelData: Record<number, EventsResponse> = {};
const processDetections = (aiResponse: any) => {
const classes: string[] = [];
for (const key of Object.keys(aiResponse ?? {})) {
if (key === 'channel')
continue;
const { alarm_state } = aiResponse[key];
if (alarm_state)
classes.push(key);
}
return classes;
}
channelsMap.forEach(({ hasPirEvents }, channel) => {
const { events, motion } = chanelIndex[channel];
channelData[channel] = { motion: false, objects: [], entries: [] };
if (hasPirEvents) {
const eventsEntry = response?.body?.[events];
const classes = processDetections(eventsEntry?.value?.ai);
channelData[channel].motion = classes.includes('other') || classes.length > 0;
channelData[channel].objects = classes.filter(cl => cl !== 'other');
channelData[channel].entries.push(eventsEntry);
} else {
const eventsEntry = response?.body?.[events];
const motionEntry = response?.body?.[motion];
const classes = processDetections(eventsEntry?.value);
channelData[channel].motion = motionEntry?.value?.state || classes.length > 0;
channelData[channel].objects = classes.filter(cl => cl !== 'other');
channelData[channel].entries.push(eventsEntry, motionEntry);
}
});
this.printErrors(response, 'getEvents', body);
return {
parsed: channelData,
response: response.body,
body: response.body
};
}
async getDevicesInfo() {
const url = new URL(`http://${this.host}/api.cgi`);
const { channels, channelsResponse } = await this.getChannels();
const body: any[] = [];
const responseMap: Record<number, { chnInfo: number, ai: number }> = {};
for (const channel of channels) {
responseMap[channel] = {
ai: body.length,
chnInfo: body.length + 1,
}
body.push(
{ cmd: "GetChnTypeInfo", action: 0, param: { channel } },
{ cmd: "GetAiState", action: 0, param: { channel } },
);
}
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
const ret: Record<number, DeviceInfoResponse> = {};
let currentChannelIndex = 0;
for (const channel of channels) {
const indexMultilpier = currentChannelIndex * 2;
const chnInfoItem = response.body[indexMultilpier];
const aiItem = response.body[indexMultilpier + 1];
const channelStatus = channelsResponse.body?.[0]?.value?.status?.find(item => item?.channel === channel);
ret[channel] = {
entries: [chnInfoItem, aiItem],
};
!chnInfoItem?.error && (ret[channel].channelInfo = chnInfoItem?.value);
!aiItem?.error && (ret[channel].ai = aiItem?.value);
ret[channel].channelStatus = channelStatus;
currentChannelIndex++;
}
this.printErrors(response, 'getDevicesInfo', body);
return {
devicesData: ret,
response: response.body,
channels,
channelsResponse,
requestBody: body,
};
}
async getPirState(channel: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [{
cmd: 'GetPirInfo',
action: 0,
param: { channel }
}];
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
}, this.createReadable(body));
const error = response?.body?.[0]?.error;
if (error) {
this.console.error('error during call to getPirState', JSON.stringify(body), error);
}
return {
enabled: response?.body?.[0]?.value?.pirInfo?.enable === 1,
state: response?.body?.[0]?.value?.pirInfo
};
}
async setPirState(channel: number, on: boolean) {
const url = new URL(`http://${this.host}/api.cgi`);
const currentPir = await this.getPirState(channel);
const newState = on ? 1 : 0;
if (!currentPir || currentPir.state?.enable === newState) {
return;
}
const pirInfo = {
...currentPir,
channel,
enable: newState
}
const body = [{
cmd: 'SetPirInfo',
action: 0,
param: { pirInfo }
}];
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
}, this.createReadable(body));
const error = response?.body?.[0]?.error;
if (error) {
this.console.error('error during call to setPirState', JSON.stringify(body), error);
}
}
async getLocalLink(channel: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [
{
cmd: 'GetLocalLink',
action: 0,
param: {}
},
{
cmd: 'GetWifiSignal',
action: 0,
param: { channel }
},
];
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
}, this.createReadable(body));
const error = response?.body?.[0]?.error;
if (error) {
this.console.error('error during call to getLocalLink', JSON.stringify(body), error);
}
const activeLink = response?.body?.find(entry => entry.cmd === 'GetLocalLink')
?.value?.LocalLink?.activeLink;
const wifiSignal = response?.body?.find(entry => entry.cmd === 'GetWifiSignal')
?.value?.wifiSignal ?? undefined
let isWifi = false;
if (wifiSignal !== undefined) {
isWifi = wifiSignal >= 0 && wifiSignal <= 4;
}
if (!isWifi && activeLink) {
isWifi = activeLink !== 'LAN';
}
return {
activeLink,
wifiSignal,
isWifi
};
}
async getVideoClips(
channel: number,
options?: VideoClipOptions,
streamType: VideoSearchType = 'main',
) {
const url = new URL(`http://${this.host}/api.cgi`);
const startTime = new Date(options.startTime);
let endTime = options.endTime ? new Date(options.endTime) : undefined;
// If the endTime is not the same day as startTime,
// or no endDate is provided, set to the end of the startTime
// Reolink only supports 1 day recordings fetching
if (!endTime || endTime.getDate() > startTime.getDate()) {
endTime = new Date(startTime);
endTime.setHours(23);
endTime.setMinutes(59);
endTime.setSeconds(59);
}
const body = [
{
cmd: "Search",
action: 1,
param: {
Search: {
channel,
streamType,
onlyStatus: 0,
StartTime: {
year: startTime.getFullYear(),
mon: startTime.getMonth() + 1,
day: startTime.getDate(),
hour: startTime.getHours(),
min: startTime.getMinutes(),
sec: startTime.getSeconds()
},
EndTime: {
year: endTime.getFullYear(),
mon: endTime.getMonth() + 1,
day: endTime.getDate(),
hour: endTime.getHours(),
min: endTime.getMinutes(),
sec: endTime.getSeconds()
}
}
}
}
];
try {
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
const error = response?.body?.[0]?.error;
if (error) {
this.console.log('Error fetching videoclips', error, JSON.stringify({ body, url }));
return [];
}
return (response?.body?.[0]?.value?.SearchResult?.File ?? []) as VideoSearchResult[];
} catch (e) {
this.console.log('Error fetching videoclips', e);
return [];
}
}
async getVideoClipUrl(videoclipPath: string, channel: number) {
const fileNameWithExtension = videoclipPath.split('/').pop();
// const fileName = fileNameWithExtension.split('.').shift();
let sanitizedPath = videoclipPath.replaceAll(' ', '%20');
if (!sanitizedPath.startsWith('/')) {
sanitizedPath = `/${sanitizedPath}`;
}
const match = fileNameWithExtension.match(/.*Rec(\w{3})(?:_|_DST)(\d{8})_(\d{6})_.*/);
const date = match[2];
const time = match[3];
const start = `${date}${time}`;
const playbackPath = `cgi-bin/api.cgi?cmd=Playback&channel=${channel}&source=${sanitizedPath}&start=${start}&type=0&seek=0&token=${this.parameters.token}`;
const downloadPath = `cgi-bin/api.cgi?cmd=Download&source=${sanitizedPath}&output=ha_playback_${start}.mp4&start=${start}&token=${this.parameters.token}`;
return {
// playbackPathWithHost: `http://${this.host}/${playbackPath}`,
downloadPathWithHost: `http://${this.host}/${downloadPath}`,
};
}
}