# 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.

Beads: dreamio-vxs Native playback Stream resolution 2026-05-25

## 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 `StremioStreamResolver` to fetch known addon resolver URLs, parse `streams` JSON, inspect `url`, `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

- `NativePlaybackRequest` now carries merged headers so resolver-provided `Referer` and `User-Agent` values can reach VLC.
- `StremioStreamResolver` accepts 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.

Dreamio/StreamCandidate.swift
-1+26

    23 unmodified lines24252627282945 unmodified lines7576777879808182838485868788899023 unmodified lines    let pageURL: URL?    let userAgent: String?    let referer: String    let 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.observedURL        let resolver = candidate.resolverURL23 unmodified lines2425262728293045 unmodified lines76777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411523 unmodified lines    let pageURL: URL?    let userAgent: String?    let referer: String    let 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.observedURL        let 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 install` could not be run because CocoaPods is not installed in this environment.
- `xcodebuild` could not be run because the active developer directory is Command Line Tools, not full Xcode.

## Issues, Limitations, and Mitigations

Manual real-device validation is still required for the South Park MKV-style stream and any live Debridio or Torrentio resolver behavior.

- 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 install` and build `Dreamio.xcworkspace` on 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.