Skip to content

Add an opt-in text clipboard packet - #155

Open
Wouze wants to merge 6 commits into
moonlight-stream:masterfrom
Wouze:clipboard-sync
Open

Wouze wants to merge 6 commits into
moonlight-stream:masterfrom
Wouze:clipboard-sync

Conversation

@Wouze

@Wouze Wouze commented Sep 23, 2026 •

Copy link
Copy Markdown

Summary

  • Add LiSendClipboardText() so a client can send UTF-8 clipboard text to the host. A zero-length send subscribes to host updates and does not change the host clipboard.
  • Add host-to-client control message 0x5505. Clients that do not implement the new callback ignore it.
  • Allow encrypted control messages larger than the old 256-byte stack buffer, which is required for a paragraph of clipboard text. Text is capped at 32,755 bytes.

This is the protocol piece for Sunshine and Moonlight Qt pull requests that use the same commit.

Test plan

  • Linux Moonlight client with Sync clipboard enabled, against a Sunshine host built with this commit, copies text both ways
  • A stock Moonlight client still connects; it does not receive clipboard text it did not subscribe for
  • Mac and Windows clients are not run yet; they share this library once their Moonlight Qt build includes it

@Wouze

Wouze commented Oct 2, 2026

Copy link
Copy Markdown
Author

Hi. I merged current master into this branch, including the extended-key modifier. Sunshine and Moonlight Qt are both waiting on this protocol change. Could you review it when you have time?

@Wouze

Wouze commented Oct 7, 2026

Copy link
Copy Markdown
Author

Hi @cgutman

ReenigneArcher approved this on October 3 and Sunshine and the Moonlight Qt setting are waiting on it. Could you take a look when you have time?

Thanks

@Wouze

Wouze commented Oct 15, 2026

Copy link
Copy Markdown
Author

Hello

Comment thread src/InputStream.c Outdated
}

// The unencrypted input path encrypts into a 128-byte stack buffer.
if (!isControlStreamEncrypted()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need to check IS_SUNSHINE() here too.

Comment thread src/InputStream.c
token ^= token << 5;
if (token == 0) {
token = 1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the purpose of the token?

Comment thread src/ControlStream.c Outdated
char stackBuffer[256];
char* plainBuffer = stackBuffer;

if (paylen < 0 || paylen > 65535) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can drop these changes. We encrypt in place now.

Comment thread src/ControlStream.c Outdated
packet = (PNVCTL_ENET_PACKET_HEADER_V2)plainBuffer;
packet->type = ptype;
packet->payloadLength = paylen;
packet->payloadLength = (unsigned short)paylen;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is an unsafe truncating cast.

Comment thread src/InputStream.c

// The unencrypted input path encrypts into a 128-byte stack buffer.
if (!isControlStreamEncrypted()) {
return -1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LI_ERR_UNSUPPORTED

Comment thread src/Limelight.h Outdated
int LiSendUtf8TextEvent(const char *text, unsigned int length);

// Send clipboard text to a Sunshine host. length 0 subscribes to host clipboard
// updates and does not change the host clipboard. Requires an encrypted control stream.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Client authors don't know what an encrypted control stream is. I would just remove that detail.

Comment thread src/InputStream.c Outdated
return -1;
}

holder->channelId = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use a new channel ID for this. You can take 0x6 which used to be for UTF-8 data.

Comment thread src/ControlStream.c Outdated
break;
case IDX_CLIPBOARD_TEXT:
if (ListenerCallbacks.clipboardText != NULL) {
ListenerCallbacks.clipboardText(queuedCb->data.clipboardText.token,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Null check is not necessary, since the callback infrastructure ensures all callback pointers are populated

Clipboard sends are Sunshine-only on channel 0x6, and the unused echo token is gone. The control stream keeps master's in-place encryption.
@Wouze

Wouze commented Oct 23, 2026 •

Copy link
Copy Markdown
Author

@cgutman the notes are in f197f1d. Clipboard sends are Sunshine-only on channel 0x6, the echo token is gone, and the control stream uses the current in-place encryption. Could you take another look?

Thanks

@Wouze
Wouze requested a review from cgutman October 31, 2026 22:55
@cgutman

cgutman commented Nov 1, 2026

Copy link
Copy Markdown
Member

I think the 32KB limit might be a problem in practice, so I introduced a new jumbo control message for you in 4853e70. Your Sunshine PR will need to be updated to add support for parsing this new header, but that should be pretty easy.

Can you adapt your changes to increase the limit clipboard data on to something larger like a few MB?

@Wouze

Wouze commented Nov 1, 2026

Copy link
Copy Markdown
Author

Yes i agree with you, i initially gave it a small limit for testing

What size do you think would be sufficient? Also should we do images and other files? Personally I use copy/paste images all the time but idk if it fits this thing without hacky workarounds

@cgutman

cgutman commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

I think we could model something after the SDL3 clipboard API with a bidirectional protocol with 3 message types:

  • Clipboard offer message with all of the MIME types of what resides in our clipboard which we send when the clipboard changes
  • Clipboard payload request message with the specific MIME type they want the data for
  • Clipboard payload response message with the MIME type and the data itself

We would likely need some sort of chunking scheme for cases where the clipboard data is larger than 32 MB (the default ENet packet size limit).

Edit: I can take a stab at a protocol design later.

Wouze added 3 commits November 4, 2026 16:14
Either side offers the MIME types on its clipboard when it changes. The
other side requests the types it wants and receives each one as data,
tied to the offer by a sequence number. Data over 4 MB is sent in chunks
of jumbo control messages, up to 64 MB. An offer with no types
subscribes to host clipboard changes. This replaces the text-only
clipboard messages.
@Wouze

Wouze commented Nov 11, 2026

Copy link
Copy Markdown
Author

@cgutman reworked this based on your offer/request payload idea:

  • Each side sends an offer with the MIME types it has, the other side requests what it wants and gets the data back, matched by a sequence number
  • Anything over 4 MB gets chunked over your jumbo control messages, capped at 64 MB
  • Removed the old text-only messages. An empty offer now just subscribes to host clipboard changes

Other side of it:

Tested 6 MB both ways between a Mac client and a Linux host, fully working

Can you take another look when you get a chance?

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.

4 participants