Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/webrtc-api-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ Browser: `ontrack` → attach to `<audio>` or WebAudio.
| `maxPacketLifeTime` / `maxRetransmits` | 🟡 | In init type; verify native SCTP mapping |
| `negotiated` | 🟡 | |
| `readyState` | ✅ | |
| `send(string \| Buffer)` | ✅ | `ArrayBuffer` / `Uint8Array` coerced |
| `send(string \| Buffer \| ArrayBuffer \| Uint8Array)` | ✅ | `Buffer` passed through on send when already a Buffer |
| `close()` | ✅ | |
| `binaryType` | 🟡 | Property only; incoming binary as `Buffer` |
| `binaryType` | | `'arraybuffer'` returns `ArrayBuffer` on receive; default `Buffer` |
| `bufferedAmount` | ✅ | Cached property synced from native after send and on low events |
| `bufferedAmountLowThreshold` | ✅ | Forwarded to SCTP stack |
| `onopen` / `onmessage` / `onclose` / `onerror` | ✅ | |
Expand Down
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Each example is an npm workspace package under this directory, authored in **Typ
| Package | Type | Default port | Description |
| ----------------------------------------- | ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **peer-connection** | CLI (exits on success) | 8080 (signaling) | Two Node peers, DataChannel over WebSocket signaling |
| **peer-connection** `start:binary` | CLI (exits on success) | 8080 | Binary ArrayBuffer round-trip (20-byte game state frame) |
| **peer-connection** `start:binary-mesh` | CLI (exits on success) | 8080 | Three Node peers mesh-sync binary state over `game-sync` DataChannels |
| **peer-connection** `start:parity` | CLI (exits on success) | 8080 | Transceivers, `getStats`, `setConfiguration`, `replaceTrack`, `readSample` tour |
| **audio-cosine** | CLI (runs ~5s) | 8080 (signaling) | Local audio track streaming a 440 Hz cosine tone in PCM |
| **audio-cosine** `start:replace-track` | CLI (runs ~4s) | 8080 | `replaceTrack` + `RemoteAudioTrack.readSample` with 440→880 Hz swap |
Expand Down Expand Up @@ -79,6 +81,8 @@ WEBRTC_DEBUG=1 npm run start --workspace=@node-webrtc-rust/example-conference-ro
| Example | Command | How to verify |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| peer-connection | `npm run start --workspace=@node-webrtc-rust/example-peer-connection` | Prints `Received: Hello from peer 1!` and exits |
| peer-connection binary | `npm run start:binary --workspace=@node-webrtc-rust/example-peer-connection` | Prints `Binary player-state round-trip OK` and exits |
| peer-connection binary mesh | `npm run start:binary-mesh --workspace=@node-webrtc-rust/example-peer-connection` | Three peers mesh-sync; prints `Mesh binary sync OK` and exits |
| peer-connection parity | `npm run start:parity --workspace=@node-webrtc-rust/example-peer-connection` | Runs three parity scenarios; prints `All parity scenarios completed` |
| audio-cosine | `npm run start --workspace=@node-webrtc-rust/example-audio-cosine` | Logs remote track + streams tone for ~5s, then exits |
| audio-cosine replace | `npm run start:replace-track --workspace=@node-webrtc-rust/example-audio-cosine` | Swaps 440→880 Hz via `replaceTrack`; logs `readSample` byte lengths |
Expand Down
120 changes: 119 additions & 1 deletion examples/browser-cosine-chat/public/client.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { attachAudioVisualizer } from '/shared/audio-visualizer.js'
import {
createStateBuffer,
decodePlayerState,
encodePlayerState,
GAME_SYNC_CHANNEL_LABEL,
} from '/shared/game-state-sync.js'

const SERVER_PEER_ID = 'cosine-server'
const ICE_SERVERS = [{ urls: 'stun:stun.l.google.com:19302' }]
Expand All @@ -18,6 +24,8 @@ const incomingVizCanvas = document.getElementById('incoming-viz')
const vizStatusEl = document.getElementById('viz-status')
const debugPanelEl = document.getElementById('debug-panel')
const debugLogEl = document.getElementById('debug-log')
const gameCanvas = document.getElementById('game-canvas')
const gameStatusEl = document.getElementById('game-status')

/** @type {WebSocket | null} */
let ws = null
Expand All @@ -34,16 +42,113 @@ const peerConnections = new Map()
const peerNames = new Map()
/** @type {Map<string, RTCDataChannel>} */
const chatChannels = new Map()
/** @type {Map<string, RTCDataChannel>} */
const gameSyncChannels = new Map()
/** @type {Map<string, RTCIceCandidateInit[]>} */
const pendingIceByPeer = new Map()
/** @type {{ stop: () => void } | null} */
let incomingVisualizer = null

const localPlayerId = Math.floor(Math.random() * 60000)
const localState = createStateBuffer()
let localX = 320
let localY = 180
let gameTick = 0
/** @type {Map<number, { x: number, y: number, peerId: string }>} */
const remotePlayers = new Map()
/** @type {number | null} */
let gameLoopTimer = null
/** @type {CanvasRenderingContext2D | null} */
const gameCtx = gameCanvas?.getContext('2d') ?? null

const PLAYER_COLORS = ['#38bdf8', '#f472b6', '#a3e635', '#fb923c', '#c084fc']

if (DEBUG && debugPanelEl && debugLogEl) {
debugPanelEl.hidden = false
debugLog('Debug mode on — reload with ?debug if you change this URL mid-session')
}

window.addEventListener('keydown', (event) => {
const step = event.shiftKey ? 24 : 8
if (event.key === 'ArrowLeft') localX = Math.max(12, localX - step)
if (event.key === 'ArrowRight') localX = Math.min((gameCanvas?.width ?? 640) - 12, localX + step)
if (event.key === 'ArrowUp') localY = Math.max(12, localY - step)
if (event.key === 'ArrowDown') localY = Math.min((gameCanvas?.height ?? 360) - 12, localY + step)
})

function startGameLoop() {
if (gameLoopTimer != null) return
gameLoopTimer = window.setInterval(() => {
gameTick++
encodePlayerState(localState.view, 0, {
tick: gameTick,
playerId: localPlayerId,
x: localX,
y: localY,
rot: 0,
})
for (const dc of gameSyncChannels.values()) {
if (dc.readyState === 'open') {
dc.send(localState.bytes)
}
}
drawGameScene()
}, 100)
}

function stopGameLoop() {
if (gameLoopTimer != null) {
clearInterval(gameLoopTimer)
gameLoopTimer = null
}
}

function drawGameScene() {
if (!gameCtx || !gameCanvas) return
gameCtx.fillStyle = '#0b1018'
gameCtx.fillRect(0, 0, gameCanvas.width, gameCanvas.height)
gameCtx.fillStyle = '#1e293b'
gameCtx.fillRect(0, 0, gameCanvas.width, gameCanvas.height)
for (const [playerId, player] of remotePlayers) {
const color = PLAYER_COLORS[playerId % PLAYER_COLORS.length]
gameCtx.fillStyle = color
gameCtx.beginPath()
gameCtx.arc(player.x, player.y, 10, 0, Math.PI * 2)
gameCtx.fill()
}
gameCtx.fillStyle = '#fbbf24'
gameCtx.beginPath()
gameCtx.arc(localX, localY, 12, 0, Math.PI * 2)
gameCtx.fill()
if (gameStatusEl) {
gameStatusEl.textContent = `Local #${localPlayerId} · ${remotePlayers.size} remote player(s) · tick ${gameTick}`
}
}

function wireGameSyncChannel(peerId, dc) {
gameSyncChannels.set(peerId, dc)
dc.binaryType = 'arraybuffer'
dc.onopen = () => {
startGameLoop()
drawGameScene()
}
dc.onmessage = (event) => {
if (typeof event.data === 'string') return
const buf = event.data instanceof ArrayBuffer ? event.data : event.data.buffer
const decoded = decodePlayerState(buf)
if (decoded.playerId === localPlayerId) return
remotePlayers.set(decoded.playerId, {
x: decoded.x,
y: decoded.y,
peerId,
})
}
dc.onclose = () => {
gameSyncChannels.delete(peerId)
if (gameSyncChannels.size === 0) stopGameLoop()
}
}

function debugLog(...args) {
if (!DEBUG) return
const line = args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')
Expand Down Expand Up @@ -142,8 +247,12 @@ function cleanupPeers() {
}
peerConnections.clear()
chatChannels.clear()
gameSyncChannels.clear()
remotePlayers.clear()
stopGameLoop()
peerNames.clear()
pendingIceByPeer.clear()
drawGameScene()
}

function handleSignal(message) {
Expand Down Expand Up @@ -245,6 +354,7 @@ function onPeerLeft(peerId) {
session.pc.close()
peerConnections.delete(peerId)
chatChannels.delete(peerId)
gameSyncChannels.delete(peerId)
}
}

Expand All @@ -257,8 +367,16 @@ async function connectToPeer(peerId, createOffer) {
if (createOffer) {
const dc = pc.createDataChannel('chat')
wireChatChannel(peerId, dc)
const gameDc = pc.createDataChannel(GAME_SYNC_CHANNEL_LABEL)
wireGameSyncChannel(peerId, gameDc)
} else {
pc.ondatachannel = (event) => wireChatChannel(peerId, event.channel)
pc.ondatachannel = (event) => {
if (event.channel.label === GAME_SYNC_CHANNEL_LABEL) {
wireGameSyncChannel(peerId, event.channel)
} else {
wireChatChannel(peerId, event.channel)
}
}
}

pc.onicecandidate = (event) => {
Expand Down
11 changes: 10 additions & 1 deletion examples/browser-cosine-chat/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ <h2>Incoming audio track</h2>
<p id="audio-status">Waiting for server track…</p>
</section>

<section class="panel game-panel">
<h2>Multiplayer sync (binary DataChannel)</h2>
<p class="game-hint">
Move with arrow keys. Positions sync over a <code>game-sync</code> channel (~10 Hz).
</p>
<canvas id="game-canvas" width="640" height="360" aria-label="Multiplayer positions"></canvas>
<p id="game-status" class="game-status">Join a room to sync player dots.</p>
</section>

<section class="panel chat-panel">
<h2>Room chat</h2>
<ul id="messages"></ul>
Expand All @@ -61,6 +70,6 @@ <h2>Debug log</h2>
</section>
</main>

<script type="module" src="/client.js?v=4"></script>
<script type="module" src="/client.js?v=5"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/browser-cosine-chat/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,24 @@ audio {
white-space: pre-wrap;
word-break: break-word;
}

.game-hint {
margin: 0 0 0.75rem;
font-size: 0.85rem;
color: #94a3b8;
}

#game-canvas {
display: block;
width: 100%;
height: auto;
border-radius: 8px;
border: 1px solid #243041;
background: #0b1018;
}

.game-status {
margin: 0.75rem 0 0;
font-size: 0.8rem;
color: #64748b;
}
2 changes: 2 additions & 0 deletions examples/peer-connection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"scripts": {
"start": "tsx src/index.ts",
"start:parity": "tsx src/parity-features.ts",
"start:binary": "tsx src/binary-roundtrip.ts",
"start:binary-mesh": "tsx src/binary-mesh.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
Expand Down
Loading
Loading