Commit Graph
347 Commits
Author SHA1 Message Date
timothyandtimothy ed8b602445 feat(735): bound the numeric FFmpeg profile fields with a 422, and expose readrate pacing (#847)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 11s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m41s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m23s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m23s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
2026-08-26 22:05:38 +00:00
timothyandClaude Opus 5 dd7b58232c fix(691): revert entity-level null guard, guard read sites instead
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 16s
PR Gates / Docs update reminder (pull_request) Successful in 59s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m54s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
review-verdict/h10 Review-verdict: MERGEABLE @ dd7b582 (base: main)
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 22m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Review verdict / Set review-verdict status (pull_request) Successful in 5s
PR Gates / decisions lifecycle (pull_request) Successful in 13s
PR Gates / Script tests (pytest) (pull_request) Successful in 46s
The prior commit (a4700185b) made SongMetadata.Artists/AlbumArtists
coalesce null to [] via backing-field getters, reasoning that EF
Core's PreferField access mode never observes the getter. Adversarial
review disproved this on real TvContext/SQLite: a single read of
.Artists on a TRACKED entity mutates the backing field through the
getter, flips the entity to Modified, and the next SaveChanges writes
[] over what was a NULL column -- silent data loss waiting on the
first tracked reader (today all readers happen to be AsNoTracking).

This also reversed docs/decisions/records/api/selection-projection-include-chain.md
(#671) without the doc update CLAUDE.md requires; #691 is that
record's own "sweep by FIELD" follow-up, so it should follow the
record, not contradict it.

Revert SongMetadata.cs to plain auto-properties (byte-identical to
origin/main, BOM still stripped per the #311 gate). Guard the read
sites instead, per the #671 convention (Optional(...).Flatten(),
matching Playouts/Mapper.cs and MediaItems/Mapper.cs):

- SongVideoGenerator.cs: hoist `artists`/`albumArtists` locals once
  near the top of the metadata loop instead of repeating the guard at
  each of the six former call sites.
- MediaCollectionRepository.cs (GroupIntoFakeCollections): guard the
  two AlbumArtists reads at lines ~1147/~1160 that #691 never named --
  dropping the entity-level fix without these would trade one bug for
  two.

Verified RED per guard by removing only the Optional(...).Flatten()
clause (not the whole file): the artists local throws
ArgumentNullException at SongVideoGenerator.cs:88, the albumArtists
local at :89 (List.ToList() on a null IList<string> source -- same
loaded-gun shape the review demonstrated, precise exception type is
ArgumentNullException rather than NullReferenceException since the
throw site is Enumerable.ToList's null-source check). Restored both;
existing SongVideoGeneratorTests still pass. Full ErsatzTV.Core.Tests:
685 passed (1 pre-existing skip), ErsatzTV.Tests: 1996 passed (4
pre-existing skips), 0 failures in each. No EF model drift
(`dotnet ef migrations has-pending-model-changes` reports none).
`dotnet format --verify-no-changes` on the three touched files exits
0.

Refs #691

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:39:52 +02:00
timothy edf8be4b5e fix(510): re-review round — make two review-added tests actually falsifiable
PR Gates / Docs update reminder (pull_request) Successful in 18s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 18s
review-verdict/h10 Awaiting review verdict for edf8be4
PR Gates / decisions lifecycle (pull_request) Successful in 20s
Review verdict / Set review-verdict status (pull_request) Successful in 3s
PR Gates / Script tests (pytest) (pull_request) Failing after 38s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m40s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m20s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review of the previous fix commit found that two tests added to close
round-1 findings could not fail. Both verified before fixing:

1. Missing_But_Named_Custom_Playout_Item_Watermark_Should_Not_Fall_Through
   gave the channel-level fallback the SAME missing custom path as the
   playout-item watermark, so a wrongly-widened guard would have fallen
   through to a fallback that also resolved to None -- the assertion held
   either way. The fallback is now an independently resolvable ChannelLogo
   whose cached file exists, so a fall-through returns it and fails the test.
   Added the matching positive control (blank -> falls through and DOES
   return that logo), so the pair shows the guard distinguishes blank from
   unresolvable instead of both landing on None.

2. Deco_With_One_Valid_And_One_Missing_Watermark... asserted a filtered list
   length while the routing claim the decision record cited it for lives in
   FFmpegLibraryProcessService.CanUseFFmpegNativeWatermark, which the test
   never called. It now calls the real predicate.

Also, three wrong claims of my own:

3. The Resource arm comment said "nothing in the app writes a Resource
   watermark to the database". False -- CreateWatermarkHandler and
   UpdateWatermarkHandler persist whatever ImageSource the request names, so
   a Resource watermark IS creatable through the API, always with
   Image = null. That is precisely why the new null guard is load-bearing,
   so the comment was arguing for its own removal.

4. "One resolver and no per-caller policy" contradicted the surviving
   playout-item blank-Custom fall-through documented a few lines later.
   Reworded in both the record and the XML docs: one resolver, and exactly
   one piece of per-caller policy which lives in the CALLER.

5. The record's "12 of 18 new tests fail pre-fix" was stale. Re-measured
   against the final fixture: 19 of 29. The other 10 pass both ways by
   design because they pin preserved behavior, which the record now says
   explicitly rather than leaving the gap to be read as weakness.

Removed the vacuous generated-URL test rather than keeping it with an honest
comment -- an empty list trivially contains no URL, so it implied coverage it
never had. Its assertion is folded into the sibling test that has a real
arrangement.

Gates: 2772 tests green across 5 projects, dotnet format exit 0, no BOMs,
decisions-validate OK, live-E2E re-run against this binary (0 changed pixels,
nameplate absent, warning emitted).

refs #510
2026-07-26 21:29:11 +02:00
timothy 1a7f15fb27 fix(510): address independent review — Resource null guard, honest routing claim
Two independent reviews (cross-family Codex + cold Opus) both returned
BLOCKED. Findings, all verified against source before acting:

1. Resource arm could throw ArgumentNullException (Codex, Medium). Making the
   channel/global Resource arm reachable exposed that CreateWatermarkHandler
   and UpdateWatermarkHandler write `Image = null` for EVERY non-Custom
   watermark, so an API-created Resource watermark reached
   Path.Combine(folder, null). Added the blank/null guard the arm never had.
   This was live at the playout-item level too, not just newly-reachable code.

2. "Routing is unaffected" was false (Codex, Low but sharp). The predicate is
   unchanged, but CanUseFFmpegNativeWatermark also tests Count == 1, and
   dropping an unresolvable watermark shortens the list. A deco with one valid
   and one missing permanent watermark now routes ffmpeg-native where it
   previously routed to the graphics engine. Intended, but observable -- so it
   is documented and pinned by a test rather than claimed away.

3. "Exactly one resolver" over-claimed (Opus, High). True of the selector, not
   the application: the song-progress overlay is built as a WatermarkOptions
   directly by the streaming and troubleshooting handlers, unchecked, and can
   still hand ffmpeg a nonexistent -i. Pre-existing; scoped the claim in the
   record and channels.md and filed #653.

4. Undeclared crash->degrade change (Opus, Medium). Channel/global Custom had
   no blank-image guard, so a cleared image hit ImageCache's fileName[..2] and
   threw out of stream startup. Now declared in the record and tested.

5. Contradictory rule text (Opus, Medium) -- the catalog one-liner said
   "always no bug" while the body documents the playout-item fall-through
   exception. Qualified; catalog regenerated.

6. History was wrong in both the record and the XML docs: the three precedence
   levels did NOT all check every source -- channel/global had no Resource arm
   and threw. Corrected.

Tests: 30 in the fixture now (was 18). New coverage for the preserved
blank-Custom fall-through (to channel AND to global), the complement case
(missing-but-named must NOT fall through), null/blank Resource, and the
valid+missing routing case. 17 of 24 failed against the pre-fix resolver
before this round; the fixture stays mutation-sensitive.

Also: hoisted the mock-filesystem Initialize() out of its loop so a
multi-file case cannot silently seed only the last file, and marked the
generated-URL test honestly as redundant-by-construction rather than
claiming independent coverage.

The decision record is now 81 prose lines, over the 60-line ceiling. Declared
as a legitimate decline per docs.corpus-size-signal: the length is the review
findings above, each a distinct fact, not redundancy.

refs #510 #652 #653
2026-07-26 21:12:13 +02:00
timothy 9cbe70e486 fix(510): one watermark resolver for all four attachment points
WatermarkSelector resolved watermarks in two places with two policies. The
three precedence levels (playout item, channel, global) existence-checked
every image source and degraded to None; the deco path had its own copy of
the same switch that returned whatever path it computed, unchecked. So one
channel could disagree with itself about whether an on-screen bug rendered,
based only on how the watermark was attached.

#502 deferred this here but scoped it to ChannelLogo. It was never
ChannelLogo-only: the deco path skipped the existence check for Custom and
Resource too. Extract one ResolveWatermark used by all four sites.

Severity is not cosmetic. A dead LOCAL path is not harmlessly skipped --
CanUseFFmpegNativeWatermark hands a single permanent watermark to ffmpeg as
a bare -i argument and excludes only URLs, so the deco path could hand
ffmpeg a nonexistent input file.

The generated-initials nameplate was real: a live-E2E on a real transcoded
frame confirmed it composited via the deco path (/iptv/logos/gen is on
ArtworkController, which has no auth filter, so the container-internal
self-fetch succeeded). The #502-era comment claiming "it has never rendered
here" was wrong, and the new record says so. It is still removed: serving it
means an HTTP fetch inside stream startup, which graphics.channel-logo-caching
(#525) eliminated for logos, and it depends on #1's hardcoded localhost.
Reviving it by caching the image instead is #652.

Measured blast radius on prod: 0 Deco rows, 0 DecoWatermark rows, all 43
channels have logo artwork -- no rendered output changes.

Preserved deliberately: a playout-item Custom watermark with a blank image
still falls THROUGH to the channel/global watermark; unifying resolution must
not change which watermark wins. Routing is untouched.

Strict improvement: the channel and global arms previously threw
NotSupportedException on a Resource watermark; they now resolve it. The
default arm still throws so a new image source fails loudly.

Tests: 18 new cases including a positive control and 8 deco-vs-channel parity
cases. 12 of the 18 fail against the pre-fix resolver, which is what proves
they are load-bearing rather than vacuous.

fixes #510
2026-07-26 20:54:00 +02:00
timothyandtimothy 0c063c23fb harden(421,559): percent-encode access_token in IPTV URLs, redact from logs, no-store on tokened manifests (#574)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 7m45s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 14m19s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 18m27s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m10s
Co-authored-by: Timothy <timothy.look@gmail.com>
Co-committed-by: Timothy <timothy.look@gmail.com>
2026-07-23 16:30:59 +00:00
timothy d6652dbe13 feat(74): selector emits channel-level graphics elements as a base layer 2026-07-22 22:11:46 +02:00
timothy 36375157ad feat(525): render path no longer fetches a URL logo; degrades to no bug 2026-07-21 13:13:21 +02:00
timothy 66448e1abf fix(502): correct the deco-scoping claim, extract + test the routing guard
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 13s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 14s
Build CI Toolchain Image / Build & push CI image (push) Successful in 1m39s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 16s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m56s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m48s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review found a documentation defect, not a code one: both
docs/decisions.md and the WatermarkSelector comment asserted the deco path
was unaffected by this change. That is true of the *resolution* half and
false of the *routing* half. SelectWatermarks puts deco-derived options
into the same list the routing guard filters, so a deco watermark whose
resolved path is a URL is rerouted to the graphics engine too — including
the generated-initials localhost URL, which only the deco path still emits
and which plausibly rendered through ffmpeg before.

That reroute is intended (routing by what the path is beats routing by
provenance, which would drift), so the fix is to say so accurately rather
than to narrow the guard. Also records the accepted per-frame cost
asymmetry the entry previously argued on correctness grounds alone.

The guard is extracted as CanUseFFmpegNativeWatermark so it can be tested
directly — review's highest-value gap was that the half of the fix which
decides whether pixels appear had no automated coverage, only the one-off
live E2E. Nine cases pin it, including the localhost-fallback reroute.

Both deferrals now point at real issues instead of an unverifiable
"tracked separately": #510 (deco vs precedence-level missing-logo policy)
and #511 (remote-fetch hardening — timeout, size cap, redirects, pooling,
caching, SSRF).

Also pins scheme-case insensitivity in the selector.
2026-07-20 23:00:27 +02:00
timothy f9bd245158 fix(502): render the on-screen bug for external-URL channel logos
A channel whose logo is an external URL never rendered a watermark, even
with an ImageSource=ChannelLogo watermark attached. WatermarkSelector
resolved the URL correctly and then existence-checked it on the
filesystem — File.Exists("https://…") is always false — so all three
precedence levels (playout item, channel, global) logged "Channel logo
no longer exists" and returned None. The channel editor advertises the
URL as winning over an uploaded logo, which was true for the guide
listing and silently false for the bug.

External artwork passes through rather than being downloaded into the
image cache: that is already the convention everywhere else (M3U, XMLTV,
SPA JSON all emit the raw URL), no fetch->SaveArtworkToCache glue exists,
and the render path does not need it — ImageElementBase.LoadImage already
fetches an http(s) path with HttpClient and decodes it for real pixel
dimensions.

A remote-URL watermark is therefore forced onto the graphics engine
instead of the ffmpeg-native shortcut, which would otherwise hand the URL
to ffprobe and ffmpeg as a bare -i argument, putting an unbounded network
fetch inside stream startup.

The three gated precedence levels now share one ChannelLogoWatermarkOptions
helper — the triplicated block is what let the defect exist three times
over. Scope held narrow: the generated-initials localhost fallback (#1)
stays disabled behind an explicit comment and a scope-guard test, and the
deco path keeps its own long-standing unchecked policy.

Verified by live-E2E against a real channel playout with an external-URL
logo: origin/main renders 0 logo pixels and logs the "no longer exists"
warning verbatim; this branch renders the logo in the expected region.

Whitespace-only reformatting in FFmpegLibraryProcessService.cs is the
fix-as-you-touch format gate on pre-existing violations, plus a BOM strip.

fixes #502
2026-07-20 23:00:27 +02:00
timothyandClaude Opus 4.8 264b17516d style(498): normalize pre-existing whitespace in touched files (#311 fix-as-you-touch)
Whitespace-only (git diff -w is empty); the #311 format job checks whole
touched files, and these two legacy files carried pre-existing violations
never caught before (no PR had touched them since the gate landed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:54:13 +02:00
timothy 5edc45ab76 feat(498): pass QsvPreferNativeDecoder from profile into FFmpegState 2026-07-20 21:54:13 +02:00
timothy 37674d6519 test(472): name the stale-playlist test for what it actually pins
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 46s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m22s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Third-round review caught that
Stale_Playlist_Guard_Should_Take_Precedence_Over_An_Otherwise_Valid_Three_Way
describes an impossible case: ThreeWay requires
processLaunched <= playlistExists, which is exactly the negation of the
guard condition, so the guard can never preempt a ThreeWay. What the
test really pins is precedence over the progress branches
(TwoWayLateProgress) — still the ordering that matters.

That is the same "rationale misstates the mechanism" defect the previous
commit existed to fix, landed inside the fix itself. Renaming rather
than leaving a test whose name teaches the next reader something false.

Also broadens the escape-hatch caveat: a stale playlist that slips past
the guard lands as TwoWay more often than ThreeWay, since FFmpeg has
usually not reported progress that early.

Test name and comments only; no logic change.
2026-07-19 23:33:28 +02:00
timothy 58b93d3e3d docs(472): correct the stale-playlist rationale; pin guard precedence
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 35s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 20s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 20s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m42s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review of the fix commit returned MERGEABLE with one Medium: the
comment justifying the stale-playlist guard misstated the mechanism, in
three places. It claimed Run "warns about a non-empty transcode folder
but does not delete it" — but StartFFmpegSessionHandler.FolderMustBeEmpty
calls EmptyFolder BEFORE the worker spawns, and Run's finally empties it
again. Verified directly rather than taken on the reviewer's word.

The real residual path is EmptyFolder FAILING: it swallows every
exception into a LogWarning and continues. Say that instead.

On a PR whose entire value is that the numbers mean what they say, a
rationale comment that misstates the mechanism is the same class of
defect the PR exists to prevent, so it does not get to ship as a nit.

Also documents that the guard is best-effort rather than a proof (if the
wipe failed, before-or-after-launch is a scheduling race, so an unlucky
sample can still slip through as an implausibly fast ThreeWay), and adds
the test the reviewer noted was missing: guard precedence over an
otherwise-valid ThreeWay.

Comments and one test only; no logic change.
2026-07-19 23:21:03 +02:00
timothy 757fb76151 fix(472): review fixes — honest bucket boundaries, stale-playlist guard
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m29s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 26s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (pull_request) Has been cancelled
From the cold adversarial review of the initial diff. No blockers were
found; these address what the numbers MEAN, which is the whole point of
an instrumentation change.

- The buckets span the worker's Run entry, not the startup stopwatch, so
  prep overlaps the tail of `setup`. Rather than let the log imply an
  invariant it does not satisfy, say "spans runEntry" in the line, spell
  it out in the doc comment, and rename the test that had codified the
  false `sum == startup` claim.
- Guard `processLaunched > playlistExists` -> Unavailable: a stale
  live.m3u8 from a previous session (Run warns about a non-empty
  transcode folder but does not delete it) would otherwise yield a
  plausible-looking sample whose prep exceeds the measured phase.
- Split the two-way fallback into TwoWay vs TwoWayLateProgress. They are
  different stories about the pipeline and discriminating stories is
  what this issue is for.
- Document the 100ms playlist-poll quantization (it lands entirely in
  firstGop, the smallest bucket) and the first-process-failed case where
  ffmpegInit spans a retry.
- Short-circuit the per-line timestamp call; static readonly Unavailable.
- Tests for the new guard, progress-before-launch, and boundary equality
  (so tightening >= to > later cannot pass silently).
2026-07-19 23:05:14 +02:00
timothy d91d6ee1ed feat(472): sub-split the HLS cold-start startup phase
#350's measurement showed `startup` is 81% of tune-in latency and carries
100% of its variance, while remaining one opaque bucket spanning FFmpeg
spawn -> input open/probe -> encoder init -> first GOP. Two hypotheses
survive that measurement (NFS input open vs VAAPI init under contention)
and they need opposite fixes, so split before optimizing.

Adds `prep` (ErsatzTV-side work before FFmpeg exists) + `ffmpegInit`
(launch -> first `-progress` output) + `firstGop` (-> live.m3u8 exists)
to the existing Information-level cold-start line.

The pipeline runs `-loglevel error -nostats -hide_banner`, so a healthy
FFmpeg writes nothing to stderr; the `-progress` stream is the only
zero-cost milestone available and `ffmpegInit` therefore still lumps
input-open with encoder-init. That limit is documented rather than
papered over, and the split degrades to the two-way form #472 accepts
when no progress arrives before the playlist.

Log-only: no transcode behavior change, no new endpoint or config knob.

fixes #472
2026-07-19 22:49:07 +02:00
timothyandClaude Opus 4.8 6c8c7feeba feat(350): instrument HLS cold-start latency (phase split + feature flags)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m27s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m10s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13m24s
Adds one Information-level structured log per HLS tune-in cold-start so the
real driver breakdown can be measured on prod before optimizing the transcode
pipeline (measure-before-optimize). Log-only; no transcode behavior change.

- WaitForPlaylistSegments returns a PlaylistSegmentsResult: Phase A (process
  startup -> playlist exists) vs Phase B (segment fill), segments reached,
  deadline-expired.
- StartFFmpegSessionHandler emits one summary: total = setup + startup + fill,
  plus cleanly-detectable feature flags (subtitle burn-in, hwaccel family).
- ColdStartFeatures: pure, unit-tested args->features helper (14 cases).

Watermark / HDR->SDR / image-subtitle burn-in are deliberately not flagged
(all reduce to overlay= in the args, indistinguishable); the full ffmpeg
arguments remain available at Debug.

Refs #350 (instrumentation slice; optimization deferred pending real data).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 15:30:56 +02:00
Jason DoveandGitHub 0d301df5e8 remove external dependencies (bugsnag, trakt) (#2840)
* remove bugsnag

* remove trakt client id (that will expire)
2026-02-26 10:43:48 -06:00
Jason DoveandGitHub 875069b927 fix stream seek value in graphics engine (#2838) 2026-02-23 14:54:28 -06:00
Jason DoveandGitHub 0c30c47ba9 nvidia - decode 10-bit h264 in software (#2833)
* output progress/speed even when copying video

* nvidia - decode 10-bit h264 in software

* fixes

* fix tests
2026-02-20 23:00:15 -06:00
Jason DoveandGitHub 08cbf59527 lower gop size and keyframe interval (#2832)
* lower gop size and keyframe interval

* update changelog

* fix build using latest dotnet sdk

* fixes
2026-02-19 13:35:27 -06:00
Jason DoveandGitHub 3e3bfbd5f5 use heuristic to work around some qsv av desync cases (#2829)
* check for multiple h264 profiles using qsv decoding

* fix build

* update changelog

* pass cancellation token
2026-02-16 12:37:40 -06:00
Jason DoveandGitHub c0b8ff1a06 generate slug instead of probing and transcoding resource (#2824)
* generate slug instead of probing and using slug resource

* refactor

* more fixes
2026-02-15 09:46:07 -06:00
Jason DoveandGitHub f47134d2d0 log warnings when transcoding speed is potentially insufficient (#2808)
* refactor parsing ffmpeg progress/speed

* log warnings when transcoding speed is potentially insufficient

* dont log progress on hls direct; fix tests
2026-02-03 08:49:07 -06:00
Jason DoveandGitHub e10b28bc0b add normalization options (#2802)
* add new fields to database

* update editor

* audio and video normalization settings appear to work

* implement optional color normalization

* fix transcoding tests

* update changelog
2026-01-26 23:43:56 -06:00
Jason DoveandGitHub 35d24ffea6 cleanup artwork cache folder (#2779)
* cleanup artwork cache folder

* fixes

* ignore watermarks that no longer exist on the file system
2026-01-16 13:38:31 -06:00
Jason DoveandGitHub ccb917d0df add ffmpeg profile pad mode (#2775)
* add ffmpeg profile pad mode

* update changelog
2026-01-15 09:39:45 -06:00
Jason DoveandGitHub e167c9318c fix failing unit tests (#2772) 2026-01-14 06:47:34 -06:00
Jason DoveandGitHub b72d150775 add day_of_week to channel stream selector content_condition (#2766) 2026-01-10 11:28:14 -06:00
Jason DoveandGitHub effb96a2c2 alternate schedule and template consistency (#2757)
* refactor classic and block schedules to use same alternate schedule selector

* handle start year and end year

* add migrations for classic and block schedules

* allow editing block template start and end year

* add tests that include years

* add date range editing to classic (alternate) schedules

* fix running tests locally

* restore media files load; needed for local folder scanners

* update changelog

* feedback
2026-01-06 12:51:07 -06:00
Jason DoveandGitHub 0af81ad839 add target loudness to ffmpeg profile (#2727)
* add target loudness to ffmpeg profile

* fix filter
2025-12-19 14:46:17 -06:00
Jason DoveandGitHub 99b8c56a31 rework fallback filler (#2719)
* fallback fixes

* use hardware encoding for fallback filler

* rework fallback filler

* fixes
2025-12-13 09:02:48 -06:00
Jason DoveandGitHub 54606c76f9 framerate improvements (#2692)
* framerate improvements

* fixes
2025-12-02 12:20:09 -06:00
Jason DoveandGitHub ec0d8ea6ac work around sequential schedule validation limit (#2655)
* remove readalltext

* remove unused method

* remove fileexists

* remove folderexists

* remove readalllines

* remove fake local file system

* show playlist name in playout build errors

* add basic sequential schedule validator tests

* work around sequential schedule validation limit
2025-11-24 12:08:43 -06:00
Jason DoveandGitHub d88e721d2f optimize database calls related to search index (#2645) 2025-11-13 13:27:37 -06:00
Jason DoveandGitHub 6603500132 fix content_total_duration in graphics engine (#2643) 2025-11-12 20:07:54 -06:00
Jason DoveandGitHub 42b35f7aae add channel playback troubleshooter (#2641)
* fix motion graphics loop when seeking

* add channel playback troubleshooter

* fix errors
2025-11-12 13:21:18 -06:00
Jason DoveandGitHub 8b18f2a304 expose arbitrary epg data to graphics engine (#2633) 2025-11-11 12:41:45 -06:00
Jason DoveandGitHub 1e0bba0dc6 allow custom song background images (#2632)
* allow custom song background images

* allow custom missing album art
2025-11-11 10:40:45 -06:00
Jason DoveandGitHub e2d8dee8cd artwork updates (#2624)
* add new logo svg; replace favicons

* replace background

* allow error/offline background customization
2025-11-10 16:01:47 -06:00
Jason DoveandGitHub b9a73226a8 fix interlaced check (#2621)
* fix interlaced check

* reset any incorrect interlaced probe results
2025-11-10 08:39:44 -06:00
Jason DoveandGitHub d0505cd5c5 add better check for interlaced content (#2620) 2025-11-10 06:39:04 -06:00
Jason DoveandGitHub dd9317e3e8 fix mpegts script on windows (#2614) 2025-11-08 10:09:56 -06:00
Jason DoveandGitHub 5083e748ed fix mpegts script loading (#2610) 2025-11-07 13:30:37 -06:00
Jason DoveandGitHub 053b3cd1d7 add mpegts script system (#2609)
* add basic mpegts script

* use custom mpegts script

* update changelog
2025-11-07 13:20:17 -06:00
Jason DoveandGitHub d2cbfcb79a fix error screen generation (#2594) 2025-11-02 10:41:56 -06:00
Jason DoveandGitHub 3b254735e6 fix transcoding tests; fix vaapi subtitle crop (#2568)
* fix transcoding tests using text subtitles

* fix vaapi picture subtitle overlay with crop

* more test improvements
2025-10-26 08:51:24 -05:00
Jason DoveandGitHub 1f8834c280 block playout fixes; hls direct fixes (#2566)
* fix block playout builder with empty collection

* fix hls direct when selecting audio

* allow embedded subtitles with hls direct
2025-10-25 06:35:56 -05:00
Jason DoveandGitHub a47510fef3 add aac (latm) audio format (#2561)
* add aac (latm) audio format

* update changelog
2025-10-23 15:56:13 -05:00
Jason DoveandGitHub 2ef2b0299a switch back from fmp4 to ts segments (#2554)
* restore pts offset calculation

* use ts segments again

* update changelog
2025-10-21 12:17:05 -05:00