php-junior/laravel-video-chat
Laravel Video Chat adds one-to-one WebRTC video calling to Laravel apps. It handles signaling through Laravel events and Echo, includes ready-made Vue call components, and lets you add real-time peer-to-peer video chat without building the WebRTC plumbing yourself.
The package is a thin Laravel wrapper around a WebRTC client and a Socket.IO signaling server. It gives you a working /video-chat page out of the box; treat it as a reference implementation you will fork and harden rather than a drop-in production feature.
Minimal path:
composer require php-junior/laravel-video-chatPHPJunior\VideoChat\VideoChatServiceProvider in config/app.php if you are on a Laravel version that does not auto-discover it (the package predates 5.5 conventions in places).php artisan vendor:publish --provider="PHPJunior\VideoChat\VideoChatServiceProvider"socket_url / host+port entry, often fed from VIDEO_CHAT_SOCKET_URL).server.js in the package, per the README) with npm install && node server.js.php artisan serve, open http://localhost:8000/video-chat in two browser tabs, grant camera/mic, and confirm the offer/answer exchange.Where to look first:
README.md — the only reliable source for the exact route, config keys and server entrypoint, since the package is unmaintained.config/videochat.php (published) — socket URL, port, and any ICE server config.routes/ inside the package — confirms the route name, URI and whether middleware is applied.resources/views/ — the Blade template that boots the WebRTC client.server.js — the signaling contract (event names, room semantics) you will need to extend.public/vendor/... or bundled in the view) — this is where RTCPeerConnection, getUserMedia and socket.emit calls live.First real use case: a two-party, authenticated room inside an existing app. Get the raw package page working first on localhost, then immediately replace the public route with an auth-gated one before wiring it into a product.
Drop-in page vs. embedded widget. The fastest integration is to keep the package route for internal demos and iframe it inside your app layout when you need isolation from your CSS/JS. For anything customer-facing, publish the views and re-implement them inside your own layout — the bundled Blade is minimal and you will want your own chrome (participant list, mute/camera toggles, hang-up button).
Auth-gated rooms. The default route is effectively public. Wrap it with auth (and any tenant/authorization middleware) and generate a room identifier server-side instead of trusting a URL segment. A simple pattern:
GET /rooms/{room} → controller checks membership → renders the video view with a signed room token.Room model. Add a video_rooms table (or reuse an existing conversation/meeting model) so rooms have owners, participants and expiry. Use the model's UUID as the room id — this avoids guessable/incrementing room names and gives you a place to hang recording metadata, transcripts, etc.
Signaling contract. Expect to fork server.js. Typical extensions: emit a participants list on join, broadcast peer-joined / peer-left, add a signal passthrough (offer/answer/ICE), and namespace events per room (socket.join(room) in Socket.IO). Keep the server stateless per-connection except for the room map; store durable state in Laravel via events if you need call history.
ICE configuration. Configure STUN and TURN in the client RTCPeerConnection config. STUN alone works on LAN and friendly NATs; a large fraction of real users need TURN. Run coturn (or a hosted TURN provider) and inject credentials from config rather than hardcoding them in the published JS.
Deployment topology. Three moving parts: PHP-FPM/Nginx for Laravel, a long-running Node process for signaling, and a TURN server for media relay. Supervise Node with systemd/supervisor, and make sure your reverse proxy forwards WebSocket upgrades:
location /socket.io/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
Serve the app over HTTPS and the socket over wss:// in production — getUserMedia is blocked on insecure origins except localhost.
Extending the client. The published JS is the extension point for screen sharing (getDisplayMedia), device switching (enumerateDevices), mute/camera toggles (track.enabled), and renegotiation. Keep these as small composable modules rather than editing the vendor file in place, so re-publishing does not destroy your work.
Scaling shape. Default topology is mesh: every participant connects to every other participant. This is fine for 2–4 people and collapses beyond that (upload bandwidth). For larger rooms you need an SFU (Janus, mediasoup, LiveKit) — the package will not get you there; plan the migration before you need it.
server.js into your app and drop the dependency entirely. The latter is usually the right call — the amount of useful code is small.php artisan route:list shows the video-chat route.config:cache and env(). Config values read via env() inside the package config are fine, but once cached, changing .env has no effect until php artisan config:clear. This bites people who rotate the socket URL or TURN credentials.vendor:publish --force overwrites your edits. If you customized published views or JS, keep them in your own resources/ and public/ paths instead of the vendor-published locations.server.js declares in package.json.chrome://webrtc-internals and confirm ICE candidate pairs succeed; if only host/srflx candidates appear, add TURN.NotAllowedError / NotFoundError from getUserMedia with a real UI message — users deny camera permission more often than you expect. Safari also enforces stricter autoplay and requires a user gesture to start media in some flows.origin in the Socket.IO server config; the default is often permissive. Combine with token verification on the handshake (socket.handshake.auth) so the server is not an open relay.socket.on('connect_error'), chrome://webrtc-internals for ICE/bitrate graphs, node server.js stdout for signaling events, and tcpdump/TURN logs when media never flows. Reproduce two-party failures with two different networks (phone hotspot + Wi-Fi), never two tabs on the same machine.server.js), write your own controller and route, and keep the composer dependency only for the signaling event names you copied. The dependency itself contributes little beyond scaffolding.How can I help you explore Laravel packages today?