Play direct media links without Yattee Server by probing the URL first

Debrid and CDN download links (TorBox, Real-Debrid, presigned S3) hide the
file behind an opaque path, so the extension-based direct media check missed
them and every one was sent to Yattee Server for yt-dlp extraction, failing
outright when no server is configured.

Add DirectMediaProbe: a HEAD request classifies the response by Content-Type
and Content-Disposition, and when the type is generic or HEAD is refused, a
ranged GET sniffs the first bytes for container signatures (Matroska/WebM,
ISO BMFF, Ogg, FLAC, MP3, RIFF, FLV, ASF, MPEG-PS/TS, HLS and DASH
manifests). Anything playable goes straight to mpv with the server-reported
filename as the title; everything else falls back to extraction as before.

The probe runs ahead of the Yattee Server path in ExternalVideoView, both
OpenLinkSheet flows and the deep-link download path, and PlayerService
rebuilds direct_media streams itself so replays from history no longer
depend on a server either.
This commit is contained in:
Arkadiusz Fal
2026-09-21 20:03:06 +02:00
parent 53fda7574c
commit 6efd60a18e
7 changed files with 515 additions and 8 deletions

View File

@@ -754,6 +754,12 @@ struct OpenLinkFormView: View {
return (video, [stream])
case .externalVideo, nil:
// A file mpv plays as-is needs no extraction
if let probe = await DirectMediaProbe.probe(url) {
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
return (video, [stream])
}
// External URL - use Yattee Server
guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
throw OpenLinkError.noYatteeServer
@@ -896,6 +902,12 @@ struct OpenLinkFormView: View {
return (video, [stream], [], [])
case .externalVideo, nil:
// A file mpv plays as-is needs no extraction, no captions/storyboards
if let probe = await DirectMediaProbe.probe(url) {
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
return (video, [stream], [], [])
}
// External URL - use Yattee Server (doesn't support storyboards)
guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
throw OpenLinkError.noYatteeServer

View File

@@ -59,6 +59,18 @@ struct ExternalVideoView: View {
isLoading = true
errorMessage = nil
// A file mpv can play as-is (debrid/CDN download links with an opaque
// path) needs no extraction and no Yattee Server.
if let probe = await DirectMediaProbe.probe(url) {
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
await MainActor.run {
shouldDismissWhenPlayerExpands = true
appEnvironment.playerService.openVideo(video, stream: stream)
}
return
}
// Find a Yattee Server instance
guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
errorMessage = String(localized: "externalVideo.noYatteeServer")