add native player controls captions and close cleanup

This commit is contained in:
dirtydishes 2026-05-25 05:49:59 -04:00
parent 8d4dd0870a
commit fdeacce389
6 changed files with 265 additions and 3 deletions

View file

@ -122,6 +122,7 @@ final class DreamioWebViewController: UIViewController {
const addSubtitleCandidate = (entry) => {
const rawURL = typeof entry === "string" ? entry : entry && (entry.url || entry.href || entry.src || entry.file || entry.download);
const url = absoluteURL(rawURL);
subtitleURLPattern.lastIndex = 0;
if (!url || !subtitleURLPattern.test(url)) {
subtitleURLPattern.lastIndex = 0;
return;
@ -517,12 +518,18 @@ final class DreamioWebViewController: UIViewController {
const clicked = clickVisible([
"[aria-label*='Close' i]",
"[aria-label*='Back' i]",
"[title*='Close' i]",
"[title*='Back' i]",
"button[class*='close' i]",
"button[class*='back' i]",
"[class*='close' i]",
"[class*='back' i]",
".player button",
"[role='button']"
]);
const stillPlayer = /player|stream|buffer|prepar/i.test(document.body.innerText || "");
const locationLooksPlayer = /\/(player|stream)\b/i.test(window.location.pathname || "") || /player|stream/i.test(window.location.hash || "");
const visibleBusyPlayer = Boolean(document.querySelector("video, .player, [class*='player' i], [class*='buffer' i]"));
const stillPlayer = locationLooksPlayer || (visibleBusyPlayer && /buffer|prepar|stream/i.test(document.body.innerText || ""));
return { clicked, stillPlayer, href: window.location.href };
})();
"""#
@ -544,7 +551,14 @@ final class DreamioWebViewController: UIViewController {
}
if self.webView.canGoBack {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.webView.evaluateJavaScript("(/player|stream|buffer|prepar/i).test(document.body.innerText || '')") { result, _ in
let stillPlayerScript = #"""
(() => {
const locationLooksPlayer = /\/(player|stream)\b/i.test(window.location.pathname || "") || /player|stream/i.test(window.location.hash || "");
const visibleBusyPlayer = Boolean(document.querySelector("video, .player, [class*='player' i], [class*='buffer' i]"));
return locationLooksPlayer || (visibleBusyPlayer && /buffer|prepar|stream/i.test(document.body.innerText || ""));
})()
"""#
self.webView.evaluateJavaScript(stillPlayerScript) { result, _ in
if (result as? Bool) == true {
self.webView.goBack()
}

View file

@ -36,6 +36,13 @@ final class NativePlayerViewController: UIViewController {
return view
}()
private let tapSurfaceView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
return view
}()
private let playPauseButton = NativePlayerViewController.iconButton(systemName: "pause.fill", label: "Play or Pause")
private let jumpBackButton = NativePlayerViewController.iconButton(systemName: "gobackward.15", label: "Jump Back 15 Seconds")
private let jumpForwardButton = NativePlayerViewController.iconButton(systemName: "goforward.15", label: "Jump Forward 15 Seconds")
@ -167,6 +174,7 @@ final class NativePlayerViewController: UIViewController {
private func configureLayout() {
view.addSubview(backend.view)
view.addSubview(tapSurfaceView)
view.addSubview(loadingView)
view.addSubview(failureLabel)
view.addSubview(controlsContainer)
@ -182,7 +190,7 @@ final class NativePlayerViewController: UIViewController {
let tap = UITapGestureRecognizer(target: self, action: #selector(toggleControlsVisibility))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
tapSurfaceView.addGestureRecognizer(tap)
let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])
controlRow.translatesAutoresizingMaskIntoConstraints = false
@ -208,6 +216,11 @@ final class NativePlayerViewController: UIViewController {
backend.view.topAnchor.constraint(equalTo: view.topAnchor),
backend.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tapSurfaceView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tapSurfaceView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tapSurfaceView.topAnchor.constraint(equalTo: view.topAnchor),
tapSurfaceView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
@ -338,6 +351,8 @@ final class NativePlayerViewController: UIViewController {
}
private func revealControls() {
controlsContainer.isUserInteractionEnabled = true
closeButton.isUserInteractionEnabled = true
UIView.animate(withDuration: 0.18) {
self.controlsContainer.alpha = 1
self.closeButton.alpha = 1
@ -346,6 +361,8 @@ final class NativePlayerViewController: UIViewController {
}
private func hideControls() {
controlsContainer.isUserInteractionEnabled = false
closeButton.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.24) {
self.controlsContainer.alpha = 0
self.closeButton.alpha = 0

View file

@ -40,6 +40,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
func play(request: NativePlaybackRequest) {
#if canImport(MobileVLCKit)
attachedSubtitleURLs.removeAll()
let media = VLCMedia(url: request.playbackURL)
let headerValue = request.headers
.map { "\($0.key): \($0.value)" }
@ -204,6 +205,12 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
print("[DreamioVLC] attached subtitle=\(URLRedactor.redactedURLString(candidate.url.absoluteString))")
#endif
}
guard !candidates.isEmpty else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.onSubtitleTracksChange?()
}
}
#endif
}