Skip to content

Second window: show a host window or a whole display in a second client window - #32

Merged
vibesoftwarecoder merged 21 commits into
masterfrom
feat/second-video-stream
Nov 5, 2026
Merged

vibesoftwarecoder merged 21 commits into
masterfrom
feat/second-video-stream

Conversation

@vibesoftwarecoder

Copy link
Copy Markdown
Owner

What this adds

A second window for a second video stream. The session, the pairing and the connection stay the same. The host sends two pictures, and the client shows the second one in a window of its own. The second picture is one of two things:

  • A window on the host, picked by its title.
  • A whole display that the host makes, like an extra monitor. Windows dragged onto it in the main window show up in the second one.

It needs an ApolloVibe host with second_video_stream = enabled (ApolloVibe PR 16). Against any other host the client drops the request and carries on with one window. With the setting off, which is the default, nothing about a stream changes.

What the user sees

  • Settings has a new "Second Window" section, after Basic Settings. It starts with a check box, "Show a second window". The rest is hidden until it is on:
    • What to show: a window, or a whole display. For a window, a title field.
    • Resolution (720p to 4K) and frame rate (30 or 60).
    • Show it on this display: which screen the second window opens on. Automatic picks a screen the main window is not on.
    • Open the second window in full screen (on by default). F11 switches between full screen and a window.
    • A note that a whole display needs the console host, and a seat can only show a window.
  • Starting a stream from the app tile now asks for the second window when the check box is on. The title and the choices are saved.
  • The main window can also be placed on a chosen display ("Stream window display").
  • Closing the second window stops only the second picture on the host. The main window keeps streaming.
  • Command line: --second-window <title>, --second-window-source window|display, --second-window-resolution WxH, --second-window-fps N, --second-window-display N, --second-window-fullscreen / --no-second-window-fullscreen, --window-display N.
  • The README is rewritten for the second window, USB forwarding and the current settings.

How it works

  • SecondaryStreamWindow (new, app/streaming/secondarystream.*) owns the second SDL window and its decoder. The first window's code is unchanged except where noted below.
  • The decoder is made through the existing Session::chooseDecoder, so the second window gets the same renderer choices as the first. Each decoder asks the connection for the frames of its own stream, through new per-stream calls in moonlight-common-c-mic. Before that, one decoder received frames from both streams and crashed.
  • Pointer input in the second window is mapped onto its own picture. The client tells the host which picture the following pointer events refer to.
  • The second window opens without taking focus, and the first window is raised when it is shown. A second window that took focus made the first one capture the mouse for the wrong window, so the pointer could not leave the second window.
  • Focus and pointer-leave events of the second window no longer reach the first window's mouse-capture code, for the same reason.
  • The library change (moonlight-common-c-mic, pinned here) gives each video stream its own depacketizer, socket and threads, adds stream-scoped keyframe requests and an input-target call, asks the host to stop one stream, and drops a request for a display when the host says it cannot make one.

Testing

Tested by hand on one Windows client (a handheld with an AMD Radeon 890M on ethernet) against an ApolloVibe console host:

  • A window as the second picture: both windows decode and update, clicks reach the right window, the second window opens on the other display.
  • A whole display: the extra display appears on the host when the stream starts, windows dragged onto it show in the second window, and closing the second window removes the display.
  • Settings: the section hides and shows, the drop-downs fill in, the title and the check boxes are still set after closing and reopening the app.
  • Resolution and frame rate options, full screen on open, F11, and the pointer moving freely between the windows.
  • The Windows build passes in CI on this branch.

Not tested, and known limits

  • macOS and Linux were not built or run with this branch. The Windows workflow is the only one run on it. The pull request runs the generic build; it is the first time the other platforms see this code. The second window uses SDL and the existing decoder code, but nothing here is Windows-only that I know of.
  • Only the D3D11VA renderer ran in the tests. The software, libplacebo and other renderers should work through the same decoder choice but were not tried.
  • Only two displays were tried for placing the windows.
  • Against a host without the feature, I checked the code path by reading it. I did not stream from one.
  • The second window uses absolute pointer positions only, with no relative mouse mode.
  • The first seconds after connecting can show some mouse lag. The same happens with one stream, a little shorter, so it is not only this feature.
  • A host window that is not 16:9 shows black bars in the second picture.
  • Starting both streams takes a bit longer than starting one.

Merging

  • Please use a merge commit. The branch has 21 commits, and its history shows how the library change was built up.
  • The moonlight-common-c submodule moves to 02525db, which is on the feat/second-video-stream branch of moonlight-common-c-mic and not yet on its master. Before this merges, that fork's master needs to be fast-forwarded to 02525db, so the pinned commit stays reachable once the branch is deleted.
  • app/version.txt is still 6.3.11. A release of this needs a version bump and a tag.

🤖 Generated with Claude Code

vibesoftwarecoder and others added 21 commits November 4, 2026 11:03
Points the submodule at feat/second-video-stream (adbabdc) so a CI build can
show that a connection with one video stream still behaves as before. Not for
release: the second-stream client work is not done.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n wiring

Not for release: nothing in the app uses the second stream yet, so this build
should behave exactly like master. It exists to show that a connection with
one video stream still works with the per-stream video path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd pointer target

Not for release: nothing in the app uses the second stream yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When the host offers a second video stream, the session can show a host window
in a client window of its own. SecondaryStreamWindow owns that SDL window and
its decoder; the session gives it the connection callbacks, the window events
and the pointer events of its window, so the first window's code is only
touched at those points.

The window to show comes from --second-window "<title>" or, per seat, from
seatprefs/<uuid>/secondWindow (with optional secondWindowWidth, Height, Fps
and Bitrate). The pointer works in absolute mode in the second window and
tells the host which picture it is over. If the host ends the second stream,
for example because its window closed, only that window closes. Closing the
first window still ends the session.

Not done: asking the host to stop the second stream when its window is closed
on the client, mouse capture and relative mode in the second window, and the
performance overlay for it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…is pull-based

The FFmpeg decoder always reports CAPABILITY_PULL_RENDERER, so the old guard
switched the second window off on every real client.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… frames

A decoder that starts on a predicted frame has no references. FFmpeg then
reported missing references and crashed (avcodec access violation, seen in a
minidump from the first end-to-end test).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…w's feed

The FFmpeg decoder always started a thread that pulled frames with
LiWaitForNextVideoFrame(), which only serves stream 0. The second window's
decoder therefore got stream 0's frames from that thread as well as its own
stream from the push callback, from two threads at once. That produced
missing references, duplicate POCs and an FFmpeg crash, and starved the
first window's decoder of half its frames.

The decoder now knows its stream and uses the per-stream pull and keyframe
calls. The second window uses the same pull model as the first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Settings has a drop-down for the stream window and one for the second window,
and the command line has --window-display and --second-window-display.
Automatic puts the second window on a display the first one is not on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…osed

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…size

Settings has a resolution and a frame rate for the second window (default
1920x1080 at 60), and the command line has --second-window-resolution and
--second-window-fps. The bitrate follows from them. The window opens at the
stream's own size, shrunk to fit its display, instead of at most 960 wide.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A maximized window loses height to the title bar and taskbar, so the picture
no longer fits and gets black bars. Fullscreen on the window's own display
keeps the full area. F11 is kept from the host while the second window has focus.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Starting a stream from the app tile could not ask for a second window, because the
title only came from the command line. The library only asks a host that says it
offers a second stream, and the session skips the window if none starts, so a saved
title does no harm against a host without the feature.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The second-window controls sat in the middle of Basic Settings. They now have a
section of their own, hidden until "Show a second window" is on, with the title
field first, then size, frame rate and the display it opens on. The performance
stats option moves next to the other on-screen options. Combo boxes rebuild their
list when they are shown, since they start hidden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With focus on the second window the first one could not capture the mouse, and its
pointer was mapped onto the second picture, so the user had to alt-tab to move over
the whole desktop. The second window now opens without activation, and when it is
shown the first window is raised and the input target is the first stream.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Settings gets a "What to show" choice under the Second Window check box: a window
on the host, or a whole display the host makes. The title field only shows for a
window. --second-window-source window|display does the same from the command line.
The library bump drops a display request when the host cannot make one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…irst window

The early window-event handling ran for every window, so the second window gaining
focus called notifyFocusGained() and re-captured the mouse. With the capture bound to
that focus, the pointer was confined to the second window and could not reach the
first one. Focus and pointer-leave now belong to the first window only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Borderless full screen on the display it opens on, as if F11 had already been
pressed; on by default, with a check box in Settings and --second-window-fullscreen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e current settings

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vibesoftwarecoder
vibesoftwarecoder merged commit c30153a into master Nov 5, 2026
2 checks passed
@vibesoftwarecoder
vibesoftwarecoder deleted the feat/second-video-stream branch November 5, 2026 22:28
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