Yoram Kornatzky
Posted on August 5, 2022
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 Auction Events Platform for Creators
The platform allows creators to run a live auction which is 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 Architecture
The platform is a web application built with the TALL stack:
LiveKit
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.
Running LiveKit
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>
To interact with LiveKit in any way, such as to create a room or connect to a room, participants need to get an access token. An access token carries certain permissions, such as the ability to create rooms, subscribe to tracks, and publish tracks.
Using LiveKit for Live Streaming of Auctions
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
- On the server side - PHP Server SDK to LiveKit
- On the client side - LiveKit browser client SDK (javascript)
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.
Issue Access Tokens
We have two types of users:
- Creators that publish video and/or audio
- Bidders that subscribe to the video and/or audio
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();
Create and Delete Rooms
On the server side, using the PHP SDK,
Create Room
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);
Delete Room
$host = env('LIVEKIT_URL');
$svc = new RoomServiceClient($host);
$rsp = $svc->deleteRoom($room_name);
Webhooks
We use webhooks to listen to events in the room, such as participants joining and leaving, to control the room capacity.
Routing
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');
Processing the Call
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':
}
}
}
Front-End
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 container of the video and audio elements 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 assure that the component is rendered before we send the event to Livewire. In this way, when Livewire dispatches an event to Alpine.js, the listener is active in the HTML.
Connecting to A Room in JavaScript
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;
}
Publishing a Video
await this.currentRoom?.localParticipant.setCameraEnabled(true);
const videoElm = this.$refs.video;
this.videoTrack = await createLocalVideoTrack();
this.videoTrack?.attach(videoElm);
Subscribing to a Track
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');
}
}
},
It was Tricky
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.
Posted on August 5, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.