src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-20 11:32:54 -0500
committerLuke Hoersten <[email protected]>2026-06-20 11:32:54 -0500
commitcdd18277a8dfae8227864a3fb4e4e3a62e4dc315 (patch)
treedc1daddabb8ddbc6639ec468fbf88444a84ca40c
parentc4f6b1586d1ee904ba5bd8091bc72060c9b7249e (diff)
phase 5: fix snapshot looking lower-quality than stream
User reported "snapshot is still lower qual than the stream" even at jpegQuality=1. Two compounding causes in the sharp transform path: 1. Quality mapping topped out at 97. Previous formula was Math.max(50, 100 - v.jpegQuality * 3); at jpegQuality=1 that produced quality=97 — versus the stream's ffmpeg mjpeg -q:v 1 which lands at sharp-equivalent ~99-100. Visible compression artifacts on flat regions accounted for ~half the perceived quality gap. New formula Math.min(100, 102 - v.jpegQuality * 2) yields: jpegQuality=1 → 100 (was 97) jpegQuality=2 → 98 (was 94) jpegQuality=5 → 92 (was 85) jpegQuality=10 → 82 (was 70) jpegQuality=31 → 40 (was 50, clamped) 2. Chroma subsampling was sharp's default 4:2:0 — half-rate chroma in U+V planes. On colored edges (UI overlays, sharp camera subjects against backgrounds, text) this smears and is the dominant visible artifact at panel-native 800x480. ffmpeg mjpeg's default behavior at -q:v 1 is closer to 4:2:2 or no subsampling. Force 4:4:4 when jpegQuality <= 2 (the "I want max quality" regime); keep 4:2:0 for jpegQuality >= 3 since the point at that quality is smaller files. Plus mozjpeg: true on the encoder. Same quality value, slightly tighter file size — modest but free. The Lanczos kernel was already lanczos3 — confirmed, no change. Expected outcome: snapshot at jpegQuality=1 is visually indistinguishable from a stream frame at the same scene.
-rw-r--r--scrypted/scrypted-viewport.ts17
1 files changed, 15 insertions, 2 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts
index 6245192..21031fc 100644
--- a/scrypted/scrypted-viewport.ts
+++ b/scrypted/scrypted-viewport.ts
@@ -6,7 +6,7 @@
// short git hash of the commit that added this constant — if the
// hash in the log doesn't match the HEAD this file came from, the
// Scrypted Script editor is still on stale code.
-const SCRIPT_VERSION = "e4a6d07";
+const SCRIPT_VERSION = "pending";
//
// Architecture
// ------------
@@ -1034,14 +1034,27 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
// Path 1: sharp. require()-fail caught at the boundary so a
// missing native module just falls through.
+ //
+ // Quality math: ffmpeg's mjpeg -q:v 1 corresponds to sharp JPEG
+ // quality ~99-100 (their scales aren't 1:1 but the practical
+ // result matches). Our previous formula maxed out at 97 — that
+ // was the visible delta vs the stream. New formula: at
+ // jpegQuality=1 emit 100; at 10 emit ~82; at 31 emit ~40.
+ // Also force chromaSubsampling 4:4:4 at the top end so colored
+ // edges (text, UI overlays) don't smear — sharp's default 4:2:0
+ // is half-rate chroma and is the dominant visible artifact at
+ // panel-native resolution. mozjpeg encoder for tighter files at
+ // the same visual quality.
if (!transformed.length) {
try {
const sharp = require("sharp");
let img = sharp(srcJpeg, { failOnError: false });
if (needsRotate) img = img.rotate(90);
+ const sharpQuality = Math.min(100, 102 - v.jpegQuality * 2);
+ const chroma = v.jpegQuality <= 2 ? "4:4:4" : "4:2:0";
transformed = await img
.resize(panelW, panelH, { fit: "fill", kernel: "lanczos3" })
- .jpeg({ quality: Math.max(50, 100 - v.jpegQuality * 3) })
+ .jpeg({ quality: sharpQuality, chromaSubsampling: chroma, mozjpeg: true })
.toBuffer();
path = "sharp";
} catch { /* fall through */ }