diff options
| author | Luke Hoersten <[email protected]> | 2026-06-15 06:47:57 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-15 06:47:57 -0500 |
| commit | 80defa7ee6815984477030464e3587b54e0e9f51 (patch) | |
| tree | bb8dc4c4db6ef7701c11a13efe1b21020b348c1e /scrypted/scrypted-viewport.ts | |
| parent | 7b04ad6faf3bb70a0c3783e8446275f6f152b61e (diff) | |
scrypted: snapshot quality — fetch native res, resize + re-encode via ffmpeg
Stopped asking cam.takePicture for a specific dimension. Most camera
plugins default snapshot JPEGs to q≈75 and do a quick bilinear downscale
from native (1920x1080 or so) to whatever we requested, which looked
visibly worse than the H.264 keyframe from the same camera at the same
panel resolution.
pushFrame now:
1. takePicture without picture.{width,height} → native-res JPEG
2. spawn ffmpeg child_process: scale w:h:flags=lanczos, mjpeg q:v 2
3. POST the result.
q:v 2 is near-lossless; Lanczos is a sharper downscale than the camera
plugin's default. Adds ~10-20 ms ffmpeg work per frame on a real CPU
(noise-level on a Pi 4), well under the snapshot-source ceiling that
already gates pushFrame at ~500 ms.
Diffstat (limited to 'scrypted/scrypted-viewport.ts')
| -rw-r--r-- | scrypted/scrypted-viewport.ts | 47 |
1 files changed, 42 insertions, 5 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts index 405b019..c29276b 100644 --- a/scrypted/scrypted-viewport.ts +++ b/scrypted/scrypted-viewport.ts @@ -562,11 +562,15 @@ class ScryptedViewportProvider extends ScryptedDeviceBase const w = v.orientation === "portrait" ? 480 : 800; const h = v.orientation === "portrait" ? 800 : 480; - const picture = await cam.takePicture({ - picture: { width: w, height: h }, - reason: "event", - }); - const buf: Buffer = await mediaManager.convertMediaObjectToBuffer(picture, "image/jpeg"); + // Request the camera's native-resolution snapshot (no width/height + // in the picture options) and downsize + re-encode locally with + // ffmpeg at high quality. Most camera plugins default snapshot + // JPEGs to q≈75 and do a quick bilinear downscale, which looked + // visibly worse than the original H.264 stream. Lanczos + q=2 + // gets snapshot fidelity back close to the keyframe. + const picture = await cam.takePicture({ reason: "event" }); + const native: Buffer = await mediaManager.convertMediaObjectToBuffer(picture, "image/jpeg"); + const buf: Buffer = await this.resizeJpegHQ(native, w, h); const res = await fetch(`http://${v.host}/frame`, { method: "POST", @@ -644,6 +648,39 @@ class ScryptedViewportProvider extends ScryptedDeviceBase async putSetting(_key: string, _value: SettingValue) {} // ------------------------------------------------------------------------ + // Image resize via ffmpeg (snapshot fidelity ≫ camera-plugin downscale) + // ------------------------------------------------------------------------ + + private async resizeJpegHQ(jpeg: any, w: number, h: number): Promise<any> { + const { spawn } = require("child_process"); + const ffmpegPath = + (mediaManager.getFFmpegPath ? await mediaManager.getFFmpegPath() : undefined) || + "ffmpeg"; + return await new Promise<any>((resolve, reject) => { + const proc = spawn(ffmpegPath, [ + "-hide_banner", "-loglevel", "error", "-y", + "-f", "image2pipe", "-c:v", "mjpeg", "-i", "pipe:0", + "-vf", `scale=${w}:${h}:flags=lanczos`, + "-c:v", "mjpeg", "-q:v", "2", + "-f", "image2pipe", "pipe:1", + ]); + const chunks: any[] = []; + const errs: any[] = []; + proc.stdout.on("data", (c: any) => chunks.push(c)); + proc.stderr.on("data", (c: any) => errs.push(c)); + proc.on("error", (e: any) => reject(e)); + proc.on("close", (code: number) => { + if (code !== 0) { + reject(new Error(`ffmpeg exit ${code}: ${Buffer.concat(errs).toString("utf8").trim()}`)); + return; + } + resolve(Buffer.concat(chunks)); + }); + proc.stdin.end(jpeg); + }); + } + + // ------------------------------------------------------------------------ // Tiny HTTP helper // ------------------------------------------------------------------------ |
