Skip to content

Commit d23943d

Browse files
committed
fix(relay): scope manifest classification to .m3u8/.mpd extensions
The relay's "manifest" URL convention (/play/<id>/manifest.<ext>) covers both HLS playlists and progressive MP4 responses. LooksLikeManifestPath was matching any filename starting with "manifest.", so manifest.mp4 went through the line-by-line text rewriter and shipped as Transfer-Encoding: chunked without Content-Length -- WMF/NSPlayer disconnects on the first byte under that response shape, which surfaced as immediate load_failure for any YouTube video that resolved to a progressive MP4 format. Tighten the heuristic to require either an extensionless "manifest" filename or a known manifest extension (.m3u8 / .mpd). The Content-Type fallback still catches manifests served from unconventional URL shapes.
1 parent f683ced commit d23943d

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

src/WKVRCProxy.Tests/LocalRelayServerTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,28 @@ await LocalRelayManifestLocalizer.LocalizeStreamAsync(
360360
}
361361

362362
[Theory]
363+
// Plain m3u8 / mpd extensions always classify as manifests.
363364
[InlineData("/play/a/manifest.m3u8", "https://node1.whyknot.dev/api/proxy/manifest.m3u8?q=abc", "text/plain", true)]
365+
[InlineData("/play/a/index.m3u8", "https://node1.whyknot.dev/api/proxy/index.m3u8", "application/vnd.apple.mpegurl", true)]
366+
[InlineData("/play/a/manifest.mpd", "https://node1.whyknot.dev/api/proxy/manifest.mpd", "application/dash+xml", true)]
367+
// Query strings don't break the extension probe.
368+
[InlineData("/play/a/manifest.m3u8?token=abc", "https://node1.whyknot.dev/api/proxy/manifest.m3u8?token=abc", "application/vnd.apple.mpegurl", true)]
369+
// Local path has a non-manifest extension; targetUrl carries .m3u8 -- second branch catches it.
364370
[InlineData("/play/a/manifest.bin", "https://node1.whyknot.dev/api/proxy/manifest.m3u8?q=abc", "application/octet-stream", true)]
371+
// Bare "manifest" (no extension) is treated as a manifest -- some providers ship that.
372+
[InlineData("/play/a/manifest", "https://node1.whyknot.dev/api/proxy/manifest", "application/vnd.apple.mpegurl", true)]
373+
// /manifest.mp4 was the 2026-05-22 YouTube load_failure bug: the
374+
// relay's own progressive-MP4 URL pattern was treated as an HLS
375+
// manifest, run through the line-by-line text rewriter, and shipped
376+
// as Transfer-Encoding: chunked with no Content-Length, so WMF/NSPlayer
377+
// disconnected on the first byte.
378+
[InlineData("/play/a/manifest.mp4", "https://node1.whyknot.dev/api/proxy/manifest.mp4?q=abc", "video/mp4", false)]
379+
[InlineData("/play/a/MANIFEST.MP4", "https://node1.whyknot.dev/api/proxy/manifest.mp4?q=abc", "video/mp4", false)]
380+
// Segment URLs never qualify as manifests, regardless of "manifest" appearing in path.
365381
[InlineData("/play/a/seg.ts", "https://node1.whyknot.dev/api/proxy/seg.ts?url=abc", "video/mp2t", false)]
382+
[InlineData("/play/a/manifest_archive/seg.ts", "https://node1.whyknot.dev/api/proxy/seg.ts", "video/mp2t", false)]
383+
// Content-type fallback: a .ts URL that's actually an m3u8 (Tubi pattern) still classifies.
384+
[InlineData("/play/a/foo.ts", "https://example.test/playlist.ts", "application/vnd.apple.mpegurl", true)]
366385
public void ManifestLocalizer_DetectsOnlyManifestShapes(
367386
string localPath,
368387
string targetUrl,

src/WKVRCProxy/LocalRelayManifestLocalizer.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,18 @@ private static bool HasManifestExtension(string path)
201201
private static bool LooksLikeManifestPath(string path)
202202
{
203203
string fileName = Path.GetFileName(path);
204-
return fileName.StartsWith("manifest.", StringComparison.OrdinalIgnoreCase)
205-
|| fileName.Equals("manifest", StringComparison.OrdinalIgnoreCase)
206-
|| path.Contains("/manifest.", StringComparison.OrdinalIgnoreCase);
204+
if (fileName.Equals("manifest", StringComparison.OrdinalIgnoreCase))
205+
return true;
206+
// A "manifest.<ext>" filename is only a manifest when <ext> is one of
207+
// the known manifest extensions. The relay also constructs
208+
// /play/<id>/manifest.mp4 for progressive MP4 responses, and the
209+
// previous unconditional StartsWith branch swept those into the
210+
// streaming text rewriter -- the response then went out as
211+
// Transfer-Encoding: chunked with no Content-Length and AVPro/WMF
212+
// disconnected on the first byte without playing anything.
213+
if (fileName.StartsWith("manifest.", StringComparison.OrdinalIgnoreCase))
214+
return HasManifestExtension(fileName);
215+
return false;
207216
}
208217

209218
private static bool TryGetUriPath(string url, out string path)

0 commit comments

Comments
 (0)