mirror of
https://github.com/koush/scrypted.git
synced 2026-02-10 17:22:03 +00:00
hikvision: two way audio. onvif works. unclear if hikvision api does.
This commit is contained in:
50
plugins/hikvision/package-lock.json
generated
50
plugins/hikvision/package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.98",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.97",
|
||||
"hasInstallScript": true,
|
||||
"version": "0.0.98",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/highland": "^2.12.14",
|
||||
"@types/lodash": "^4.14.172",
|
||||
@@ -25,9 +25,24 @@
|
||||
"xml2js": "^0.4.23"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.199",
|
||||
"version": "0.0.206",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
@@ -81,6 +96,10 @@
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
@@ -187,9 +206,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -450,6 +469,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
@@ -562,9 +592,9 @@
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
},
|
||||
"get-symbol-from-current-process-h": {
|
||||
"version": "1.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.98",
|
||||
"description": "HikVision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -35,6 +35,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/highland": "^2.12.14",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Readable } from 'stream';
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { EventEmitter } from "stream";
|
||||
import { IncomingMessage } from 'http';
|
||||
import https from 'https';
|
||||
|
||||
function getChannel(channel: string) {
|
||||
export const hikvisionHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
export function getChannel(channel: string) {
|
||||
return channel || '101';
|
||||
}
|
||||
|
||||
@@ -45,6 +48,7 @@ export class HikVisionCameraAPI {
|
||||
this.deviceModel = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/ISAPI/System/deviceInfo`,
|
||||
@@ -78,6 +82,7 @@ export class HikVisionCameraAPI {
|
||||
}
|
||||
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/ISAPI/Streaming/channels/${getChannel(channel)}/capabilities`,
|
||||
@@ -98,6 +103,7 @@ export class HikVisionCameraAPI {
|
||||
const url = `http://${this.ip}/ISAPI/Streaming/channels/${getChannel(channel)}/picture?snapShotImageType=JPEG`
|
||||
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: url,
|
||||
@@ -114,6 +120,7 @@ export class HikVisionCameraAPI {
|
||||
// this.console.log('listener url', url);
|
||||
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
url,
|
||||
responseType: 'stream',
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import sdk, { MediaObject, Camera, ScryptedInterface, Setting } from "@scrypted/sdk";
|
||||
import { EventEmitter } from "stream";
|
||||
import { HikVisionCameraAPI } from "./hikvision-camera-api";
|
||||
import { Destroyable, UrlMediaStreamOptions, RtspProvider, RtspSmartCamera } from "../../rtsp/src/rtsp";
|
||||
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
|
||||
import { readLength } from '@scrypted/common/src/read-stream';
|
||||
import sdk, { Camera, FFmpegInput, Intercom, MediaObject, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { sleep } from "../../../common/src/sleep";
|
||||
import { HikVisionCameraEvent } from "./hikvision-camera-api";
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { getChannel, HikVisionCameraAPI, HikVisionCameraEvent, hikvisionHttpsAgent } from "./hikvision-camera-api";
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
class HikVisionCamera extends RtspSmartCamera implements Camera {
|
||||
class HikVisionCamera extends RtspSmartCamera implements Camera, Intercom {
|
||||
channelIds: Promise<string[]>;
|
||||
client: HikVisionCameraAPI;
|
||||
onvifIntercom = new OnvifIntercom(this);
|
||||
cp: ChildProcess;
|
||||
|
||||
// bad hack, but whatever.
|
||||
codecCheck = (async () => {
|
||||
@@ -29,6 +35,24 @@ class HikVisionCamera extends RtspSmartCamera implements Camera {
|
||||
}
|
||||
})();
|
||||
|
||||
constructor(nativeId: string, provider: RtspProvider) {
|
||||
super(nativeId, provider);
|
||||
|
||||
this.updateManagementUrl();
|
||||
}
|
||||
|
||||
updateManagementUrl() {
|
||||
const ip = this.storage.getItem('ip');
|
||||
if (!ip)
|
||||
return;
|
||||
const info = this.info || {};
|
||||
const managementUrl = `http://${ip}`;
|
||||
if (info.managementUrl !== managementUrl) {
|
||||
info.managementUrl = managementUrl;
|
||||
this.info = info;
|
||||
}
|
||||
}
|
||||
|
||||
async listenEvents() {
|
||||
let motionTimeout: NodeJS.Timeout;
|
||||
const api = (this.provider as HikVisionProvider).createSharedClient(this.getHttpAddress(), this.getUsername(), this.getPassword());
|
||||
@@ -169,6 +193,7 @@ class HikVisionCamera extends RtspSmartCamera implements Camera {
|
||||
resolve([camNumber + '01', camNumber + '02']);
|
||||
} else try {
|
||||
const response = await client.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
url: `http://${this.getHttpAddress()}/ISAPI/Streaming/channels`,
|
||||
responseType: 'text',
|
||||
});
|
||||
@@ -213,6 +238,156 @@ class HikVisionCamera extends RtspSmartCamera implements Camera {
|
||||
this.client = undefined;
|
||||
this.channelIds = undefined;
|
||||
super.putSetting(key, value);
|
||||
|
||||
const doorbellType = this.storage.getItem('doorbellType');
|
||||
const isDoorbell = doorbellType === 'true';
|
||||
|
||||
const twoWayAudio = this.storage.getItem('twoWayAudio') === 'true'
|
||||
|| this.storage.getItem('twoWayAudio') === 'ONVIF'
|
||||
|| this.storage.getItem('twoWayAudio') === 'Hikvision';
|
||||
|
||||
const interfaces = this.provider.getInterfaces();
|
||||
let type: ScryptedDeviceType = undefined;
|
||||
if (isDoorbell) {
|
||||
type = ScryptedDeviceType.Doorbell;
|
||||
interfaces.push(ScryptedInterface.BinarySensor)
|
||||
}
|
||||
if (isDoorbell || twoWayAudio) {
|
||||
interfaces.push(ScryptedInterface.Intercom);
|
||||
}
|
||||
|
||||
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
|
||||
|
||||
this.updateManagementUrl();
|
||||
}
|
||||
|
||||
async getOtherSettings(): Promise<Setting[]> {
|
||||
const ret = await super.getOtherSettings();
|
||||
|
||||
const doorbellType = this.storage.getItem('doorbellType');
|
||||
const isDoorbell = doorbellType === 'true';
|
||||
|
||||
let twoWayAudio = this.storage.getItem('twoWayAudio');
|
||||
|
||||
const choices = [
|
||||
'Hikvision',
|
||||
'ONVIF',
|
||||
];
|
||||
|
||||
if (!isDoorbell)
|
||||
choices.unshift('None');
|
||||
|
||||
twoWayAudio = choices.find(c => c === twoWayAudio);
|
||||
|
||||
if (!twoWayAudio)
|
||||
twoWayAudio = isDoorbell ? 'Hikvision' : 'None';
|
||||
|
||||
ret.push(
|
||||
{
|
||||
title: 'Doorbell',
|
||||
type: 'boolean',
|
||||
description: 'This device is a Hikvision doorbell.',
|
||||
value: isDoorbell,
|
||||
key: 'doorbellType',
|
||||
},
|
||||
{
|
||||
title: 'Two Way Audio',
|
||||
value: twoWayAudio,
|
||||
key: 'twoWayAudio',
|
||||
description: 'Hikvision cameras may support both Hikvision and ONVIF two way audio protocols. ONVIF generally performs better when supported.',
|
||||
choices,
|
||||
},
|
||||
);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
async startIntercom(media: MediaObject): Promise<void> {
|
||||
if (this.storage.getItem('twoWayAudio') === 'ONVIF') {
|
||||
const options = await this.getConstructedVideoStreamOptions();
|
||||
const stream = options[0];
|
||||
const url = new URL(stream.url);
|
||||
// amcrest onvif requires this proto query parameter, or onvif two way
|
||||
// will not activate.
|
||||
url.searchParams.set('proto', 'Onvif');
|
||||
this.onvifIntercom.url = url.toString();
|
||||
return this.onvifIntercom.startIntercom(media);
|
||||
}
|
||||
|
||||
const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput);
|
||||
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
|
||||
|
||||
const args = ffmpegInput.inputArguments.slice();
|
||||
args.unshift('-hide_banner');
|
||||
|
||||
args.push(
|
||||
"-vn",
|
||||
'-ar', '8000',
|
||||
'-ac', '1',
|
||||
'-acodec', 'pcm_mulaw',
|
||||
'-f', 'mulaw',
|
||||
'pipe:3',
|
||||
);
|
||||
|
||||
this.console.log('ffmpeg intercom', args);
|
||||
|
||||
const ffmpeg = await mediaManager.getFFmpegPath();
|
||||
this.cp = child_process.spawn(ffmpeg, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
this.cp.on('exit', () => this.cp = undefined);
|
||||
ffmpegLogInitialOutput(this.console, this.cp);
|
||||
const socket = this.cp.stdio[3] as Readable;
|
||||
|
||||
(async () => {
|
||||
const url = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${this.getRtspChannel() || '1'}/audioData`;
|
||||
this.console.log('posting audio data to', url);
|
||||
|
||||
// seems the dahua doorbells preferred 1024 chunks. should investigate adts
|
||||
// parsing and sending multipart chunks instead.
|
||||
const passthrough = new PassThrough();
|
||||
this.getClient().digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: 'PUT',
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'Audio/G.711Mu',
|
||||
// 'Connection': 'close',
|
||||
// 'Content-Length': '9999999'
|
||||
},
|
||||
data: passthrough,
|
||||
});
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const data = await readLength(socket, 1024);
|
||||
passthrough.push(data);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
finally {
|
||||
this.console.log('audio finished');
|
||||
passthrough.end();
|
||||
}
|
||||
|
||||
this.stopIntercom();
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
async stopIntercom(): Promise<void> {
|
||||
if (this.storage.getItem('twoWayAudio') === 'ONVIF') {
|
||||
return this.onvifIntercom.stopIntercom();
|
||||
}
|
||||
|
||||
const client = this.getClient();
|
||||
await client.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: 'PUT',
|
||||
url: `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${this.getRtspChannel() || '1'}/close`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user