Skip to content

Commit

Permalink
cameras: update http utils again
Browse files Browse the repository at this point in the history
  • Loading branch information
koush committed Jan 10, 2024
1 parent 56bda46 commit bab3bef
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 99 deletions.
47 changes: 29 additions & 18 deletions plugins/amcrest/src/amcrest-api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { RequestOptions } from 'http';
import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { Readable } from 'stream';
import { BufferParser, FetchParser, StreamParser, TextParser } from '../../../server/src/http-fetch-helpers';
import { amcrestHttpsAgent, getDeviceInfo } from './probe';
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
import { getDeviceInfo } from './probe';

export enum AmcrestEvent {
MotionStart = "Code=VideoMotion;action=Start",
Expand Down Expand Up @@ -32,23 +31,33 @@ export class AmcrestCameraClient {
};
}

async request<T>(url: string, parser: FetchParser<T>, init?: RequestOptions, body?: Readable) {
const response = await authHttpFetch({
url,
httpsAgent: amcrestHttpsAgent,
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
const options: AuthFetchOptions<T> = {
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
rejectUnauthorized: false,
credential: this.credential,
body,
}, init, parser);
};

const response = await authHttpFetch(options);
return response;
}

async reboot() {
const response = await this.request(`http://${this.ip}/cgi-bin/magicBox.cgi?action=reboot`, TextParser);
const response = await this.request({
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=reboot`,
responseType: 'text',
});
return response.body;
}

async checkTwoWayAudio() {
const response = await this.request(`http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`, TextParser);
const response = await this.request({
url: `http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`,
responseType: 'text',
});
return response.body.includes('result=1');
}

Expand All @@ -64,7 +73,8 @@ export class AmcrestCameraClient {
}

async jpegSnapshot(): Promise<Buffer> {
const response = await this.request(`http://${this.ip}/cgi-bin/snapshot.cgi`, BufferParser, {
const response = await this.request({
url: `http://${this.ip}/cgi-bin/snapshot.cgi`,
timeout: 60000,
});

Expand All @@ -75,11 +85,10 @@ export class AmcrestCameraClient {
const url = `http://${this.ip}/cgi-bin/eventManager.cgi?action=attach&codes=[All]`;
console.log('preparing event listener', url);

const response = await authHttpFetch({
credential: this.credential,
httpsAgent: amcrestHttpsAgent,
const response = await this.request({
url,
}, undefined, StreamParser);
responseType: 'readable',
});
const stream = response.body;
stream.socket.setKeepAlive(true);

Expand Down Expand Up @@ -111,9 +120,11 @@ export class AmcrestCameraClient {
async enableContinousRecording(channel: number) {
for (let i = 0; i < 7; i++) {
const url = `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&Record[${channel - 1}].TimeSection[${i}][0]=1 00:00:00-23:59:59`;
const response = await this.request(url, TextParser, {
const response = await this.request({
url,
method: 'POST',
});
responseType: 'text',
},);
this.console.log(response.body);
}
}
Expand Down
30 changes: 23 additions & 7 deletions plugins/amcrest/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { readLength } from "@scrypted/common/src/read-stream";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, PictureOptions, Reboot, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { PassThrough, Readable, Stream } from "stream";
import { StreamParser, TextParser } from '../../../server/src/http-fetch-helpers';
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api";
Expand Down Expand Up @@ -94,7 +93,10 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,

for (const element of deviceParameters) {
try {
const response = await this.getClient().request(`http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`, TextParser);
const response = await this.getClient().request({
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`,
responseType: 'text',
});
const result = String(response.body).replace(element.replace, "").trim();
deviceInfo[element.parameter] = result;
}
Expand Down Expand Up @@ -143,7 +145,10 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (![...params.keys()].length)
return;

const response = await this.getClient().request(`http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`, TextParser);
const response = await this.getClient().request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
responseType: 'text',
});
this.console.log('reconfigure result', response.body);
}

Expand Down Expand Up @@ -337,7 +342,10 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
this.videoStreamOptions = (async () => {
let mas: string;
try {
const response = await client.request(`http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`, TextParser)
const response = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
responseType: 'text',
})
mas = response.body.split('=')[1].trim();
this.storage.setItem('maxExtraStreams', mas.toString());
}
Expand All @@ -351,9 +359,15 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));

try {
const capResponse = await client.request(`http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`, TextParser);
const capResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
responseType: 'text',
});
this.console.log(capResponse.body);
const encodeResponse = await client.request(`http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`, TextParser);
const encodeResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
responseType: 'text',
});
this.console.log(encodeResponse.body);

for (let i = 0; i < vsos.length; i++) {
Expand Down Expand Up @@ -515,12 +529,14 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
// seems the dahua doorbells preferred 1024 chunks. should investigate adts
// parsing and sending multipart chunks instead.
const passthrough = new PassThrough();
this.getClient().request(url, StreamParser, {
this.getClient().request({
url,
method: 'POST',
headers: {
'Content-Type': 'Audio/AAC',
'Content-Length': '9999999'
},
responseType: 'readable',
}, passthrough);

try {
Expand Down
11 changes: 3 additions & 8 deletions plugins/amcrest/src/probe.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import https from 'https';
import { TextParser } from '../../../server/src/http-fetch-helpers';

export const amcrestHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});

// appAutoStart=true
// deviceType=IP4M-1041B
Expand All @@ -18,9 +12,10 @@ export const amcrestHttpsAgent = new https.Agent({
export async function getDeviceInfo(credential: AuthFetchCredentialState, address: string) {
const response = await authHttpFetch({
credential,
httpsAgent: amcrestHttpsAgent,
url: `http://${address}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
}, undefined, TextParser);
rejectUnauthorized: false,
responseType: 'text',
});
const lines = response.body.split('\n');
const vals: {
[key: string]: string,
Expand Down
56 changes: 30 additions & 26 deletions plugins/hikvision/src/hikvision-camera-api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { IncomingMessage } from 'http';
import { getDeviceInfo, hikvisionHttpsAgent } from './probe';
import { BufferParser, FetchParser, StreamParser, TextParser } from '../../../server/src/http-fetch-helpers';
import { RequestOptions } from 'http';
import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { IncomingMessage, RequestOptions } from 'http';
import { Readable } from 'stream';
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
import { getDeviceInfo } from './probe';

export function getChannel(channel: string) {
return channel || '101';
Expand Down Expand Up @@ -42,23 +41,26 @@ export class HikvisionCameraAPI {
};
}

async request<T>(url: string, parser: FetchParser<T>, init?: RequestOptions, body?: Readable) {
const response = await authHttpFetch({
url,
httpsAgent: hikvisionHttpsAgent,
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
const options: AuthFetchOptions<T> = {
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
rejectUnauthorized: false,
credential: this.credential,
body,
}, init, parser);
};

const response = await authHttpFetch(options);
return response;
}

async reboot() {
const response = await authHttpFetch({
const response = await this.request({
url: `http://${this.ip}/ISAPI/System/reboot`,
credential: this.credential,
}, {
method: "PUT",
}, TextParser);
responseType: 'text',
});

return response.body;
}
Expand All @@ -68,7 +70,10 @@ export class HikvisionCameraAPI {
}

async checkTwoWayAudio() {
const response = await this.request(`http://${this.ip}/ISAPI/System/TwoWayAudio/channels`, TextParser);
const response = await this.request({
url: `http://${this.ip}/ISAPI/System/TwoWayAudio/channels`,
responseType: 'text',
});

return response.body.includes('Speaker');
}
Expand Down Expand Up @@ -100,7 +105,10 @@ export class HikvisionCameraAPI {
}
}

const response = await this.request(`http://${this.ip}/ISAPI/Streaming/channels/${getChannel(channel)}/capabilities`, TextParser);
const response = await this.request({
url: `http://${this.ip}/ISAPI/Streaming/channels/${getChannel(channel)}/capabilities`,
responseType: 'text',
});

// this is bad:
// <videoCodecType opt="H.264,H.265">H.265</videoCodecType>
Expand All @@ -116,27 +124,23 @@ export class HikvisionCameraAPI {
async jpegSnapshot(channel: string): Promise<Buffer> {
const url = `http://${this.ip}/ISAPI/Streaming/channels/${getChannel(channel)}/picture?snapShotImageType=JPEG`

const response = await authHttpFetch({
credential: this.credential,
httpsAgent: hikvisionHttpsAgent,
const response = await this.request({
url: url,
}, {
timeout: 60000,
}, BufferParser);
});

return Buffer.from(response.body);
return response.body;
}

async listenEvents() {
// support multiple cameras listening to a single single stream
if (!this.listenerPromise) {
const url = `http://${this.ip}/ISAPI/Event/notification/alertStream`;

this.listenerPromise = authHttpFetch({
credential: this.credential,
httpsAgent: hikvisionHttpsAgent,
this.listenerPromise = this.request({
url,
}, undefined, StreamParser).then(response => {
responseType: 'readable',
}).then(response => {
const stream = response.body;
stream.socket.setKeepAlive(true);

Expand Down
25 changes: 17 additions & 8 deletions plugins/hikvision/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api";
import { hikvisionHttpsAgent } from './probe';
import { StreamParser, TextParser } from "../../../server/src/http-fetch-helpers";

const { mediaManager } = sdk;

Expand Down Expand Up @@ -208,7 +206,10 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo
try {
let xml: string;
try {
const response = await client.request(`http://${this.getHttpAddress()}/ISAPI/Streaming/channels`, TextParser);
const response = await client.request({
url: `http://${this.getHttpAddress()}/ISAPI/Streaming/channels`,
responseType: 'text',
});
xml = response.body;
this.storage.setItem('channels', xml);
}
Expand Down Expand Up @@ -379,7 +380,10 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo

try {
const parameters = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels`;
const { body } = await this.getClient().request(parameters, TextParser);
const { body } = await this.getClient().request({
url: parameters,
responseType: 'text',
});

const parsedXml = await xml2js.parseStringPromise(body);
for (const twoWayChannel of parsedXml.TwoWayAudioChannelList.TwoWayAudioChannel) {
Expand Down Expand Up @@ -417,15 +421,19 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo

const passthrough = new PassThrough();
const open = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/open`;
const { body } = await this.getClient().request(open, TextParser, {
method: 'PUT'
const { body } = await this.getClient().request({
url: open,
responseType: 'text',
method: 'PUT',
});
this.console.log('two way audio opened', body);

const url = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/audioData`;
this.console.log('posting audio data to', url);

const put = this.getClient().request(url, StreamParser, {
const put = this.getClient().request({
url,
responseType: 'readable',
headers: {
'Content-Type': 'application/octet-stream',
// 'Connection': 'close',
Expand Down Expand Up @@ -472,7 +480,8 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo
}

const client = this.getClient();
await client.request(`http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${this.getRtspChannel() || '1'}/close`, TextParser, {
await client.request({
url: `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${this.getRtspChannel() || '1'}/close`,
method: 'PUT',
});
}
Expand Down
11 changes: 4 additions & 7 deletions plugins/hikvision/src/probe.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import https from 'https';
import { TextParser, checkStatus, fetchStatusCodeOk } from '../../../server/src/http-fetch-helpers';
import { checkStatus } from '../../../server/src/http-fetch-helpers';

export const hikvisionHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});

export async function getDeviceInfo(credential: AuthFetchCredentialState, address: string) {
const response = await authHttpFetch({
credential,
httpsAgent: hikvisionHttpsAgent,
url: `http://${address}/ISAPI/System/deviceInfo`,
ignoreStatusCode: true,
}, undefined, TextParser);
responseType: 'text',
rejectUnauthorized: false,
});

if (response.body.includes('notActivated'))
throw new Error(`Camera must be first be activated at http://${address}.`);
Expand Down
Loading

0 comments on commit bab3bef

Please sign in to comment.