Dreamio turn document
Accept Stremio subtitle download URLs
External subtitles from Stremio can arrive as subs*.strem.io/en/download/... URLs with no subtitle file extension. Dreamio now recognizes that shape in the injected bridge, Swift parser, and native subtitle resolver so those tracks can reach VLC instead of disappearing before playback.
Summary
The pasted logs showed Stremio reporting a failed external subtitle track at https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941, while Dreamio logged zero parsed subtitle candidates. The fix adds Stremio subtitle download URL recognition so those URLs are accepted as real external subtitle candidates.
Changes Made
- Updated the injected WebKit subtitle bridge to treat
strem.ioand*.strem.iodownload paths as subtitle URLs. - Updated
SubtitleCandidateParserto parse the same Stremio download URL shape from Stremio payloads. - Updated
SubtitleResolverso Stremio subtitle download URLs are considered directly attachable instead of requiring a second resolver response. - Added a focused regression test for the exact
subs5.strem.io/en/download/...form from the runtime log.
Context
Before this change, Dreamio only accepted direct subtitle file extensions or OpenSubtitles-looking download endpoints. Stremio’s web player can expose external subtitles through a host like subs5.strem.io, where the path identifies a subtitle download but the URL does not end in .srt, .vtt, or similar.
That mismatch explains the log pattern: bridge inspection saw likely payload objects, but Swift parsed zero usable candidates and the native player only saw embedded VLC subtitle tracks.
Important Implementation Details
- The accepted Stremio pattern is intentionally narrow: hosts must be
strem.ioor end in.strem.io, and paths must include/download, with language-prefixed forms such as/en/download/...supported. - Image and addon endpoints such as
www.strem.io/images/addons/opensubtitles-logo.pngare still rejected by the non-subtitle extension filter. - Marking these URLs as direct subtitle files lets VLC receive the URL directly through
addPlaybackSlave, which matches the way Stremio labels the track URL.
Relevant Diff Snippets
Rendered with @pierre/diffs/ssr using one patch per changed file.
Dreamio/DreamioWebViewController.swift
168 unmodified lines169170171172173174175176177178179180168 unmodified lines}};const isSubtitleURL = (url) => {if (!url || isOpenSubtitlesManifestID(url)) {return false;}return !isProbablyNonSubtitleAssetURL(url)&& (isDirectSubtitleFileURL(url) || isOpenSubtitlesDownloadURL(url));};const findResolverURL = () => {168 unmodified lines169170171172173174175176177178179180181182183184185186187188189190191192193168 unmodified lines}};const isStremioSubtitleDownloadURL = (url) => {try {const parsed = new URL(url, window.location.href);const host = parsed.hostname.toLowerCase();const path = parsed.pathname.toLowerCase();return host === "strem.io" || host.endsWith(".strem.io")? /\/[a-z]{2,3}\/download(?:\/|$)/i.test(path) || /\/download(?:\/|$)/i.test(path): false;} catch (_) {return false;}};const isSubtitleURL = (url) => {if (!url || isOpenSubtitlesManifestID(url)) {return false;}return !isProbablyNonSubtitleAssetURL(url)&& (isDirectSubtitleFileURL(url) || isOpenSubtitlesDownloadURL(url) || isStremioSubtitleDownloadURL(url));};const findResolverURL = () => {
Dreamio/StreamCandidate.swift
255 unmodified lines25625725825926026120 unmodified lines282283284285286287255 unmodified lines}guard isDirectSubtitleFile(url)|| isOpenSubtitlesDownloadURL(url)else {return nil}20 unmodified lines|| path.range(of: #"(^|/)subtitles?(/|$)"#, options: .regularExpression) != nil}private static func isOpenSubtitlesManifestIdentifier(_ url: URL) -> Bool {guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else {return false255 unmodified lines25625725825926026126220 unmodified lines283284285286287288289290291292293294295296297298299300255 unmodified lines}guard isDirectSubtitleFile(url)|| isOpenSubtitlesDownloadURL(url)|| isStremioSubtitleDownloadURL(url)else {return nil}20 unmodified lines|| path.range(of: #"(^|/)subtitles?(/|$)"#, options: .regularExpression) != nil}private static func isStremioSubtitleDownloadURL(_ url: URL) -> Bool {guard let host = url.host?.lowercased(),host == "strem.io" || host.hasSuffix(".strem.io")else {return false}let path = url.path.lowercased()return path.range(of: #"^/[a-z]{2,3}/download(/|$)"#, options: .regularExpression) != nil|| path.range(of: #"(^|/)download(/|$)"#, options: .regularExpression) != nil}private static func isOpenSubtitlesManifestIdentifier(_ url: URL) -> Bool {guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else {return false
Dreamio/StreamResolver.swift
130 unmodified lines1311321331341351361 unmodified line138139140141142143130 unmodified lineslet lowercased = url.absoluteString.lowercased()return ["srt", "vtt", "ass", "ssa", "sub"].contains(url.pathExtension.lowercased())|| [".srt?", ".vtt?", ".ass?", ".ssa?", ".sub?", ".srt&", ".vtt&", ".ass&", ".ssa&", ".sub&"].contains(where: lowercased.contains)}private static func shouldResolve(_ url: URL) -> Bool {1 unmodified linereturn lowercased.contains("opensubtitles")|| lowercased.contains("/subtitle")|| lowercased.contains("subtitle")}private static func logRejected(_ candidate: SubtitleCandidate, responseURL: URL?, data: Data) -> SubtitleCandidate? {130 unmodified lines1311321331341351361371 unmodified line139140141142143144145146147148149150151152153154155156157130 unmodified lineslet lowercased = url.absoluteString.lowercased()return ["srt", "vtt", "ass", "ssa", "sub"].contains(url.pathExtension.lowercased())|| [".srt?", ".vtt?", ".ass?", ".ssa?", ".sub?", ".srt&", ".vtt&", ".ass&", ".ssa&", ".sub&"].contains(where: lowercased.contains)|| isStremioSubtitleDownloadURL(url)}private static func shouldResolve(_ url: URL) -> Bool {1 unmodified linereturn lowercased.contains("opensubtitles")|| lowercased.contains("/subtitle")|| lowercased.contains("subtitle")|| isStremioSubtitleDownloadURL(url)}private static func isStremioSubtitleDownloadURL(_ url: URL) -> Bool {guard let host = url.host?.lowercased(),host == "strem.io" || host.hasSuffix(".strem.io")else {return false}let path = url.path.lowercased()return path.range(of: #"^/[a-z]{2,3}/download(/|$)"#, options: .regularExpression) != nil|| path.range(of: #"(^|/)download(/|$)"#, options: .regularExpression) != nil}private static func logRejected(_ candidate: SubtitleCandidate, responseURL: URL?, data: Data) -> SubtitleCandidate? {
Tests/StreamResolverTests.swift
12 unmodified lines131415161718221 unmodified lines24024124224324424512 unmodified linestestOpenSubtitlesNestedAttributesFilesParsing()testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles()testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored()testOpenSubtitlesV3DownloadResponseResolution()testOpenSubtitlesNestedDownloadResponseResolution()await testSubtitleResolverDownloadJSONReturningLink()221 unmodified linesassertEqual(candidates[0].label, "English")}private static func testOpenSubtitlesV3DownloadResponseResolution() {let payload = """{12 unmodified lines13141516171819221 unmodified lines24124224324424524624724824925025125225325425525625725825926026126226326426526626726826927012 unmodified linestestOpenSubtitlesNestedAttributesFilesParsing()testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles()testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored()testStremioSubtitleDownloadURLParsing()testOpenSubtitlesV3DownloadResponseResolution()testOpenSubtitlesNestedDownloadResponseResolution()await testSubtitleResolverDownloadJSONReturningLink()221 unmodified linesassertEqual(candidates[0].label, "English")}private static func testStremioSubtitleDownloadURLParsing() {let payload: [String: Any] = ["subtitles": [["label": "English","lang": "eng","url": "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941"],["label": "Not a subtitle","url": "https://www.strem.io/images/addons/opensubtitles-logo.png"]]]let candidates = SubtitleCandidateParser.candidates(in: payload)assertEqual(candidates.count, 1)assertEqual(candidates[0].url.absoluteString, "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941")assertEqual(candidates[0].label, "English")assertEqual(candidates[0].language, "eng")assert(SubtitleResolver.isDirectSubtitleFile(candidates[0].url), "Expected Stremio subtitle downloads to be attachable without another resolver hop")}private static func testOpenSubtitlesV3DownloadResponseResolution() {let payload = """{
Expected Impact for End-Users
When Stremio exposes external subtitles through subs*.strem.io download URLs, Dreamio should now carry those subtitles into the native VLC player instead of showing only embedded subtitle tracks in the UI.
Validation
- Passed:
swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests - Passed:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -sdk iphonesimulator build
Issues, Limitations, and Mitigations
- This was validated against the URL shape visible in the logs, not by replaying the exact remote Stremio session inside the app.
- If Stremio introduces a different subtitle CDN path that does not include
/download, another narrow allow-list entry may be needed. - The existing debug logging should now show parsed candidates for this URL form, which makes the next runtime check straightforward.
Follow-up Work
No new follow-up issue was filed. The next useful check is runtime validation on the same episode: look for parsed=1, nonzero native subtitle candidates, and a [DreamioVLC] attach accepted subtitle=... line for the Stremio subtitle download URL.