The Auction Events Platform is a Laravel PHP web application streaming live video from an auction event, using the LiveKit Open source WebRTC infrastructure.
The platform allows creators to run a live auction, a hybrid event, with bidders in the auction hall and online. Online bidders watch a live video and audio stream of the auction hall and bid via a web application. An auction event can be purely virtual with no auction hall.
An auction may run for a few hours with items coming up for bidding in sequence. A creator runs the auction, streaming it from a camera and microphone attached to the creator's computer.
The platform is a web application built with the TALL stack:
LiveKit is an open-source platform for live audio and video. It works by creating rooms in which participants can join and then publish video and audio tracks. Other participants can subscribe to tracks published in the room and stream the track to an HTML video and audio tags.
First, you need to obtain the API key and secret and generate an initial configuration file, livekit.yaml
, by running:
docker run --rm -v$PWD:/output livekit/generate --local
To operate a LiveKit in the simplest way possible, you run a docker container:
docker run --rm -p 7880:7880 \
-p 7881:7881 \
-p 7882:7882/udp \
-v $PWD/livekit.yaml:/livekit.yaml \
livekit/livekit-server \
--config /livekit.yaml \
--node-ip <your local IP>
Participants need to get an access token to interact with LiveKit in any way, such as to create a room or connect to a room. An access token carries certain permissions, such as creating rooms, subscribing to tracks, and publishing tracks.
In the Auction Events Platform for Creators, the platform generates the rooms a few hours before the auction. So the creators can get the setup right. The platform deletes the room a short while after the auction is completed.
Creators publish video and audio, and bidders subscribe to the tracks and watch them.
To integrate LiveKit into our platform, we use the
We use the following LiveKit configuration:
port: 7880
rtc:
udp_port: 7882
tcp_port: 7881
use_external_ip: false
room:
auto_create: false
keys:
xxxxxxx: yyyyyy
logging:
json: true
level: debug
pion_level: debug
webhook:
api_key: xxxxxxxx
urls:
- <our webhooks url>
Since the platform creates the rooms, we set auto_create to false, preventing a participant from creating a room upon joining it.
We have two types of users:
Consequently, we generate using the PHP SDK access token as:
use Agence104\LiveKit\AccessToken;
use Agence104\LiveKit\AccessTokenOptions;
use Agence104\LiveKit\VideoGrant;
// Define the token options.
$token_options = (new AccessTokenOptions())
->setIdentity($participant_name)
->setTtl($key_expiry_seconds);
// Define the video grants.
$video_grant = (new VideoGrant())
->setRoomJoin(true) // TRUE by default.
->setRoomName($room_name)
->setCanPublish($is_creator) // only creators can publish
->setCanSubscribe(true) // TRUE by default.
->setRoomCreate(false);
// Initialize and fetch the JWT access token.
$token = (new AccessToken())
->init($token_options)
->setGrant($video_grant)
->toJwt();
On the server side, using the PHP SDK,
use Agence104\LiveKit\RoomServiceClient;
use Agence104\LiveKit\RoomCreateOptions;
$opts = (new RoomCreateOptions())
->setName($room_name)
->setMaxParticipants(env('MAX_LIVEKIT_ROOM_PARTICIPANTS'))
->setEmptyTimeout(env('LIVEKIT_EMPTY_ROOM_TIMEOUT_SECONDS'));
$room = $svc->createRoom($opts);
$host = env('LIVEKIT_URL');
$svc = new RoomServiceClient($host);
$rsp = $svc->deleteRoom($room_name);
We use webhooks to listen to events in the room, such as participants joining and leaving, to control the room capacity.
The webhook part of the configuration points to a controller action through a webhook.php
file in routes:
use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
use App\Http\Controllers\LiveKitWebhookController;
Route::post('handler', LiveKitWebhookController::class)->name('livekit_webhook');
We have a single action controller using the PHP SDK WebhookReceiver:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Agence104\LiveKit\WebhookReceiver;
class LiveKitWebhookController extends Controller {
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
$content = $request->getContent();
$header = $request->header('authorization');
$receiver = new WebhookReceiver(env('LIVEKIT_API_KEY'), env('LIVEKIT_API_SECRET'));
$data = $receiver->receive($content, $header);
$event = $data->getEvent();
switch($event)
{
case 'participant_joined':
case 'participant_left':
}
}
}
The front-end is a close integration of Livewire components that run on the server and Alpine.js that runs on the browser. Alpine.js integrates the LiveKit Client JS SDK to connect to rooms and publish and subscribe.
We import LiveKit into app.js
:
import {
connect,
createLocalTracks,
createLocalVideoTrack,
createLocalAudioTrack,
Room,
RoomEvent,
RemoteParticipant,
RemoteTrackPublication,
RemoteTrack,
Participant,
RoomOptions,
VideoPresets,
RoomConnectOptions,
ParticipantEvent,
MediaDeviceFailure,
MediaDevicesError,
MediaDevicesChanged,
RoomMetadataChanged,
Track,
VideoQuality,
} from 'livekit-client';
The video and audio elements container sets the LiveKit configuration in the browser in x-data, then initializes the device list and emits a rendered event.
<div class="py-4 bg-white"
x-data="av(true)"
x-init="acquireDeviceList(); $nextTick(() => { Livewire.emit('rendered') });"
>
The Livewire component, on receiving the event, runs code to check permissions and status of the auction and only then gets the access token and LiveKit URL for the connection and sends it to the browser with dispatchBrowserEvent
:
$this->set_user_access_token($this->auction, true);
$this->dispatchBrowserEvent('connect-live', [
'url' => $this->livekit_url,
'token' => $this->livekit_access_token
]);
The Alpine.js, on receiving the connect-live event, connects the room.
@connect-live.window="(event) => connectToRoom(event.detail.url,
event.detail.token, false)"
The connectToRoom works via the Client JS SDK.
Because we need the Livewire component to check certain conditions, such as if there is a live stream, we do not connect to the room automatically. We use nextTick to ensure that the component is rendered before sending the event to Livewire. This way, when Livewire dispatches an event to Alpine.js, the listener is active in the HTML.
async connectToRoom(url, token, forceTURN) {
this.token = token;
this.url = url;
const roomOptions =
this.isPublisher ?
{
adaptiveStream: true,
dynacast: true,
publishDefaults: {
simulcast: true,
videoSimulcastLayers: [
VideoPresets.h90,
VideoPresets.h180,
VideoPresets.h216,
VideoPresets.h360, VideoPresets.h540
],
videoCodec: this.selectedVideoCodec,
},
videoCaptureDefaults: {
resolution: this.selectedVideoResolution.id,
},
}
:
{};
const connectOptions = {
autoSubscribe: true,
publishOnly: undefined,
};
if (forceTURN) {
connectOptions.rtcConfig = {
iceTransportPolicy: 'relay',
};
}
try {
await room.connect(this.url, this.token, connectOptions);
this.currentRoom = room;
window.currentRoom = room;
} catch (error) {
message = error;
if (error.message) {
message = error.message;
}
console.log('could not connect:', message);
} finally {
if (message) {
Livewire.emit('connectionFailure');
}
return room;
}
await this.currentRoom?.localParticipant.setCameraEnabled(true);
const videoElm = this.$refs.video;
this.videoTrack = await createLocalVideoTrack();
this.videoTrack?.attach(videoElm);
Once the track is published, we render it:
renderParticipant(participant) {
const cameraPub = participant.getTrack(Track.Source.Camera);
const micPub = participant.getTrack(Track.Source.Microphone);
if (cameraPub) {
const cameraEnabled = cameraPub && cameraPub.isSubscribed &&
!cameraPub.isMuted;
if (cameraEnabled) {
const videoElm = this.$refs.video;
this.attachTrack(cameraPub?.videoTrack, 'video');
}
}
if (micPub) {
const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
if (micEnabled) {
const audioElm = this.$refs.audio;
this.attachTrack(micPub?.audioTrack, 'audio');
}
}
},
The integration involved a complicated interplay of server and client code. And the client code was an interplay of PHP and JavaScript.
We love LiveKit and hope to carry this into production. So far, it was a single Docker container in a local environment. We have lots of work to do on the production setup.
Also published here.