src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-19 19:41:20 -0500
committerLuke Hoersten <[email protected]>2026-06-19 19:41:20 -0500
commit1063a4e33022efdbe071df07abe59d9c0ba01173 (patch)
tree1ce071b90e3b11d2e6d11b78f8c48eabd35c594c
parentbccf0fa25e59f4fa9df9d0b3a129aeb369f17a87 (diff)
scrypted: user-controllable JPEG quality (1–31) in the viewport Settings
Adds a "JPEG quality" field under Display alongside frame_interval_ms, brightness, etc. ffmpeg -q:v range 1..31 — 1 = highest quality + biggest JPEG (~140KB at panel-native), higher numbers = smaller + lossier. Plumbed through both the live-stream encoder and the snapshot one-shot encoder. Reads from viewport storage; defaults to 1 if unset; clamps to [1, 31] on read so an out-of-range input from the UI doesn't crash ffmpeg. Changing the value triggers the existing onBindingChanged debounce which already restarts the live stream — picks up the new quality within ~300ms of Save.
-rw-r--r--scrypted/scrypted-viewport.ts24
1 files changed, 22 insertions, 2 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts
index 16b6e02..b06e93a 100644
--- a/scrypted/scrypted-viewport.ts
+++ b/scrypted/scrypted-viewport.ts
@@ -125,6 +125,17 @@ class Viewport extends ScryptedDeviceBase implements Settings {
const parsed = v ? parseInt(v, 10) : NaN;
return Number.isFinite(parsed) ? Math.max(33, parsed) : DEFAULT_FRAME_INTERVAL_MS;
}
+ // ffmpeg mjpeg encoder -q:v. Valid range 1..31, lower = higher
+ // quality + bigger JPEG (1 ≈ visually lossless, 31 ≈ very lossy).
+ // Default 1 — with HTTP keep-alive + NODELAY we have plenty of
+ // body-upload headroom for the bigger frames. Bump up only if
+ // you're chasing fewer bytes on the wire at the cost of visible
+ // artifacts.
+ get jpegQuality(): number {
+ const v = this.storage.getItem("jpeg_quality");
+ const parsed = v ? parseInt(v, 10) : NaN;
+ return Number.isFinite(parsed) ? Math.max(1, Math.min(31, parsed)) : 1;
+ }
// Which camera-event types wake this viewport. Empty = tap-only,
// never woken by Scrypted. Default = all three (doorbell + motion +
// person detection).
@@ -195,6 +206,14 @@ class Viewport extends ScryptedDeviceBase implements Settings {
value: this.frameIntervalMs,
} as any,
{
+ group: "Display",
+ key: "jpeg_quality",
+ title: "JPEG quality (1–31, lower = better)",
+ description: "ffmpeg mjpeg encoder -q:v. 1 ≈ visually lossless (~140KB at panel-native), 5 ≈ good (~70KB), 10+ noticeably lossy. Default 1.",
+ type: "number",
+ value: this.jpegQuality,
+ } as any,
+ {
group: "Actions",
key: "action_wake",
title: "Wake now",
@@ -727,6 +746,7 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
? `transpose=1,scale=${panelW}:${panelH}:flags=lanczos,setsar=1`
: `scale=${panelW}:${panelH}:flags=lanczos,setsar=1`;
const fps = Math.max(1, Math.round(1000 / v.frameIntervalMs));
+ const qv = String(v.jpegQuality);
// Diagnostic — confirms which filter chain the *currently loaded*
// script is actually using. If you don't see this line in the
// plugin log, the Scrypted Script editor is still on stale code
@@ -823,7 +843,7 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
// this, ffmpeg's output queue fills up and the displayed
// image lags further and further behind reality.
"-fps_mode", "drop",
- "-c:v", "mjpeg", "-q:v", "1",
+ "-c:v", "mjpeg", "-q:v", qv,
"-f", "image2pipe", "-flush_packets", "1",
"pipe:1",
]);
@@ -1049,7 +1069,7 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
"-f", "image2pipe", "-i", "pipe:0",
"-vf", vf,
"-frames:v", "1",
- "-c:v", "mjpeg", "-q:v", "1",
+ "-c:v", "mjpeg", "-q:v", String(v.jpegQuality),
"-f", "image2pipe", "pipe:1",
]);
const chunks: Buffer[] = [];