feat(stream): transcode debrid sources to HLS from a URL (hueco #2/2b)

Non-browser-native debrid content (mkv/HEVC/…) can now stream: ffmpeg reads
the debrid HTTPS link directly (-i <url>) and transcodes to HLS, instead of
2a's raw direct-play which only works for mp4/m4v.

- HLSSessionConfig gains SourceURL + CacheID; sourceRef() feeds ffprobe,
  ffmpeg -i, and subtitle extraction from one place. HTTP-resilience flags
  (-reconnect*, -rw_timeout) are added only for a URL source; a seek-restart
  re-opens the URL with a Range request (-ss before -i = input seek).
- Segment cache keys by CacheID (the torrent info_hash) for URL sessions so
  re-plays hit cache despite the debrid URL changing each resolution
  (KeyForID, no filepath.Abs).
- OnStreamSession: the 2a direct-play branch is now gated on PlayMethod != "hls";
  a new branch handles DirectURL + PlayMethod=="hls" → HLS-from-URL. The
  local-file and both debrid HLS paths share a startHLSPlayback helper.
- ExtractMediaInfo no longer masks a URL probe failure as "file not found"
  (surfaces ffprobe's real stderr, e.g. "Protocol not found" on a TLS-less
  ffmpeg build).
- Bump 0.11.0 -> 0.12.0 as the HLS-from-URL floor the web gates on.

Validated e2e against real AllDebrid: a cached HEVC x265 mkv transcodes
(h264_nvenc) from the debrid URL and plays 1080p in Chrome via hls.js,
subtitles extracted from the remote mkv.
This commit is contained in:
Deivid Soto 2026-05-31 16:22:14 +02:00
parent b8d2b90370
commit 992e16ba05
6 changed files with 270 additions and 51 deletions

View file

@ -162,6 +162,16 @@ func (c *HLSCache) KeyFor(sourcePath, quality string, audioIndex int) string {
return hex.EncodeToString(h[:8]) // 16 hex chars — collision-safe enough for per-host cache
}
// KeyForID derives a cache key from a caller-supplied stable identity instead
// of a filesystem path (hueco #2 / 2b). Used for debrid HLS-from-URL sessions:
// the debrid direct URL is re-resolved per play and would never cache-hit, so
// we key by the torrent info_hash — the same content always maps to the same
// key across plays. NOT run through filepath.Abs (an id/URL is not a path).
func (c *HLSCache) KeyForID(id, quality string, audioIndex int) string {
h := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%d", id, quality, audioIndex)))
return hex.EncodeToString(h[:8])
}
// DirFor returns the on-disk directory for a cache key. Caller is responsible
// for creating it.
func (c *HLSCache) DirFor(key string) string {
@ -407,4 +417,3 @@ func (c *HLSCache) StartSweeper(ctx context.Context, interval time.Duration) {
func (c *HLSCache) Invalidate(key string) error {
return os.RemoveAll(c.DirFor(key))
}