mirror of
https://github.com/koush/scrypted.git
synced 2026-03-03 17:52:02 +00:00
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
import sdk, { MediaObject, Camera, ScryptedInterface } from "@scrypted/sdk";
|
|
import { EventEmitter } from "stream";
|
|
import { HikVisionCameraAPI } from "./hikvision-camera-api";
|
|
import { Destroyable, UrlMediaStreamOptions, RtspProvider, RtspSmartCamera } from "../../rtsp/src/rtsp";
|
|
import { sleep } from "../../../common/src/sleep";
|
|
import { HikVisionCameraEvent } from "./hikvision-camera-api";
|
|
const { mediaManager } = sdk;
|
|
|
|
class HikVisionCamera extends RtspSmartCamera implements Camera {
|
|
channelIds: Promise<string[]>;
|
|
client: HikVisionCameraAPI;
|
|
|
|
// bad hack, but whatever.
|
|
codecCheck = (async () => {
|
|
while (true) {
|
|
try {
|
|
const streamSetup = await this.client.checkStreamSetup(this.getRtspChannel(), await this.isOld());
|
|
if (streamSetup.videoCodecType !== 'H.264') {
|
|
this.log.a(`This camera is configured for ${streamSetup.videoCodecType} on the main channel. Configuring it it for H.264 is recommended for optimal performance.`);
|
|
}
|
|
if (!this.isAudioDisabled() && streamSetup.audioCodecType && streamSetup.audioCodecType !== 'AAC') {
|
|
this.log.a(`This camera is configured for ${streamSetup.audioCodecType} on the main channel. Configuring it for AAC is recommended for optimal performance.`);
|
|
}
|
|
break;
|
|
}
|
|
catch (e) {
|
|
await sleep(60000);
|
|
}
|
|
}
|
|
})();
|
|
|
|
listenEvents() {
|
|
let motionTimeout: NodeJS.Timeout;
|
|
const ret = new EventEmitter() as (EventEmitter & Destroyable);
|
|
ret.destroy = () => {
|
|
};
|
|
(async () => {
|
|
const api = (this.provider as HikVisionProvider).createSharedClient(this.getHttpAddress(), this.getUsername(), this.getPassword());
|
|
try {
|
|
const events = await api.listenEvents();
|
|
ret.destroy = () => {
|
|
events.removeAllListeners();
|
|
};
|
|
|
|
let ignoreCameraNumber: boolean;
|
|
|
|
events.on('close', () => ret.emit('error', new Error('close')));
|
|
events.on('error', e => ret.emit('error', e));
|
|
events.on('event', async (event: HikVisionCameraEvent, cameraNumber: string) => {
|
|
if (event === HikVisionCameraEvent.MotionDetected
|
|
|| event === HikVisionCameraEvent.LineDetection
|
|
|| event === HikVisionCameraEvent.FieldDetection) {
|
|
|
|
// check if the camera+channel field is in use, and filter events.
|
|
if (this.getRtspChannel()) {
|
|
// it is possible to set it up to use a camera number
|
|
// on an nvr IP (which gives RTSP urls through the NVR), but then use a http port
|
|
// that gives a filtered event stream from only that camera.
|
|
// this this case, the camera numbers will not
|
|
// match as they will be always be "1".
|
|
// to detect that a camera specific endpoint is being used
|
|
// can look at the channel ids, and see if that camera number is found.
|
|
// this is different from the use case where the NVR or camera
|
|
// is using a port other than 80 (the default).
|
|
// could add a setting to have the user explicitly denote nvr usage
|
|
// but that is error prone.
|
|
const userCameraNumber = this.getCameraNumber();
|
|
if (ignoreCameraNumber === undefined && this.channelIds) {
|
|
const channelIds = await this.channelIds;
|
|
ignoreCameraNumber = true;
|
|
for (const id of channelIds) {
|
|
if (id.startsWith(userCameraNumber)) {
|
|
ignoreCameraNumber = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!ignoreCameraNumber && cameraNumber !== userCameraNumber) {
|
|
// this.console.error(`### Skipping motion event ${cameraNumber} != ${this.getCameraNumber()}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// this.console.error('### Detected motion, camera: ', cameraNumber);
|
|
this.motionDetected = true;
|
|
clearTimeout(motionTimeout);
|
|
motionTimeout = setTimeout(() => this.motionDetected = false, 30000);
|
|
}
|
|
})
|
|
}
|
|
catch (e) {
|
|
ret.emit('error', e);
|
|
}
|
|
})();
|
|
return ret;
|
|
}
|
|
|
|
createClient() {
|
|
return new HikVisionCameraAPI(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.console);
|
|
}
|
|
|
|
getClient() {
|
|
if (!this.client)
|
|
this.client = this.createClient();
|
|
return this.client;
|
|
}
|
|
|
|
async takeSmartCameraPicture(): Promise<MediaObject> {
|
|
const api = this.getClient();
|
|
return mediaManager.createMediaObject(await api.jpegSnapshot(this.getRtspChannel()), 'image/jpeg');
|
|
}
|
|
|
|
async getUrlSettings() {
|
|
return [
|
|
{
|
|
key: 'rtspChannel',
|
|
title: 'Channel Number',
|
|
description: "Optional: The channel number to use for snapshots. E.g., 101, 201, etc. The camera portion, e.g., 1, 2, etc, will be used to construct the RTSP stream.",
|
|
placeholder: '101',
|
|
value: this.storage.getItem('rtspChannel'),
|
|
},
|
|
...await super.getUrlSettings(),
|
|
{
|
|
key: 'rtspUrlParams',
|
|
title: 'RTSP URL Parameters Override',
|
|
description: "Optional: Override the RTSP URL parameters. E.g.: ?transportmode=unicast",
|
|
placeholder: this.getRtspUrlParams(),
|
|
value: this.storage.getItem('rtspUrlParams'),
|
|
},
|
|
]
|
|
}
|
|
|
|
getRtspChannel() {
|
|
return this.storage.getItem('rtspChannel');
|
|
}
|
|
|
|
getCameraNumber() {
|
|
const channel = this.getRtspChannel();
|
|
// have users with more than 10 cameras. unsure if it is possible
|
|
// to have more than 10 substreams...
|
|
if (channel?.length > 3)
|
|
return channel.substring(0, channel.length - 2);
|
|
return channel?.substring(0, 1) || '1';
|
|
}
|
|
|
|
getRtspUrlParams() {
|
|
return this.storage.getItem('rtspUrlParams') || '?transportmode=unicast';
|
|
}
|
|
|
|
async isOld() {
|
|
const client = this.getClient();
|
|
let isOld: boolean;
|
|
if (this.storage.getItem('isOld')) {
|
|
isOld = this.storage.getItem('isOld') === 'true';
|
|
}
|
|
else {
|
|
isOld = await client.checkIsOldModel();
|
|
this.storage.setItem('isOld', isOld?.toString());
|
|
}
|
|
return isOld;
|
|
}
|
|
|
|
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
|
if (!this.channelIds) {
|
|
const client = this.getClient();
|
|
this.channelIds = new Promise(async (resolve, reject) => {
|
|
const isOld = await this.isOld();
|
|
|
|
if (isOld) {
|
|
this.console.error('Old NVR. Defaulting to two camera configuration');
|
|
const camNumber = this.getCameraNumber() || '1';
|
|
resolve([camNumber + '01', camNumber + '02']);
|
|
} else try {
|
|
const response = await client.digestAuth.request({
|
|
url: `http://${this.getHttpAddress()}/ISAPI/Streaming/channels`,
|
|
responseType: 'text',
|
|
});
|
|
const xml: string = response.data;
|
|
const matches = xml.matchAll(/<id>(.*?)<\/id>/g);
|
|
const ids = [];
|
|
for (const m of matches) {
|
|
ids.push(m[1]);
|
|
}
|
|
resolve(ids);
|
|
}
|
|
catch (e) {
|
|
const cameraNumber = this.getCameraNumber() || '1';
|
|
this.console.error('error retrieving channel ids', e);
|
|
resolve([cameraNumber + '01', cameraNumber + '02']);
|
|
this.channelIds = undefined;
|
|
}
|
|
})
|
|
}
|
|
const channelIds = await this.channelIds;
|
|
const params = this.getRtspUrlParams() || '?transportmode=unicast';
|
|
|
|
// due to being able to override the channel number, and NVR providing per channel port access,
|
|
// do not actually use these channel ids, and just use it to determine the number of channels
|
|
// available for a camera.
|
|
const ret = [];
|
|
const cameraNumber = this.getCameraNumber() || '1';
|
|
for (let index = 0; index < channelIds.length; index++) {
|
|
const channel = (index + 1).toString().padStart(2, '0');
|
|
const mso = this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/ISAPI/Streaming/channels/${cameraNumber}${channel}/${params}`, index);
|
|
ret.push(mso);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
showRtspUrlOverride() {
|
|
return false;
|
|
}
|
|
|
|
async putSetting(key: string, value: string) {
|
|
this.client = undefined;
|
|
this.channelIds = undefined;
|
|
super.putSetting(key, value);
|
|
}
|
|
}
|
|
|
|
class HikVisionProvider extends RtspProvider {
|
|
clients: Map<string, HikVisionCameraAPI>;
|
|
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
getAdditionalInterfaces() {
|
|
return [
|
|
ScryptedInterface.Camera,
|
|
ScryptedInterface.MotionSensor,
|
|
];
|
|
}
|
|
|
|
createSharedClient(address: string, username: string, password: string) {
|
|
if (!this.clients)
|
|
this.clients = new Map();
|
|
|
|
const key = `${address}#${username}#${password}`;
|
|
const check = this.clients.get(key);
|
|
if (check)
|
|
return check;
|
|
const client = new HikVisionCameraAPI(address, username, password, this.console);
|
|
this.clients.set(key, client);
|
|
return client;
|
|
}
|
|
|
|
createCamera(nativeId: string) {
|
|
return new HikVisionCamera(nativeId, this);
|
|
}
|
|
}
|
|
|
|
export default new HikVisionProvider();
|