The key insight: a 2-party / small-mesh call needs NO server-side media — the server only relays SDP/ICE and the browsers connect peer-to-peer. So the webrtc package now has a genuinely working call MVP with no Rust/media engine. - Signaling\RelaySignalingHandler: a blax-software/laravel-ws MessageHandler that relays between browsers — welcome/join/joined/peer-joined/relay/broadcast/ peer-left. Peers in the kernel ConnectionRegistry, rooms in RoomManager. - webrtc:serve now runs the relay over a laravel-ws WebSocketServer (browser-usable ws://). require blax-software/laravel-ws. - The engine-backed path (SignalingHandler/SignalingServer + MediaEngine) stays for server-terminated media (recording/AI-bridge/SFU, Str0m = #1051). - Tests 26 / 60 incl a REAL WebSocket welcome round-trip (browser->laravel-ws->relay); verified with laravel-ws + reactphp-kernel resolved from Forgejo. README leads with the browser P2P quickstart + JS example. rel learn-atc #1055 #1051 #1019 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| config | ||
| rust | ||
| src | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| README.md | ||
| composer.json | ||
| phpunit.xml.dist | ||
| pint.json | ||
| test.sh | ||
README.md
Laravel WebRTC
WebRTC for Laravel on the shared reactphp-kernel backbone. A WebSocket signaling relay connects browsers for peer-to-peer calls today (no server-side media needed); when you need the server in the media path — recording, AI bridging, an SFU — a pluggable media engine (a Rust str0m core via ext-php-rs) takes over.
Features
- 📞 Browser-to-browser calls, working today — two browsers
joina room and the server relays their SDP/ICE so they connect peer-to-peer; the audio never touches the server, so no media engine is required - 👥 Rooms & mesh signaling — peer discovery on join,
peer-joined/peer-leftnotifications, targetedrelay, and roombroadcast - 🧩 On the shared kernel — runs on
reactphp-kernelover thelaravel-wsWebSocket transport; one process, one loop, alongside your other realtime servers - 🎙️ Server-side media, when you need it — a pluggable
MediaEngineterminates media for recording / intercept, AI-realtime bridging, and SFU group calls - 🤖 AI realtime, provider hidden — bridge a caller to an external model (e.g. OpenAI Realtime) server-side; the browser only ever talks to you, and the model is a config swap
- 🔌 Pluggable engine —
NullMediaEngine(signaling-only) now;Str0mMediaEngine(Ruststr0mviaext-php-rs, shipped inrust/) for real server-terminated media
Installation
composer require blax-software/laravel-webrtc
php artisan vendor:publish --tag="webrtc-config"
It depends on blax-software/reactphp-kernel + blax-software/laravel-ws, resolved from git.blax.at via the repositories entry in composer.json.
Quick Start
Run the signaling relay (browsers connect here):
php artisan webrtc:serve # ws://127.0.0.1:8090 by default
That's all a peer-to-peer call needs — the server only relays signaling; the browsers exchange audio directly:
const ws = new WebSocket('ws://127.0.0.1:8090')
const pc = new RTCPeerConnection()
let peer
ws.onmessage = async ({ data }) => {
const m = JSON.parse(data)
if (m.type === 'welcome') ws.send(JSON.stringify({ type: 'join', room: 'lobby' }))
if (m.type === 'joined') m.peers.forEach(p => (peer = p, call(p))) // someone's already here → call them
if (m.type === 'peer-joined') peer = m.peer // they'll send us an offer
if (m.type === 'relay') { // SDP / ICE from the other browser
peer = m.from
if (m.data.sdp) { await pc.setRemoteDescription(m.data); if (m.data.type === 'offer') answer() }
if (m.data.candidate) await pc.addIceCandidate(m.data)
}
}
pc.onicecandidate = e => e.candidate && ws.send(JSON.stringify({ type: 'relay', to: peer, data: e.candidate }))
pc.ontrack = e => audioEl.srcObject = e.streams[0]
const send = (data) => ws.send(JSON.stringify({ type: 'relay', to: peer, data }))
async function call() { const o = await pc.createOffer(); await pc.setLocalDescription(o); send(o) }
async function answer() { const a = await pc.createAnswer(); await pc.setLocalDescription(a); send(a) }
Relay protocol (JSON over WebSocket)
← { "type": "welcome", "peer": "<yourId>" } // on connect
→ { "type": "join", "room": "lobby" }
← { "type": "joined", "room": "lobby", "peers": ["<id>", ...] } // who's already here
← { "type": "peer-joined", "room": "lobby", "peer": "<id>" } // sent to the others
→ { "type": "relay", "to": "<peerId>", "data": { ...sdp | candidate... } }
← { "type": "relay", "from": "<peerId>", "data": { ... } } // delivered to the target
→ { "type": "broadcast", "data": { ... } } // to the rest of your room
→ { "type": "leave" } ← { "type": "left" } / others get { "type": "peer-left", "peer" }
Server-terminated media (recording / AI bridge / SFU)
When the server must be in the media path, configure a MediaEngine. NullMediaEngine (default) is signaling-only; Str0mMediaEngine is backed by a Rust str0m core via ext-php-rs (see rust/README.md):
WEBRTC_MEDIA_ENGINE="Blax\WebRtc\Media\Str0mMediaEngine"
The engine-backed path uses Signaling\SignalingHandler (offer/ice/record/connect/bridge/close) + Server\SignalingServer; it's independent of the browser relay above and shares the same RoomManager.
Configuration
config/webrtc.php covers the signaling bind (host/port/tls), the media_engine binding, recording disk/path, advertised ice_servers, and the external bridge (e.g. OpenAI model + key). Swapping the realtime model — or the whole provider — is a server-side config change the browser never sees.
Status
Working MVP. Browser-to-browser (mesh) calls work today via the WebSocket signaling relay — no server-side media required. Rooms, peer discovery, targeted relay and broadcast are tested, including a real WebSocket round-trip. Server-terminated media (recording, AI bridge, SFU) is the pluggable MediaEngine; the real one (Str0mMediaEngine + rust/) is the next build.
Architecture
reactphp-kernel shared backbone (loop, IPC, signals)
├─ laravel-ws WebSocket transport (RFC6455)
└─ laravel-webrtc (this)
├─ RelaySignalingHandler browser P2P calls (no media engine needed)
└─ MediaEngine (optional) server-terminated media
└─ Str0mMediaEngine Rust/str0m via ext-php-rs (rust/)
Testing
composer install
composer test
The relay + signaling routers and engines are unit-tested with fakes; a real WebSocket round-trip proves the relay over laravel-ws, and a raw-socket test drives the engine signaling server — no browser or external services required.
License
MIT. See LICENSE.