barivanlab / video-call-meet-laravel-webrtc
Self-hosted, peer-to-peer video meetings for Laravel. Zero external services β WebRTC signaling runs entirely on your own cache. Ships with drop-in routes, Blade views and a framework-free WebRTC client.
Package info
github.com/barivanlab/video-call-meet-laravel-webrtc
pkg:composer/barivanlab/video-call-meet-laravel-webrtc
Requires
- php: ^8.2
- illuminate/cache: ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/routing: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- laravel/pint: ^1.21
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- pestphp/pest: ^3.0 || ^4.0
- phpunit/phpunit: ^11.0 || ^12.0
README
Self-hosted, peer-to-peer video meetings for Laravel β zero external services.
This package adds real-time video/audio calling to any Laravel 11/12/13 application. WebRTC signaling runs entirely on your own cache store, so you need no database, no Redis, no message broker and no third-party signaling provider. It ships with a drop-in web UI (Blade + vanilla JS/CSS) and a clean PHP API you can use to build your own interface.
- π Private by design β media is peer-to-peer (browser β browser); your server only relays tiny signaling messages and never sees the call.
- β‘ Zero infrastructure β signaling state lives in Laravel's cache; works on plain PHP + a shared cache out of the box.
- π΅ High-fidelity audio β a "music mode" disables echo cancellation / noise suppression and captures stereo 48 kHz Opus, ideal for lessons and live performance.
- ποΈ Configurable quality β per-call video resolution (360p β 1080p) and bitrate, switchable live without reconnecting.
- π¦ Standard & convention-driven β PSR-4, auto-discovery, publishable config/views/
assets,
vendor:publish, and aMeetfacade.
Requirements
- PHP
^8.2 - Laravel
^11.0 || ^12.0 || ^13.0 - A cache store reachable by every worker (the default
file/arraycache works for a single server; userediswhen you run multiple web workers/nodes).
TURN for hard NATs: calls between devices on the same network (or behind a normal home router) connect using only STUN. To support restrictive/symmetric NATs you can point the package at your own TURN server (see Configuration).
Installation
Require the package via Composer β Laravel's package auto-discovery will register the
service provider and the Meet facade automatically:
composer require barivanlab/video-call-meet-laravel-webrtc
Publish the assets
The bundled UI (CSS/JS) and the Blade views are not served from the package by default β publish them into your app so they live alongside your own assets:
# Publish everything (config + views + assets) php artisan vendor:publish --tag=meet # β¦or publish selectively php artisan vendor:publish --tag=meet-config # config/meet.php php artisan vendor:publish --tag=meet-views # resources/views/vendor/meet php artisan vendor:publish --tag=meet-assets # public/vendor/meet/{css,js}
After publishing, the client script and stylesheet are available at
/vendor/meet/js/meet.js and /vendor/meet/css/app.css.
Optional: TURN server
If you operate a TURN server (e.g. coturn), expose it through environment variables:
MEET_TURN_URL=turn:turn.example.com:3478 MEET_TURN_USERNAME=meet MEET_TURN_CREDENTIAL=secret
Leave them unset to use STUN-only (sufficient for most calls).
Usage
Once installed and published, the UI is immediately available:
| Action | URL |
|---|---|
| Landing page (join / new meeting) | /meet |
| Start a new meeting | /meet/create |
| Open a room | /meet/{room} |
Open /meet, type a room code (or click New meeting) and share the room URL. That's
it β no controllers, models or migrations to wire up.
Routes
| Method | URI | Purpose |
|---|---|---|
GET |
/meet |
Landing page |
GET |
/meet/create |
Create a room and redirect into it |
GET |
/meet/{room} |
Room page |
POST |
/api/meet/join |
Register a participant |
POST |
/api/meet/signal |
Send an SDP offer/answer, ICE candidate or media state |
GET |
/api/meet/poll |
Short-poll for messages addressed to you |
POST |
/api/meet/heartbeat |
Refresh presence |
POST |
/api/meet/leave |
Leave a room |
GET |
/api/meet/peers |
List current participants |
Everything is wrapped in the web middleware (so CSRF + session apply). The signaling
client sends the CSRF token on every POST.
Using the API directly (headless)
You can call the signaling service from your own code via the Meet facade or the
SignalingServiceContract binding:
use Barivanlab\VideoCallMeet\Facades\Meet; // Register a participant $join = Meet::join('team-standup', 'Alice'); // ['peer_id' => 'β¦', 'peers' => [['id' => 'β¦', 'name' => 'Bob']]] // Deliver a signaling message (offer / answer / candidate / state) Meet::signal('team-standup', $fromPeerId, $toPeerId, 'offer', $sdpPayload); // Collect messages for a peer since a sequence number $messages = Meet::poll('team-standup', $peerId, $lastSequence);
The contract is available for dependency injection:
use Barivanlab\VideoCallMeet\Contracts\SignalingServiceContract; public function join(SignalingServiceContract $meet) { return $meet->join('team-standup', 'Alice'); }
Configuration
All behaviour is driven by config/meet.php (published via --tag=meet-config):
| Key | Env var | Default | Description |
|---|---|---|---|
ice_servers |
MEET_TURN_URL, MEET_TURN_USERNAME, MEET_TURN_CREDENTIAL |
Google STUN | ICE servers offered to peers |
routes.enabled |
MEET_ROUTES_ENABLED |
true |
Set false to use the package as a pure library (no routes/views) |
routes.web_prefix |
MEET_WEB_PREFIX |
meet |
Prefix for the web UI |
routes.api_prefix |
MEET_API_PREFIX |
api/meet |
Prefix for the JSON signaling API |
cache_store |
MEET_CACHE_STORE |
null (app default) |
Cache store used for meeting state |
peer_timeout |
MEET_PEER_TIMEOUT |
20 |
Seconds without a heartbeat before a peer is evicted |
signal_ttl |
MEET_SIGNAL_TTL |
60 |
Seconds a signaling message is kept before GC |
lock_path |
MEET_LOCK_PATH |
storage/.../meet-locks |
Directory for advisory file locks |
Customizing the UI
The Blade views are published to resources/views/vendor/meet/ and use the meet::
namespace, so you can override them exactly like any Laravel vendor view. Edit
home.blade.php / room.blade.php, or point your own views at the same routes.
The client reads everything it needs from <meta> tags injected by the room view:
roomβ the room codecsrf-tokenβ CSRF protectionpeer-nameβ the participant's display nameapi-baseβ the signaling API prefix (kept in sync withconfig('meet.routes.api_prefix'))ice-serversβ the JSON-encoded ICE server list fromconfig('meet.ice_servers')
You only need to touch meet.js/app.css if you want to change the client behaviour or
styling β they are published under public/vendor/meet/.
How it works
Browser A βββ βββ Browser B
(WebRTC) β Laravel cache β (WebRTC)
offer βββββΌβββΊ join / signal ββββββΌβββΊ poll ββββΊ answer / ICE
ICE ββββββΌβββ poll / signal βββββΌββββ offer
βββ (no media, just βββ
tiny JSON messages) media is P2P,
never on server
- A participant
joins a room; the server stores their peer id + name in the cache and broadcasts apeer-joinedevent. - Each peer opens an
RTCPeerConnectionandPOSTs its SDP offer/answer and ICE candidates to/api/meet/signal. - Peers
GET/api/meet/pollevery ~400 ms to collect any messages addressed to them. pollalso refreshes presence; peers that stop heartbeating withinpeer_timeoutseconds are evicted and apeer-leftevent is broadcast.
Because each room's peer list is mutated behind an advisory file lock, the signaling state stays consistent even under PHP's single-threaded request model.
Testing
The package ships with a Pest + Orchestra Testbench suite:
composer install composer test # runs pest composer format # runs laravel/pint
License
Released under the MIT License.