Skip to content

Reapply media-playback hibernation fix (issue 5409) - #5441

Merged
austinywang merged 3 commits into
mainfrom
reapply-5409-browser-media-playback
Jun 5, 2026
Merged

austinywang merged 3 commits into
mainfrom
reapply-5409-browser-media-playback

Conversation

@austinywang

@austinywang austinywang commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Verification

  • ./scripts/normalize-pbxproj.py cmux.xcodeproj/project.pbxproj
  • ./scripts/check-pbxproj.sh
  • ./scripts/reload.sh --tag reapply-5409
  • scripts/launch-tagged-automation.sh reapply-5409 --mode allowAll --wait-socket 12 (socket came up just after the 12s wait)
  • CMUX_TAG=reapply-5409 scripts/cmux-debug-cli.sh new-pane --type browser --url https://example.com --focus true
  • CMUX_TAG=reapply-5409 scripts/cmux-debug-cli.sh browser --surface surface:2 wait --load-state complete --timeout-ms 10000
  • Confirmed https://example.com/ loaded with title Example Domain.
  • Confirmed Cmd+L focused the browser address bar and was consumed by handleCustomShortcut.
  • Confirmed Cmd+Shift+L opened a new browser and focused the address bar after the tagged app was fully ready.

Notes

  • The previous local dogfood on macOS 26 also loaded YouTube, switched workspaces, waited past a reduced hidden-discard delay, and returned to a responsive YouTube page.
  • The prior revert was for shortcut/loading regressions, so this should still get reporter-environment or macOS 15.7.4 dogfood before merge.

Note

Medium Risk
Touches hidden-webview discard policy and injects JS into all frames; prior revert cited shortcut/loading regressions, so behavior under navigation and workspace switches still warrants careful dogfood.

Overview
Reapplies the media-playback hibernation fix (issue 5409): hidden browser panes with active <video>/<audio> playback are no longer discarded after the hidden delay.

Detection injects a document-start hook in every frame (main + cross-origin iframes) inside an isolated WKContentWorld, posts { frameID, playing } to a native handler, and aggregates per-frame state into isPlayingMedia. Muted playback still counts; Web Audio–only pages are intentionally out of scope.

Discard integration adds isPlayingMedia to the hidden-webview blocker snapshot and a new media_playback blocker in BrowserHiddenWebViewDiscardManager. Playback changes re-arm discard scheduling like other blockers.

Lifecycle hardening resets tracking on top-level didCommit (not provisional start), ignores reports from superseded webview generations, applies script messages synchronously on the main actor for ordering vs navigation, and uses pagehide plus a targeted MutationObserver when media nodes are removed.

Tests add Swift Testing coverage that playing media blocks discard scheduling while idle panes still schedule it.

Reviewed by Cursor Bugbot for commit e797acf. Bugbot is set up for automated code reviews on this repo. Configure here.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Reapplies the media-playback hibernation fix so hidden browser panes with active audio/video aren’t discarded. Restores all-frame playback reporting and the media_playback discard blocker to keep background videos running (issue 5409).

  • Bug Fixes
    • Inject a document-start hook in every frame (main + cross-origin iframes) to track
    • Run the hook and handler in an isolated WKContentWorld with synchronous main-actor handling; reset on top-level didCommit and drop stale reports from replaced webviews; send a final stop on pagehide and use a MutationObserver to handle removed media.
    • Add isPlayingMedia to the hidden-webview snapshot and a media_playback blocker; tests verify playing panes block discard and idle panes still schedule discard.

Written for commit e797acf. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added cross-frame media playback detection (per-frame reporting aggregated to a single playing state). Web views with actively playing audio/video are now exempt from hidden-memory discard to avoid interrupting playback.
  • Tests

    • Added regression tests covering media playback detection and that active playback prevents discard; test helpers updated to exercise both playing and non-playing scenarios.

@vercel

vercel Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 5, 2026 6:55am
cmux-staging Building Building Preview, Comment Jun 5, 2026 6:55am

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR injects per-frame JS to detect playing

Changes

Media Playback Detection and Discard Integration

Layer / File(s) Summary
Data Contracts and Message Parsing
Sources/Panels/BrowserMediaPlaybackReport.swift, Sources/Panels/BrowserMediaPlaybackMessageHandler.swift, Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
Introduces BrowserMediaPlaybackReport, a WKScriptMessageHandler adapter to parse {frameID, playing} messages and forward typed reports, and adds isPlayingMedia to BlockerSnapshot.
JavaScript Injection and Media Event Tracking
Sources/Panels/BrowserPanel+MediaPlayback.swift
Injects document-start JS that computes stable frameIDs, deduplicates reports, listens for media events, observes DOM mutations, posts playback messages, and sends final playing:false on pagehide. Native setup registers the handler in an isolated content world and filters by webview generation.
BrowserPanel State and Script Configuration
Sources/Panels/BrowserPanel.swift
Adds isPlayingMedia and playingMediaFrameIDs, injects the playback script into all frames during webview configuration, wires the message handler in bindWebView, and resets playback tracking on top-level navigation commits.
Discard Blocker Logic and Snapshot Integration
Sources/Panels/BrowserHiddenWebViewDiscardManager.swift, Sources/Panels/BrowserPanel.swift
Extends blocker snapshot to include isPlayingMedia and appends "media_playback" in blockers(for:) when playback is detected, preventing hidden-webview discard scheduling.
Build Configuration
cmux.xcodeproj/project.pbxproj
Adds PBX entries so the new media playback Swift files are included in the app build.
Test Helper Updates and Regression Tests
cmuxTests/BrowserPanelTests.swift
Updates test helper to accept isPlayingMedia and adds serialized tests asserting that active media yields media_playback blocker and prevents discard scheduling, while idle panes allow scheduling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#4912: Adds a media-related discard blocker (media_capture) and similar BlockerSnapshot/blockers(for:) gating logic.
  • manaflow-ai/cmux#5412: Implements similar isPlayingMedia + media_playback blocker and BrowserPanel media-playback plumbing.
  • manaflow-ai/cmux#5315: Updates discard eligibility/scheduling logic and BlockerSnapshot integration in the same hidden-webview discard lifecycle.

Poem

🐰 I hopped through frames both wide and deep,

Listening where tiny videos sleep.
Reports hop home on message springs,
BrowserPanel counts the playing things.
No discard today—media keeps the keep.

🚥 Pre-merge checks | ✅ 18 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: reapplying a media-playback hibernation fix for issue 5409, which matches the main objective of preventing hidden browser panes with active media from being discarded.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed BrowserMediaPlaybackReport (Sendable) not implicitly @MainActor, WebKit handler correct, state properly isolated on @MainActor BrowserPanel, MainActor.assumeIsolated used correctly.
Cmux Swift Blocking Runtime ✅ Passed No blocking primitives (semaphores, sleep, locks) introduced. Code uses MainActor.assumeIsolated, event listeners, and MutationObserver—all non-blocking patterns.
Cmux No Hacky Sleeps ✅ Passed PR introduces no sleep, setTimeout, setInterval, or polling patterns in TypeScript, JavaScript, shell, or non-Swift build/runtime code. Uses event-driven synchronization instead.
Cmux Algorithmic Complexity ✅ Passed Swift uses O(1) Set operations on playingMediaFrameIDs, never iterated; JS anyPlaying() bounded by media elements; blockers() is O(1) check; no nested scans or batch rescans.
Cmux Swift Concurrency ✅ Passed PR introduces media tracking using WKScriptMessageHandler adapter pattern matching ReactGrabMessageHandler; no DispatchQueues, Combine, completion handlers, or fire-and-forget Tasks in new code.
Cmux Swift @Concurrent ✅ Passed PR adds synchronous, main-actor-isolated methods to BrowserPanel and uses standard MainActor.assumeIsolated pattern for WKScriptMessageHandler—no violations of @concurrent annotation rules.
Cmux Swift File And Package Boundaries ✅ Passed New files under 400 lines with clear single responsibilities. 59 lines added to existing oversized file (below 250 threshold). Code is WebKit/UI glue inherently tied to the app target.
Cmux Swift Logging ✅ Passed All new logging in the PR is properly #if DEBUG guarded (3 cmuxDebugLog calls) with no sensitive data, no NSLog/print in media playback code, and complies with swift-logging.md rules.
Cmux User-Facing Error Privacy ✅ Passed Internal media playback tracking code uses strings like "media_playback" only for internal blocker tracking and logging, never exposed to users in alerts, errors, or UI.
Cmux Full Internationalization ✅ Passed PR contains no user-facing text. Only internal protocols (frameID, playing), blocker identifiers (media_playback), and debug logs (#if DEBUG guarded) were added.
Cmux Swiftui State Layout ✅ Passed PR introduces media-playback tracking without violating SwiftUI state rules: no new @Published, no GeometryReader, no store refs in lazy lists, no render-time mutation.
Cmux Architecture Rethink ✅ Passed The PR implements media playback detection with clear ownership in BrowserPanel, proper lifecycle guards via webViewInstanceID, single state entrypoint, and documented platform bridge usage.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add/change user-visible windows; it only adds media playback tracking (WKScriptMessageHandler, data struct, and BrowserPanel extension) to existing browser panes.
Cmux Source Artifacts ✅ Passed All changed files are hand-written source code, required Xcode project config, and tests. No artifacts, logs, caches, or scratch directories present.
Description check ✅ Passed The PR description is comprehensive with clear Summary, Verification, and Notes sections covering the reapply, testing steps, and risk assessment.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch reapply-5409-browser-media-playback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e17dadc. Configure here.

Comment thread Sources/Panels/BrowserPanel+MediaPlayback.swift
@greptile-apps

greptile-apps Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Reapplies the media-playback hibernation fix (originally #5412, reverted in #5436 for unrelated shortcut/loading regressions). Hidden browser panes that are actively playing <video> or <audio> are now exempted from memory discard by adding an isPlayingMedia flag and a media_playback blocker to BrowserHiddenWebViewDiscardManager.

  • Injects a document-start JS hook in every frame (main + cross-origin iframes) inside an isolated WKContentWorld so page JS cannot spoof a playing report; aggregates per-frame state by stable frameID on the Swift side.
  • Applies reports synchronously via MainActor.assumeIsolated to preserve delivery order relative to didCommit resets; guards stale reports from superseded webview generations via an instance-ID check.
  • Resets per-frame tracking on top-level didCommit only (not provisional start), preserving SPA same-document navigations; uses a MutationObserver to detect playing elements removed from the DOM while the page is hidden.

Confidence Score: 5/5

The change is safe to merge for the media-playback discard fix itself; the remaining risk is the shortcut/loading regression that triggered the prior revert, which is runtime behaviour not visible in the diff and requires macOS 15.7.4 dogfood as noted in the PR.

The implementation is well-scoped: per-frame state is tracked in an isolated content world, reports are applied synchronously on the main actor preserving delivery order relative to didCommit resets, stale reports from superseded webview generations are dropped, and the JS hook is purely passive. No blocking primitives, no legacy concurrency, no logging leaks, and the new tests cover the key regression and the happy path.

No files require special attention from a code-correctness standpoint. The prior revert was for shortcut/loading behaviour under real navigation, so end-to-end testing on macOS 15.7.4 before merging is the main remaining gate.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel+MediaPlayback.swift New 207-line extension adding JS injection, message handler setup, report handling, and debug logging; well-structured with a single clear responsibility.
Sources/Panels/BrowserMediaPlaybackMessageHandler.swift Thin NSObject adapter bridging WKScriptMessageHandler to a @mainactor closure using MainActor.assumeIsolated; mirrors the ReactGrabMessageHandler pattern correctly.
Sources/Panels/BrowserMediaPlaybackReport.swift Minimal Sendable value type carrying frameID and isPlaying; correct and complete.
Sources/Panels/BrowserPanel.swift Adds isPlayingMedia flag with didSet discard re-evaluation, playingMediaFrameIDs set, resetMediaPlaybackTracking() on didCommit, and isPlayingMedia in the discard snapshot; correctly placed and guarded.
Sources/Panels/BrowserHiddenWebViewDiscardManager.swift Adds isPlayingMedia to BlockerSnapshot and media_playback to the blockers list; minimal, correct change.
cmuxTests/BrowserPanelTests.swift Adds Swift Testing @suite covering the playing-pane-blocks-discard and idle-pane-still-schedules cases; correctly uses withHiddenWebViewDiscardPolicyEnabled helper and asserts all relevant state.
cmux.xcodeproj/project.pbxproj Registers three new Swift source files in the app target; IDs follow the project's naming conventions; no artifacts or stray entries.

Sequence Diagram

sequenceDiagram
    participant Page as Page JS (isolated world)
    participant WK as WebKit IPC
    participant MH as BrowserMediaPlaybackMessageHandler
    participant BP as BrowserPanel (MainActor)
    participant DM as BrowserHiddenWebViewDiscardManager

    Page->>Page: play/pause/ended/pagehide event fires
    Page->>Page: "report() → anyPlaying() → post({frameID, playing})"
    Page->>WK: "postMessage({frameID, playing})"
    WK->>MH: userContentController(_:didReceive:) [main thread]
    MH->>MH: "MainActor.assumeIsolated { onReport(report) }"
    MH->>BP: handleMediaPlaybackReport(_:fromWebViewInstanceID:)
    BP->>BP: "guard instanceID == webViewInstanceID"
    BP->>BP: applyMediaPlaybackReport(frameID:isPlaying:)
    BP->>BP: isPlayingMedia.didSet → reevaluateHiddenWebViewDiscardScheduling

    alt Media playing
        BP->>DM: scheduleIfNeeded → blockers includes media_playback → no timer
    else Media stopped
        BP->>DM: scheduleIfNeeded → no blockers → discard timer armed
    end

    Note over WK,BP: didCommit fires on main-frame navigation
    WK->>BP: navigationDelegate.didCommit
    BP->>BP: resetMediaPlaybackTracking() → playingMediaFrameIDs.removeAll()
    BP->>DM: reevaluateHiddenWebViewDiscardScheduling (loading blocker still active)
Loading

Reviews (3): Last reviewed commit: "Address media playback review feedback" | Re-trigger Greptile

Comment on lines +3 to +4
/// A per-frame media-playback report from the injected media-playback hook.
struct BrowserMediaPlaybackReport {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 BrowserMediaPlaybackReport passes through a @MainActor closure in BrowserMediaPlaybackMessageHandler but does not explicitly declare Sendable conformance. Both stored properties (String, Bool) are Sendable, so Swift 6 will infer it, but making the conformance explicit documents the concurrency contract and matches the preferred shape from the actor isolation rule for pure value model types.

Suggested change
/// A per-frame media-playback report from the injected media-playback hook.
struct BrowserMediaPlaybackReport {
/// A per-frame media-playback report from the injected media-playback hook.
struct BrowserMediaPlaybackReport: Sendable {

Rule Used: Flag new or materially worsened Swift 6 actor isol... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by making BrowserMediaPlaybackReport explicitly Sendable.

— Claude Code

Comment thread cmuxTests/BrowserPanelTests.swift Outdated
Comment on lines +183 to +194
func testIdlePaneWithoutMediaPlaybackStillSchedulesHiddenWebViewDiscard() {
let snapshot = makeHiddenWebViewDiscardBlockerSnapshot(isPlayingMedia: false)
let manager = BrowserHiddenWebViewDiscardManager()
let delegate = BrowserHiddenWebViewDiscardTestDelegate(snapshot: snapshot, hiddenAt: Date())
manager.delegate = delegate

XCTAssertEqual(manager.blockers(for: snapshot), [])

manager.scheduleIfNeeded(reason: "test.hidden")

XCTAssertTrue(manager.hasScheduledDiscard)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing discardRequestCount assertion in the idle-pane test

The sibling test (testActiveMediaPlaybackBlocksHiddenWebViewDiscardScheduling) also asserts delegate.discardRequestCount == 0 after scheduleIfNeeded. This test uses hiddenAt: Date() (pane hidden just now) so no immediate discard fires and the assert would pass — but omitting it means a future regression where scheduleIfNeeded incorrectly calls the delegate immediately would go undetected here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by adding the missing delegate.discardRequestCount == 0 assertion to the idle-pane Swift Testing case.

— Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/BrowserPanelTests.swift`:
- Around line 161-195: These two XCTest-style methods
(testActiveMediaPlaybackBlocksHiddenWebViewDiscardScheduling and
testIdlePaneWithoutMediaPlaybackStillSchedulesHiddenWebViewDiscard) must be
converted in-place to the repository's Swift Testing style used for other
migrated tests instead of remaining as XCTestCase methods; update each test to
the Swift Testing equivalent used elsewhere in this file (preserve the same
setup: makeHiddenWebViewDiscardBlockerSnapshot(isPlayingMedia:),
BrowserHiddenWebViewDiscardManager, and BrowserHiddenWebViewDiscardTestDelegate)
and register them following the existing Swift Testing pattern in this file so
they run under the new harness while keeping semantics and assertions identical.
Ensure you do not introduce a new XCTestCase class or restore XCTest-only APIs;
follow the surrounding migrated-tests' structure and naming so test
discovery/registration matches the repo convention.

In `@Sources/Panels/BrowserPanel`+MediaPlayback.swift:
- Line 5: Replace the hardcoded message handler string with the single
source-of-truth constant: use the existing mediaPlaybackMessageHandlerName
wherever "cmuxMediaPlayback" is currently hardcoded (e.g., in the message
registration/handler attachment in BrowserPanel+MediaPlayback, around the
register/observe calls near the playback setup code and any removeHandler
calls). Ensure all references (registration, callback lookup, and removal)
reference mediaPlaybackMessageHandlerName so the handler name cannot drift
between locations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d5318cb-5a67-4147-81af-76e1038e4ef6

📥 Commits

Reviewing files that changed from the base of the PR and between c3acdb8 and e17dadc.

📒 Files selected for processing (7)
  • Sources/Panels/BrowserHiddenWebViewDiscardManager.swift
  • Sources/Panels/BrowserMediaPlaybackMessageHandler.swift
  • Sources/Panels/BrowserMediaPlaybackReport.swift
  • Sources/Panels/BrowserPanel+MediaPlayback.swift
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserPanelTests.swift

Comment thread cmuxTests/BrowserPanelTests.swift Outdated
Comment thread Sources/Panels/BrowserPanel+MediaPlayback.swift

This branch was successfully deployed

1 active deployment
Preview – cmux — e797acf1 Deployed Jun 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant