Fix/cast failure reporting and portal virtual - #8
Open
X-Ryl669 wants to merge 4 commits into
Open
Conversation
The comment claimed "the LAPTOP serves its own screen on [this port]; the
phone connects out to it". It is the other way round: `laptop_cast` dials
the phone on 51823, exactly as the phone→laptop path dials 51822. A live
socket says so plainly —
ESTAB 192.168.0.119:43574 → 192.168.0.79:51823 (vortex-ui-tauri)
(and no listener on 51823 anywhere on the laptop)
This cost real time: the wording led to "the phone must connect inbound,
so open 51823 in the firewall", and a `ufw allow` rule was added chasing a
laptop→phone mirror failure that had nothing to do with the firewall. The
actual property is the opposite and worth stating outright: every video
path is dialled OUTWARD from the laptop, so Vortex needs no inbound rule
for any of them — which the module header already says for VIDEO_PORT.
… source
"Extended display" called `org.gnome.Mutter.ScreenCast` directly, so on any
other compositor it died instantly with
CreateSession: org.freedesktop.DBus.Error.ServiceUnknown:
The name is not activatable
and — because the phone re-asserts `laptop_mirror_req` on every heartbeat —
retried every 4 minutes indefinitely with nothing on either screen to say
why. On a KDE session the feature was simply unreachable.
The ScreenCast portal's `SourceType::Virtual` is the cross-desktop
equivalent of Mutter's `CreateVirtualMonitor`, and KWin implements it (its
portal reports MONITOR|WINDOW|VIRTUAL in AvailableSourceTypes). So try
Mutter first — it stays the tuned path on GNOME, and it carries its own
cursor overlay because Mutter will not composite a pointer into a virtual
monitor — then fall back to the portal.
Verified on Plasma 6.7.4 / KWin 6.7.4: the portal creates a real output,
Plasma prompts for its placement, and it lands in the display layout as a
draggable target —
Output: 1 Virtual-virtual-xdp-kde-… Geometry: 2058,0 1920x1080
Output: 2 eDP-1 Geometry: 0,0 2058x1286
laptop-cast: portal stream ready node_id=144 size=Some((1920, 1080))
laptop-cast: connected to phone viewer — streaming
The portal body is split into `start_portal(.., source)` so mirror
(`Monitor`) and extend (`Virtual`) share it; everything downstream —
encode, seal, transport, the phone's viewer — was already identical.
Caveat found while testing, not caused by this change:
xdg-desktop-portal-kde 6.7.4 can ABORT on a Virtual request ("Object
destroyed while one of its QML signal handlers is in progress", SIGABRT
with a coredump), which kills the session and leaves the request hanging.
Restarting the portal service clears it. Worth reporting upstream.
…lent requests
A cast the laptop could not start was invisible and unrecoverable. The
phone re-asserted `laptop_mirror_req` on every heartbeat, the laptop failed
again and logged a WARN nobody reads, and nothing ever told the phone. Its
UI then wedged, because:
fun requestView(extend: Boolean) {
if (requestActive) return // every further tap is a no-op
}
fun onLaptopCastEnded() {
if (!viewerOpen) return // give-up needs a viewer to exist
if (++castMisses < MISS_LIMIT) return
}
`onLaptopCastEnded` only ever protected an already-open viewer, so a request
that never produced an offer had NO timeout at all: `castMisses` was not even
incremented and `requestActive` stayed true forever. Observed in practice as
half an hour of 4-minute retries against "Extended display" on a KDE
session, with the phone showing nothing and taps doing nothing — recoverable
only by force-stopping the app.
Laptop: `laptop_cast_error` rides AppState next to `laptop_cast`, set when a
start fails, cleared when a fresh attempt begins and on the falling edge so
a stale reason is never reported against a new request. Optional + skipped
when absent, so peers on either side that predate it are unaffected.
Phone: an explicit reason clears the request, invokes `onCastFailed` (toast)
and closes any viewer; and `onLaptopCastSilent` gives up after
SILENT_LIMIT=10 heartbeats when there is neither an offer nor a reason —
covering a laptop that never answers at all, or one too old to send one.
SILENT_LIMIT is deliberately above MISS_LIMIT: consent dialog, portal
session and encoder startup are legitimately slow, and giving up while the
user is still reading the consent prompt would be worse than waiting.
The toast is English only. The app localizes via `ui/Strings.kt`, whose
`str()` is @composable and so unusable from a service, and a service has no
locale to pick with — noted in a comment rather than papered over.
…ession
A cast whose viewer went away kept running, and for an extend cast that
left a phantom 1920x1080 output in the desktop layout — a screen windows
can be dragged into and lost. Reproduced by swiping the phone's viewer
away (which kills it without its normal close path, so the phone never
tells us to stop) and watching `kscreen-doctor` keep listing
`Virtual-virtual-xdp-kde-…` at 2058,0 for as long as the app lived.
Two independent causes, which is why it looked so stubborn:
1. Nothing acted on the transport giving up. `run_tcp_video_client` already
bounds its reconnects (~60 s) and returns, but the pipeline and portal
session live in a DIFFERENT task, so its give-up meant nothing: the
capture ran on with no viewer. Both spawn sites now go through
`spawn_video_sender`, which stops the cast and records the reason — the
phone then learns why instead of sitting on a black screen. The
`CAST.is_some()` guard keeps it off the normal stop path, where `stop()`
has already taken the handle.
2. The session was dropped, never closed. The old comment claimed
"dropping `fd`/`session` closes the PipeWire stream and the portal
session" — but ashpd 0.9's `Session` has `close()` and NO `Drop` impl
that calls it, so the session stayed open until our whole D-Bus
connection went away. That is why killing the app cleared the output and
nothing short of it did. Pipeline-to-Null stops the CAPTURE; the SOURCE
stays allocated, and for `SourceType::Virtual` the source is an output.
Teardown now awaits `session.close()` and warns if it fails.
Verified on Plasma 6.7.4: viewer swiped away → output cleared in ~39 s,
laptop-cast: phone viewer unreachable — stopping the cast
laptop-cast: stopped (capture + portal session closed)
and `kscreen-doctor` back to one output with no manual intervention.
The ~39 s is the existing reconnect budget, deliberately left alone: a
viewer legitimately drops and re-accepts across a network blip or an
activity recreation, and killing the cast on the first broken pipe would
trade a phantom monitor for a mirror that cannot survive a hiccup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds screen extension for KDE Plasma based desktop. The code actual path is based on Gnome's Mutter and does not work on a KDE system. This code, authored by Claude, add the fallback using the cross desktop "ScreenCast" solution (so it should work on any XDG compliant desktop manager). This also fixes teardown and creation race with the current code (where the session was created twice before the attempt succeeded, leaking the former handle).