Skip to content

Commit f78c7f6

Browse files
committed
browser-emulator: reimplement livekit cli emulated browsers using sockets instead of ffmpeg encoding
This should mean a lot better performance without the transcoding, but requires specific files for it
1 parent e91ca00 commit f78c7f6

22 files changed

Lines changed: 1393 additions & 338 deletions

browser-emulator/docker-compose.dev.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ services:
3434
- ./logs:/app/logs
3535
- ./mediafiles:/app/mediafiles
3636
- /var/run/docker.sock:/var/run/docker.sock
37+
- /tmp/openvidu-loadtest:/tmp/openvidu-loadtest
3738
depends_on:
3839
- livekit-server
3940
# Override the container command to enable optional debugging

browser-emulator/docker-compose.test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ services:
3434
- ./logs:/app/logs
3535
- ./mediafiles:/app/mediafiles
3636
- /var/run/docker.sock:/var/run/docker.sock
37+
- /tmp/openvidu-loadtest:/tmp/openvidu-loadtest
3738
# Override the container command to enable optional debugging
3839
# When DOCKER_DEBUG=true the app starts with the Node inspector
39-
command: sh -lc "if [ \"${DOCKER_DEBUG:-false}\" = \"true\" ]; then pnpm run test --inspect-brk=0.0.0.0:9229 --no-file-parallelism tests/e2e/emulated-browsers.test.ts; else pnpm run test:coverage; fi"
40+
command: sh -lc "if [ \"${DOCKER_DEBUG:-false}\" = \"true\" ]; then pnpm run test:e2e --inspect-brk=0.0.0.0:9229 --no-file-parallelism; else pnpm run test:e2e; fi"
4041
networks:
4142
browseremulator:
4243
name: browseremulator

browser-emulator/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ services:
3030
- ./logs:/app/logs
3131
- ./mediafiles:/app/mediafiles
3232
- /var/run/docker.sock:/var/run/docker.sock
33+
- /tmp/openvidu-loadtest:/tmp/openvidu-loadtest
3334

3435
networks:
3536
browseremulator:

browser-emulator/mediafile_generation/generate-mediafiles-qoe.sh

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,32 @@
44
# DEFAULT VALUES
55
##################################################################################
66

7-
VIDEO_SAMPLE_URL=https://archive.org/download/e-dv548_lwe08_christa_casebeer_003.ogg/e-dv548_lwe08_christa_casebeer_003.mp4
7+
# Big Buck Bunny is the default source video
8+
BUNNY_ZIP_URL="https://download.blender.org/demo/movies/BBB/bbb_sunflower_1080p_60fps_normal.mp4.zip"
9+
BUNNY_ZIP_FILE="bbb_sunflower_1080p_60fps_normal.mp4.zip"
10+
BUNNY_VIDEO_FILE="bbb_sunflower_1080p_60fps_normal.mp4"
11+
INTERVIEW_VIDEO_URL="https://archive.org/download/e-dv548_lwe08_christa_casebeer_003.ogg/e-dv548_lwe08_christa_casebeer_003.mp4"
12+
13+
VIDEO_SAMPLE_URL="$BUNNY_VIDEO_FILE" # Default to bunny (will be downloaded if needed)
814
WIDTH=640
915
HEIGHT=480
10-
VIDEO_DURATION=00:00:30
16+
VIDEO_DURATION=00:00:05
17+
START_POSITION=00:00:52
1118
PADDING_DURATION_SEC=1
1219
AUDIO_SAMPLE_RATE_HZ=48000
1320
TONE_FREQUENCY_HZ=1000
1421
AUDIO_CHANNELS_NUMBER=2
1522
FPS=30
23+
USE_BUNNY=true # Bunny is default
1624
FFMPEG_LOG="-loglevel error"
17-
TARGET_VIDEO=./test.y4m
18-
TARGET_AUDIO=./test.wav
1925
GENERATE_DEFAULT_REF=false
2026
DEFAULT_VIDEO_REF=./test-no-padding.yuv
2127
DEFAULT_AUDIO_REF=./test-no-padding.wav
2228
FONT=./arial.ttf
2329
CLEANUP=true
2430
GET_ORIGINAL_STATS=false
25-
USAGE="Usage: `basename $0` [-d=duration] [-p=padding_duration_sec] [--game] [--generate_default_ref] [--no_cleanup] [--clean] [-v=video_url] [-w=width] [-h=height] [-a=audio_url] [-f=fps] [--original]"
31+
GENERATE_STREAMING=true
32+
USAGE="Usage: $(basename $0) [-d=duration] [-p=padding_duration_sec] [--game] [--interview] [--generate_default_ref] [--no_cleanup] [--clean] [--no-streaming] [-v=video_url] [-w=width] [-h=height] [-a=audio_url] [-f=fps] [--original]"
2633

2734
##################################################################################
2835
# FUNCTIONS
@@ -120,6 +127,16 @@ for i in "$@"; do
120127
case $i in
121128
--game)
122129
VIDEO_SAMPLE_URL=https://ia802808.us.archive.org/6/items/ForniteBattle8/fornite%20battle%202.mp4
130+
USE_BUNNY=false
131+
VIDEO_DURATION=00:00:30
132+
START_POSITION=00:00:00
133+
shift
134+
;;
135+
--interview)
136+
VIDEO_SAMPLE_URL="$INTERVIEW_VIDEO_URL"
137+
USE_BUNNY=false
138+
VIDEO_DURATION=00:00:15
139+
START_POSITION=00:00:00
123140
shift
124141
;;
125142
--generate_default_ref)
@@ -143,6 +160,10 @@ for i in "$@"; do
143160
exit 0
144161
shift
145162
;;
163+
--no-streaming)
164+
GENERATE_STREAMING=false
165+
shift
166+
;;
146167
-v=*)
147168
VIDEO_SAMPLE_URL="${i#*=}"
148169
shift
@@ -178,17 +199,63 @@ done
178199
# INIT
179200
##################################################################################
180201

202+
# Download Big Buck Bunny if it's the source and doesn't exist yet
203+
if [ "$USE_BUNNY" = true ]; then
204+
# Download zip if not exists
205+
if [ ! -f "$BUNNY_ZIP_FILE" ] && [ ! -f "$BUNNY_VIDEO_FILE" ]; then
206+
echo "Downloading Big Buck Bunny video from blender.org..."
207+
if command -v curl &> /dev/null; then
208+
curl -L -o "$BUNNY_ZIP_FILE" "$BUNNY_ZIP_URL" --progress-bar
209+
elif command -v wget &> /dev/null; then
210+
wget -O "$BUNNY_ZIP_FILE" "$BUNNY_ZIP_URL"
211+
else
212+
echo "Error: curl or wget required"
213+
exit 1
214+
fi
215+
fi
216+
217+
# Unzip if video file doesn't exist
218+
if [ ! -f "$BUNNY_VIDEO_FILE" ]; then
219+
echo "Extracting Big Buck Bunny video from zip..."
220+
unzip -o "$BUNNY_ZIP_FILE"
221+
fi
222+
223+
VIDEO_SAMPLE_URL="$BUNNY_VIDEO_FILE"
224+
fi
225+
226+
# Determine media type from source URL
227+
case "$VIDEO_SAMPLE_URL" in
228+
*christa_casebeer*|*interview*)
229+
MEDIA_TYPE="interview"
230+
;;
231+
*ForniteBattle*)
232+
MEDIA_TYPE="game"
233+
;;
234+
*bbb_sunflower*|*BBB*)
235+
MEDIA_TYPE="bunny"
236+
;;
237+
*)
238+
MEDIA_TYPE="custom"
239+
;;
240+
esac
241+
242+
# Output filenames using consistent naming: {type}_{height}p_{fps}fps.{extension}
243+
BASE_NAME="${MEDIA_TYPE}_${HEIGHT}p_${FPS}fps"
244+
TARGET_VIDEO="./${BASE_NAME}.y4m"
245+
TARGET_AUDIO="./${MEDIA_TYPE}.wav"
246+
STREAMING_VIDEO="./${BASE_NAME}.h264"
247+
STREAMING_AUDIO="./${MEDIA_TYPE}.ogg"
181248

182249
##########################
183250
# 1. Download video sample
184251
##########################
185252
VIDEO_SAMPLE_NAME=$(echo ${VIDEO_SAMPLE_URL##*/} | sed -e 's/%20/ /g')
186253

187254
if [ ! -f "$VIDEO_SAMPLE_NAME" ]; then
188-
echo "Content video ($VIDEO_SAMPLE_NAME) not exits ... downloading"
255+
echo "Content video ($VIDEO_SAMPLE_NAME) not exists ... downloading"
189256
wget $VIDEO_SAMPLE_URL
190257
else
191-
echo "Content video ($VIDEO_SAMPLE_NAME) already exits"
258+
echo "Content video ($VIDEO_SAMPLE_NAME) already exists"
192259
fi
193260
if [ -z "$FPS" ]; then
194261
FPS=$(ffprobe -v error -select_streams v -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 "$VIDEO_SAMPLE_NAME")
@@ -208,7 +275,7 @@ fi
208275
#######################
209276
if [ "$GET_ORIGINAL_STATS" = false ]; then
210277
echo "Cutting original video (duration $VIDEO_DURATION)"
211-
ffmpeg $FFMPEG_LOG -y -i "$VIDEO_SAMPLE_NAME" -ss 00:00:00 -t $VIDEO_DURATION -vf scale="$WIDTH:$HEIGHT",setsar=1:1 -r $FPS test-no-frame-number.mp4
278+
ffmpeg $FFMPEG_LOG -y -i "$VIDEO_SAMPLE_NAME" -ss $START_POSITION -t $VIDEO_DURATION -vf scale="$WIDTH:$HEIGHT",setsar=1:1 -r $FPS test-no-frame-number.mp4
212279
OUTPUT="test-no-frame-number.mp4"
213280
fi
214281

@@ -349,7 +416,59 @@ if $GENERATE_DEFAULT_REF; then
349416
fi
350417

351418
################################
352-
# 9. Delete temporal video files
419+
# 9. Generate streaming files (H.264 + Opus Ogg)
420+
################################
421+
if $GENERATE_STREAMING; then
422+
echo "=========================================="
423+
echo "Generating streaming media files for LiveKit"
424+
echo "=========================================="
425+
426+
# Get FPS as integer for keyint calculation
427+
FPS_INT=$(echo "$FPS" | awk '{if ($1 ~ /\//) {split($1, a, "/"); print a[1]/a[2]} else {print $1}}' | awk '{printf "%d", $1}')
428+
if [ -z "$FPS_INT" ] || [ "$FPS_INT" -le 0 ]; then
429+
FPS_INT=30
430+
fi
431+
432+
# Generate H.264 Annex B video from test.mp4 (same content as QoE Y4M)
433+
echo "Generating H.264 video ($STREAMING_VIDEO)..."
434+
ffmpeg $FFMPEG_LOG -y -i test.mp4 \
435+
-c:v libx264 -preset veryfast -profile:v baseline \
436+
-pix_fmt yuv420p \
437+
-x264-params "keyint=$((FPS_INT * 4)):min-keyint=$((FPS_INT * 4))" \
438+
-bf 0 -b:v 2M \
439+
-an \
440+
-f h264 \
441+
"$STREAMING_VIDEO"
442+
443+
echo " -> $(ls -lh "$STREAMING_VIDEO" 2>/dev/null | awk '{print $5}' || echo 'not found')"
444+
445+
# Generate Opus Ogg audio from test-audio-raw.wav (same content as QoE WAV)
446+
echo "Generating Opus audio ($STREAMING_AUDIO)..."
447+
ffmpeg $FFMPEG_LOG -y -i test-audio-raw.wav \
448+
-c:a libopus -page_duration 20000 \
449+
-ar $AUDIO_SAMPLE_RATE_HZ -ac $AUDIO_CHANNELS_NUMBER \
450+
-f opus \
451+
"$STREAMING_AUDIO"
452+
453+
echo " -> $(ls -lh "$STREAMING_AUDIO" 2>/dev/null | awk '{print $5}' || echo 'not found')"
454+
455+
echo ""
456+
echo "Streaming files generated:"
457+
echo " Video: $(pwd)/$STREAMING_VIDEO"
458+
echo " Audio: $(pwd)/$STREAMING_AUDIO"
459+
echo ""
460+
echo "These files are compatible with LiveKit CLI's h264:// and opus:// protocols."
461+
echo "=========================================="
462+
463+
echo ""
464+
echo "=========================================="
465+
echo "All generated files:"
466+
ls -lh ./${BASE_NAME}.* 2>/dev/null | awk '{print " " $NF " (" $5 ")"}'
467+
echo "=========================================="
468+
fi
469+
470+
################################
471+
# 10. Delete temporal video files
353472
################################
354473
if $CLEANUP; then
355474
cleanup

browser-emulator/mediafile_generation/upload-to-s3.sh

100644100755
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ $AWS_CLI sts get-caller-identity >/dev/null 2>&1 ||
5050
# --- Main ---------------------------------------------------------------------
5151

5252
# Find .y4m and .wav files in the current directory (non-recursive), safely handling spaces
53-
mapfile -d '' FILES < <(find . -maxdepth 1 -type f \( -iname "*.y4m" -o -iname "*.wav" \) -print0)
53+
mapfile -d '' FILES < <(find . -maxdepth 1 -type f \( -iname "*.y4m" -o -iname "*.wav" -o -iname "*.h264" -o -iname "*.ogg" \) -print0)
5454

5555
if [[ ${#FILES[@]} -eq 0 ]]; then
56-
echo "No .y4m or .wav files found in the current directory."
56+
echo "No valid files found in the current directory."
5757
exit 0
5858
fi
5959

browser-emulator/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async function cleanup() {
2020
const emulatedFilePublishStreamService = container.resolve(
2121
'emulatedFilePublishStreamService',
2222
);
23+
const socketWriterService = container.resolve('socketWriterService');
2324
try {
2425
await browserManager.clean();
2526
} catch (err) {
@@ -36,6 +37,7 @@ async function cleanup() {
3637
scriptRunnerService.killAllDetached(),
3738
instanceService.removeMetricBeat(),
3839
emulatedFilePublishStreamService.stopPublishing(),
40+
socketWriterService.stopAllWriters(),
3941
]);
4042
console.log('Cleanup finished');
4143
}

browser-emulator/src/container.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { FakeMediaDevicesService } from './services/fake-media/fake-media-device
2828
import { QoeAnalyzerService } from './services/qoe-analysis/qoe-analyzer.service.ts';
2929
import { QoeCommandRunner } from './services/qoe-analysis/qoe-command-runner.ts';
3030
import { EmulatedFilePublishStreamService } from './services/browser/emulated/emulated-file-publish-stream.service.ts';
31+
import { SocketWriterService } from './services/streaming/socket-writer.service.ts';
32+
import { SocketWriterHealthService } from './services/streaming/socket-writer-health.service.ts';
3133

3234
// Define the container interface for type safety
3335
export interface DIContainer {
@@ -36,6 +38,8 @@ export interface DIContainer {
3638
realBrowserService: RealBrowserService;
3739
emulatedBrowserService: EmulatedBrowserService;
3840
emulatedFilePublishStreamService: EmulatedFilePublishStreamService;
41+
socketWriterService: SocketWriterService;
42+
healthService: SocketWriterHealthService;
3943
instanceService: InstanceService;
4044
elasticSearchService: ElasticSearchService;
4145
wsService: WsService;
@@ -95,6 +99,8 @@ export async function configureContainer(): Promise<
9599
realBrowserService: asClass(RealBrowserService).singleton(),
96100
emulatedBrowserService: asClass(EmulatedBrowserService).singleton(),
97101
browserManagerService: asClass(BrowserManagerService).singleton(),
102+
socketWriterService: asClass(SocketWriterService).singleton(),
103+
healthService: asClass(SocketWriterHealthService).singleton(),
98104
emulatedFilePublishStreamService: asClass(
99105
EmulatedFilePublishStreamService,
100106
).singleton(),

browser-emulator/src/repositories/files/local-files.repository.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export class LocalFilesRepository {
1515

1616
private _fakevideo: string | undefined;
1717
private _fakeaudio: string | undefined;
18+
private _fakevideoStreaming: string | undefined;
19+
private _fakeaudioStreaming: string | undefined;
1820

1921
constructor() {
2022
this.createNeededDirectories();
@@ -28,6 +30,38 @@ export class LocalFilesRepository {
2830
return this._fakeaudio;
2931
}
3032

33+
/**
34+
* Get path to H.264 streaming video file (for Unix socket streaming)
35+
*/
36+
public get fakevideoStreaming(): string | undefined {
37+
return this._fakevideoStreaming;
38+
}
39+
40+
/**
41+
* Get path to Opus Ogg streaming audio file (for Unix socket streaming)
42+
*/
43+
public get fakeaudioStreaming(): string | undefined {
44+
return this._fakeaudioStreaming;
45+
}
46+
47+
/**
48+
* Check if streaming media files exist
49+
*/
50+
public async existStreamingMediaFiles(): Promise<boolean> {
51+
if (!this._fakevideoStreaming || !this._fakeaudioStreaming) {
52+
return false;
53+
}
54+
try {
55+
await Promise.all([
56+
fsPromises.access(this._fakevideoStreaming, fs.constants.F_OK),
57+
fsPromises.access(this._fakeaudioStreaming, fs.constants.F_OK),
58+
]);
59+
return true;
60+
} catch {
61+
return false;
62+
}
63+
}
64+
3165
private createNeededDirectories() {
3266
const requiredDirs = [
3367
LocalFilesRepository.FULLSCREEN_RECORDING_DIR,
@@ -56,6 +90,22 @@ export class LocalFilesRepository {
5690
return filePaths;
5791
}
5892

93+
public async downloadStreamingFiles(
94+
videoFile: string,
95+
videoUrl: string,
96+
audioFile: string,
97+
audioUrl: string,
98+
): Promise<string[]> {
99+
const filePaths = await Promise.all([
100+
this.downloadFile(videoFile, videoUrl),
101+
this.downloadFile(audioFile, audioUrl),
102+
]);
103+
this._fakevideoStreaming = filePaths[0];
104+
this._fakeaudioStreaming = filePaths[1];
105+
106+
return filePaths;
107+
}
108+
59109
private async downloadFile(name: string, fileUrl: string): Promise<string> {
60110
const filePath = LocalFilesRepository.MEDIAFILES_DIR + '/' + name;
61111
try {

0 commit comments

Comments
 (0)