refactor(hls): critico-driven hardening of fase 3.2

Addresses items raised by the multi-agent code review of the 0.9.9
HW accel + first-start work:

- EncoderProfile now carries DecodeHwAccel so the demuxer `-hwaccel`
  flag and the encoder argv derive from a single resolved profile.
  Adding a new backend can no longer leave the two switches out of
  sync.
- VAAPI no longer passes `-hwaccel_output_format vaapi`. That option
  pinned decoded frames to GPU memory, but the filter chain (scale,
  format, setparams) runs on CPU and would fail with "impossible to
  convert between formats". Frames now decode HW + flow on CPU; the
  encoder uploads back to GPU. Pre-existing bug, never reported because
  no one had VAAPI auto-detected in practice.
- readyMax field comment + name: documented that it's a COUNT
  (segments ready), not an index. The semantics were correct but the
  comment read "highest index" which made `idx < readyMax` look like
  an off-by-one to reviewers.
- probe_cache background janitor: 5-minute sweeper that drops expired
  entries even when no lookup retouches the key. Lookup-only eviction
  was fine for small libraries but unbounded for users who browse and
  abandon thousands of files within a TTL window. Lazy + sync.Once.
- probe_cache TTL eviction now re-checks under the write lock so a
  concurrent re-insert isn't accidentally evicted.
- probe_cache size-change test now Chtimes the file back to its
  original mtime so only `size` differs between store and lookup
  keys — properly exercises the size-check path.
- New TestProbeCache_SweepDropsExpired covers the janitor sweep.
- CHANGELOG: backfilled missing compare links 0.6.4 → 0.9.9.
- Stale "line ~1119" reference in VideoToolbox comment dropped; the
  bitrate block moved a few lines and the comment was already wrong.
This commit is contained in:
Deivid Soto 2026-05-27 11:15:44 +02:00
parent 0f4ad67827
commit bf8ed0d928
5 changed files with 181 additions and 47 deletions

View file

@ -73,15 +73,24 @@ func TestProbeCache_SizeChangeInvalidates(t *testing.T) {
if err := os.WriteFile(path, []byte("aaaaa"), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
originalMtime := time.Now().Add(-1 * time.Hour) // stable, in the past
if err := os.Chtimes(path, originalMtime, originalMtime); err != nil {
t.Fatalf("chtimes original: %v", err)
}
probe := &StreamProbe{VideoCodec: "h264", DurationSec: 100}
storeProbeCache(path, probe)
// Truncate file to a different size + reset mtime to the original (to
// isolate the size-check path). Stat picks up new size immediately.
// Truncate to a different size, then reset mtime to the original so
// only `size` differs between store and lookup keys — isolates the
// size-check path. Without the Chtimes, WriteFile bumps mtime and the
// test would pass via mtime invalidation regardless of size logic.
if err := os.WriteFile(path, []byte("a"), 0o644); err != nil {
t.Fatalf("rewrite: %v", err)
}
if err := os.Chtimes(path, originalMtime, originalMtime); err != nil {
t.Fatalf("chtimes restore: %v", err)
}
if _, ok := lookupProbeCache(path); ok {
t.Fatal("expected MISS after size change")
@ -152,3 +161,42 @@ func TestProbeCache_StoreNonexistentNoOp(t *testing.T) {
t.Fatalf("expected 0 entries; got %d", ProbeCacheSize())
}
}
func TestProbeCache_SweepDropsExpired(t *testing.T) {
ResetProbeCache()
t.Cleanup(ResetProbeCache)
dir := t.TempDir()
// Two entries: one expired, one fresh.
expiredPath := filepath.Join(dir, "old.mkv")
freshPath := filepath.Join(dir, "new.mkv")
if err := os.WriteFile(expiredPath, []byte("a"), 0o644); err != nil {
t.Fatalf("write expired: %v", err)
}
if err := os.WriteFile(freshPath, []byte("b"), 0o644); err != nil {
t.Fatalf("write fresh: %v", err)
}
now := time.Now()
fiExp, _ := os.Stat(expiredPath)
fiFresh, _ := os.Stat(freshPath)
probeCacheMu.Lock()
probeCache[probeCacheKey{path: expiredPath, mtime: fiExp.ModTime().UnixNano(), size: fiExp.Size()}] = probeCacheEntry{
probe: &StreamProbe{VideoCodec: "h264"},
expires: now.Add(-1 * time.Minute), // expired
}
probeCache[probeCacheKey{path: freshPath, mtime: fiFresh.ModTime().UnixNano(), size: fiFresh.Size()}] = probeCacheEntry{
probe: &StreamProbe{VideoCodec: "h264"},
expires: now.Add(10 * time.Minute), // fresh
}
probeCacheMu.Unlock()
removed := sweepProbeCache(now)
if removed != 1 {
t.Fatalf("expected 1 expired entry removed; got %d", removed)
}
if ProbeCacheSize() != 1 {
t.Fatalf("expected 1 fresh entry kept; got %d", ProbeCacheSize())
}
}