mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 13:37:24 +00:00
add webview inspection diagnostics
This commit is contained in:
parent
f22df976e4
commit
1563a4c067
3 changed files with 509 additions and 0 deletions
|
|
@ -1,2 +1,4 @@
|
||||||
|
{"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"dreamio-evt","title":"Enable WebView inspection and playback diagnostics","description":"Add development-only WKWebView inspection and token-safe playback diagnostics so Dreamio can debug hosted Stremio media failures without changing app navigation, login, or playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T02:30:26Z","created_by":"dirtydishes","updated_at":"2026-05-25T02:34:55Z","started_at":"2026-05-25T02:30:32Z","closed_at":"2026-05-25T02:34:55Z","close_reason":"Implemented debug-only WKWebView inspection, token-safe playback diagnostics, navigation logging, validation build, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-a5b","title":"Track HTML diff rendering tooling as dev dependency","description":"Move the HTML diff rendering package into devDependencies and ignore installed Node modules so the repo tracks reproducible tooling without vendoring dependencies.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:12:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:12:44Z","started_at":"2026-05-25T01:12:14Z","closed_at":"2026-05-25T01:12:44Z","close_reason":"Moved @pierre/diffs to devDependencies and ignored node_modules.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-a5b","title":"Track HTML diff rendering tooling as dev dependency","description":"Move the HTML diff rendering package into devDependencies and ignore installed Node modules so the repo tracks reproducible tooling without vendoring dependencies.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:12:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:12:44Z","started_at":"2026-05-25T01:12:14Z","closed_at":"2026-05-25T01:12:44Z","close_reason":"Moved @pierre/diffs to devDependencies and ignored node_modules.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import WebKit
|
||||||
final class DreamioWebViewController: UIViewController {
|
final class DreamioWebViewController: UIViewController {
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
static let stremioWebURL = URL(string: "https://web.stremio.com/")!
|
static let stremioWebURL = URL(string: "https://web.stremio.com/")!
|
||||||
|
static let diagnosticsMessageHandler = "dreamioDiagnostics"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var webView: WKWebView = {
|
private lazy var webView: WKWebView = {
|
||||||
|
|
@ -12,6 +13,13 @@ final class DreamioWebViewController: UIViewController {
|
||||||
configuration.allowsInlineMediaPlayback = true
|
configuration.allowsInlineMediaPlayback = true
|
||||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||||
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
||||||
|
#if DEBUG
|
||||||
|
configuration.userContentController.add(
|
||||||
|
WeakScriptMessageHandler(delegate: self),
|
||||||
|
name: Constants.diagnosticsMessageHandler
|
||||||
|
)
|
||||||
|
configuration.userContentController.addUserScript(Self.playbackDiagnosticsScript)
|
||||||
|
#endif
|
||||||
|
|
||||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
@ -20,6 +28,11 @@ final class DreamioWebViewController: UIViewController {
|
||||||
webView.navigationDelegate = self
|
webView.navigationDelegate = self
|
||||||
webView.uiDelegate = self
|
webView.uiDelegate = self
|
||||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
#if DEBUG
|
||||||
|
if #available(iOS 16.4, *) {
|
||||||
|
webView.isInspectable = true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return webView
|
return webView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -32,6 +45,96 @@ final class DreamioWebViewController: UIViewController {
|
||||||
|
|
||||||
private var progressObservation: NSKeyValueObservation?
|
private var progressObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
private static let playbackDiagnosticsScript = WKUserScript(
|
||||||
|
source: """
|
||||||
|
(() => {
|
||||||
|
if (window.__dreamioPlaybackDiagnosticsInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.__dreamioPlaybackDiagnosticsInstalled = true;
|
||||||
|
|
||||||
|
const post = (type, payload = {}) => {
|
||||||
|
try {
|
||||||
|
window.webkit.messageHandlers.dreamioDiagnostics.postMessage({
|
||||||
|
type,
|
||||||
|
payload,
|
||||||
|
href: window.location.href
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const describeValue = (value) => {
|
||||||
|
if (value instanceof Error) {
|
||||||
|
return {
|
||||||
|
name: value.name,
|
||||||
|
message: value.message,
|
||||||
|
stack: value.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} catch (_) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
["error", "warn"].forEach((level) => {
|
||||||
|
const original = console[level];
|
||||||
|
console[level] = (...args) => {
|
||||||
|
post(`console.${level}`, { args: args.map(describeValue) });
|
||||||
|
original.apply(console, args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
|
post("unhandledrejection", {
|
||||||
|
reason: describeValue(event.reason)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoState = (video) => ({
|
||||||
|
currentSrc: video.currentSrc || video.src || "",
|
||||||
|
networkState: video.networkState,
|
||||||
|
readyState: video.readyState,
|
||||||
|
errorCode: video.error ? video.error.code : null,
|
||||||
|
errorMessage: video.error ? video.error.message : null
|
||||||
|
});
|
||||||
|
|
||||||
|
const attachVideoDiagnostics = (video) => {
|
||||||
|
if (!video || video.__dreamioDiagnosticsAttached) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
video.__dreamioDiagnosticsAttached = true;
|
||||||
|
video.addEventListener("error", () => {
|
||||||
|
post("video.error", videoState(video));
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll("video").forEach(attachVideoDiagnostics);
|
||||||
|
|
||||||
|
new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node instanceof HTMLVideoElement) {
|
||||||
|
attachVideoDiagnostics(node);
|
||||||
|
}
|
||||||
|
if (node.querySelectorAll) {
|
||||||
|
node.querySelectorAll("video").forEach(attachVideoDiagnostics);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
|
""",
|
||||||
|
injectionTime: .atDocumentStart,
|
||||||
|
forMainFrameOnly: false
|
||||||
|
)
|
||||||
|
#endif
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
@ -77,6 +180,70 @@ final class DreamioWebViewController: UIViewController {
|
||||||
})
|
})
|
||||||
present(alert, animated: true)
|
present(alert, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
private func logDiagnostic(type: String, payload: Any, pageURL: String?) {
|
||||||
|
let redactedPageURL = pageURL.map(redactedURLString) ?? "unknown"
|
||||||
|
let redactedPayload = redactDiagnosticValue(payload)
|
||||||
|
print("[DreamioDiagnostics] \(type) page=\(redactedPageURL) payload=\(redactedPayload)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func redactDiagnosticValue(_ value: Any) -> Any {
|
||||||
|
switch value {
|
||||||
|
case let string as String:
|
||||||
|
return redactedURLString(string)
|
||||||
|
case let array as [Any]:
|
||||||
|
return array.map(redactDiagnosticValue)
|
||||||
|
case let dictionary as [String: Any]:
|
||||||
|
return dictionary.reduce(into: [String: Any]()) { result, entry in
|
||||||
|
result[entry.key] = redactDiagnosticValue(entry.value)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func redactedURLString(_ value: String) -> String {
|
||||||
|
guard var components = URLComponents(string: value), components.scheme != nil else {
|
||||||
|
return redactTokenLikeFragments(in: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
components.query = nil
|
||||||
|
components.fragment = nil
|
||||||
|
if let path = components.percentEncodedPath.nilIfEmpty {
|
||||||
|
components.percentEncodedPath = redactTokenLikePathSegments(in: path)
|
||||||
|
}
|
||||||
|
return redactTokenLikeFragments(in: components.string ?? value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func redactTokenLikePathSegments(in path: String) -> String {
|
||||||
|
path
|
||||||
|
.split(separator: "/", omittingEmptySubsequences: false)
|
||||||
|
.map { segment -> String in
|
||||||
|
let text = String(segment)
|
||||||
|
if text.range(of: #"^[A-Za-z0-9_-]{24,}$"#, options: .regularExpression) != nil {
|
||||||
|
return "[redacted]"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
.joined(separator: "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func redactTokenLikeFragments(in value: String) -> String {
|
||||||
|
let patterns = [
|
||||||
|
#"(?i)((?:token|access_token|auth|signature|sig|key|apikey|api_key|jwt|session|password)=)([^&\s]+)"#,
|
||||||
|
#"(?i)(bearer\s+)[A-Za-z0-9._~+/=-]+"#
|
||||||
|
]
|
||||||
|
|
||||||
|
return patterns.reduce(value) { redacted, pattern in
|
||||||
|
redacted.replacingOccurrences(
|
||||||
|
of: pattern,
|
||||||
|
with: "$1[redacted]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DreamioWebViewController: WKNavigationDelegate {
|
extension DreamioWebViewController: WKNavigationDelegate {
|
||||||
|
|
@ -99,11 +266,31 @@ extension DreamioWebViewController: WKNavigationDelegate {
|
||||||
decisionHandler(.allow)
|
decisionHandler(.allow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func webView(
|
||||||
|
_ webView: WKWebView,
|
||||||
|
decidePolicyFor navigationResponse: WKNavigationResponse,
|
||||||
|
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
|
||||||
|
) {
|
||||||
|
#if DEBUG
|
||||||
|
if let response = navigationResponse.response as? HTTPURLResponse {
|
||||||
|
let url = response.url?.absoluteString ?? "unknown"
|
||||||
|
print("[DreamioNavigation] status=\(response.statusCode) url=\(redactedURLString(url))")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
decisionHandler(.allow)
|
||||||
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||||
|
#if DEBUG
|
||||||
|
print("[DreamioNavigation] didFail url=\(redactedURLString(webView.url?.absoluteString ?? "unknown")) error=\(redactedURLString(error.localizedDescription))")
|
||||||
|
#endif
|
||||||
showLoadFailure(error)
|
showLoadFailure(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||||
|
#if DEBUG
|
||||||
|
print("[DreamioNavigation] didFailProvisional url=\(redactedURLString(webView.url?.absoluteString ?? "unknown")) error=\(redactedURLString(error.localizedDescription))")
|
||||||
|
#endif
|
||||||
showLoadFailure(error)
|
showLoadFailure(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,6 +311,44 @@ extension DreamioWebViewController: WKNavigationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
extension DreamioWebViewController: WKScriptMessageHandler {
|
||||||
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
guard message.name == Constants.diagnosticsMessageHandler,
|
||||||
|
let body = message.body as? [String: Any],
|
||||||
|
let type = body["type"] as? String
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logDiagnostic(
|
||||||
|
type: type,
|
||||||
|
payload: body["payload"] ?? [:],
|
||||||
|
pageURL: body["href"] as? String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||||
|
weak var delegate: WKScriptMessageHandler?
|
||||||
|
|
||||||
|
init(delegate: WKScriptMessageHandler) {
|
||||||
|
self.delegate = delegate
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
delegate?.userContentController(userContentController, didReceive: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
var nilIfEmpty: String? {
|
||||||
|
isEmpty ? nil : self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
extension DreamioWebViewController: WKUIDelegate {
|
extension DreamioWebViewController: WKUIDelegate {
|
||||||
func webView(
|
func webView(
|
||||||
_ webView: WKWebView,
|
_ webView: WKWebView,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Dreamio Web Inspector and Playback Diagnostics</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--ink: #202126;
|
||||||
|
--muted: #62646f;
|
||||||
|
--paper: #f8f7fb;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--line: #ddd8e8;
|
||||||
|
--accent: #6f4fd8;
|
||||||
|
--accent-soft: #eee9ff;
|
||||||
|
--code: #2d2638;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(111, 79, 216, 0.08), rgba(111, 79, 216, 0) 260px),
|
||||||
|
var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: min(1040px, calc(100vw - 40px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 56px 0 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
max-width: 820px;
|
||||||
|
margin-bottom: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
max-width: 780px;
|
||||||
|
font-size: clamp(2rem, 5vw, 4.4rem);
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
max-width: 760px;
|
||||||
|
margin-top: 18px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 26px 0;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li + li {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: var(--code);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-shell {
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="module">
|
||||||
|
import { FileDiff } from "https://esm.sh/@pierre/diffs";
|
||||||
|
|
||||||
|
const snippets = [
|
||||||
|
{
|
||||||
|
id: "inspector-diff",
|
||||||
|
oldFile: {
|
||||||
|
name: "DreamioWebViewController.swift",
|
||||||
|
contents: `let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||||
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
webView.allowsBackForwardNavigationGestures = true
|
||||||
|
webView.customUserAgent = "Dreamio/0.1 WKWebView"`
|
||||||
|
},
|
||||||
|
newFile: {
|
||||||
|
name: "DreamioWebViewController.swift",
|
||||||
|
contents: `let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||||
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
webView.allowsBackForwardNavigationGestures = true
|
||||||
|
webView.customUserAgent = "Dreamio/0.1 WKWebView"
|
||||||
|
#if DEBUG
|
||||||
|
if #available(iOS 16.4, *) {
|
||||||
|
webView.isInspectable = true
|
||||||
|
}
|
||||||
|
#endif`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "diagnostics-diff",
|
||||||
|
oldFile: {
|
||||||
|
name: "DreamioWebViewController.swift",
|
||||||
|
contents: `configuration.preferences.javaScriptCanOpenWindowsAutomatically = true`
|
||||||
|
},
|
||||||
|
newFile: {
|
||||||
|
name: "DreamioWebViewController.swift",
|
||||||
|
contents: `configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
||||||
|
#if DEBUG
|
||||||
|
configuration.userContentController.add(
|
||||||
|
WeakScriptMessageHandler(delegate: self),
|
||||||
|
name: Constants.diagnosticsMessageHandler
|
||||||
|
)
|
||||||
|
configuration.userContentController.addUserScript(Self.playbackDiagnosticsScript)
|
||||||
|
#endif`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "navigation-diff",
|
||||||
|
oldFile: {
|
||||||
|
name: "DreamioWebViewController.swift",
|
||||||
|
contents: `func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||||
|
showLoadFailure(error)
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
newFile: {
|
||||||
|
name: "DreamioWebViewController.swift",
|
||||||
|
contents: `func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||||
|
#if DEBUG
|
||||||
|
print("[DreamioNavigation] didFail url=... error=...")
|
||||||
|
#endif
|
||||||
|
showLoadFailure(error)
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const snippet of snippets) {
|
||||||
|
const target = document.getElementById(snippet.id);
|
||||||
|
if (!target) continue;
|
||||||
|
new FileDiff({ theme: "github-light" }).render({
|
||||||
|
oldFile: snippet.oldFile,
|
||||||
|
newFile: snippet.newFile,
|
||||||
|
containerWrapper: target
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<div class="eyebrow">Repository implementation turn</div>
|
||||||
|
<h1>Dreamio Web Inspector and Playback Diagnostics</h1>
|
||||||
|
<p class="summary">Enabled development-only Safari inspection for Dreamio's <code>WKWebView</code> and added token-safe diagnostics for console warnings, promise rejections, video failures, navigation errors, and HTTP navigation statuses.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Summary</h2>
|
||||||
|
<p>This pass improves observability without changing Dreamio's login, navigation, addon browsing, or playback behavior. Debug builds on iOS 16.4 and newer now opt the WebView into Safari Web Inspector, and page diagnostics flow back to Xcode logs with URL queries, fragments, bearer tokens, and long token-like path segments redacted.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Changes Made</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Enabled <code>webView.isInspectable</code> for <code>DEBUG</code> builds on iOS 16.4 and newer.</li>
|
||||||
|
<li>Installed a small <code>WKUserScript</code> at document start to observe console warnings, console errors, unhandled promise rejections, and dynamically inserted <code><video></code> elements.</li>
|
||||||
|
<li>Added native logging for <code>WKNavigationDelegate</code> response statuses and load failures.</li>
|
||||||
|
<li>Added native redaction helpers before diagnostic data is printed.</li>
|
||||||
|
<li>Kept all diagnostics behind <code>#if DEBUG</code> so release builds do not expose the inspection or message bridge surface.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Context</h2>
|
||||||
|
<p>The current Debridio VOD failure appears to need browser-level evidence: stream URL shape, request headers, response metadata, MIME type, and JavaScript media errors. Before adding a native player path, Dreamio needs a reliable way to inspect hosted Stremio Web inside the app and collect media failure details from the page itself.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Important Implementation Details</h2>
|
||||||
|
<ul>
|
||||||
|
<li>The diagnostics bridge posts messages through <code>window.webkit.messageHandlers.dreamioDiagnostics</code>.</li>
|
||||||
|
<li>The user script attaches to existing videos and videos inserted later through a <code>MutationObserver</code>.</li>
|
||||||
|
<li>Video diagnostics include <code>networkState</code>, <code>readyState</code>, <code>currentSrc</code>, media error code, and media error message.</li>
|
||||||
|
<li>The native logger strips URL query strings and fragments, redacts obvious token-like key values, redacts bearer credentials, and replaces long token-like path segments.</li>
|
||||||
|
<li>A weak message-handler wrapper avoids the common <code>WKUserContentController</code> retain cycle.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Relevant Diff Snippets</h2>
|
||||||
|
<p>The snippets below are rendered with <code>@pierre/diffs</code> from diffs.com-compatible components.</p>
|
||||||
|
<div class="diff-shell" id="inspector-diff"></div>
|
||||||
|
<div class="diff-shell" id="diagnostics-diff" style="margin-top: 14px;"></div>
|
||||||
|
<div class="diff-shell" id="navigation-diff" style="margin-top: 14px;"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Expected Impact for End-Users</h2>
|
||||||
|
<p>There should be no visible behavior change for ordinary app use. For development builds, Safari should now show an inspectable Dreamio or <code>web.stremio.com</code> target while the app is foregrounded, making the playback failure much easier to diagnose.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Validation</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Ran <code>DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -list -project Dreamio.xcodeproj</code> to confirm the <code>Dreamio</code> scheme.</li>
|
||||||
|
<li>Ran <code>DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -project Dreamio.xcodeproj -scheme Dreamio -configuration Debug -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build</code>.</li>
|
||||||
|
<li>The Debug simulator build succeeded.</li>
|
||||||
|
</ul>
|
||||||
|
<div class="callout">Manual real-device validation is still needed on <code>kellcd</code>: launch Dreamio, open Safari inspection, reproduce the Debridio VOD failure, and collect Console, Network, and media logs.</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Issues, Limitations, and Mitigations</h2>
|
||||||
|
<ul>
|
||||||
|
<li>This does not fix playback. It adds the evidence-gathering surface for the next diagnosis step.</li>
|
||||||
|
<li>Safari Web Inspector availability still depends on the device, iOS version, Safari settings, and the app being a debug/development build.</li>
|
||||||
|
<li>Redaction is intentionally conservative but cannot prove every possible secret shape is removed. It strips common URL and token forms before logging.</li>
|
||||||
|
<li>The first <code>xcodebuild</code> attempt failed because the active developer directory pointed at Command Line Tools. Re-running with <code>DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer</code> succeeded.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Follow-up Work</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Use Safari Web Inspector on <code>kellcd</code> to capture the failing stream request and media error details.</li>
|
||||||
|
<li>File a follow-up Beads issue if the evidence points to a native-player fallback, MIME/header adjustment, or hosted Stremio compatibility gap.</li>
|
||||||
|
<li>Consider adding a temporary debug menu to toggle diagnostics if the log volume gets noisy during broader testing.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue