mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 13:37:24 +00:00
add subtitle pipeline proof logging
This commit is contained in:
parent
fdc4444f6a
commit
d8ebc7c7f9
7 changed files with 473 additions and 7 deletions
|
|
@ -16,3 +16,4 @@
|
|||
{"id":"int-38a97132","kind":"field_change","created_at":"2026-05-25T10:43:21.805452Z","actor":"dirtydishes","issue_id":"dreamio-lw6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented late subtitle forwarding into active native playback, added VLC append path and parser tests."}}
|
||||
{"id":"int-ddab585f","kind":"field_change","created_at":"2026-05-25T11:07:34.849628Z","actor":"dirtydishes","issue_id":"dreamio-8cz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation."}}
|
||||
{"id":"int-e07aeefe","kind":"field_change","created_at":"2026-05-25T13:50:43.373777Z","actor":"dirtydishes","issue_id":"dreamio-h5q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Resolved OpenSubtitles V3 API-style subtitle download URLs to direct subtitle files before VLC attachment; added parser/resolver coverage and simulator build validation."}}
|
||||
{"id":"int-c7246990","kind":"field_change","created_at":"2026-05-25T14:07:13.774172Z","actor":"dirtydishes","issue_id":"dreamio-e9p","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added DEBUG-only subtitle pipeline proof logging and documented validation."}}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
{"_type":"issue","id":"dreamio-l68","title":"Add native playback for direct debrid streams","description":"Implement a WKWebView JavaScript bridge that detects direct-file debrid media URLs and routes unsupported containers to a native player backend, initially MobileVLCKit, while preserving normal Stremio Web playback for compatible streams.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:13:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:20:17Z","started_at":"2026-05-25T03:13:28Z","closed_at":"2026-05-25T03:20:17Z","close_reason":"Implemented native direct-stream bridge, classification, MobileVLCKit backend wiring, CocoaPods workflow docs, and turn documentation. Full iOS build is blocked locally by missing CocoaPods and iPhoneOS SDK.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_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-e9p","title":"Add native subtitle pipeline proof logging","description":"Add DEBUG-only logs across the web bridge, native player, subtitle resolution, and VLC attachment points so the next Xcode run can identify where external subtitles disappear without changing playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:03:18Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:07:14Z","started_at":"2026-05-25T14:03:22Z","closed_at":"2026-05-25T14:07:14Z","close_reason":"Added DEBUG-only subtitle pipeline proof logging and documented validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"dreamio-88m","title":"Make caption selection states clearer","description":"The native player caption menu should behave like a simple single-choice menu with None and loaded caption tracks, making the current caption state visually obvious.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T10:22:12Z","created_by":"dirtydishes","updated_at":"2026-05-25T10:25:23Z","started_at":"2026-05-25T10:22:48Z","closed_at":"2026-05-25T10:25:23Z","close_reason":"Implemented captions as a single-choice menu with None and selected loaded tracks, updated tests and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"dreamio-7w6","title":"Streamline native player controls","description":"Make the native playback controls take up less screen space while preserving play, seek, jump, captions, and close actions.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T10:15:49Z","created_by":"dirtydishes","updated_at":"2026-05-25T10:18:31Z","started_at":"2026-05-25T10:15:59Z","closed_at":"2026-05-25T10:18:31Z","close_reason":"Streamlined native player controls into a compact bottom overlay and validated the simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"dreamio-mj8","title":"Add native player controls and captions","description":"Implement a fuller VLC-backed native playback surface with transport controls, caption controls, external subtitle discovery, and a clean close flow back to Stremio episode selection.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T04:57:53Z","created_by":"dirtydishes","updated_at":"2026-05-25T05:04:55Z","started_at":"2026-05-25T04:57:57Z","closed_at":"2026-05-25T05:04:55Z","close_reason":"Implemented native VLC player controls, caption controls, subtitle candidate discovery, and close-flow cleanup.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ final class DreamioWebViewController: UIViewController {
|
|||
};
|
||||
|
||||
const postSubtitleCandidates = (candidates) => {
|
||||
const discoveredCount = candidates.length;
|
||||
const fresh = candidates.filter((candidate) => {
|
||||
if (postedSubtitleURLs.has(candidate.url)) {
|
||||
return false;
|
||||
|
|
@ -135,12 +136,28 @@ final class DreamioWebViewController: UIViewController {
|
|||
return true;
|
||||
});
|
||||
if (fresh.length === 0) {
|
||||
try {
|
||||
window.webkit.messageHandlers.dreamioSubtitleCandidate.postMessage({
|
||||
pageUrl: window.location.href,
|
||||
subtitles: [],
|
||||
debug: {
|
||||
discovered: discoveredCount,
|
||||
deduped: 0,
|
||||
forwarded: 0
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.webkit.messageHandlers.dreamioSubtitleCandidate.postMessage({
|
||||
pageUrl: window.location.href,
|
||||
subtitles: fresh
|
||||
subtitles: fresh,
|
||||
debug: {
|
||||
discovered: discoveredCount,
|
||||
deduped: fresh.length,
|
||||
forwarded: fresh.length
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
};
|
||||
|
|
@ -480,6 +497,9 @@ final class DreamioWebViewController: UIViewController {
|
|||
return
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
print("[DreamioSubtitles] native discovered=\(candidates.count) playerActive=\(currentNativePlayer != nil) candidates=\(SubtitleDebugFormatter.candidateSummary(candidates))")
|
||||
#endif
|
||||
guard let currentNativePlayer else {
|
||||
#if DEBUG
|
||||
print("[DreamioSubtitles] discovered=\(candidates.count) forwarded=0 reason=no-active-native-player")
|
||||
|
|
@ -489,7 +509,7 @@ final class DreamioWebViewController: UIViewController {
|
|||
|
||||
let forwarded = currentNativePlayer.addSubtitleCandidates(candidates)
|
||||
#if DEBUG
|
||||
print("[DreamioSubtitles] discovered=\(candidates.count) forwarded=\(forwarded)")
|
||||
print("[DreamioSubtitles] discovered=\(candidates.count) forwarded=\(forwarded) reason=active-native-player")
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -658,6 +678,16 @@ final class DreamioWebViewController: UIViewController {
|
|||
private func redactedURLString(_ value: String) -> String {
|
||||
URLRedactor.redactedURLString(value)
|
||||
}
|
||||
|
||||
private func logSubtitleBridgeMessage(_ body: Any, parsedCandidates: [SubtitleCandidate]) {
|
||||
let dictionary = body as? [String: Any]
|
||||
let debug = dictionary?["debug"] as? [String: Any]
|
||||
let discovered = debug?["discovered"] as? Int ?? parsedCandidates.count
|
||||
let deduped = debug?["deduped"] as? Int ?? parsedCandidates.count
|
||||
let posted = debug?["forwarded"] as? Int ?? parsedCandidates.count
|
||||
let pageURL = dictionary?["pageUrl"] as? String
|
||||
print("[DreamioSubtitles] bridge discovered=\(discovered) deduped=\(deduped) posted=\(posted) parsed=\(parsedCandidates.count) playerActive=\(currentNativePlayer != nil) page=\(pageURL.map(redactedURLString) ?? "unknown") candidates=\(SubtitleDebugFormatter.candidateSummary(parsedCandidates))")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -735,7 +765,11 @@ extension DreamioWebViewController: WKScriptMessageHandler {
|
|||
}
|
||||
|
||||
if message.name == Constants.subtitleCandidateMessageHandler {
|
||||
handleSubtitleCandidates(SubtitleCandidateParser.candidates(in: message.body))
|
||||
let candidates = SubtitleCandidateParser.candidates(in: message.body)
|
||||
#if DEBUG
|
||||
logSubtitleBridgeMessage(message.body, parsedCandidates: candidates)
|
||||
#endif
|
||||
handleSubtitleCandidates(candidates)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ final class NativePlayerViewController: UIViewController {
|
|||
let pendingCandidates = candidates.filter { !attachedSubtitleURLs.contains($0.url) }
|
||||
guard !pendingCandidates.isEmpty else {
|
||||
#if DEBUG
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=0 duplicates=\(candidates.count)")
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=0 duplicates=\(candidates.count) resolved=0 attached=0 tracks=\(SubtitleDebugFormatter.trackSummary(backend.subtitleTracks)) selected=\(backend.selectedSubtitleTrackID)")
|
||||
#endif
|
||||
return 0
|
||||
}
|
||||
|
|
@ -155,7 +155,7 @@ final class NativePlayerViewController: UIViewController {
|
|||
await MainActor.run {
|
||||
guard !resolvedCandidates.isEmpty else {
|
||||
#if DEBUG
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=\(pendingCandidates.count) resolved=0 attached=0")
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=\(pendingCandidates.count) resolved=0 attached=0 tracks=\(SubtitleDebugFormatter.trackSummary(self.backend.subtitleTracks)) selected=\(self.backend.selectedSubtitleTrackID) candidates=\(SubtitleDebugFormatter.candidateSummary(pendingCandidates))")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ final class NativePlayerViewController: UIViewController {
|
|||
}
|
||||
#if DEBUG
|
||||
let duplicateCount = candidates.count - pendingCandidates.count + resolvedCandidates.count - attachableCandidates.count
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=\(pendingCandidates.count) resolved=\(resolvedCandidates.count) attached=\(attachedCount) duplicates=\(duplicateCount)")
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=\(pendingCandidates.count) resolved=\(resolvedCandidates.count) attachable=\(attachableCandidates.count) attached=\(attachedCount) duplicates=\(duplicateCount) tracks=\(SubtitleDebugFormatter.trackSummary(self.backend.subtitleTracks)) selected=\(self.backend.selectedSubtitleTrackID) resolvedCandidates=\(SubtitleDebugFormatter.candidateSummary(resolvedCandidates))")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,33 @@ struct SubtitleTrack: Equatable {
|
|||
let name: String
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
enum SubtitleDebugFormatter {
|
||||
static func candidateSummary(_ candidates: [SubtitleCandidate]) -> String {
|
||||
guard !candidates.isEmpty else {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
return candidates.map { candidate in
|
||||
let extensionLabel = candidate.url.pathExtension.isEmpty ? "none" : candidate.url.pathExtension.lowercased()
|
||||
let language = candidate.language?.isEmpty == false ? candidate.language! : "unknown"
|
||||
let label = candidate.label.isEmpty ? "External Subtitle" : candidate.label
|
||||
return "{label=\(label), language=\(language), ext=\(extensionLabel)}"
|
||||
}.joined(separator: ", ")
|
||||
}
|
||||
|
||||
static func trackSummary(_ tracks: [SubtitleTrack]) -> String {
|
||||
guard !tracks.isEmpty else {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
return tracks.map { track in
|
||||
"{id=\(track.id), name=\(track.name)}"
|
||||
}.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
enum PlaybackTimeFormatter {
|
||||
static func label(for seconds: TimeInterval) -> String {
|
||||
guard seconds.isFinite, seconds > 0 else {
|
||||
|
|
|
|||
|
|
@ -214,7 +214,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
|
||||
attachedCount += 1
|
||||
#if DEBUG
|
||||
print("[DreamioVLC] attached subtitle=\(URLRedactor.redactedURLString(candidate.url.absoluteString))")
|
||||
print("[DreamioVLC] addPlaybackSlave subtitle=\(URLRedactor.redactedURLString(candidate.url.absoluteString)) label=\(candidate.label) language=\(candidate.language ?? "unknown") ext=\(candidate.url.pathExtension.lowercased())")
|
||||
logSubtitleTracks(reason: "after-addPlaybackSlave")
|
||||
#endif
|
||||
}
|
||||
#if DEBUG
|
||||
|
|
@ -226,10 +227,21 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
return attachedCount
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
||||
#if DEBUG
|
||||
self?.logSubtitleTracks(reason: "delayed-refresh")
|
||||
#endif
|
||||
self?.onSubtitleTracksChange?()
|
||||
}
|
||||
return attachedCount
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private func logSubtitleTracks(reason: String) {
|
||||
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
|
||||
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []
|
||||
print("[DreamioVLC] subtitle tracks reason=\(reason) names=\(names) indexes=\(indexes.map { $0.int32Value }) selected=\(mediaPlayer.currentVideoSubTitleIndex)")
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -248,6 +260,9 @@ extension VLCNativePlaybackBackend: VLCMediaPlayerDelegate {
|
|||
case .paused, .stopped, .ended:
|
||||
onStateChange?()
|
||||
case .esAdded:
|
||||
#if DEBUG
|
||||
logSubtitleTracks(reason: "esAdded")
|
||||
#endif
|
||||
onSubtitleTracksChange?()
|
||||
default:
|
||||
break
|
||||
|
|
|
|||
388
docs/turns/2026-05-25-prove-native-subtitle-pipeline.html
Normal file
388
docs/turns/2026-05-25-prove-native-subtitle-pipeline.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue