Discord: share a screen into a guild's voice channel

The share was named by the call's conversation, which a guild voice
channel does not have, so the button did nothing there - in the stage and
through the general share path both. A DM's call still goes by its
conversation; a guild's goes by the account in it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-01 15:41:12 -04:00
co-authored by Claude Opus 5.5
parent df03c2f39d
commit 27f15dff93
3 changed files with 22 additions and 20 deletions
+1 -1
Submodule nobilis updated: f33c9880e4...a5eb3b4f92
+1 -3
View File
@@ -223,9 +223,7 @@ export function DiscordStage({ session, where }: { session: VoiceSession; where:
// From this call's own account. The window can be in two accounts' calls,
// and asking the store for "the" Discord call picks whichever came first.
const share = (quality?: VideoQuality): Promise<void> =>
session.bufferId
? store.toggleDiscordScreenShare(accountId, session.bufferId, quality)
: store.toggleScreenShare(quality)
store.toggleDiscordScreenShare(accountId, session.bufferId, quality)
const buttons: StageButton[] = [
{
+20 -16
View File
@@ -944,8 +944,8 @@ export class ChatStore {
* `discordSharing` beside it.
*/
private discordShare: { stop: () => void; stream: MediaStream } | null = null
/** The conversation the share was started against, which is what stops it. */
private discordShareBuffer: string | null = null
/** Where the share was started, which is what stops it. */
private discordShareWhere: { bufferId: string } | { accountId: string } | null = null
/** The Discord camera's encoder, held for the same reason. */
private discordCameraSend: { stop: () => void; stream: MediaStream } | null = null
/**
@@ -2568,13 +2568,10 @@ export class ChatStore {
// its audio lives in the daemon - so it is answered first and on its own
// terms. The gesture and the button are the same either way, which is
// the whole point of asking here rather than in two places.
// A conversation is needed as well as a session: the stream is asked for
// against a channel, and a call whose buffer this window has never opened
// has nothing to ask against.
const discord = this.state.voiceSessions.find(
(s) => s.accountId.startsWith('discord:') && !!s.bufferId
)
if (discord?.bufferId && !this.state.activeCall) {
// A DM's call has a conversation and a guild's voice channel does not;
// either can be shared into, named by its account where there is none.
const discord = this.state.voiceSessions.find((s) => s.accountId.startsWith('discord:'))
if (discord && !this.state.activeCall) {
await this.toggleDiscordScreenShare(discord.accountId, discord.bufferId, quality)
return
}
@@ -2626,18 +2623,25 @@ export class ChatStore {
* draw from the other side, where the browser holds the whole call because
* WebRTC is the browser's.
*/
async toggleDiscordScreenShare(accountId: string, bufferId: string, quality: VideoQuality = DEFAULT_QUALITY): Promise<void> {
async toggleDiscordScreenShare(
accountId: string,
bufferId: string | undefined,
quality: VideoQuality = DEFAULT_QUALITY
): Promise<void> {
// A DM's call is named by its conversation; a guild's voice channel has
// none, and is named by the account in it.
const where = bufferId ? { bufferId } : { accountId }
if (this.discordShare) {
const was = this.state.discordSharing
this.discordShare.stop()
this.discordShare = null
this.set({ discordSharing: null, discordShareStream: null })
await window.moho.rpc('stopDiscordScreenShare', { bufferId: this.discordShareBuffer ?? bufferId }).catch(() => {})
await window.moho.rpc('stopDiscordScreenShare', this.discordShareWhere ?? where).catch(() => {})
// One screen, one stream: pressing share in another account's call
// moves it there rather than only stopping it.
if (was === accountId) return
}
this.discordShareBuffer = bufferId
this.discordShareWhere = where
try {
// The screen first, then Discord. The person chooses while the click
@@ -2651,7 +2655,7 @@ export class ChatStore {
// follows the screen's own shape, and the daemon tells viewers exactly
// what the encoder below will send.
const settings = encodeSettings(stream.getVideoTracks()[0]?.getSettings() ?? {}, quality)
await window.moho.rpc('startDiscordScreenShare', { bufferId, ...settings })
await window.moho.rpc('startDiscordScreenShare', { ...where, ...settings })
this.toast('info', 'Setting up the stream…')
// The connection is opened by the daemon when Discord answers, which
@@ -2664,7 +2668,7 @@ export class ChatStore {
// opened the stream" is true and useless; a token it would not take
// or a server it could not find is a sentence somebody can act on.
this.toast('error', ready.error ? `Couldn’t open the stream: ${ready.error}` : 'Discord never opened the stream')
await window.moho.rpc('stopDiscordScreenShare', { bufferId }).catch(() => {})
await window.moho.rpc('stopDiscordScreenShare', where).catch(() => {})
// A capture already open has nobody to send it to.
for (const track of stream.getTracks()) track.stop()
return
@@ -2674,12 +2678,12 @@ export class ChatStore {
this.toast('error', message)
this.discordShare = null
this.set({ discordSharing: null, discordShareStream: null })
void window.moho.rpc('stopDiscordScreenShare', { bufferId }).catch(() => {})
void window.moho.rpc('stopDiscordScreenShare', where).catch(() => {})
})
this.set({ discordSharing: accountId, discordShareStream: stream })
} catch (e) {
this.toast('error', `Couldn't share the screen: ${(e as Error).message}`)
await window.moho.rpc('stopDiscordScreenShare', { bufferId }).catch(() => {})
await window.moho.rpc('stopDiscordScreenShare', where).catch(() => {})
}
}