Frontend Overview
Laravel Echo set the pattern: PHP authorises a private channel, the browser subscribes with one line, and every framework wraps that same line in its own idiom. Beam's client is one framework-free class, @realrashid/beam, and each adapter is about forty lines around it.
| Frontend | Import | The two lines |
|---|---|---|
| Blade | @beamAssets → window.Beam, or import Beam from '@realrashid/beam' | const call = await Beam.join(token); call.attach({ local: '#me', remote: '#them' }); |
| jQuery | @realrashid/beam/jquery → $.beam | $.beam.join(token).then(call => call.attach({ local: $('#me'), remote: $('#them') })) |
| Livewire | the class, through $wire.on | $wire.on('beam:join', ({ token }) => Beam.join(token).then(…)) |
| Inertia + Vue | @realrashid/beam/vue → useBeam | const { local, remote, state, hangUp } = useBeam(props.token) |
| Inertia + React | @realrashid/beam/react → useBeam | const { local, remote, state, hangUp } = useBeam(token) |
Getting the client onto the page
Vite — npm install @realrashid/beam and import; the package ships ESM with types for the class and each adapter.
No build step — @beamAssets in <head> serves the compiled client from the Composer package (versioned, cached for a year, refetched after an update) and prints the meta tags Beam reads: the signed-in user's id, the route prefix and the CSRF token. It exposes window.Beam, installs $.beam when jQuery is on the page, and prints no ICE servers and no keys.
Either way Beam needs Laravel Echo on window.Echo (what php artisan install:broadcasting sets up in resources/js/echo.js), or Beam.configure({ echo }).
Beam.configure({ echo, userId, csrf, prefix }); // each optional; the defaults come from Echo and the meta tagsThe class, in one screen
const call = await Beam.join(token, { media: { video: true, audio: true } }); // media: false for a data-only connection
call.attach({ local: '#me', remote: '#them' }); // a selector, an element, or a jQuery object
call.state; // 'ringing' | 'connecting' | 'connected' | 'reconnecting' | 'ended'
call.on('state' | 'connected' | 'reconnecting' | 'ended' | 'error' | 'quality' | 'tick' | 'muted' | 'camera' | 'sharing' | 'remotetrack', fn);
call.mute(); call.unmute(); call.toggleMute();
call.cameraOff(); call.cameraOn(); await call.switchCamera();
await call.shareScreen(); await call.stopSharing();
await call.setSpeaker(deviceId);
await call.hangUp();
Beam.onIncoming((incoming) => incoming.answer() | incoming.decline());
Beam.send(file).to(userId); Beam.onFileOffered((offer) => offer.accept() | offer.decline());
await Beam.push.subscribe();
await Beam.answerFromNotification();
Beam.platform; // isSafari, isIOS, isAndroid, isFirefox, supportsDisplayMedia(), supportsPush(), supportsSetSinkId(), …Rules that hold in every frontend
- A call starts from a click. Browsers refuse to open a camera without a user gesture, and refuse to play sound without one on Safari. Do not
join()on page load; do it in the handler of the button that was pressed. - The remote
<video>isautoplay playsinline; the local one is alsomuted. Beam sets these onattach(), but writing them in the markup avoids a flash. - Let go with
call.release(), nothangUp(). In a component that joins, release on unmount: two mounts of the same token share one call (React's StrictMode makes two), and only the last holder to let go hangs it up.hangUp()ends the call for both sides; that belongs on a button. The Vue and React adapters do this for you. - Handle
reconnecting. It is a banner, not an end;endedwith reasonconnection-lostcomes only afterreconnect_timeout. - The token is the only configuration. Never pass ICE servers or channel names to the client; they are in the token.
Made with ❤️ from Pakistan