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:
parent
b8d2b90370
commit
992e16ba05
6 changed files with 270 additions and 51 deletions
122
internal/engine/hls_url_args_test.go
Normal file
122
internal/engine/hls_url_args_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// hueco #2 / 2b — buildHLSFFmpegArgsAt must feed a debrid URL straight to
|
||||
// ffmpeg's -i with HTTP-resilience flags, and must NOT add those flags for a
|
||||
// local file.
|
||||
func TestBuildHLSFFmpegArgsFromURL(t *testing.T) {
|
||||
const url = "https://cdn.debrid.it/dl/abc/Movie.mkv"
|
||||
cfg := HLSSessionConfig{
|
||||
SessionID: "test",
|
||||
SourceURL: url,
|
||||
CacheID: "deadbeef",
|
||||
Quality: "720p",
|
||||
Transcode: TranscodeRuntime{
|
||||
FFmpegPath: "/usr/bin/ffmpeg",
|
||||
FFprobePath: "/usr/bin/ffprobe",
|
||||
HWAccel: HWAccelNone,
|
||||
},
|
||||
}
|
||||
probe := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100}
|
||||
args := buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0)
|
||||
got := strings.Join(args, " ")
|
||||
|
||||
for _, want := range []string{
|
||||
"-reconnect 1",
|
||||
"-reconnect_streamed 1",
|
||||
"-reconnect_delay_max 5",
|
||||
"-rw_timeout 30000000",
|
||||
"-i " + url,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("URL argv missing %q\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A seek (startSec>0) on a URL source must keep BOTH the -ss input seek AND the
|
||||
// HTTP-resilience flags, so a seek-restart re-opens the URL with a Range request
|
||||
// instead of re-downloading from zero. (-ss before -i = input seek.)
|
||||
func TestBuildHLSFFmpegArgsFromURLWithSeek(t *testing.T) {
|
||||
const url = "https://cdn.debrid.it/dl/abc/Movie.mkv"
|
||||
cfg := HLSSessionConfig{
|
||||
SessionID: "test",
|
||||
SourceURL: url,
|
||||
CacheID: "deadbeef",
|
||||
Quality: "720p",
|
||||
Transcode: TranscodeRuntime{
|
||||
FFmpegPath: "/usr/bin/ffmpeg",
|
||||
FFprobePath: "/usr/bin/ffprobe",
|
||||
HWAccel: HWAccelNone,
|
||||
},
|
||||
}
|
||||
probe := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100}
|
||||
got := strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 5, 30), " ")
|
||||
|
||||
for _, want := range []string{
|
||||
"-ss 30.000", // input seek before -i
|
||||
"-reconnect 1", // resilience flags still present on a restart
|
||||
"-rw_timeout 30000000",
|
||||
"-i " + url,
|
||||
"-output_ts_offset 30.000", // PTS shift so the manifest numbering holds
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("seek+URL argv missing %q\n%s", want, got)
|
||||
}
|
||||
}
|
||||
// -ss must come before -i (fast input seek, not slow output seek).
|
||||
if strings.Index(got, "-ss 30.000") > strings.Index(got, "-i "+url) {
|
||||
t.Errorf("-ss must precede -i for input seek:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHLSFFmpegArgsLocalNoNetworkFlags(t *testing.T) {
|
||||
cfg := HLSSessionConfig{
|
||||
SessionID: "test",
|
||||
SourcePath: "/tmp/test.mkv",
|
||||
Quality: "720p",
|
||||
Transcode: TranscodeRuntime{
|
||||
FFmpegPath: "/usr/bin/ffmpeg",
|
||||
FFprobePath: "/usr/bin/ffprobe",
|
||||
HWAccel: HWAccelNone,
|
||||
},
|
||||
}
|
||||
probe := &StreamProbe{Width: 1920, Height: 1080, DurationSec: 100}
|
||||
got := strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/tmpdir", 0, 0), " ")
|
||||
|
||||
if strings.Contains(got, "-reconnect") || strings.Contains(got, "-rw_timeout") {
|
||||
t.Errorf("local source must not carry HTTP-resilience flags: %s", got)
|
||||
}
|
||||
if !strings.Contains(got, "-i /tmp/test.mkv") {
|
||||
t.Errorf("local argv missing -i /tmp/test.mkv: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// sourceRef + cache-key identity: a URL session keys by CacheID, a local one by
|
||||
// path. Guards the "re-plays of the same debrid content hit cache despite the
|
||||
// URL changing" invariant.
|
||||
func TestHLSSourceRefAndCacheID(t *testing.T) {
|
||||
urlCfg := HLSSessionConfig{SourceURL: "https://cdn/x.mkv", CacheID: "hash1"}
|
||||
if urlCfg.sourceRef() != "https://cdn/x.mkv" {
|
||||
t.Errorf("sourceRef = %q, want the URL", urlCfg.sourceRef())
|
||||
}
|
||||
localCfg := HLSSessionConfig{SourcePath: "/m/x.mkv"}
|
||||
if localCfg.sourceRef() != "/m/x.mkv" {
|
||||
t.Errorf("sourceRef = %q, want the path", localCfg.sourceRef())
|
||||
}
|
||||
|
||||
c := &HLSCache{root: "/tmp/cache"}
|
||||
// Same CacheID + quality + audio → same key regardless of the (volatile) URL.
|
||||
k1 := c.KeyForID("hash1", "720p", -1)
|
||||
k2 := c.KeyForID("hash1", "720p", -1)
|
||||
if k1 != k2 {
|
||||
t.Errorf("KeyForID not stable: %q != %q", k1, k2)
|
||||
}
|
||||
if c.KeyForID("hash2", "720p", -1) == k1 {
|
||||
t.Error("KeyForID collision across distinct ids")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue