snapshot: use libvips

This commit is contained in:
Koushik Dutta
2023-03-10 19:45:37 -08:00
parent 11a1a1134d
commit cc7271f0a2
6 changed files with 107 additions and 33 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/snapshot",
"version": "0.0.46",
"version": "0.0.48",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/snapshot",
"version": "0.0.46",
"version": "0.0.48",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@types/node": "^16.6.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/snapshot",
"version": "0.0.46",
"version": "0.0.48",
"description": "Snapshot Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
@@ -26,16 +26,19 @@
"name": "Snapshot Plugin",
"type": "API",
"interfaces": [
"DeviceProvider",
"Settings",
"MixinProvider",
"BufferConverter"
]
},
"optionalDependencies": {
"sharp": "^0.31.3"
},
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@types/node": "^16.6.1",
"axios": "^0.24.0",
"sharp": "^0.31.3",
"whatwg-mimetype": "^3.0.0"
},
"devDependencies": {

View File

@@ -0,0 +1,37 @@
import sdk, { BufferConverter, Image, ImageOptions, MediaObject, MediaObjectOptions, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
import sharp, { Sharp } from 'sharp';
export class ImageReader extends ScryptedDeviceBase implements BufferConverter {
constructor(nativeId: string) {
super(nativeId);
this.fromMimeType = 'image/*';
this.toMimeType = ScryptedMimeTypes.Image;
}
async convert(data: Buffer, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<Image> {
const image = sharp(data, {
failOnError: false,
});
const metadata = await image.metadata();
const ret: Image = await sdk.mediaManager.createMediaObject(image, ScryptedMimeTypes.Image, {
width: metadata.width,
height: metadata.height,
format: metadata.format,
toBuffer: (options: ImageOptions) => {
let transformed = image;
if (options?.crop) {
transformed = transformed.extract({
...options.crop,
});
}
if (options?.resize)
transformed = transformed.resize(options.resize.width, options.resize.height);
return transformed.toBuffer();
},
})
return ret;
}
}

View File

@@ -2,13 +2,15 @@ import AxiosDigestAuth from '@koush/axios-digest-auth';
import { AutoenableMixinProvider } from "@scrypted/common/src/autoenable-mixin-provider";
import { createMapPromiseDebouncer, RefreshPromise, singletonPromise, TimeoutError } from "@scrypted/common/src/promise-utils";
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
import sdk, { BufferConverter, MediaObjectOptions, Camera, FFmpegInput, MediaObject, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
import sdk, { BufferConverter, MediaObjectOptions, Camera, FFmpegInput, MediaObject, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, DeviceProvider } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import axios, { AxiosInstance } from "axios";
import https from 'https';
import path from 'path';
import MimeType from 'whatwg-mimetype';
import { ffmpegFilterImage, ffmpegFilterImageBuffer } from './ffmpeg-image-filter';
import { ffmpegFilterImage } from './ffmpeg-image-filter';
import { ImageReader } from './image-reader';
import { sharpFilterImage } from './sharp-image-filter';
const { mediaManager, systemManager } = sdk;
@@ -299,11 +301,9 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
} : undefined);
picture = await this.cropAndScale(picture);
if (needSoftwareResize) {
picture = await ffmpegFilterImageBuffer(picture, {
picture = await sharpFilterImage(picture, {
console: this.debugConsole,
ffmpegPath: await mediaManager.getFFmpegPath(),
resize: options?.picture,
timeout: 10000,
});
}
this.clearCachedPictures();
@@ -353,9 +353,8 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
const xmax = Math.max(...this.storageSettings.values.snapshotCropScale.map(([x, y]) => x)) / 100;
const ymax = Math.max(...this.storageSettings.values.snapshotCropScale.map(([x, y]) => y)) / 100;
return ffmpegFilterImageBuffer(buffer, {
return sharpFilterImage(buffer, {
console: this.debugConsole,
ffmpegPath: await mediaManager.getFFmpegPath(),
crop: {
fractional: true,
left: xmin,
@@ -363,7 +362,6 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
width: xmax - xmin,
height: ymax - ymin,
},
timeout: 10000,
});
}
@@ -447,16 +445,14 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
})
}
else {
return ffmpegFilterImageBuffer(errorBackground, {
return sharpFilterImage(errorBackground, {
console: this.debugConsole,
ffmpegPath: await mediaManager.getFFmpegPath(),
blur: true,
brightness: -.2,
text: {
fontFile,
text,
},
timeout: 10000,
});
}
}
@@ -501,7 +497,7 @@ export function parseDims<T extends string>(dict: DimDict<T>) {
return ret;
}
class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings {
class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, DeviceProvider {
storageSettings = new StorageSettings(this, {
debugLogging: {
title: 'Debug Logging',
@@ -515,6 +511,28 @@ class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, B
this.fromMimeType = ScryptedMimeTypes.FFmpegInput;
this.toMimeType = 'image/jpeg';
process.nextTick(() => {
sdk.deviceManager.onDevicesChanged({
devices: [
{
name: 'Image Reader',
type: ScryptedDeviceType.Builtin,
nativeId: 'reader',
interfaces: [
ScryptedInterface.BufferConverter,
]
}
]
})
})
}
async getDevice(nativeId: string): Promise<any> {
return new ImageReader('reader')
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
getSettings(): Promise<Setting[]> {

View File

@@ -4,10 +4,10 @@ export interface SharpImageFilterOptions {
console?: Console,
blur?: boolean;
brightness?: number;
// text?: {
// text: string;
// fontFile: string;
// };
text?: {
text: string;
fontFile: string;
};
resize?: {
fractional?: boolean;
@@ -28,7 +28,9 @@ export interface SharpImageFilterOptions {
export async function sharpFilterImage(inputJpeg: Buffer | string, options: SharpImageFilterOptions) {
let image = sharp(inputJpeg);
let image = sharp(inputJpeg, {
failOnError: false,
});
const metadata = await image.metadata();
if (options?.crop) {
let { left, top, width, height, fractional } = options.crop;
@@ -59,7 +61,7 @@ export async function sharpFilterImage(inputJpeg: Buffer | string, options: Shar
if (options?.brightness) {
image = image.modulate({
lightness: options.brightness * 100,
lightness: options.brightness * 100,
});
}
@@ -67,9 +69,22 @@ export async function sharpFilterImage(inputJpeg: Buffer | string, options: Shar
image = image.blur(25);
}
// if (options?.text) {
// }
if (options?.text) {
image = image.composite([
{
input: {
text: {
rgba: true,
text: `<span foreground="white">${options.text.text}</span>`,
// this is not working?
// font: 'Lato',
// fontfile: options?.text.fontFile,
dpi: metadata.height,
},
},
}
])
}
image = image.toFormat(options?.format || 'jpg');

View File

@@ -1,6 +1,7 @@
import { writeFileSync } from "fs";
import { ffmpegFilterImage } from "../src/ffmpeg-image-filter";
import { sharpFilterImage } from "../src/sharp-image-filter";
import path from 'path';
async function main1() {
const ret = await ffmpegFilterImage(['-i', '/Users/koush/Downloads/151-1678381127261.jpg'],
@@ -14,10 +15,10 @@ async function main1() {
// // height: 500,
// // }
brightness: -.2,
// // text: {
// // fontFile: path.join(__dirname, '../fs/Lato-Bold.ttf'),
// // text: 'Hello World',
// // }
text: {
fontFile: path.join(__dirname, '../fs/Lato-Bold.ttf'),
text: 'Hello World',
}
// }
// { "crop": { "left": 0.216796875, "top": 0.2552083333333333, "width": 0.318359375, "height": 0.17907714843749994, "fractional": true }
}
@@ -38,10 +39,10 @@ async function main2() {
// height: 500,
// },
brightness: -.2,
// // text: {
// // fontFile: path.join(__dirname, '../fs/Lato-Bold.ttf'),
// // text: 'Hello World',
// // }
text: {
fontFile: path.join(__dirname, '../fs/Lato-Bold.ttf'),
text: 'Hello World',
}
// }
// { "crop": { "left": 0.216796875, "top": 0.2552083333333333, "width": 0.318359375, "height": 0.17907714843749994, "fractional": true }
}