Skip to content

Commit 338886a

Browse files
fix: Wayland screenshare preview and snap arm64 build (#1865)
* fix: use desktopCapturer source ID format for Wayland screen sharing preview - Remove MediaStream UUID logic from injectedScreenSharing.js - Add validation in IPC handler to prevent UUID overwrites - Send null from renderer to preserve correct source ID - Add ADR 001 documenting technical decision - Update screenSharing README with Wayland requirements Fixes #1853: Preview window now works on Wayland by using screen:x:y format Related to tasks-prd-wayland-screenshare-preview-fix.md * chore: hide menu bar on screenshare preview window - Add autoHideMenuBar: true to preview window config - Provides cleaner UX for preview window - Completes manual testing tasks on Wayland Related to #1853 * chore: bump version to 2.5.13 - Fix Wayland screenshare preview window - Fix snap workflow arm64 build command typo * Apply suggestion from @IsmaelMartinez * Update app/index.js Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * refactor: use positive condition for SonarQube compliance - Use 'if (sourceId)' for cleaner positive condition check - Resolves SonarQube 'unexpected negated condition' warning --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent f1480d1 commit 338886a

File tree

10 files changed

+280
-25
lines changed

10 files changed

+280
-25
lines changed

.github/workflows/snap.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,6 @@ jobs:
8383

8484
- name: Release
8585
if: contains(github.ref, 'main')
86-
run: npm run dist:linux:snap:armv64 -- --publish always
86+
run: npm run dist:linux:snap:arm64 -- --publish always
8787

8888

app/index.js

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,29 +141,44 @@ if (gotTheLock) {
141141
ipcMain.on("screen-sharing-started", (event, sourceId) => {
142142
try {
143143
console.debug("[SCREEN_SHARE_DIAG] Screen sharing session started", {
144-
sourceId: sourceId,
144+
receivedSourceId: sourceId,
145+
existingSourceId: globalThis.selectedScreenShareSource,
145146
timestamp: new Date().toISOString(),
146-
previousSource: globalThis.selectedScreenShareSource,
147147
hasExistingPreview: globalThis.previewWindow && !globalThis.previewWindow.isDestroyed(),
148148
mainWindowVisible: mainAppWindow?.isVisible?.() || false,
149149
mainWindowFocused: mainAppWindow?.isFocused?.() || false
150150
});
151151

152-
// Check for potential duplicate sessions - this could cause echo issues
153-
if (globalThis.selectedScreenShareSource !== null) {
154-
console.warn("[SCREEN_SHARE_DIAG] Multiple screen sharing sessions detected", {
155-
previousSource: globalThis.selectedScreenShareSource,
156-
newSource: sourceId,
157-
riskLevel: "HIGH - may cause audio feedback or duplicate windows",
158-
previewWindowState: globalThis.previewWindow?.isDestroyed?.() ? "destroyed" : "active"
152+
// Only update if we received a valid source ID
153+
if (sourceId) {
154+
// Validate format - must be screen:x:y or window:x:y (not UUID)
155+
const isValidFormat = sourceId.startsWith('screen:') || sourceId.startsWith('window:');
156+
157+
if (isValidFormat) {
158+
console.debug("[SCREEN_SHARE_DIAG] Received valid source ID format, updating", {
159+
sourceId: sourceId,
160+
sourceType: sourceId.startsWith('screen:') ? 'screen' : 'window'
161+
});
162+
globalThis.selectedScreenShareSource = sourceId;
163+
} else {
164+
// UUID format detected - this is the bug we're fixing
165+
console.warn("[SCREEN_SHARE_DIAG] Received invalid source ID format (UUID?), keeping existing", {
166+
received: sourceId,
167+
existing: globalThis.selectedScreenShareSource,
168+
note: "MediaStream.id (UUID) cannot be used for preview window - see ADR"
169+
});
170+
// Keep existing value, don't overwrite
171+
}
172+
} else {
173+
console.debug("[SCREEN_SHARE_DIAG] No source ID received (null), keeping existing", {
174+
existing: globalThis.selectedScreenShareSource,
175+
note: "Source ID was already set correctly by setupScreenSharing()"
159176
});
160177
}
161178

162-
globalThis.selectedScreenShareSource = sourceId;
163-
164179
console.debug("[SCREEN_SHARE_DIAG] Screen sharing source registered", {
165-
sourceId: sourceId,
166-
sourceType: sourceId?.startsWith?.('screen:') ? 'screen' : 'window',
180+
sourceId: globalThis.selectedScreenShareSource,
181+
sourceType: globalThis.selectedScreenShareSource?.startsWith?.('screen:') ? 'screen' : 'window',
167182
willCreatePreview: true
168183
});
169184

app/mainAppWindow/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ function createScreenSharePreviewWindow() {
9090
show: false,
9191
resizable: true,
9292
alwaysOnTop: thumbnailConfig.alwaysOnTop || false,
93+
autoHideMenuBar: true,
9394
webPreferences: {
9495
preload: path.join(
9596
__dirname,

app/screenSharing/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,24 @@ No direct configuration options, but behavior is influenced by:
184184
### Linux
185185
- **X11**: Direct screen capture support
186186
- **Wayland**: Uses PipeWire portal for desktop capture
187+
- **Important**: Wayland requires source IDs in `screen:x:y` or `window:x:y` format (from desktopCapturer)
188+
- MediaStream UUIDs will cause preview window failures on Wayland
189+
- See [ADR 001](../../docs/adr/001-use-desktopcapturer-source-id-format.md) for technical details
187190

188-
### macOS
191+
### macOS
189192
- **Screen Recording Permission**: Required for desktop capture
190193
- **Retina Displays**: High-DPI thumbnail generation
191194

192195
### Windows
193196
- **DWM Integration**: Desktop Window Manager compatibility
194197
- **Multi-monitor**: Proper handling of multiple displays
195198

199+
## Architecture Decisions
200+
201+
For important technical decisions regarding screen sharing implementation:
202+
203+
- [ADR 001: Use desktopCapturer Source ID Format](../../docs/adr/001-use-desktopcapturer-source-id-format.md) - Why we use `screen:x:y` format instead of MediaStream UUIDs
204+
196205
---
197206

198207
For more information about the complete screen sharing implementation, see [Screen Sharing Documentation](../../docs-site/docs/screen-sharing.md).

app/screenSharing/injectedScreenSharing.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,10 @@
109109

110110
// Send screen sharing started event
111111
if (electronAPI.sendScreenSharingStarted) {
112-
// Prefer the MediaStream's own id when available to avoid collisions
113-
const sourceId = stream?.id
114-
? stream.id
115-
: `screen-share-${crypto.randomUUID()}`;
116-
117112
console.debug(`[SCREEN_SHARE_DIAG] Sending screen-sharing-started event (preview window will open)`);
118-
119-
electronAPI.sendScreenSharingStarted(sourceId);
113+
console.debug(`[SCREEN_SHARE_DIAG] Not sending stream.id to preserve desktopCapturer source ID`);
114+
115+
electronAPI.sendScreenSharingStarted(null);
120116
electronAPI.send("active-screen-share-stream", stream);
121117
}
122118

com.github.IsmaelMartinez.teams_for_linux.appdata.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
<url type="bugtracker">https://github.com/IsmaelMartinez/teams-for-linux/issues</url>
1515
<launchable type="desktop-id">com.github.IsmaelMartinez.teams_for_linux.desktop</launchable>
1616
<releases>
17+
<release version="2.5.13" date="2025-10-08">
18+
<description>
19+
<ul>
20+
<li>Fix: Wayland screenshare preview window now works correctly by using desktopCapturer source ID format (screen:x:y) instead of MediaStream UUIDs</li>
21+
<li>Fix: Correct snap workflow arm64 build command typo (armv64 -> arm64) that prevented arm64 snap releases</li>
22+
</ul>
23+
</description>
24+
</release>
1725
<release version="2.5.12" date="2025-10-06">
1826
<description>
1927
<ul>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# ADR 001: Use desktopCapturer Source ID Format for Screen Sharing
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
When implementing screen sharing preview windows in Teams for Linux, we need to identify which screen or window the user selected for sharing. There are two potential identifiers available:
10+
11+
1. **desktopCapturer source ID**: Format `screen:x:y` or `window:x:y` (e.g., `screen:602:0`)
12+
- Provided by Electron's `desktopCapturer.getSources()` API
13+
- Stable identifier for the selected screen/window
14+
- Required format for Wayland's xdg-desktop-portal
15+
16+
2. **MediaStream ID**: UUID format (e.g., `a1b2c3d4-5678-90ab-cdef-1234567890ab`)
17+
- Generated when the MediaStream is created from the source
18+
- Changes on each new stream instance
19+
- Not recognized by Wayland's preview window APIs
20+
21+
### The Problem
22+
23+
On Wayland, the preview window creation fails with "Invalid State" error when passed a MediaStream UUID because:
24+
25+
- Wayland's `xdg-desktop-portal` ScreenCast API expects the desktopCapturer source ID format
26+
- The portal uses this ID to identify which screen/window to capture
27+
- UUIDs are not valid portal source identifiers
28+
29+
On X11, the system is more permissive and accepts various formats, which masked this bug.
30+
31+
### Technical Background
32+
33+
**Wayland Portal Requirements:**
34+
- [xdg-desktop-portal ScreenCast API](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.ScreenCast) specifies string source identifiers
35+
- [Electron Issue #33613](https://github.com/electron/electron/issues/33613) documents: "Wayland expects string ID, not UUID"
36+
- [Chromium Bug #1343766](https://bugs.chromium.org/p/chromium/issues/detail?id=1343766) confirms Wayland requires specific format
37+
38+
**Electron API:**
39+
- [DesktopCapturer API](https://www.electronjs.org/docs/latest/api/desktop-capturer) returns source objects with `id` property in format `screen:display_id:window_id`
40+
- This `id` is the canonical identifier for desktop capture sources
41+
42+
## Decision
43+
44+
**We will use the desktopCapturer source ID format (`screen:x:y` or `window:x:y`) throughout the screen sharing pipeline, not the MediaStream UUID.**
45+
46+
### Implementation
47+
48+
1. **Source Selection** (`app/mainAppWindow/index.js`):
49+
- Store the desktopCapturer source object from `setupScreenSharing()`
50+
- Set `global.selectedScreenShareSource` with the source ID in correct format
51+
52+
2. **Stream Creation** (`app/screenSharing/injectedScreenSharing.js`):
53+
- Do NOT send `stream.id` (UUID) via IPC
54+
- Send `null` to preserve the source ID already stored in main process
55+
56+
3. **IPC Handler** (`app/index.js`):
57+
- Validate received source IDs match format `screen:x:y` or `window:x:y`
58+
- Reject UUID format with warning
59+
- Only update global state if format is valid
60+
61+
4. **Preview Window** (`app/screenSharing/previewWindow.html`):
62+
- Use the validated source ID from global state
63+
- Pass to `getUserMedia()` as `chromeMediaSourceId`
64+
65+
## Consequences
66+
67+
### Positive
68+
69+
- ✅ Screen sharing preview works on Wayland
70+
- ✅ Consistent behavior across X11 and Wayland
71+
- ✅ Uses canonical Electron API identifiers
72+
- ✅ Future-proof against platform changes
73+
- ✅ Validation prevents regression to UUID format
74+
75+
### Negative
76+
77+
- ⚠️ Requires coordination between main and renderer processes
78+
- ⚠️ The renderer process (injectedScreenSharing.js) doesn't have access to the desktopCapturer source ID, only the MediaStream
79+
80+
### Alternatives Considered
81+
82+
**Alternative 1: Pass desktopCapturer source to renderer**
83+
- ❌ Would require exposing more of Electron's APIs to renderer
84+
- ❌ Security concerns with broader API access
85+
- ❌ Violates separation of concerns
86+
87+
**Alternative 2: Platform detection (use UUID on X11, source ID on Wayland)**
88+
- ❌ Adds complexity
89+
- ❌ Inconsistent behavior across platforms
90+
- ❌ X11 lenience is not guaranteed to continue
91+
92+
**Alternative 3: Store source ID in a different global variable**
93+
- ❌ Adds state management complexity
94+
- ❌ Risk of desynchronization
95+
- ✅ Selected approach uses existing `global.selectedScreenShareSource`
96+
97+
## Notes
98+
99+
- Bug reported in [teams-for-linux Issue #1853](https://github.com/IsmaelMartinez/teams-for-linux/issues/1853)
100+
- Fix implemented in v2.5.13
101+
- The StreamSelector module (app/screenSharing/index.js:73-74) already expected this format
102+
- Console logging with `[SCREEN_SHARE_DIAG]` prefix helps verify correct format
103+
104+
## References
105+
106+
- [Electron DesktopCapturer API](https://www.electronjs.org/docs/latest/api/desktop-capturer)
107+
- [xdg-desktop-portal ScreenCast API](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.ScreenCast)
108+
- [Electron Issue #33613](https://github.com/electron/electron/issues/33613)
109+
- [Chromium Bug #1343766](https://bugs.chromium.org/p/chromium/issues/detail?id=1343766)
110+
- [teams-for-linux Issue #1853](https://github.com/IsmaelMartinez/teams-for-linux/issues/1853)

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "teams-for-linux",
3-
"version": "2.5.12",
3+
"version": "2.5.13",
44
"main": "app/index.js",
55
"description": "Unofficial client for Microsoft Teams for Linux",
66
"homepage": "https://github.com/IsmaelMartinez/teams-for-linux",
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Tasks: Fix Wayland Screenshare Preview Window
2+
3+
Based on PRD: [prd-wayland-screenshare-preview-fix.md](prd-wayland-screenshare-preview-fix.md)
4+
5+
## System Analysis
6+
7+
### ADR Review
8+
9+
No formal Architecture Decision Records (ADRs) found in `docs/adr/`. However, the codebase follows these implicit architectural decisions:
10+
11+
- **IPC Communication Pattern**: Use of `ipcMain.on` for notifications and `ipcMain.handle` for request-response patterns
12+
- **Global State Management**: Screen sharing state stored in `global.selectedScreenShareSource`
13+
- **Diagnostic Logging**: Consistent use of `[SCREEN_SHARE_DIAG]` prefix for screen sharing related logs
14+
- **Code Style**: Use of `const`/`let` (no `var`), async/await patterns, private fields with `#` syntax
15+
16+
### Documentation Review
17+
18+
From `app/screenSharing/README.md`:
19+
- StreamSelector expects source IDs in format `screen:x:0` or `window:x:0` (line 73-74 of `app/screenSharing/index.js`)
20+
- desktopCapturer.getSources() provides source objects with `id`, `name`, `thumbnail`, and other properties
21+
- The module bridges Teams web app with Electron's native desktopCapturer API
22+
- Platform differences documented for X11 vs Wayland (X11 is more permissive, Wayland requires strict format)
23+
24+
### Pattern Analysis
25+
26+
**Current Screen Sharing Flow:**
27+
1. User clicks "Share" in Teams → triggers `setDisplayMediaRequestHandler` (app/mainAppWindow/index.js)
28+
2. StreamSelector.show() displays source picker
29+
3. User selects source → `setupScreenSharing(selectedSource)` called (app/mainAppWindow/index.js:226)
30+
4. **Line 228**: `global.selectedScreenShareSource = selectedSource` stores desktopCapturer source object
31+
5. **Line 231**: `createScreenSharePreviewWindow()` creates preview window
32+
6. Electron creates MediaStream from selected source
33+
7. **injectedScreenSharing.js:113-119**: Intercepts stream, sends IPC with `stream.id` (UUID)
34+
8. **app/index.js:162**: IPC handler **OVERWRITES** `global.selectedScreenShareSource` with UUID
35+
9. Preview window tries to use UUID → fails on Wayland
36+
37+
**Key Pattern Identified**: The bug occurs because step 8 overwrites the correct value set in step 4.
38+
39+
### Conflicts and Constraints
40+
41+
**Conflict**: Two different processes setting `global.selectedScreenShareSource`:
42+
- `setupScreenSharing()` sets it correctly (desktopCapturer source object/id)
43+
- `screen-sharing-started` IPC handler overwrites it incorrectly (MediaStream UUID)
44+
45+
**Constraint**: The `injectedScreenSharing.js` runs in the renderer process and does NOT have access to the desktopCapturer source object - it only receives the MediaStream after creation.
46+
47+
**Resolution Strategy**: Prevent the IPC handler from overwriting the correct value by either:
48+
1. Not sending sourceId from renderer process, OR
49+
2. Only updating global state if received value is in correct format
50+
51+
### Research Spikes Identified
52+
53+
No research spikes required - this is a straightforward bug fix with clear root cause.
54+
55+
## Relevant Files
56+
57+
### Files Modified
58+
59+
- `app/screenSharing/injectedScreenSharing.js` - Removed stream.id UUID logic, now sends null to preserve desktopCapturer source ID
60+
- `app/index.js` - Added validation to `screen-sharing-started` IPC handler to prevent overwriting with invalid format
61+
- `app/screenSharing/README.md` - Added documentation about source ID format requirement and ADR reference
62+
- `docs/adr/001-use-desktopcapturer-source-id-format.md` - Created ADR documenting the technical decision
63+
64+
### Files Already Correct (No Changes)
65+
66+
- `app/mainAppWindow/index.js` - Line 228 already stores correct source object
67+
- `app/screenSharing/previewWindow.html` - Already uses sourceId correctly, just needs correct value
68+
- `app/screenSharing/index.js` - StreamSelector already expects correct format
69+
70+
### Notes
71+
72+
- Run `npm run lint` before committing
73+
- Test on both Wayland (`XDG_SESSION_TYPE=wayland`) and X11 (`XDG_SESSION_TYPE=x11`)
74+
- No automated tests exist for this module - rely on manual testing
75+
- Follow existing diagnostic logging patterns with `[SCREEN_SHARE_DIAG]` prefix
76+
77+
## Tasks
78+
79+
- [x] 1.0 Fix source ID handling in renderer process (injectedScreenSharing.js)
80+
- [x] 1.1 Remove the sourceId calculation from lines 113-115 (stream.id logic)
81+
- [x] 1.2 Update line 119 to send `null` instead of `sourceId` to `sendScreenSharingStarted()`
82+
83+
- [x] 2.0 Add validation to IPC handler to prevent incorrect overwrites
84+
- [x] 2.1 In `app/index.js` line 162, check if `sourceId` is null before updating
85+
- [x] 2.2 If `sourceId` is not null, validate it matches format `screen:x:y` or `window:x:y`
86+
- [x] 2.3 Only update `global.selectedScreenShareSource` if validation passes
87+
- [x] 2.4 Log warning if UUID format detected (helps catch future regressions)
88+
89+
- [x] 3.0 Update diagnostic logging for better debugging
90+
- [x] 3.1 Log the received sourceId vs existing sourceId in IPC handler
91+
- [x] 3.2 Add log in injectedScreenSharing.js showing we're NOT sending stream.id
92+
- [x] 3.3 Update existing log at line 166 to show validation result
93+
94+
- [x] 4.0 Create ADR and update documentation
95+
- [x] 4.1 Create `docs/adr/` directory if it doesn't exist
96+
- [x] 4.2 Create ADR documenting decision to use desktopCapturer source ID format (`screen:x:y` or `window:x:y`)
97+
- [x] 4.3 ADR should explain: why MediaStream.id (UUID) cannot be used, Wayland requirements, and impact on preview window
98+
- [x] 4.4 Update `app/screenSharing/README.md` referencing the ADR
99+
100+
- [x] 5.0 Manual testing on Wayland and X11
101+
- [x] 5.1 Test on Wayland: Start screenshare, verify preview opens, check console for `screen:x:y` format
102+
- [x] 5.2 Test on X11: Start screenshare, verify no regression, preview still works
103+
- [x] 5.3 Test multiple screens on both platforms
104+
- [x] 5.4 Test window sharing on both platforms
105+
- [x] 5.5 Run `npm run lint` and fix any violations
106+
- [x] 5.6 Verify console logs show correct source ID format (not UUID)
107+
108+
## Future Improvements
109+
110+
### Priority 2 (Nice-to-Have)
111+
112+
- Refactor global state management into a proper ScreenSharingManager class to encapsulate `global.selectedScreenShareSource` and related state
113+
114+
### Technical Debt Considerations
115+
116+
- Global state management (`global.selectedScreenShareSource`) should be encapsulated in a dedicated module

0 commit comments

Comments
 (0)