diff --git a/plugins/snapshot/src/main.ts b/plugins/snapshot/src/main.ts index 2bde35815..0c10e721a 100644 --- a/plugins/snapshot/src/main.ts +++ b/plugins/snapshot/src/main.ts @@ -11,6 +11,7 @@ import MimeType from 'whatwg-mimetype'; import { ffmpegFilterImage, ffmpegFilterImageBuffer } from './ffmpeg-image-filter'; import { ImageReader, ImageReaderNativeId, loadVipsImage, loadSharp } from './image-reader'; import { ImageWriter, ImageWriterNativeId } from './image-writer'; +import { parseDims, parseImageOp, processImageOp } from './parse-dims'; const { mediaManager, systemManager } = sdk; @@ -532,31 +533,6 @@ class SnapshotMixin extends SettingsMixinDeviceBase implements Camera { } } -type DimDict = { - [key in T]: string; -}; - -export function parseDims(dict: DimDict) { - const ret: { - [key in T]?: number; - } & { - fractional?: boolean; - } = { - }; - - for (const t of Object.keys(dict)) { - const val = dict[t as T]; - if (val?.endsWith('%')) { - ret.fractional = true; - ret[t] = parseFloat(val?.substring(0, val?.length - 1)) / 100; - } - else { - ret[t] = val ? parseFloat(val) : undefined; - } - } - return ret; -} - class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, DeviceProvider { storageSettings = new StorageSettings(this, { debugLogging: { @@ -629,100 +605,10 @@ class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, B async convert(data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise { const mime = new MimeType(toMimeType); + const op = parseImageOp(mime.parameters); const ffmpegInput = JSON.parse(data.toString()) as FFmpegInput; - const { - width, - height, - fractional - } = parseDims({ - width: mime.parameters.get('width'), - height: mime.parameters.get('height'), - }); - - const { - left, - top, - right, - bottom, - fractional: cropFractional, - } = parseDims({ - left: mime.parameters.get('left'), - top: mime.parameters.get('top'), - right: mime.parameters.get('right'), - bottom: mime.parameters.get('bottom'), - }); - - const filename = ffmpegInput.url?.startsWith('file:') && new URL(ffmpegInput.url).pathname; - if (filename && loadSharp()) { - const vips = await loadVipsImage(filename, options?.sourceId); - - const resize = width && { - width, - height, - }; - - if (fractional) { - if (resize.width) - resize.width *= vips.width; - if (resize.height) - resize.height *= vips.height; - } - - const crop = left && { - left, - top, - width: right - left, - height: bottom - top, - }; - - if (cropFractional) { - crop.left *= vips.width; - crop.top *= vips.height; - crop.width *= vips.width; - crop.height *= vips.height; - } - - try { - const ret = await vips.toBuffer({ - resize, - crop, - format: 'jpg', - }); - return ret; - } - finally { - vips.close(); - } - } - - const args = [ - ...ffmpegInput.inputArguments, - ...(ffmpegInput.h264EncoderArguments || []), - ]; - - return ffmpegFilterImage(args, { - console: this.debugConsole, - ffmpegPath: await mediaManager.getFFmpegPath(), - resize: width === undefined && height === undefined - ? undefined - : { - width, - height, - fractional, - }, - crop: left === undefined || right === undefined || top === undefined || bottom === undefined - ? undefined - : { - left, - top, - width: right - left, - height: bottom - top, - fractional: cropFractional, - }, - timeout: 10000, - time: parseFloat(mime.parameters.get('time')), - }); + return processImageOp(ffmpegInput, op, parseFloat(mime.parameters.get('time')), options?.sourceId, this.debugConsole); } async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise { diff --git a/plugins/snapshot/src/parse-dims.ts b/plugins/snapshot/src/parse-dims.ts new file mode 100644 index 000000000..e39a35756 --- /dev/null +++ b/plugins/snapshot/src/parse-dims.ts @@ -0,0 +1,135 @@ +import sdk, { FFmpegInput } from '@scrypted/sdk'; +import type { MIMETypeParameters } from 'whatwg-mimetype'; +import { loadSharp, loadVipsImage } from './image-reader'; +import { ffmpegFilterImage } from './ffmpeg-image-filter'; + +export type DimDict = { + [key in T]: string; +}; + +export function parseDims(dict: DimDict) { + const ret: { + [key in T]?: number; + } & { + fractional?: boolean; + } = { + }; + + for (const t of Object.keys(dict)) { + const val = dict[t as T]; + if (val?.endsWith('%')) { + ret.fractional = true; + ret[t] = parseFloat(val?.substring(0, val?.length - 1)) / 100; + } + else { + ret[t] = val ? parseFloat(val) : undefined; + } + } + return ret; +} + +export interface ImageOp { + resize?: ReturnType>; + crop?: ReturnType>; +} + +export function parseImageOp(parameters: MIMETypeParameters | URLSearchParams): ImageOp { + return { + resize: parseDims({ + width: parameters.get('width'), + height: parameters.get('height'), + }), + crop: parseDims({ + left: parameters.get('left'), + top: parameters.get('top'), + right: parameters.get('right'), + bottom: parameters.get('bottom'), + }), + }; +} + +export async function processImageOp(input: string | FFmpegInput, op: ImageOp, time: number, sourceId: string, debugConsole: Console): Promise { + const { crop, resize } = op; + const { width, height, fractional } = resize; + const { left, top, right, bottom, fractional: cropFractional } = crop; + + const filename = typeof input === 'string' ? input : input.url?.startsWith('file:') && new URL(input.url).pathname; + + if (filename && loadSharp()) { + const vips = await loadVipsImage(filename, sourceId); + + const resize = width && { + width, + height, + }; + + if (fractional) { + if (resize.width) + resize.width *= vips.width; + if (resize.height) + resize.height *= vips.height; + } + + const crop = left && { + left, + top, + width: right - left, + height: bottom - top, + }; + + if (cropFractional) { + crop.left *= vips.width; + crop.top *= vips.height; + crop.width *= vips.width; + crop.height *= vips.height; + } + + try { + const ret = await vips.toBuffer({ + resize, + crop, + format: 'jpg', + }); + return ret; + } + finally { + vips.close(); + } + } + + const ffmpegInput: FFmpegInput = typeof input !== 'string' + ? input + : { + inputArguments: [ + '-i', input, + ] + }; + + const args = [ + ...ffmpegInput.inputArguments, + ...(ffmpegInput.h264EncoderArguments || []), + ]; + + return ffmpegFilterImage(args, { + console: debugConsole, + ffmpegPath: await sdk.mediaManager.getFFmpegPath(), + resize: width === undefined && height === undefined + ? undefined + : { + width, + height, + fractional, + }, + crop: left === undefined || right === undefined || top === undefined || bottom === undefined + ? undefined + : { + left, + top, + width: right - left, + height: bottom - top, + fractional: cropFractional, + }, + timeout: 10000, + time, + }); +}