mirror of
https://github.com/koush/scrypted.git
synced 2026-02-10 09:12:03 +00:00
snapshot: add some basic blurs and error logging to snapshots
This commit is contained in:
36
common/src/promise-utils.ts
Normal file
36
common/src/promise-utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface RefreshPromise<T> {
|
||||
promise: Promise<T>;
|
||||
cacheDuration: number;
|
||||
}
|
||||
|
||||
export function singletonPromise<T>(rp: RefreshPromise<T>, method: () => Promise<T>) {
|
||||
if (rp?.promise)
|
||||
return rp;
|
||||
|
||||
const promise = method();
|
||||
if (!rp) {
|
||||
rp = {
|
||||
promise,
|
||||
cacheDuration: 0,
|
||||
}
|
||||
}
|
||||
else {
|
||||
rp.promise = promise;
|
||||
}
|
||||
promise.finally(() => setTimeout(() => rp.promise = undefined, rp.cacheDuration));
|
||||
return rp;
|
||||
}
|
||||
|
||||
export class TimeoutError extends Error {
|
||||
constructor() {
|
||||
super('Operation Timed Out');
|
||||
}
|
||||
}
|
||||
|
||||
export function timeoutPromise<T>(timeout: number, promise: Promise<T>): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
setTimeout(() => reject(new TimeoutError()), timeout);
|
||||
promise.then(resolve);
|
||||
promise.catch(reject);
|
||||
})
|
||||
}
|
||||
1363
plugins/snapshot/package-lock.json
generated
1363
plugins/snapshot/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"description": "Snapshot Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
@@ -24,6 +24,9 @@
|
||||
"MixinProvider"
|
||||
]
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"jimp": "^0.16.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@types/node": "^16.6.1",
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
import sdk, { Camera, MediaObject, MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue } from "@scrypted/sdk";
|
||||
import sdk, { Camera, MediaObject, MixinProvider, PictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue } from "@scrypted/sdk";
|
||||
import { SettingsMixinDeviceBase } from "@scrypted/common/src/settings-mixin"
|
||||
import { StorageSettings } from "@scrypted/common/src/settings"
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import https from 'https';
|
||||
import axios, { Axios } from "axios";
|
||||
import { TimeoutError, timeoutPromise } from "@scrypted/common/src/promise-utils";
|
||||
import jimp from 'jimp';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
const fontPromise = jimp.loadFont(jimp.FONT_SANS_64_WHITE);
|
||||
|
||||
class NeverWaitError extends Error {
|
||||
|
||||
}
|
||||
|
||||
class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
snapshotUrl: {
|
||||
title: 'Snapshot URL',
|
||||
description: 'The http(s) URL that points that retrieves the latest image from your camera.',
|
||||
title: this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)
|
||||
? 'Override Snapshot URL'
|
||||
: 'Snapshot URL',
|
||||
description: (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)
|
||||
? 'Optional: '
|
||||
: '')
|
||||
+ 'The http(s) URL that points that retrieves the latest image from your camera.',
|
||||
placeholder: 'https://ip:1234/cgi-bin/snapshot.jpg',
|
||||
},
|
||||
snapshotMode: {
|
||||
title: 'Snapshot Mode',
|
||||
description: 'Set the snapshot mode to accomodate cameras with slow snapshots that may hang HomeKit.\nSetting the mode to "Never Wait" will only use recently available snapshots.\nSetting the mode to "Timeout" will cancel slow snapshots.',
|
||||
choices: [
|
||||
'Normal',
|
||||
'Never Wait',
|
||||
'Timeout',
|
||||
],
|
||||
defaultValue: 'Normal',
|
||||
}
|
||||
});
|
||||
axiosClient: Axios | AxiosDigestAuth;
|
||||
pendingPicture: Promise<Buffer>;
|
||||
lastPicture: Buffer;
|
||||
outdatedPicture: Buffer;
|
||||
|
||||
constructor(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }, providerNativeId: string) {
|
||||
super(mixinDevice, mixinDeviceState, {
|
||||
@@ -29,36 +55,115 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
});
|
||||
}
|
||||
|
||||
async takePicture(): Promise<MediaObject> {
|
||||
if (!this.axiosClient) {
|
||||
let username: string;
|
||||
let password: string;
|
||||
async takePicture(options?: PictureOptions): Promise<MediaObject> {
|
||||
let takePicture: () => Promise<Buffer>;
|
||||
if (!this.storageSettings.values.snapshotUrl && this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
|
||||
takePicture = () => this.mixinDevice.takePicture(options).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'));
|
||||
}
|
||||
else {
|
||||
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings)) {
|
||||
const settings = await this.mixinDevice.getSettings();
|
||||
username = settings?.find(setting => setting.key === 'username')?.value?.toString();
|
||||
password = settings?.find(setting => setting.key === 'password')?.value?.toString();
|
||||
if (!this.axiosClient) {
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings)) {
|
||||
const settings = await this.mixinDevice.getSettings();
|
||||
username = settings?.find(setting => setting.key === 'username')?.value?.toString();
|
||||
password = settings?.find(setting => setting.key === 'password')?.value?.toString();
|
||||
}
|
||||
|
||||
if (username && password) {
|
||||
this.axiosClient = new AxiosDigestAuth({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.axiosClient = axios;
|
||||
}
|
||||
}
|
||||
|
||||
if (username && password) {
|
||||
this.axiosClient = new AxiosDigestAuth({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.axiosClient = axios;
|
||||
}
|
||||
takePicture = () => this.axiosClient.request({
|
||||
httpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: this.storageSettings.values.snapshotUrl,
|
||||
}).then((response: { data: any; }) => response.data);
|
||||
}
|
||||
|
||||
const response = await this.axiosClient.request({
|
||||
httpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: this.storageSettings.values.snapshotUrl,
|
||||
});
|
||||
if (!this.pendingPicture) {
|
||||
this.pendingPicture = takePicture().then(lastPicture => {
|
||||
this.lastPicture = lastPicture;
|
||||
this.outdatedPicture = this.lastPicture;
|
||||
return lastPicture;
|
||||
})
|
||||
.finally(() => this.pendingPicture = undefined);
|
||||
}
|
||||
|
||||
return mediaManager.createMediaObject(Buffer.from(response.data), 'image/jpeg');
|
||||
const clearLastPicture = () => {
|
||||
const lp = this.lastPicture;
|
||||
setTimeout(() => {
|
||||
if (this.lastPicture === lp) {
|
||||
this.lastPicture = undefined;
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
let data: Buffer;
|
||||
if (this.storageSettings.values.snapshotMode === 'Normal') {
|
||||
data = await this.pendingPicture;
|
||||
clearLastPicture();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
if (this.storageSettings.values.snapshotMode === 'Never Wait') {
|
||||
if (!this.lastPicture) {
|
||||
// this triggers an event to refresh the web ui.
|
||||
this.pendingPicture.then(() => this.onDeviceEvent(ScryptedInterface.Camera, undefined));
|
||||
throw new NeverWaitError();
|
||||
}
|
||||
data = this.lastPicture;
|
||||
}
|
||||
else {
|
||||
data = await timeoutPromise(1000, this.pendingPicture);
|
||||
clearLastPicture();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
let text: string;
|
||||
if (e instanceof TimeoutError)
|
||||
text = 'Snapshot Timed Out';
|
||||
else if (e instanceof NeverWaitError)
|
||||
text = 'Snapshot in Progress';
|
||||
else
|
||||
text = 'Snapshot Failed';
|
||||
data = this.lastPicture || this.outdatedPicture;
|
||||
if (!data) {
|
||||
const img = await jimp.create(1920 / 2, 1080 / 2);
|
||||
const font = await fontPromise;
|
||||
img.print(font, 0, 0, {
|
||||
text,
|
||||
alignmentX: jimp.HORIZONTAL_ALIGN_CENTER,
|
||||
alignmentY: jimp.VERTICAL_ALIGN_MIDDLE,
|
||||
}, img.getWidth(), img.getHeight());
|
||||
data = await img.getBufferAsync('image/jpeg');
|
||||
}
|
||||
else {
|
||||
const img = await jimp.read(data);
|
||||
img.resize(1920 / 2, jimp.AUTO);
|
||||
img.blur(15);
|
||||
img.brightness(-.2);
|
||||
const font = await fontPromise;
|
||||
img.print(font, 0, 0, {
|
||||
text,
|
||||
alignmentX: jimp.HORIZONTAL_ALIGN_CENTER,
|
||||
alignmentY: jimp.VERTICAL_ALIGN_MIDDLE,
|
||||
}, img.getWidth(), img.getHeight());
|
||||
data = await img.getBufferAsync('image/jpeg');
|
||||
}
|
||||
}
|
||||
}
|
||||
return mediaManager.createMediaObject(Buffer.from(data), 'image/jpeg');
|
||||
}
|
||||
|
||||
async getPictureOptions() {
|
||||
|
||||
Reference in New Issue
Block a user