[![Blax Software OSS](https://raw.githubusercontent.com/blax-software/laravel-workkit/master/art/oss-initiative-banner.svg)](https://github.com/blax-software) # Laravel WebRTC [![PHP Version](https://img.shields.io/badge/php-%5E8.1-blue?style=flat-square)](https://php.net) [![Laravel](https://img.shields.io/badge/laravel-10.x--12.x-orange?style=flat-square)](https://laravel.com) [![Built on](https://img.shields.io/badge/on-reactphp--kernel-4b275f?style=flat-square)](https://github.com/blax-software/reactphp-kernel) [![Tests](https://img.shields.io/badge/tests-26%20passing-success?style=flat-square)](#testing) [![Assertions](https://img.shields.io/badge/assertions-60-blue?style=flat-square)](#testing) [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE) WebRTC for Laravel on the shared [`reactphp-kernel`](https://github.com/blax-software/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 `join` a 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-left` notifications, targeted `relay`, and room `broadcast` - 🧩 **On the shared kernel** — runs on `reactphp-kernel` over the [`laravel-ws`](https://github.com/blax-software/laravel-ws) WebSocket transport; one process, one loop, alongside your other realtime servers - 🎙️ **Server-side media, when you need it** — a pluggable `MediaEngine` terminates 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` (Rust `str0m` via `ext-php-rs`, shipped in `rust/`) for real server-terminated media ## Installation ```bash composer require blax-software/laravel-webrtc php artisan vendor:publish --tag="webrtc-config" ``` It depends on [`blax-software/reactphp-kernel`](https://github.com/blax-software/reactphp-kernel) + [`blax-software/laravel-ws`](https://github.com/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): ```bash 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: ```js 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) ```jsonc ← { "type": "welcome", "peer": "" } // on connect → { "type": "join", "room": "lobby" } ← { "type": "joined", "room": "lobby", "peers": ["", ...] } // who's already here ← { "type": "peer-joined", "room": "lobby", "peer": "" } // sent to the others → { "type": "relay", "to": "", "data": { ...sdp | candidate... } } ← { "type": "relay", "from": "", "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`](rust/README.md)): ```dotenv 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 ```bash 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](LICENSE). ## Star History Star History Chart