Fix Native Playback Resolution
Dreamio now resolves known Stremio addon stream responses before opening native playback, gives VLC the final direct media URL when one is available, and recovers visibly when stream resolution or VLC startup fails.
Summary
Fixed the native playback path so direct-file selections no longer default to the Debridio or Torrentio resolver URL. Dreamio either uses the observed direct media URL immediately or fetches a known addon resolver and selects a playable direct URL from its Stremio JSON response.
Changes Made
- Changed native request creation to prefer the observed direct media URL instead of the resolver URL.
- Added
StremioStreamResolverto fetch known addon resolver URLs, parsestreamsJSON, inspecturl,externalUrl, and related fields, and merge required request headers. - Stopped intercepted WebKit media elements before handing the selection to native playback.
- Added a closeable resolver failure alert so bad, expired, or unresolvable streams return the user to Stremio.
- Hardened VLC startup with state logging, a 20 second startup timeout, failure messages, and explicit media/drawable detachment on stop.
- Added focused Swift tests for stream classification and resolver stream selection.
Context
The prior native playback bridge intercepted unsupported direct-file streams, but it could pass VLC the resolver/addon URL instead of a final file URL. That made VLC try to play an endpoint intended to return JSON or perform resolution, while WebKit could still continue attempting unsupported playback underneath.
The 127.0.0.1:11470 companion-service errors remain treated as secondary noise. This change focuses on the direct stream URL that Dreamio controls before opening MobileVLCKit.
Important Implementation Details
NativePlaybackRequestnow carries merged headers so resolver-providedRefererandUser-Agentvalues can reach VLC.StremioStreamResolveraccepts resolver redirects to direct files and JSON responses containing direct stream fields.- The resolver selects unsupported direct files first, so MKV, AVI, and WebM go native while HLS-only resolver responses stay out of VLC.
- Duplicate suppression keys by resolver when present, preventing repeated bridge posts during async resolution while still allowing retry after dismiss or failure.
Relevant Diff Snippets
Rendered with @pierre/diffs; this excerpt shows the core classifier change that stops preferring resolver URLs by default.
23 unmodified lines24252627282945 unmodified lines7576777879808182838485868788899023 unmodified lineslet pageURL: URL?let userAgent: String?let referer: Stringlet classification: StreamClassification}45 unmodified lines}return NativePlaybackRequest(playbackURL: candidate.resolverURL ?? candidate.observedURL,observedURL: candidate.observedURL,resolverURL: candidate.resolverURL,pageURL: candidate.pageURL,userAgent: userAgent,referer: referer,classification: classification)}static func classify(candidate: StreamCandidate) -> StreamClassification {let observed = candidate.observedURLlet resolver = candidate.resolverURL23 unmodified lines2425262728293045 unmodified lines76777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411523 unmodified lineslet pageURL: URL?let userAgent: String?let referer: Stringlet headers: [String: String]let classification: StreamClassification}45 unmodified lines}return NativePlaybackRequest(playbackURL: candidate.observedURL,observedURL: candidate.observedURL,resolverURL: candidate.resolverURL,pageURL: candidate.pageURL,userAgent: userAgent,referer: referer,headers: Self.defaultHeaders(userAgent: userAgent),classification: classification)}static func defaultHeaders(userAgent: String?) -> [String: String] {var headers = ["Referer": referer]if let userAgent, !userAgent.isEmpty {headers["User-Agent"] = userAgent}return headers}static func isDirectPlayableFileURL(_ url: URL) -> Bool {let container = containerGuess(for: url, resolverURL: nil)return [.mp4, .mkv, .avi, .webm].contains(container)}static func isWebKitCompatibleURL(_ url: URL) -> Bool {let container = containerGuess(for: url, resolverURL: nil)return container == .hls || container == .mp4}static func isKnownResolverURL(_ url: URL) -> Bool {matches(url, host: "addon.debridio.com", pathPrefix: "/play/")|| matches(url, host: "torrentio.strem.fun", pathPrefix: "/resolve/")}static func classify(candidate: StreamCandidate) -> StreamClassification {let observed = candidate.observedURLlet resolver = candidate.resolverURL
Expected Impact for End-Users
Users still pick streams inside Stremio Web. Direct MKV, AVI, WebM, and known debrid resolver selections should now open the native player with a final playable file URL when one can be resolved. When resolution or startup fails, Dreamio shows a visible failure state and stays usable.
Validation
- Ran
swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/dreamio-stream-tests && /tmp/dreamio-stream-tests: passed. - Ran
swiftc -typecheck Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift: passed. - Ran
git diff --check: passed. pod installcould not be run because CocoaPods is not installed in this environment.xcodebuildcould not be run because the active developer directory is Command Line Tools, not full Xcode.
Issues, Limitations, and Mitigations
- Expired or revoked debrid URLs can still fail after successful resolution. The failure should now be visible and recoverable.
- Resolver parsing covers common Stremio stream fields and headers, but unusual addon response shapes may need another field mapping.
- MobileVLCKit codec limitations remain possible even when Dreamio supplies the correct URL.
Follow-up Work
- Run
pod installand buildDreamio.xcworkspaceon a machine with CocoaPods and full Xcode. - Validate a real Debridio or Torrentio MKV selection on device and confirm logs show
[DreamioStreamResolver]with a final direct URL. - Add mappings for any addon-specific stream fields discovered during device testing.