Dreamio turn document
Remote Stremio Server
Dreamio now has an advanced, user-owned Stremio Server configuration path that passes a self-hosted server into Stremio Web without hardcoded infrastructure.
Summary
Implemented the optional Remote Stremio Server feature as a power-user control. Users can save, validate, reload, or clear their own server URL. Dreamio injects it through Stremio Web's existing streamingServerUrl query flow and keeps native VLC playback available as the reliable fallback.
Changes Made
- Added
RemoteStremioServer.swiftfor URL normalization, private-network HTTP policy, local storage, safe display redaction, and/settingsvalidation. - Added a compact advanced server button to
DreamioWebViewControllerwith configure, test, reload, and clear actions. - Changed Dreamio launch URLs so a configured server is passed to hosted Stremio Web as
streamingServerUrl. - Added standalone Swift coverage for URL policy, Web URL injection, and settings-endpoint validation.
- Documented what
Stremio/server-dockerexposes, what it can help with, and where transcoding expectations must remain cautious.
Context
Upstream research showed that Stremio/server-docker exposes HTTP on 11470, HTTPS on 12470, and server endpoints such as /settings, /network-info, /device-info, /casting, torrent creation, statistics, proxy, and stream URLs. Stremio Web already supports a streamingServerUrl query parameter that dispatches AddServerUrl and updates profile settings. That existing integration is less brittle than rewriting stream URLs inside Dreamio.
Important Implementation Details
- No Dreamio-owned server URL is hardcoded. The only persistent value is the user's local setting.
- URLs without a scheme default to HTTPS. HTTP is allowed automatically for localhost and private-network addresses, while public HTTP requires explicit confirmation.
- Credentials, query strings, and fragments are rejected so tokens are not stored as part of the server base URL.
- The connection test reads
/settings, requiresvalues.serverVersion, reports the upstreambaseUrl, and notes whethertranscodeProfileis advertised. - Clearing Dreamio's override stops future injection. The UI explains that Stremio Web may still retain the URL in its own Settings > Streaming profile state.
Relevant Diff Snippets
Dreamio/RemoteStremioServer.swift ยท validation, storage, and server health checks
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239import Foundation
enum RemoteStremioServerURLValidationError: LocalizedError, Equatable { case empty case unsupportedScheme(String) case missingHost case containsCredentials case containsQueryOrFragment case insecureRemoteHTTP(String)
var errorDescription: String? { switch self { case .empty: return "Enter the base URL for your Stremio Server." case let .unsupportedScheme(scheme): return "Stremio Server URLs must use http or https, not \(scheme)." case .missingHost: return "The Stremio Server URL needs a host name or IP address." case .containsCredentials: return "Do not include usernames, passwords, or tokens in the Stremio Server URL." case .containsQueryOrFragment: return "Use the Stremio Server base URL only, without query strings or fragments." case let .insecureRemoteHTTP(host): return "HTTP is only accepted automatically for localhost or private-network addresses. Use HTTPS for \(host), or explicitly allow insecure HTTP." } }}
struct RemoteStremioServerConfiguration: Equatable { static let streamingServerQueryItem = "streamingServerUrl"
let baseURL: URL
init(input: String, allowInsecureRemoteHTTP: Bool = false) throws { baseURL = try Self.normalizedURL(from: input, allowInsecureRemoteHTTP: allowInsecureRemoteHTTP) }
static func normalizedURL(from input: String, allowInsecureRemoteHTTP: Bool = false) throws -> URL { var value = input.trimmingCharacters(in: .whitespacesAndNewlines) guard !value.isEmpty else { throw RemoteStremioServerURLValidationError.empty }
if !value.contains("://") { value = "https://\(value)" }
guard var components = URLComponents(string: value) else { throw RemoteStremioServerURLValidationError.missingHost }
let scheme = components.scheme?.lowercased() ?? "" guard ["http", "https"].contains(scheme) else { throw RemoteStremioServerURLValidationError.unsupportedScheme(scheme.isEmpty ? "missing scheme" : scheme) } components.scheme = scheme
guard let host = components.host, !host.isEmpty else { throw RemoteStremioServerURLValidationError.missingHost }
guard components.user == nil, components.password == nil else { throw RemoteStremioServerURLValidationError.containsCredentials }
guard components.query == nil, components.fragment == nil else { throw RemoteStremioServerURLValidationError.containsQueryOrFragment }
if scheme == "http", !isLocalOrPrivateHost(host), !allowInsecureRemoteHTTP { throw RemoteStremioServerURLValidationError.insecureRemoteHTTP(host) }
if components.path.isEmpty { components.path = "/" } else if !components.path.hasSuffix("/") { components.path += "/" }
guard let url = components.url else { throw RemoteStremioServerURLValidationError.missingHost }
return url }
static func stremioWebURL(baseURL: URL, serverURL: URL?) -> URL { guard let serverURL, var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { return baseURL }
var queryItems = components.queryItems?.filter { $0.name != streamingServerQueryItem } ?? [] queryItems.append(URLQueryItem(name: streamingServerQueryItem, value: serverURL.absoluteString)) components.queryItems = queryItems return components.url ?? baseURL }
static func settingsEndpoint(for serverURL: URL) -> URL { serverURL.appendingPathComponent("settings") }
static func redactedDisplayString(for serverURL: URL?) -> String { guard let serverURL, var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else { return "Not configured" }
components.user = nil components.password = nil components.query = nil components.fragment = nil return components.url?.absoluteString ?? "Configured server" }
static func isLocalOrPrivateHost(_ host: String) -> Bool { let lowercased = host.lowercased() if lowercased == "localhost" || lowercased == "::1" || lowercased == "[::1]" { return true }
if lowercased.hasPrefix("fe80:") || lowercased.hasPrefix("fc") || lowercased.hasPrefix("fd") { return true }
let octets = lowercased.split(separator: ".").compactMap { Int($0) } guard octets.count == 4 else { return false }
if octets[0] == 10 || octets[0] == 127 { return true } if octets[0] == 192, octets[1] == 168 { return true } if octets[0] == 172, (16...31).contains(octets[1]) { return true } return false }}
struct RemoteStremioServerStore { static let storageKey = "Dreamio.RemoteStremioServer.baseURL"
private let userDefaults: UserDefaults
init(userDefaults: UserDefaults = .standard) { self.userDefaults = userDefaults }
var serverURL: URL? { guard let value = userDefaults.string(forKey: Self.storageKey), let url = try? RemoteStremioServerConfiguration.normalizedURL( from: value, allowInsecureRemoteHTTP: true ) else { return nil } return url }
func save(serverURL: URL) { userDefaults.set(serverURL.absoluteString, forKey: Self.storageKey) }
func clear() { userDefaults.removeObject(forKey: Self.storageKey) }}
struct RemoteStremioServerValidationSummary: Equatable { let serverURL: URL let settingsEndpoint: URL let serverVersion: String let reportedBaseURL: URL? let hasTranscodingSetting: Bool}
enum RemoteStremioServerValidationError: LocalizedError { case invalidResponse case httpStatus(Int) case missingServerVersion
var errorDescription: String? { switch self { case .invalidResponse: return "The server did not return a valid Stremio Server settings response." case let .httpStatus(status): return "The Stremio Server settings endpoint returned HTTP \(status)." case .missingServerVersion: return "The settings response did not include a Stremio Server version." } }}
final class RemoteStremioServerValidator { private let session: URLSession
init(session: URLSession = .shared) { self.session = session }
func validate(serverURL: URL) async throws -> RemoteStremioServerValidationSummary { let endpoint = RemoteStremioServerConfiguration.settingsEndpoint(for: serverURL) var request = URLRequest(url: endpoint) request.timeoutInterval = 10 request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw RemoteStremioServerValidationError.invalidResponse } guard (200...299).contains(httpResponse.statusCode) else { throw RemoteStremioServerValidationError.httpStatus(httpResponse.statusCode) } guard let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any], let values = payload["values"] as? [String: Any] else { throw RemoteStremioServerValidationError.invalidResponse } guard let serverVersion = values["serverVersion"] as? String, !serverVersion.isEmpty else { throw RemoteStremioServerValidationError.missingServerVersion }
let reportedBaseURL = (payload["baseUrl"] as? String).flatMap(URL.init(string:)) return RemoteStremioServerValidationSummary( serverURL: serverURL, settingsEndpoint: endpoint, serverVersion: serverVersion, reportedBaseURL: reportedBaseURL, hasTranscodingSetting: values.keys.contains("transcodeProfile") ) }}
Dreamio/DreamioWebViewController.swift ยท advanced user controls and Stremio Web injection
60 unmodified lines616263646566656 unmodified lines7237247257267277282 unmodified lines7317327337347357367377 unmodified lines74574674774874975075114 unmodified lines76676776876977077160 unmodified linesprivate var currentNativePlaybackKey: URL?private weak var currentNativePlayer: NativePlayerViewController?private let streamResolver: StreamResolving = StremioStreamResolver()private static let streamCandidateScript = WKUserScript(source: #"""656 unmodified linesview.backgroundColor = .systemBackgroundview.addSubview(webView)view.addSubview(progressView)NSLayoutConstraint.activate([webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),2 unmodified lineswebView.bottomAnchor.constraint(equalTo: view.bottomAnchor),progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)])progressObservation = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in7 unmodified lines}private func loadDreamio() {let request = URLRequest(url: Constants.stremioWebURL)webView.load(request)}14 unmodified linespresent(alert, animated: true)}private func handleStreamCandidate(_ candidate: StreamCandidate) {guard let request = StreamClassifier.playbackRequest(from: candidate, userAgent: userAgent) else {return60 unmodified lines6162636465666768697071727374757677787980818283656 unmodified lines7407417427437447457462 unmodified lines7497507517527537547557567577587597 unmodified lines76776876977077177277377477577677714 unmodified lines79279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295360 unmodified linesprivate var currentNativePlaybackKey: URL?private weak var currentNativePlayer: NativePlayerViewController?private let streamResolver: StreamResolving = StremioStreamResolver()private let remoteServerStore = RemoteStremioServerStore()private let remoteServerValidator = RemoteStremioServerValidator()private lazy var remoteServerButton: UIButton = {let button = UIButton(type: .system)button.translatesAutoresizingMaskIntoConstraints = falsebutton.setImage(UIImage(systemName: "server.rack"), for: .normal)button.tintColor = UIColor(red: 0.55, green: 0.35, blue: 0.95, alpha: 1.0)button.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.72)button.layer.cornerRadius = 22button.layer.borderColor = UIColor.label.withAlphaComponent(0.12).cgColorbutton.layer.borderWidth = 1button.accessibilityLabel = "Remote Stremio Server"button.accessibilityHint = "Opens advanced settings for a self-hosted Stremio Server."button.addTarget(self, action: #selector(showRemoteServerMenu), for: .touchUpInside)return button}()private static let streamCandidateScript = WKUserScript(source: #"""656 unmodified linesview.backgroundColor = .systemBackgroundview.addSubview(webView)view.addSubview(progressView)view.addSubview(remoteServerButton)NSLayoutConstraint.activate([webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),2 unmodified lineswebView.bottomAnchor.constraint(equalTo: view.bottomAnchor),progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),remoteServerButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),remoteServerButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),remoteServerButton.widthAnchor.constraint(equalToConstant: 44),remoteServerButton.heightAnchor.constraint(equalToConstant: 44)])progressObservation = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in7 unmodified lines}private func loadDreamio() {let url = RemoteStremioServerConfiguration.stremioWebURL(baseURL: Constants.stremioWebURL,serverURL: remoteServerStore.serverURL)let request = URLRequest(url: url)webView.load(request)}14 unmodified linespresent(alert, animated: true)}@objc private func showRemoteServerMenu() {let currentURL = remoteServerStore.serverURLlet currentDisplay = RemoteStremioServerConfiguration.redactedDisplayString(for: currentURL)let message = currentURL == nil? "Optional power-user feature. Configure your own Stremio Server; Dreamio does not provide or hardcode a server.": "Current server: \(currentDisplay)\nDreamio passes this to Stremio Web as streamingServerUrl and keeps native VLC playback as fallback."let alert = UIAlertController(title: "Remote Stremio Server",message: message,preferredStyle: .actionSheet)alert.addAction(UIAlertAction(title: "Configure Server URL", style: .default) { [weak self] _ inself?.showRemoteServerConfigurationPrompt(prefill: currentURL)})if let currentURL {alert.addAction(UIAlertAction(title: "Test Connection", style: .default) { [weak self] _ inself?.testRemoteServer(currentURL)})alert.addAction(UIAlertAction(title: "Reload Stremio Web", style: .default) { [weak self] _ inself?.loadDreamio()})alert.addAction(UIAlertAction(title: "Clear Dreamio Override", style: .destructive) { [weak self] _ inself?.remoteServerStore.clear()self?.loadDreamio()self?.showRemoteServerNotice(title: "Remote server override cleared",message: "Dreamio will stop injecting a streamingServerUrl on load. If Stremio Web already saved this server, remove or change it in Stremio Web Settings > Streaming.")})}alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))if let popover = alert.popoverPresentationController {popover.sourceView = remoteServerButtonpopover.sourceRect = remoteServerButton.bounds}present(alert, animated: true)}private func showRemoteServerConfigurationPrompt(prefill: URL?) {let alert = UIAlertController(title: "Configure Remote Stremio Server",message: "Enter the base URL for a Stremio Server you run yourself. HTTPS is recommended for remote servers; localhost and private-network HTTP are allowed for advanced setups.",preferredStyle: .alert)alert.addTextField { textField intextField.placeholder = "https://stremio.example.com:12470/"textField.text = prefill?.absoluteStringtextField.keyboardType = .URLtextField.autocapitalizationType = .nonetextField.autocorrectionType = .notextField.clearButtonMode = .whileEditing}alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))alert.addAction(UIAlertAction(title: "Save", style: .default) { [weak self, weak alert] _ inlet input = alert?.textFields?.first?.text ?? ""self?.saveRemoteServerInput(input)})present(alert, animated: true)}private func saveRemoteServerInput(_ input: String, allowInsecureRemoteHTTP: Bool = false) {do {let configuration = try RemoteStremioServerConfiguration(input: input,allowInsecureRemoteHTTP: allowInsecureRemoteHTTP)remoteServerStore.save(serverURL: configuration.baseURL)loadDreamio()showRemoteServerSavedAlert(configuration.baseURL)} catch let validationError as RemoteStremioServerURLValidationError {if case .insecureRemoteHTTP = validationError, !allowInsecureRemoteHTTP {showInsecureHTTPConfirmation(input: input, message: validationError.localizedDescription)} else {showRemoteServerNotice(title: "Invalid Stremio Server URL", message: validationError.localizedDescription)}} catch {showRemoteServerNotice(title: "Invalid Stremio Server URL", message: error.localizedDescription)}}private func showInsecureHTTPConfirmation(input: String, message: String) {let alert = UIAlertController(title: "Use insecure HTTP?",message: "\(message)\n\nOnly continue if this server is reachable through a trusted VPN, tunnel, or private network. The server can see stream URLs sent to it.",preferredStyle: .alert)alert.addAction(UIAlertAction(title: "Edit URL", style: .cancel) { [weak self] _ inself?.showRemoteServerConfigurationPrompt(prefill: nil)})alert.addAction(UIAlertAction(title: "Use HTTP Anyway", style: .destructive) { [weak self] _ inself?.saveRemoteServerInput(input, allowInsecureRemoteHTTP: true)})present(alert, animated: true)}private func showRemoteServerSavedAlert(_ serverURL: URL) {let displayURL = RemoteStremioServerConfiguration.redactedDisplayString(for: serverURL)let alert = UIAlertController(title: "Remote server saved",message: "Dreamio reloaded Stremio Web with \(displayURL). Test the connection if this is a new server.",preferredStyle: .alert)alert.addAction(UIAlertAction(title: "Test Connection", style: .default) { [weak self] _ inself?.testRemoteServer(serverURL)})alert.addAction(UIAlertAction(title: "OK", style: .cancel))present(alert, animated: true)}private func testRemoteServer(_ serverURL: URL) {let progress = UIAlertController(title: "Testing Remote Stremio Server",message: RemoteStremioServerConfiguration.redactedDisplayString(for: serverURL),preferredStyle: .alert)present(progress, animated: true)Task { [weak self, weak progress] inguard let self else {return}do {let summary = try await remoteServerValidator.validate(serverURL: serverURL)await MainActor.run {progress?.dismiss(animated: true) {self.showRemoteServerTestResult(summary)}}} catch {await MainActor.run {progress?.dismiss(animated: true) {self.showRemoteServerNotice(title: "Remote server test failed",message: error.localizedDescription)}}}}}private func showRemoteServerTestResult(_ summary: RemoteStremioServerValidationSummary) {let reportedBaseURL = summary.reportedBaseURL.map {RemoteStremioServerConfiguration.redactedDisplayString(for: $0)} ?? "Not reported"let message = "Server version: \(summary.serverVersion)\nSettings endpoint: \(RemoteStremioServerConfiguration.redactedDisplayString(for: summary.settingsEndpoint))\nReported base URL: \(reportedBaseURL)\nTranscoding setting advertised: \(summary.hasTranscodingSetting ? "Yes" : "No")"showRemoteServerNotice(title: "Remote server is reachable", message: message)}private func showRemoteServerNotice(title: String, message: String) {let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)alert.addAction(UIAlertAction(title: "OK", style: .default))present(alert, animated: true)}private func handleStreamCandidate(_ candidate: StreamCandidate) {guard let request = StreamClassifier.playbackRequest(from: candidate, userAgent: userAgent) else {return
Tests/StreamResolverTests.swift ยท routing and validation coverage
26 unmodified lines272829303132503 unmodified lines53653753853954054126 unmodified linestestNativePlaybackTogglePolicy()testNativePlaybackAudioSessionPolicy()testNativePlaybackStreamingOptionsPolicy()print("StreamResolverTests passed")}503 unmodified linesassertEqual(NativePlaybackStreamingOptionsPolicy.mediaOptions().contains(":live-caching=1000"), false)}private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)}26 unmodified lines272829303132333435503 unmodified lines53954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662726 unmodified linestestNativePlaybackTogglePolicy()testNativePlaybackAudioSessionPolicy()testNativePlaybackStreamingOptionsPolicy()testRemoteStremioServerURLNormalization()testRemoteStremioServerWebURLInjection()await testRemoteStremioServerValidatorReadsSettingsEndpoint()print("StreamResolverTests passed")}503 unmodified linesassertEqual(NativePlaybackStreamingOptionsPolicy.mediaOptions().contains(":live-caching=1000"), false)}private static func testRemoteStremioServerURLNormalization() {let serverURL = try! RemoteStremioServerConfiguration.normalizedURL(from: "stremio.example.test:12470")assertEqual(serverURL.absoluteString, "https://stremio.example.test:12470/")let reverseProxyURL = try! RemoteStremioServerConfiguration.normalizedURL(from: "https://media.example.test/stremio")assertEqual(reverseProxyURL.absoluteString, "https://media.example.test/stremio/")let localHTTPURL = try! RemoteStremioServerConfiguration.normalizedURL(from: "http://192.168.1.10:11470")assertEqual(localHTTPURL.absoluteString, "http://192.168.1.10:11470/")do {_ = try RemoteStremioServerConfiguration.normalizedURL(from: "http://public.example.test:11470")assertionFailure("Expected remote HTTP to require an explicit override")} catch RemoteStremioServerURLValidationError.insecureRemoteHTTP(let host) {assertEqual(host, "public.example.test")} catch {assertionFailure("Unexpected error: \(error)")}do {_ = try RemoteStremioServerConfiguration.normalizedURL(from: "https://user:secret@stremio.example.test:12470/")assertionFailure("Expected credentials to be rejected")} catch RemoteStremioServerURLValidationError.containsCredentials {} catch {assertionFailure("Unexpected error: \(error)")}do {_ = try RemoteStremioServerConfiguration.normalizedURL(from: "https://stremio.example.test:12470/?token=secret")assertionFailure("Expected query strings to be rejected")} catch RemoteStremioServerURLValidationError.containsQueryOrFragment {} catch {assertionFailure("Unexpected error: \(error)")}}private static func testRemoteStremioServerWebURLInjection() {let webURL = URL(string: "https://web.stremio.com/")!let serverURL = URL(string: "https://stremio.example.test:12470/")!let configuredURL = RemoteStremioServerConfiguration.stremioWebURL(baseURL: webURL,serverURL: serverURL)let queryItems = URLComponents(url: configuredURL, resolvingAgainstBaseURL: false)?.queryItems ?? []assertEqual(queryItems.first(where: { $0.name == RemoteStremioServerConfiguration.streamingServerQueryItem })?.value,"https://stremio.example.test:12470/")assertEqual(RemoteStremioServerConfiguration.stremioWebURL(baseURL: webURL, serverURL: nil).absoluteString,"https://web.stremio.com/")}private static func testRemoteStremioServerValidatorReadsSettingsEndpoint() async {let session = mockSession()let serverURL = URL(string: "https://stremio.example.test:12470/")!let endpoint = URL(string: "https://stremio.example.test:12470/settings")!let body = #"""{"baseUrl": "https://stremio.example.test:12470/","values": {"serverVersion": "4.20.8","appPath": "/root/.stremio-server","cacheRoot": "/root/.stremio-server","cacheSize": 2147483648,"remoteHttps": "","transcodeProfile": null}}"""#MockURLProtocol.handlers = [endpoint.absoluteString: (status: 200, url: endpoint, data: Data(body.utf8))]let summary = try! await RemoteStremioServerValidator(session: session).validate(serverURL: serverURL)assertEqual(summary.serverVersion, "4.20.8")assertEqual(summary.settingsEndpoint.absoluteString, "https://stremio.example.test:12470/settings")assertEqual(summary.reportedBaseURL?.absoluteString, "https://stremio.example.test:12470/")assertEqual(summary.hasTranscodingSetting, true)}private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)}
README.md ยท power-user setup and limitations
51 unmodified lines52535455565751 unmodified linesThe official CocoaPods getting started guide documents the RubyGems installpath: https://guides.cocoapods.org/using/getting-started.html## Validation NotesCocoaPods 1.16.2 was installed with Homebrew on this repository machine, and51 unmodified lines525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979851 unmodified linesThe official CocoaPods getting started guide documents the RubyGems installpath: https://guides.cocoapods.org/using/getting-started.html## Optional Remote Stremio ServerDreamio includes an advanced, opt-in **Remote Stremio Server** setting for userswho run their own Stremio Server, such as the official[`stremio/server`](https://github.com/Stremio/server-docker) Docker image. Tapthe server button in the top-right corner of Dreamio to configure, test, reload,or clear a user-provided server URL.Dreamio does **not** provide, bundle, or hardcode a Stremio Server address. Theconfigured URL is stored locally and passed to hosted Stremio Web with itsexisting `streamingServerUrl` query-parameter flow so Stremio Web can add andselect that server in Settings > Streaming.What the official Docker image exposes:- `11470` for HTTP and `12470` for HTTPS.- `/settings`, `/network-info`, `/device-info`, `/casting`, torrent creation,statistics, proxy, and media streaming endpoints used by Stremio Web/Core.- `FFMPEG_BIN`, `FFPROBE_BIN`, `APP_PATH`, and `NO_CORS` environment variables.- A Docker build with `ffmpeg-jellyfin`, which enables server-side media workwhen the upstream Stremio Server and selected stream path support it.A remote server can help with torrent-backed streams, proxy-header streams,archive/FTP/YouTube handling, and some transcoding-capable playback paths. It isnot guaranteed to turn every MKV into a WebKit-friendly HLS or MP4 stream. KeepMobileVLCKit native playback available as the fallback for MKV, AVI, WebM, codec,audio-track, subtitle, or server-transcoding failures.Security and privacy notes:- Prefer HTTPS for remote servers. Dreamio only accepts HTTP automatically forlocalhost and private-network addresses; public HTTP requires explicitconfirmation. Use a trusted certificate or reverse proxy for remote HTTPS;self-signed certificates may fail in iOS networking and `WKWebView`.- Do not put usernames, passwords, tokens, query strings, or fragments in theconfigured base URL.- A configured server can see stream URLs sent to it. Run a server you trust.- Clearing the Dreamio override stops Dreamio from injecting `streamingServerUrl`on load. If Stremio Web already saved the URL in its own profile settings,remove or change it in Stremio Web Settings > Streaming.## Validation NotesCocoaPods 1.16.2 was installed with Homebrew on this repository machine, and
Diffs were rendered with @pierre/diffs/ssr. Each file diff is contained in its own shell so the document remains a readable static artifact.
Expected Impact for End-Users
Power users can now point Dreamio at their own Stremio Server, including a Docker deployment behind their own HTTPS endpoint. The feature may help with torrent-backed, proxy-header, archive, FTP, YouTube, and some transcoding-capable paths, but users still get Dreamio's MobileVLCKit fallback for MKV, AVI, WebM, codec, audio, subtitle, or server limitations.
Validation
- Passed:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcrun --sdk iphonesimulator swiftc -target arm64-apple-ios18.0-simulator Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Dreamio/RemoteStremioServer.swift Dreamio/NativePlaybackBackend.swift Tests/StreamResolverTests.swift -o /tmp/dreamio-stream-tests-ios. - Passed through simulator spawn:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcrun simctl spawn 881FA280-CAA8-49B5-85BB-E7D393CCA6D2 /tmp/dreamio-stream-tests-ios. - Passed:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build. - Not run: real-device media playback against a user-owned Stremio Server. That requires a server, TLS setup, and legal representative streams, and is tracked by
dreamio-8i5.
Issues, Limitations, and Mitigations
- Server-side MKV to WebKit-friendly output is not guaranteed. The README now states this explicitly and Dreamio keeps native VLC fallback behavior.
- Self-signed or untrusted certificates may fail in iOS networking and
WKWebView. The documentation recommends a trusted certificate or reverse proxy. - The Stremio Web profile can save the server independently of Dreamio. The clear action explains how to remove that saved value inside Stremio Web.
- Public HTTP can expose stream URLs. Dreamio blocks it by default and requires an explicit warning confirmation.
Follow-up Work
- Complete
dreamio-8i5: validate direct MP4, HLS, MKV, unsupported audio/subtitle MKV, and torrent-backed samples on real iPhone or iPad with a user-owned server. - Consider a richer first-run explanation if the compact server button is too discoverable for a power-user feature or too subtle for testers.
- After device validation, adjust routing copy or fallback behavior if Stremio Server reliably emits WebKit-friendly HLS for specific stream classes.