Skip to content

Commit 5e940ac

Browse files
lukemarsdenclaude
andcommitted
fix: use RecordMonitor instead of RecordVirtual to capture virtual monitor at correct resolution
Root cause: RecordVirtual creates its OWN virtual monitor using PipeWire format negotiation, which defaults to 1280x720 (hardcoded in Mutter's meta-screen-cast-stream-src.c:67). This ignored the virtual monitor created by gnome-shell --virtual-monitor WxH. Solution: Use RecordMonitor with connector "Meta-0" to capture the EXISTING virtual monitor created by --virtual-monitor at the correct resolution. Virtual monitor connector naming in Mutter: "Meta-{id}" (meta-output-virtual.c:44). Also includes various improvements to immediate lobby attachment feature. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 247629d commit 5e940ac

File tree

28 files changed

+1203
-679
lines changed

28 files changed

+1203
-679
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: PipeWire Streaming Tests
2+
3+
on:
4+
push:
5+
paths:
6+
- 'wolf/ubuntu-config/remotedesktop-session.py'
7+
- 'tests/pipewire-streaming/**'
8+
- '.github/workflows/pipewire-streaming-tests.yml'
9+
pull_request:
10+
paths:
11+
- 'wolf/ubuntu-config/remotedesktop-session.py'
12+
- 'tests/pipewire-streaming/**'
13+
- '.github/workflows/pipewire-streaming-tests.yml'
14+
15+
jobs:
16+
python-tests:
17+
name: Python Unit Tests
18+
runs-on: ubuntu-24.04
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: '3.12'
26+
27+
- name: Install dependencies
28+
working-directory: tests/pipewire-streaming
29+
run: |
30+
pip install -r requirements.txt
31+
32+
- name: Run Python tests
33+
working-directory: tests/pipewire-streaming
34+
run: |
35+
python -m pytest test_remotedesktop_session.py -v --tb=short
36+
37+
# Note: Rust tests for gst-pipewire-zerocopy run during Wolf builds.
38+
# See wolf/docker/wolf.Dockerfile line 72: `cargo test --lib`
39+
# The tests require GStreamer CUDA libraries from the GOW base image.

api/pkg/external-agent/executor.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ type Executor interface {
3131

3232
// Reconciliation support
3333
HasRunningContainer(ctx context.Context, sessionID string) bool
34+
35+
// Streaming session support
36+
// ConfigurePendingSession pre-configures Wolf to attach a client to a lobby when it connects.
37+
// The frontend calls this BEFORE connecting to moonlight-web with the same clientUniqueID.
38+
// This enables immediate lobby attachment without auto-join polling.
39+
ConfigurePendingSession(ctx context.Context, sessionID string, clientUniqueID string) error
3440
}
3541

3642
// Shared types used by all executor implementations

api/pkg/external-agent/wolf_client_interface.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ type WolfClientInterface interface {
2929
ResetKeyboardState(ctx context.Context, sessionID string) (*wolf.KeyboardResetResponse, error)
3030
// Raw HTTP access (used for SSE streaming)
3131
Get(ctx context.Context, path string) (*http.Response, error)
32+
// Session pre-configuration for immediate lobby attachment
33+
// Allows configuring immediate_lobby_id before Moonlight client connects
34+
ConfigurePendingSession(ctx context.Context, clientUniqueID string, immediateLobbyID string) error
3235
}
3336

3437
// Ensure *wolf.Client implements WolfClientInterface at compile time

api/pkg/external-agent/wolf_client_interface_mocks.go

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/pkg/external-agent/wolf_executor.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ func (w *WolfExecutor) StartDesktop(ctx context.Context, agent *types.ZedAgent)
887887
extraEnv = append(extraEnv,
888888
fmt.Sprintf("GAMESCOPE_WIDTH=%d", displayWidth),
889889
fmt.Sprintf("GAMESCOPE_HEIGHT=%d", displayHeight),
890+
fmt.Sprintf("GAMESCOPE_REFRESH=%d", displayRefreshRate),
890891
fmt.Sprintf("HELIX_ZOOM_LEVEL=%d", zoomLevel),
891892
fmt.Sprintf("HELIX_DESKTOP_TYPE=%s", string(desktopType)), // For container hostname reconstruction
892893
)
@@ -1212,6 +1213,11 @@ func (w *WolfExecutor) StartDesktop(ctx context.Context, agent *types.ZedAgent)
12121213
Str("lobby_pin", lobbyPINString).
12131214
Msg("Wolf lobby created successfully - container starting immediately")
12141215

1216+
// NOTE: Session pre-configuration for immediate lobby attachment is now done by the frontend.
1217+
// The frontend generates a random client_unique_id and calls ConfigurePendingSession API
1218+
// BEFORE connecting to moonlight-web. This ensures Wolf is ready to attach the client
1219+
// directly to the lobby's interpipe when the Moonlight connection arrives.
1220+
12151221
// Step 3: Bridge desktop to Hydra network (if Hydra is enabled)
12161222
// This injects a veth pair connecting the desktop container to the Hydra network
12171223
// enabling Firefox/Zed in the desktop to access dev containers started via docker compose
@@ -2481,6 +2487,44 @@ func (w *WolfExecutor) FindExistingLobbyForSession(ctx context.Context, sessionI
24812487
return foundLobbyID, nil
24822488
}
24832489

2490+
// ConfigurePendingSession pre-configures Wolf to attach a client to a lobby when it connects.
2491+
// The frontend calls this BEFORE connecting to moonlight-web with the same clientUniqueID.
2492+
// This enables immediate lobby attachment without auto-join polling.
2493+
func (w *WolfExecutor) ConfigurePendingSession(ctx context.Context, sessionID string, clientUniqueID string) error {
2494+
// Find the existing lobby for this session
2495+
lobbyID, err := w.FindExistingLobbyForSession(ctx, sessionID)
2496+
if err != nil {
2497+
return fmt.Errorf("failed to find lobby for session: %w", err)
2498+
}
2499+
if lobbyID == "" {
2500+
return fmt.Errorf("no lobby found for session %s", sessionID)
2501+
}
2502+
2503+
// Look up session to get Wolf instance ID
2504+
session, err := w.store.GetSession(ctx, sessionID)
2505+
if err != nil {
2506+
return fmt.Errorf("failed to get session: %w", err)
2507+
}
2508+
2509+
wolfInstanceID := session.WolfInstanceID
2510+
if wolfInstanceID == "" {
2511+
return fmt.Errorf("session %s has no Wolf instance ID", sessionID)
2512+
}
2513+
2514+
// Configure Wolf's pending session
2515+
if err := w.getWolfClient(wolfInstanceID).ConfigurePendingSession(ctx, clientUniqueID, lobbyID); err != nil {
2516+
return fmt.Errorf("failed to configure pending session: %w", err)
2517+
}
2518+
2519+
log.Info().
2520+
Str("session_id", sessionID).
2521+
Str("client_unique_id", clientUniqueID).
2522+
Str("lobby_id", lobbyID).
2523+
Msg("Session pre-configured for immediate lobby attachment via frontend API")
2524+
2525+
return nil
2526+
}
2527+
24842528
// cleanupOrphanedWolfUISessions removes Wolf-UI streaming sessions without active Zed containers
24852529
func (w *WolfExecutor) cleanupOrphanedWolfUISessions(ctx context.Context) {
24862530
// Get all Wolf instances

0 commit comments

Comments
 (0)