snapshot: add some basic blurs and error logging to snapshots

This commit is contained in:
Koushik Dutta
2022-02-23 21:49:39 -08:00
parent 544a9d8661
commit b690cd71cc
4 changed files with 1530 additions and 33 deletions

View 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);
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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() {