diff options
Diffstat (limited to 'scrypted')
| -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 // ------------------------------------------------------------------------ |
