Location: /data/src/netplay/compositing/StreamCompositor.js
Status: Created (463 lines)
Type: New class - Canvas-based stream compositing engine
class StreamCompositor {
constructor(width = 1280, height = 720)
registerStream(producerId, name, mediaStream, pinned = false)
unregisterStream(producerId)
togglePin(producerId)
startRendering()
stopRendering()
render()
calculateColumns(count, width, height)
drawGridSection(streams, sectionX, sectionY, sectionWidth, sectionHeight, options)
drawStreamCell(stream, x, y, width, height)
drawPlaceholder(x, y, width, height, text)
getCanvas()
getStreams()
getStreamCount()
dispose()
}Key Features:
- Hidden canvas element (display: none)
- Continuous requestAnimationFrame rendering
- 80/20 grid split for pinned/other streams
- Video element management
- Aspect ratio preservation
- Stream name overlays
- Pin status indicators
// Line ~90 - In constructor
this.streamCompositor = null;Location: Lines ~5176-5250
Changes:
- Added check for active
streamCompositorat start - If compositor has streams, capture from it
- Otherwise fall back to emulator canvas
- Compositor canvas takes priority
Old Behavior:
// Just captured emulator canvas
const emulatorCanvas = window.EJS_emulator?.canvas || this.emulator?.canvas;
// ... capture logic ...New Behavior:
// Check compositor first
if (this.streamCompositor && this.streamCompositor.getStreamCount() > 0) {
// Capture from compositor canvas
const compositorCanvas = this.streamCompositor.getCanvas();
// ... capture logic ...
}
// Fall back to emulator canvas
const emulatorCanvas = window.EJS_emulator?.canvas || this.emulator?.canvas;
// ... capture logic ...- Initialize compositor instance
- Check StreamCompositor class availability
- Create new instance with specified dimensions
- Return compositor reference
- Validate compositor exists
- Delegate to
streamCompositor.registerStream() - Handle null/undefined guards
- Validate compositor exists
- Delegate to
streamCompositor.unregisterStream() - Clean up stream resources
- Validate compositor exists
- Delegate to
streamCompositor.togglePin() - Trigger re-render
- Create consumer via SFU transport
- Check if arcade lobby mode active
- Extract display name or fallback to producerId
- Create MediaStream from track
- Register with compositor
- Return consumer object
- Call
streamCompositor.dispose() - Set to null
- Log cleanup message
Location: Lines 1383-1729 (removed first duplicate implementation)
setupArcadeLobbyGrid()(first version with 347 lines)createArcadeCell()(first version with 114 lines)updateArcadeLobbyStreams()(first version with 144 lines)
Reason: Duplicate implementation. Second version kept and updated.
Location: Lines ~1800-2150
Before: ~60 lines of DOM grid creation After: ~5 lines delegating to compositor
// NEW VERSION
setupArcadeLobbyGrid() {
console.log("[NetplayMenu] Setting up arcade lobby (stream compositor mode)");
// Initialize the stream compositor
this.initializeArcadeLobbyCompositor();
// No DOM grid needed - the compositor renders to hidden canvas
// Remote viewers will see the composited grid through SFU streaming
}- Now works with compositor instead of DOM grid
- Pin button calls
arcadeLobbyTogglePin()instead of re-layout - Kept for backward compatibility
- Now a no-op (compositor handles rendering)
- Kept logging for debug purposes
- Streams handled by StreamCompositor, not DOM
Location: Lines ~1800-1820
- Get engine reference
- Call
engine.initializeStreamCompositor(1280, 720) - Handle errors gracefully
- Wrapper to register stream from menu layer
- Gets engine reference
- Delegates to
engine.registerProducerStream()
- Wrapper to unregister stream from menu layer
- Delegates to
engine.unregisterProducerStream()
- Wrapper to toggle pin from menu layer
- Delegates to
engine.toggleProducerPin()
| File | Type | Changes | Lines |
|---|---|---|---|
| StreamCompositor.js | NEW | Complete class implementation | 463 |
| NetplayEngine.js | MODIFIED | 1 property + 6 methods + 1 method update | ~280 |
| NetplayMenu.js | MODIFIED | Remove duplicates + 4 new methods + 3 updates | ~550 |
| TOTAL | 1,293 |
- Arcade lobby had DOM-based grid sidebar
- Each stream had a separate video element in the grid
- Video capture was always from emulator canvas
- No stream compositing to remote viewers
- Remote viewers only saw single game canvas
- Arcade lobby uses hidden canvas compositor
- Multiple streams composited into single grid
- Video capture prioritizes compositor canvas
- Composited grid sent to remote viewers
- Remote viewers see all producer streams in grid layout
- 80/20 split: 80% left for pinned, 20% right for others
- Dynamic stream registration on consumer creation
- Pin/unpin toggle updates grid layout
- Smooth 30 FPS rendering
- Complete resource cleanup on room exit
- All existing methods preserved
- New functionality is additive
- Fallback to old behavior when compositor unavailable
- Backward compatible with existing code
✅ No Errors in any modified files ✅ No Warnings in modified files ✅ All Changes compile successfully