mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-07 13:58:15 +00:00
forward late subtitles to native player
This commit is contained in:
parent
66f66faf28
commit
d6bcb52e8a
8 changed files with 729 additions and 7 deletions
|
|
@ -6,6 +6,7 @@ final class DreamioWebViewController: UIViewController {
|
|||
static let stremioWebURL = URL(string: "https://web.stremio.com/")!
|
||||
static let diagnosticsMessageHandler = "dreamioDiagnostics"
|
||||
static let streamCandidateMessageHandler = "dreamioStreamCandidate"
|
||||
static let subtitleCandidateMessageHandler = "dreamioSubtitleCandidate"
|
||||
}
|
||||
|
||||
private lazy var webView: WKWebView = {
|
||||
|
|
@ -18,6 +19,10 @@ final class DreamioWebViewController: UIViewController {
|
|||
WeakScriptMessageHandler(delegate: self),
|
||||
name: Constants.streamCandidateMessageHandler
|
||||
)
|
||||
configuration.userContentController.add(
|
||||
WeakScriptMessageHandler(delegate: self),
|
||||
name: Constants.subtitleCandidateMessageHandler
|
||||
)
|
||||
configuration.userContentController.addUserScript(Self.streamCandidateScript)
|
||||
#if DEBUG
|
||||
configuration.userContentController.add(
|
||||
|
|
@ -52,6 +57,7 @@ final class DreamioWebViewController: UIViewController {
|
|||
private var progressObservation: NSKeyValueObservation?
|
||||
private var userAgent: String?
|
||||
private var lastNativePlaybackURL: URL?
|
||||
private weak var currentNativePlayer: NativePlayerViewController?
|
||||
private let streamResolver: StreamResolving = StremioStreamResolver()
|
||||
|
||||
private static let streamCandidateScript = WKUserScript(
|
||||
|
|
@ -73,6 +79,7 @@ final class DreamioWebViewController: UIViewController {
|
|||
/\.mp4(?:[?#]|$)/i
|
||||
];
|
||||
const subtitleCandidates = [];
|
||||
const postedSubtitleURLs = new Set();
|
||||
const subtitleURLPattern = /https?:\/\/[^\s"'<>]+(?:\.srt|\.vtt|\.ass|\.ssa|\.sub|opensubtitles|subtitle)[^\s"'<>]*/ig;
|
||||
|
||||
const looksNative = (url) => {
|
||||
|
|
@ -119,6 +126,25 @@ final class DreamioWebViewController: UIViewController {
|
|||
} catch (_) {}
|
||||
};
|
||||
|
||||
const postSubtitleCandidates = (candidates) => {
|
||||
const fresh = candidates.filter((candidate) => {
|
||||
if (postedSubtitleURLs.has(candidate.url)) {
|
||||
return false;
|
||||
}
|
||||
postedSubtitleURLs.add(candidate.url);
|
||||
return true;
|
||||
});
|
||||
if (fresh.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.webkit.messageHandlers.dreamioSubtitleCandidate.postMessage({
|
||||
pageUrl: window.location.href,
|
||||
subtitles: fresh
|
||||
});
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
const addSubtitleCandidate = (entry) => {
|
||||
const rawURL = typeof entry === "string" ? entry : entry && (entry.url || entry.href || entry.src || entry.file || entry.download);
|
||||
const url = absoluteURL(rawURL);
|
||||
|
|
@ -131,11 +157,13 @@ final class DreamioWebViewController: UIViewController {
|
|||
if (subtitleCandidates.some((candidate) => candidate.url === url)) {
|
||||
return;
|
||||
}
|
||||
subtitleCandidates.push({
|
||||
const candidate = {
|
||||
url,
|
||||
label: entry && (entry.label || entry.name || entry.title || entry.lang || entry.language) || "External Subtitle",
|
||||
language: entry && (entry.lang || entry.language) || ""
|
||||
});
|
||||
};
|
||||
subtitleCandidates.push(candidate);
|
||||
postSubtitleCandidates([candidate]);
|
||||
};
|
||||
|
||||
const inspectSubtitlePayload = (payload) => {
|
||||
|
|
@ -422,7 +450,7 @@ final class DreamioWebViewController: UIViewController {
|
|||
|
||||
#if DEBUG
|
||||
let classification = request.classification
|
||||
print("[DreamioStream] class=\(classification.sourceKind.rawValue) container=\(classification.containerGuess.rawValue) reason=\(classification.reason) observed=\(classification.sanitizedObservedURL) resolver=\(classification.sanitizedResolverURL ?? "none")")
|
||||
print("[DreamioStream] class=\(classification.sourceKind.rawValue) container=\(classification.containerGuess.rawValue) reason=\(classification.reason) subtitles=\(request.subtitleCandidates.count) observed=\(classification.sanitizedObservedURL) resolver=\(classification.sanitizedResolverURL ?? "none")")
|
||||
#endif
|
||||
|
||||
Task { [weak self] in
|
||||
|
|
@ -430,6 +458,24 @@ final class DreamioWebViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
private func handleSubtitleCandidates(_ candidates: [SubtitleCandidate]) {
|
||||
guard !candidates.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let currentNativePlayer else {
|
||||
#if DEBUG
|
||||
print("[DreamioSubtitles] discovered=\(candidates.count) forwarded=0 reason=no-active-native-player")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
let forwarded = currentNativePlayer.addSubtitleCandidates(candidates)
|
||||
#if DEBUG
|
||||
print("[DreamioSubtitles] discovered=\(candidates.count) forwarded=\(forwarded)")
|
||||
#endif
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func resolveAndPresentNativePlayback(_ request: NativePlaybackRequest) async {
|
||||
guard VLCNativePlaybackBackend.isAvailable else {
|
||||
|
|
@ -455,8 +501,10 @@ final class DreamioWebViewController: UIViewController {
|
|||
subtitleCandidates: request.subtitleCandidates
|
||||
)
|
||||
let player = NativePlayerViewController(request: resolvedRequest)
|
||||
currentNativePlayer = player
|
||||
player.onDismiss = { [weak self] in
|
||||
self?.lastNativePlaybackURL = nil
|
||||
self?.currentNativePlayer = nil
|
||||
self?.cleanUpStremioPlayerAfterNativeDismiss()
|
||||
}
|
||||
present(player, animated: true)
|
||||
|
|
@ -669,6 +717,11 @@ extension DreamioWebViewController: WKScriptMessageHandler {
|
|||
return
|
||||
}
|
||||
|
||||
if message.name == Constants.subtitleCandidateMessageHandler {
|
||||
handleSubtitleCandidates(SubtitleCandidateParser.candidates(in: message.body))
|
||||
return
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
guard message.name == Constants.diagnosticsMessageHandler,
|
||||
let body = message.body as? [String: Any],
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ protocol NativePlaybackBackend: AnyObject {
|
|||
func jump(by seconds: TimeInterval)
|
||||
func selectSubtitleTrack(id: Int32)
|
||||
func adjustSubtitleDelay(by seconds: TimeInterval)
|
||||
@discardableResult
|
||||
func addSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int
|
||||
func stop()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ final class NativePlayerViewController: UIViewController {
|
|||
private var controlsTimer: Timer?
|
||||
private var progressTimer: Timer?
|
||||
private var isScrubbing = false
|
||||
private var attachedSubtitleURLs: Set<URL>
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
private let loadingView: UIActivityIndicatorView = {
|
||||
|
|
@ -94,6 +95,7 @@ final class NativePlayerViewController: UIViewController {
|
|||
init(request: NativePlaybackRequest, backend: NativePlaybackBackend = VLCNativePlaybackBackend()) {
|
||||
self.request = request
|
||||
self.backend = backend
|
||||
self.attachedSubtitleURLs = Set(request.subtitleCandidates.map(\.url))
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .fullScreen
|
||||
modalTransitionStyle = .crossDissolve
|
||||
|
|
@ -126,6 +128,26 @@ final class NativePlayerViewController: UIViewController {
|
|||
backend.play(request: request)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {
|
||||
let newCandidates = candidates.filter { candidate in
|
||||
guard !attachedSubtitleURLs.contains(candidate.url) else {
|
||||
return false
|
||||
}
|
||||
attachedSubtitleURLs.insert(candidate.url)
|
||||
return true
|
||||
}
|
||||
let attachedCount = backend.addSubtitleCandidates(newCandidates)
|
||||
if attachedCount > 0 {
|
||||
refreshControls()
|
||||
}
|
||||
#if DEBUG
|
||||
let duplicateCount = candidates.count - newCandidates.count
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) forwarded=\(newCandidates.count) attached=\(attachedCount) duplicates=\(duplicateCount)")
|
||||
#endif
|
||||
return attachedCount
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
startupTimer?.invalidate()
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
print("[DreamioVLC] opening url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
|
||||
#endif
|
||||
mediaPlayer.play()
|
||||
attachSubtitles(request.subtitleCandidates)
|
||||
addSubtitleCandidates(request.subtitleCandidates)
|
||||
#else
|
||||
onFailure?(NativePlaybackError.backendUnavailable)
|
||||
#endif
|
||||
|
|
@ -113,6 +113,15 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
#endif
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {
|
||||
#if canImport(MobileVLCKit)
|
||||
return attachSubtitles(candidates)
|
||||
#else
|
||||
return 0
|
||||
#endif
|
||||
}
|
||||
|
||||
func stop() {
|
||||
#if canImport(MobileVLCKit)
|
||||
mediaPlayer.stop()
|
||||
|
|
@ -194,23 +203,33 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
}
|
||||
|
||||
#if canImport(MobileVLCKit)
|
||||
private func attachSubtitles(_ candidates: [SubtitleCandidate]) {
|
||||
private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {
|
||||
var attachedCount = 0
|
||||
var duplicateCount = 0
|
||||
candidates.forEach { candidate in
|
||||
guard !attachedSubtitleURLs.contains(candidate.url) else {
|
||||
duplicateCount += 1
|
||||
return
|
||||
}
|
||||
attachedSubtitleURLs.insert(candidate.url)
|
||||
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
|
||||
attachedCount += 1
|
||||
#if DEBUG
|
||||
print("[DreamioVLC] attached subtitle=\(URLRedactor.redactedURLString(candidate.url.absoluteString))")
|
||||
#endif
|
||||
}
|
||||
guard !candidates.isEmpty else {
|
||||
return
|
||||
#if DEBUG
|
||||
if !candidates.isEmpty {
|
||||
print("[DreamioVLC] subtitle candidates=\(candidates.count) attached=\(attachedCount) duplicates=\(duplicateCount)")
|
||||
}
|
||||
#endif
|
||||
guard attachedCount > 0 else {
|
||||
return attachedCount
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
||||
self?.onSubtitleTracksChange?()
|
||||
}
|
||||
return attachedCount
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue