Reduces first-segment latency on cache MISS so the player doesn't sit on
"preparando sesión". Three independent levers:
1. ProbeFile memoised by (path, mtime, size) for 30 min — second play of
the same source skips ffprobe (1-3 s on 50+ GB MKVs).
2. HLS encoder presets biased for latency over quality:
- libx264 default veryfast → superfast (~15-20% faster, marginal
quality loss at 5-25 Mbps target bitrates).
- NVENC: -preset p4 -tune hq → -preset p3 -tune ll. First-segment
~0.8 s on RTX-class GPUs (was ~1.5 s).
- QSV: -preset medium → -preset veryfast (keeps look_ahead=0).
- VideoToolbox: adds -realtime 1 (was unset). Bitrate args still
drive rate control; -q:v dropped to avoid the silent conflict
where ffmpeg ignored it under -b:v.
3. Per-session log surfaces encoder + accel + preset so "first-start
was slow" complaints can be triaged from the journal alone.
Diagnostic helpers (DetectHWAccelDiagnostic + HWAccelDiagnostic) added
for future wiring into daemon startup / agent register; users today can
already inspect via `unarr probe-hwaccel`.
Web: AgentsTab profile page now shows the agent's chosen encoder
(amber if software libx264, green if HW) plus the transcode-resolution
cap. Hidden for pre-0.9.9 agents that haven't reported hwAccel.
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
package engine
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// probeCacheTTL is how long a cached probe stays usable. The cache key
|
|
// already incorporates mtime + size, so the TTL is a defense against
|
|
// runaway memory growth from stale paths, not a freshness guarantee — a
|
|
// rename + recreate at the same inode (rare) would still be caught by the
|
|
// mtime delta.
|
|
const probeCacheTTL = 30 * time.Minute
|
|
|
|
type probeCacheEntry struct {
|
|
probe *StreamProbe
|
|
expires time.Time
|
|
}
|
|
|
|
type probeCacheKey struct {
|
|
path string
|
|
mtime int64 // ModTime().UnixNano()
|
|
size int64
|
|
}
|
|
|
|
var (
|
|
probeCacheMu sync.RWMutex
|
|
probeCache = make(map[probeCacheKey]probeCacheEntry)
|
|
)
|
|
|
|
// lookupProbeCache returns the cached StreamProbe for the given path if its
|
|
// mtime + size still match the value recorded at insert time, AND the cache
|
|
// entry hasn't expired. Any stat failure / mismatch returns (nil, false) so
|
|
// the caller falls through to a fresh ffprobe run.
|
|
func lookupProbeCache(path string) (*StreamProbe, bool) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
key := probeCacheKey{
|
|
path: path,
|
|
mtime: fi.ModTime().UnixNano(),
|
|
size: fi.Size(),
|
|
}
|
|
probeCacheMu.RLock()
|
|
entry, ok := probeCache[key]
|
|
probeCacheMu.RUnlock()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if time.Now().After(entry.expires) {
|
|
probeCacheMu.Lock()
|
|
delete(probeCache, key)
|
|
probeCacheMu.Unlock()
|
|
return nil, false
|
|
}
|
|
return entry.probe, true
|
|
}
|
|
|
|
// storeProbeCache stashes a fresh probe result under the (path, mtime, size)
|
|
// key. A subsequent ffprobe-skipping HIT requires the file to still have the
|
|
// same mtime + size — anything else (re-encoded, renamed+recreated at the
|
|
// same path, truncated) misses and triggers a re-probe.
|
|
func storeProbeCache(path string, probe *StreamProbe) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
key := probeCacheKey{
|
|
path: path,
|
|
mtime: fi.ModTime().UnixNano(),
|
|
size: fi.Size(),
|
|
}
|
|
probeCacheMu.Lock()
|
|
probeCache[key] = probeCacheEntry{
|
|
probe: probe,
|
|
expires: time.Now().Add(probeCacheTTL),
|
|
}
|
|
probeCacheMu.Unlock()
|
|
}
|
|
|
|
// ResetProbeCache clears the in-memory probe cache. Test-only.
|
|
func ResetProbeCache() {
|
|
probeCacheMu.Lock()
|
|
probeCache = make(map[probeCacheKey]probeCacheEntry)
|
|
probeCacheMu.Unlock()
|
|
}
|
|
|
|
// ProbeCacheSize returns the number of entries currently cached. Exposed
|
|
// for diagnostics + tests.
|
|
func ProbeCacheSize() int {
|
|
probeCacheMu.RLock()
|
|
defer probeCacheMu.RUnlock()
|
|
return len(probeCache)
|
|
}
|