From 1609f5ed7adb713d159f969e74f8db76db5a9920 Mon Sep 17 00:00:00 2001 From: Shannon Atkinson Date: Fri, 14 Aug 2026 17:27:13 -0700 Subject: [PATCH] docs(web): two false claims about a competitor, and a precondition the copy did not know about ## Two cells asserted things about Restreamer that their own source refutes "Metrics / API" gave them REST and us "Prometheus + REST". Their README advertises resource monitoring "optionally by Prom-Metrics", datarhei Core documents Prometheus support, and they serve GraphQL too -- so the cell understated them twice and implied Prometheus was ours alone. "Per-destination loudness target" gave them a flat No. They mount a filter select per publication service and loudnorm is one of the filters offered, so they DO apply a per-destination loudness filter. What they do not offer is a target you set: the control is an on/off checkbox emitting the bare string `loudnorm`, so it takes FFmpeg's defaults and an operator cannot ask for -16 LUFS instead of -24. Both rows now state what polyemesis does and say nothing about what they do. A blank cell asserts nothing; a wrong one is a claim about somebody else's product on a public page. The narrowed loudness row is also a better claim -- "a target you set, I, LRA and TP" is checkable, where "loudness target" was vague enough to be wrong. ## And a precondition that did not exist when the copy was written Wiring Enhanced Broadcasting introduced a second requirement nothing said out loud: `settings.multitrack.gpus` is EMPTY BY DEFAULT, `multitrackGPUs` returns nil when it is empty, and `Negotiate` short-circuits to Refused on `len(a.Hardware.GPU) == 0`. So owning a supported GPU is not sufficient -- an operator with an RTX 4090 who never fills in the settings block gets the quiet fallback and no explanation. The copy said "it needs a GPU". It now says it needs a GPU and needs you to declare it, and why: Twitch validates the inventory and refuses a fabricated one by name, so polyemesis does not guess at hardware it cannot measure. The comparison table's bare "Yes" for the second audio mix also became wrong the moment the feature started working -- on the headless VPS this audience runs, ticking the toggle now does something real and falls back. COPY-CONSTRAINTS.md requires that in the same block as the feature, not a footnote. Verified: the row-parity guard between the site and docs/COMPARISON.md was mutation-tested -- reverting the document's row label while leaving the site's new one fails the build with the drifted row named. Restored; build passes. Claude-Session: https://claude.ai/code/session_01HeLrWaDmsNeeNSbHQfEofX --- docs/COMPARISON.md | 23 ++++++++++++++++++++--- web/src/pages/comparison.astro | 23 ++++++++++++++++++++--- web/src/pages/features.astro | 3 ++- web/src/pages/index.astro | 2 +- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index 035bbebc..03c73cdd 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -103,12 +103,12 @@ but it is not zero, and a selector's is. |---|---|---|---|---|---| | **Per-destination audio mix** from one multitrack ingest | No | No | No | No | No | | **Channel-level mix matrix** with per-cell gain | No | No | No | No | No | -| **Per-destination loudness target**, measured after routing | No | No | No | No | No | +| **Per-destination loudness TARGET you set** — I, LRA and TP, measured after routing | Filter only4 | No | No | No | No | | **Multitrack archive** — every ingest track preserved, stream-copied | No | No | Via OBS | Via OBS | Yes | | **Per-track stems** as 24-bit WAV or FLAC, segment-aligned to the master | No | No | No | No | No | | Track annotations — what each incoming track actually is | No | No | n/a | n/a | Unverified | | Typed SRT rejections — the publisher is told *why* it was refused | No | n/a | n/a | n/a | Unverified | -| A second audio mix to the same destination, from one ingest | No | n/a | n/a | n/a | Twitch Enhanced Broadcasting only; competitors unverified | +| A second audio mix to the same destination, from one ingest | No | n/a | n/a | n/a | Twitch Enhanced Broadcasting, and it needs a supported GPU; competitors unverified | Two cells say **Unverified** rather than "No" on purpose. MistServer's own recording row is a loss and is marked as one: a recording target with @@ -221,7 +221,7 @@ reconnect when a platform drops, show you whether it is working. | Video re-encoded per destination | **No** (`-c:v copy`) | Optional | Yes, server-side | Optional, as a process | | Recording | Multitrack, stream-copied | No built-in2 | Yes | Yes, tracks selectable | | Unified chat | Yes | No | Yes | No | -| Metrics / API | Prometheus + REST | REST | REST | REST | +| Metrics / API | Prometheus + REST | See4 | REST | REST | | Hardware encoding | NVENC, QSV, VA-API, VideoToolbox, AMF | Yes | n/a | NVENC | | Public release in the last 12 months | Yes3 | No3 | n/a, hosted | Yes3 | | Cost | Your hardware | Your hardware | Subscription | Your hardware | @@ -244,6 +244,23 @@ tagged v0.1.0 on 2026-07-31 and v0.6.0 on 2026-08-09 — which is the velocity o something pre-release with one maintainer, and is worth exactly that much. All three checked 2026-08-09. +4 **This row said "No" against Restreamer and that was wrong.** +Restreamer mounts a filter select per publication service, and `loudnorm` is one +of the filters it offers, so it does apply a per-destination loudness filter. +What it does not offer is a *target you set*: its control is an on/off checkbox +that emits the bare string `loudnorm` with no I, LRA or TP, so it takes FFmpeg's +defaults and an operator cannot ask for −16 LUFS rather than −24. That is the +real difference and it is narrower than the row used to claim. Corrected +2026-08-14 after a sweep read their source rather than their feature list — the +same correction the recording row needed in the other direction, and the second +time a cell here has asserted a competitor lacks something they ship. + +The metrics row above was wrong the same way and is now blank on their side: +Restreamer's own README advertises resource monitoring "optionally by +Prom-Metrics", datarhei Core documents Prometheus support, and it serves GraphQL +as well as REST — so "REST" understated them twice over and implied Prometheus +was ours alone. + --- ## See also diff --git a/web/src/pages/comparison.astro b/web/src/pages/comparison.astro index 83a985a6..04b67d1b 100644 --- a/web/src/pages/comparison.astro +++ b/web/src/pages/comparison.astro @@ -35,7 +35,12 @@ const shared = [ // Restreamer product simply does not put recording in front of you, which is // why the request exists. See docs/COMPARISON.md for the full footnote. ["Recording", "Multitrack, stream-copied", "No built-in", "Yes"], - ["Metrics / API", "Prometheus + REST", "REST", "REST"], + // Restreamer's cell said "REST" and implied Prometheus was ours alone. Its + // own README advertises resource monitoring "optionally by Prom-Metrics" and + // datarhei Core documents Prometheus support, and it serves GraphQL as well + // as REST. Neither half of the old cell survived contact with their docs, so + // the row now states what we do and says nothing about what they do. + ["Metrics / API", "Prometheus + REST", "", ""], ["Hardware encoding", "NVENC, QSV, VA-API, VideoToolbox, AMF", "Yes", "n/a"], ["Unified chat", "Yes", "No", "Yes"], ["Self-hosted", "Yes", "Yes", "No"], @@ -50,12 +55,24 @@ const shared = [ const wins = [ ["Per-destination audio mix from one multitrack ingest", "Yes", "No", "No"], ["Channel-level mix matrix with per-cell gain", "Yes", "No", "No"], - ["Per-destination loudness target, measured after routing", "Yes", "No", "No"], + // "No" against Restreamer was wrong as written: it mounts a filter select per + // publication service and loudnorm is one of the filters offered, so it DOES + // apply a per-destination loudness filter. What it does not offer is a + // configurable target -- its control is an on/off checkbox emitting the bare + // string `loudnorm`, taking FFmpeg's defaults for I, LRA and TP. That is the + // real difference, so the row states it and leaves their cell alone. + ["Per-destination loudness TARGET you set — I, LRA and TP, measured after routing", "Yes", "", ""], ["Multitrack archive — every ingest track preserved, stream-copied", "Yes", "No", "No"], ["Per-track stems as 24-bit WAV or FLAC, segment-aligned", "Yes", "No", "No"], ["Track annotations — what each incoming track actually is", "Yes", "No", "No"], ["Typed SRT rejections — the publisher is told why it was refused", "Yes", "No", "n/a"], - ["A second audio mix to the same destination, from one ingest", "Yes", "—", "—"], + // "Yes" on its own was the wrong shape for this cell, and wiring the feature + // is what made it matter: Twitch refuses Enhanced Broadcasting without a + // supported GPU, so on the headless VPS this audience actually runs, ticking + // the toggle gets a quiet fallback to the ordinary ingest. A bare "Yes" in a + // comparison table is the worst place to omit that. COPY-CONSTRAINTS.md says + // to state it in the same block that announces the feature, not a footnote. + ["A second audio mix to the same destination, from one ingest", "Yes — Twitch Enhanced Broadcasting, needs a GPU", "—", "—"], ["Runs on hardware you control, no per-destination pricing", "Yes", "Yes", "No"], ]; diff --git a/web/src/pages/features.astro b/web/src/pages/features.astro index 92adac53..297a6092 100644 --- a/web/src/pages/features.astro +++ b/web/src/pages/features.astro @@ -28,7 +28,8 @@ const sections = [ body: [ "Twitch Enhanced Broadcasting negotiates a second audio track alongside the live one. polyemesis builds both from the same ingest, in one filter graph, and sends them over one connection — the live mix with the music bed, and a VOD track without it. No second upload, no second encode of the picture.", "A single video rendition is enough. Multitrack video is not a precondition for the second audio track, which is the part most people expect to be required — the feature is named for it and does not need it.", - "It needs a GPU. Twitch refuses the negotiation outright if the encoder reports none, so this is the one thing here a headless server cannot use. Every other destination still receives its own single mix, exactly as before. The picture is still copied rather than re-encoded; the second track is a second AAC encode, which is cheap and is not free.", + "It needs a GPU, and it needs you to say so. Twitch refuses the negotiation outright if the request reports no GPU, so this is the one thing here a headless server cannot use. polyemesis does not guess at your hardware — you declare it in Settings, because Twitch validates the inventory and a fabricated one is refused by name. Leave it blank and the destination publishes to the ordinary Twitch ingest and tells you once, which is the right behaviour on the majority of installs rather than an error.", + "Every other destination still receives its own single mix, exactly as before. The picture is still copied rather than re-encoded; the second track is a second AAC encode, which is cheap and is not free.", ], shot: "02-routing.png", caption: "The archive mix is per destination, and off unless asked for.", diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index 9e483627..b598d974 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -201,7 +201,7 @@ const features = [ polyemesis ingests once and lets you pick, per destination, which tracks get summed into the stereo feed that destination receives. YouTube gets the clean mix. Your local archive keeps everything. Your - Discord restream keeps the mic hot. On Twitch it goes one step further — the broadcast can carry the music bed while a separate archive track leaves it out, from the same upload. + Discord restream keeps the mic hot. On Twitch it goes one step further — the broadcast can carry the music bed while a separate archive track leaves it out, from the same upload. That last one is Twitch Enhanced Broadcasting, and it needs a supported GPU: Twitch refuses to negotiate it otherwise, so on a headless server the destination publishes to the ordinary ingest instead and says so.