From fdeacce3892876eb346d8220fcf842166fd306bb Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Mon, 25 May 2026 05:49:59 -0400 Subject: [PATCH] add native player controls captions and close cleanup --- .beads/interactions.jsonl | 1 + .beads/issues.jsonl | 2 + Dreamio/DreamioWebViewController.swift | 18 +- Dreamio/NativePlayerViewController.swift | 19 +- Dreamio/VLCNativePlaybackBackend.swift | 7 + ...e-player-controls-captions-close-flow.html | 221 ++++++++++++++++++ 6 files changed, 265 insertions(+), 3 deletions(-) diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index e8fa5cb..876c137 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -9,3 +9,4 @@ {"id":"int-74805ffd","kind":"field_change","created_at":"2026-05-25T04:21:42.440755Z","actor":"dirtydishes","issue_id":"dreamio-2k5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added native backend availability guard, installed CocoaPods, generated workspace metadata, documented setup, and validated available checks."}} {"id":"int-27a61615","kind":"field_change","created_at":"2026-05-25T04:44:35.633997Z","actor":"dirtydishes","issue_id":"dreamio-ija","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed MobileVLCKit linker failures by preparing the XCFramework slice before app linking and preserving the integration through pod install."}} {"id":"int-fad68cb4","kind":"field_change","created_at":"2026-05-25T05:04:55.103302Z","actor":"dirtydishes","issue_id":"dreamio-mj8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented native VLC player controls, caption controls, subtitle candidate discovery, and close-flow cleanup."}} +{"id":"int-6b806f87","kind":"field_change","created_at":"2026-05-25T09:49:39.908604Z","actor":"dirtydishes","issue_id":"dreamio-poo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and validated native player controls, subtitle handling refinements, and close-flow cleanup."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index dfac9eb..f61e815 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,5 @@ +{"_type":"issue","id":"dreamio-poo","title":"Native player controls captions and close flow","description":"Add and validate VLC-backed native playback transport controls, subtitle track controls, external subtitle discovery, and Stremio Web close cleanup after native playback dismisses.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T09:47:56Z","created_by":"dirtydishes","updated_at":"2026-05-25T09:49:40Z","started_at":"2026-05-25T09:48:00Z","closed_at":"2026-05-25T09:49:40Z","close_reason":"Implemented and validated native player controls, subtitle handling refinements, and close-flow cleanup.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"dreamio-wgk","title":"Fix native player controls tap-to-show","description":"Native player controls can be hidden by tapping, but subsequent taps on the player do not bring them back. Investigate the overlay gesture handling and restore reliable tap-to-show/tap-to-hide behavior.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T09:27:58Z","created_by":"dirtydishes","updated_at":"2026-05-25T09:28:11Z","started_at":"2026-05-25T09:28:11Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-ija","title":"Fix MobileVLCKit linker dependency","description":"Dreamio fails to link because the MobileVLCKit framework is not found. Investigate how the dependency is configured and update the repository so the framework is available to Xcode builds.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T04:40:49Z","created_by":"dirtydishes","updated_at":"2026-05-25T04:44:36Z","started_at":"2026-05-25T04:40:57Z","closed_at":"2026-05-25T04:44:36Z","close_reason":"Fixed MobileVLCKit linker failures by preparing the XCFramework slice before app linking and preserving the integration through pod install.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-2k5","title":"Guard native playback when MobileVLCKit is unavailable","description":"Dreamio can currently present its native player from raw xcodeproj builds where MobileVLCKit is not linked, which leads to the fallback backend message instead of an actionable setup path. Add a runtime/build availability check, document the CocoaPods workspace requirement, and validate the fallback remains buildable.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T04:15:47Z","created_by":"dirtydishes","updated_at":"2026-05-25T04:21:42Z","started_at":"2026-05-25T04:15:56Z","closed_at":"2026-05-25T04:21:42Z","close_reason":"Added native backend availability guard, installed CocoaPods, generated workspace metadata, documented setup, and validated available checks.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-8vi","title":"Fix URL redaction crash on percent-encoded paths","description":"## Why\nDreamio can crash while logging WebKit navigation and playback URLs because URLRedactor writes raw replacement text back into URLComponents.percentEncodedPath.\n\n## What needs to be done\n- Update URL redaction to avoid assigning invalid characters to percentEncodedPath\n- Preserve token/path redaction behavior for diagnostics\n- Add a regression test covering percent-encoded path input similar to the Stremio crash logs\n\n## Acceptance criteria\n- Redacting a URL with percent-encoded path segments does not crash\n- Diagnostics still remove query strings/fragments and redact token-like path segments\n- Tests cover the regression","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:50:04Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:51:39Z","started_at":"2026-05-25T03:50:08Z","closed_at":"2026-05-25T03:51:39Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift index 06ecfe8..301ef99 100644 --- a/Dreamio/DreamioWebViewController.swift +++ b/Dreamio/DreamioWebViewController.swift @@ -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() } diff --git a/Dreamio/NativePlayerViewController.swift b/Dreamio/NativePlayerViewController.swift index 54de22d..a8d5fa5 100644 --- a/Dreamio/NativePlayerViewController.swift +++ b/Dreamio/NativePlayerViewController.swift @@ -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 diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift index 167b241..d891c6f 100644 --- a/Dreamio/VLCNativePlaybackBackend.swift +++ b/Dreamio/VLCNativePlaybackBackend.swift @@ -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 } diff --git a/docs/turns/2026-05-25-native-player-controls-captions-close-flow.html b/docs/turns/2026-05-25-native-player-controls-captions-close-flow.html index 14256ed..fbaaf5e 100644 --- a/docs/turns/2026-05-25-native-player-controls-captions-close-flow.html +++ b/docs/turns/2026-05-25-native-player-controls-captions-close-flow.html @@ -380,6 +380,227 @@
  • Add a UI test harness or injectable mock backend for exercising native player overlay behavior without MobileVLCKit.
  • +
    +

    New Changes as of May 25, 2026 at 05:49 EDT

    +

    Summary of changes

    +

    After the broader native-player pass, I tightened three follow-up details: taps now use a dedicated transparent surface so hidden controls do not steal overlay button touches, VLC clears per-playback subtitle attachment bookkeeping and refreshes the captions list after remote subtitle slaves are added, and the Stremio close cleanup uses a more specific stuck-player check before falling back to history navigation.

    +

    Why this change was made

    +

    The adjustments reduce two practical failure modes: controls becoming hard to re-open or press after auto-hide, and the captions sheet missing remote subtitle tracks until VLC finishes exposing them. The close-flow probe was narrowed so Dreamio is less likely to go back merely because unrelated page text contains stream-related words.

    +

    Code diffs

    +

    Rendered with @pierre/diffs/ssr and preloadPatchDiff. These snippets cover the incremental follow-up edits made in this turn.

    +
    Dreamio/NativePlayerViewController.swift
    -1+18
    35 unmodified lines
    36
    37
    38
    39
    40
    41
    125 unmodified lines
    167
    168
    169
    170
    171
    172
    9 unmodified lines
    182
    183
    184
    185
    186
    187
    188
    19 unmodified lines
    208
    209
    210
    211
    212
    213
    124 unmodified lines
    338
    339
    340
    341
    342
    343
    2 unmodified lines
    346
    347
    348
    349
    350
    351
    35 unmodified lines
    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")
    125 unmodified lines
    +
    private func configureLayout() {
    view.addSubview(backend.view)
    view.addSubview(loadingView)
    view.addSubview(failureLabel)
    view.addSubview(controlsContainer)
    9 unmodified lines
    +
    let tap = UITapGestureRecognizer(target: self, action: #selector(toggleControlsVisibility))
    tap.cancelsTouchesInView = false
    view.addGestureRecognizer(tap)
    +
    let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])
    controlRow.translatesAutoresizingMaskIntoConstraints = false
    19 unmodified lines
    backend.view.topAnchor.constraint(equalTo: view.topAnchor),
    backend.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
    +
    loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    +
    124 unmodified lines
    }
    +
    private func revealControls() {
    UIView.animate(withDuration: 0.18) {
    self.controlsContainer.alpha = 1
    self.closeButton.alpha = 1
    2 unmodified lines
    }
    +
    private func hideControls() {
    UIView.animate(withDuration: 0.24) {
    self.controlsContainer.alpha = 0
    self.closeButton.alpha = 0
    35 unmodified lines
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    125 unmodified lines
    174
    175
    176
    177
    178
    179
    180
    9 unmodified lines
    190
    191
    192
    193
    194
    195
    196
    19 unmodified lines
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    124 unmodified lines
    351
    352
    353
    354
    355
    356
    357
    358
    2 unmodified lines
    361
    362
    363
    364
    365
    366
    367
    368
    35 unmodified lines
    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")
    125 unmodified lines
    +
    private func configureLayout() {
    view.addSubview(backend.view)
    view.addSubview(tapSurfaceView)
    view.addSubview(loadingView)
    view.addSubview(failureLabel)
    view.addSubview(controlsContainer)
    9 unmodified lines
    +
    let tap = UITapGestureRecognizer(target: self, action: #selector(toggleControlsVisibility))
    tap.cancelsTouchesInView = false
    tapSurfaceView.addGestureRecognizer(tap)
    +
    let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])
    controlRow.translatesAutoresizingMaskIntoConstraints = false
    19 unmodified lines
    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),
    +
    124 unmodified lines
    }
    +
    private func revealControls() {
    controlsContainer.isUserInteractionEnabled = true
    closeButton.isUserInteractionEnabled = true
    UIView.animate(withDuration: 0.18) {
    self.controlsContainer.alpha = 1
    self.closeButton.alpha = 1
    2 unmodified lines
    }
    +
    private func hideControls() {
    controlsContainer.isUserInteractionEnabled = false
    closeButton.isUserInteractionEnabled = false
    UIView.animate(withDuration: 0.24) {
    self.controlsContainer.alpha = 0
    self.closeButton.alpha = 0
    +
    Dreamio/VLCNativePlaybackBackend.swift
    +7
    39 unmodified lines
    40
    41
    42
    43
    44
    45
    158 unmodified lines
    204
    205
    206
    207
    208
    209
    39 unmodified lines
    +
    func play(request: NativePlaybackRequest) {
    #if canImport(MobileVLCKit)
    let media = VLCMedia(url: request.playbackURL)
    let headerValue = request.headers
    .map { "\($0.key): \($0.value)" }
    158 unmodified lines
    print("[DreamioVLC] attached subtitle=\(URLRedactor.redactedURLString(candidate.url.absoluteString))")
    #endif
    }
    }
    #endif
    }
    39 unmodified lines
    40
    41
    42
    43
    44
    45
    46
    158 unmodified lines
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    39 unmodified lines
    +
    func play(request: NativePlaybackRequest) {
    #if canImport(MobileVLCKit)
    attachedSubtitleURLs.removeAll()
    let media = VLCMedia(url: request.playbackURL)
    let headerValue = request.headers
    .map { "\($0.key): \($0.value)" }
    158 unmodified lines
    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
    }
    +
    Dreamio/DreamioWebViewController.swift
    -2+16
    121 unmodified lines
    122
    123
    124
    125
    126
    127
    389 unmodified lines
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    15 unmodified lines
    544
    545
    546
    547
    548
    549
    550
    121 unmodified lines
    const addSubtitleCandidate = (entry) => {
    const rawURL = typeof entry === "string" ? entry : entry && (entry.url || entry.href || entry.src || entry.file || entry.download);
    const url = absoluteURL(rawURL);
    if (!url || !subtitleURLPattern.test(url)) {
    subtitleURLPattern.lastIndex = 0;
    return;
    389 unmodified lines
    const clicked = clickVisible([
    "[aria-label*='Close' i]",
    "[aria-label*='Back' i]",
    "button[class*='close' i]",
    "button[class*='back' i]",
    ".player button",
    "[role='button']"
    ]);
    const stillPlayer = /player|stream|buffer|prepar/i.test(document.body.innerText || "");
    return { clicked, stillPlayer, href: window.location.href };
    })();
    """#
    15 unmodified lines
    }
    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
    if (result as? Bool) == true {
    self.webView.goBack()
    }
    121 unmodified lines
    122
    123
    124
    125
    126
    127
    128
    389 unmodified lines
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    15 unmodified lines
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    121 unmodified lines
    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;
    389 unmodified lines
    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 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 };
    })();
    """#
    15 unmodified lines
    }
    if self.webView.canGoBack {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
    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()
    }
    +

    Related issues or PRs

    +

    Related Beads issue: dreamio-poo. No pull request was created in this local workflow.

    +
    +