Skip to main content

How to Run Ghost Client on Raspberry Pi

This guide demonstrates how to start an Eyeson meeting on a Raspberry Pi 5, run a Ghost Client, and stream video from a camera module using FFmpeg to an RTMP server.

Set up

Before running the script, ensure that the Raspberry Pi is up to date and install required libraries for HTTP requests:

sudo apt update && sudo apt upgrade -y
sudo apt install python3-requests

Start a Meeting

To use Eyeson, you must have a valid API key. You can obtain one from the Eyeson API Dashboard. When the script is executed, it prompts for your API key and user name.

In the options:

  • sfu_mode is disabled, allowing you to access MCU mode features even if you are alone in the session.
  • widescreen is set to true, a modern aspect ratio of 16:9.
  • and a custom background_color is chosen.

On a successful run:

  • access_key is returned, enabling modifications to the meeting room while it is running.
  • room.id can be used to forward sources to a WHIP endpoint or to connect drones via Ghost Client.
  • links.guest_join provides an URL to invite participants or to connect drones via Ghost Client.o
  • Finally, links.gui is opened using Python's webbrowser module to simulate an active user session.
eyeson.py
import requests
import json
import webbrowser

key = input("\033[33mEnter your API key: \033[0m").strip()
name = input("\033[33mEnter your name: \033[0m").strip()

address = "https://api.eyeson.team"
headers = {
'Authorization': key,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
payload = {
'user': {
'name': name,
'id': name
},
'options': {
'sfu_mode': 'disabled',
'widescreen': True,
'background_color': '#6900FF'
}
}
try:
response = requests.post(address + '/rooms', headers=headers, json=payload, timeout=100)
response.raise_for_status()
try:
data = response.json()
except json.JSONDecodeError as e:
print(f"\033[31mFailed to decode JSON response: {e}\033[0m")
print(f"\033[33mRaw response: {response.text}\033[0m")
exit(1)

access = data['access_key']
room = data['room']['id']
guest = data['links']['guest_join']
gui = data['links']['gui']

print(f"\n\033[32mEyeson room created!\033[0m")
print(f"Access key: \033[36m{access}\033[0m")
print(f"Room ID: {room}")
print(f"Guest link: \033[35m{guest}\033[0m")
print(f"GUI link: {gui}")
webbrowser.open(gui)
except requests.exceptions.RequestException as e:
print(f"\033[31mRequest failed: {e}\033[0m")
example response
Enter your API key: <YOUR_API_KEY>
Enter your name: <YOUR_NAME> #Observer

Eyeson Room Created!
Access key: m4SXo1jyAUsrxDrElcGK2hkU
Room ID: 69394444df3181ee664d8e61
Guest link: https://app.eyeson.team/?guest=5TmP3RvYKfO4EObeJKsymaPJ
GUI link: https://app.eyeson.team/?m4SXo1jyAUsrxDrElcGK2hkU

Run Ghost Client

Eyeson provides Ghost Clients that connect to any Eyeson session. In this example we are going to run the RTMP Server on a Raspberry Pi. The video source is going to be a camera module, attached to the board.

tip

To find the IP address of your Raspberry Pi. Run ip addr show and look for inet plus wlan0 in the output.

To connect the RTMP Server to the running Eyeson session, you need the room ID and the same API key used to start the session. Also you can add the port and the authentifications the RTMP address.

./rtmp-server_linux_arm64 --rtmp-listen-addr rtmp://192.168.50.30:1950/rasp/live --room-id 69394444df3181ee664d8e61 --user-id rpi5 --user RaspberryPi5 <YOUR_API_KEY>
example response
15:43:46.108 INF Guest-link: https://app.eyeson.team/?guest=5TmP3RvYKfO4EObeJKsymaPJ
15:43:46.108 INF GUI-link: https://app.eyeson.team/?m4SXo1jyAUsrxDrElcGK2hkU
15:43:51.213 INF ICE Connection State has changed: checking
15:43:51.373 INF ICE Connection State has changed: connected
15:43:51.373 INF RTMP server listening: rtmp://192.168.50.30:1950/rasp/live
info

You can also use the Guest Link to connect a Ghost Client.

./rtmp-server_linux_arm64 --rtmp-listen-addr rtmp://192.168.50.30:1950/rasp/live --user-id rpi5 --user RaspberryPi5 https://app.eyeson.team/?guest=5TmP3RvYKfO4EObeJKsymaPJ

Pipe Camera Feed

The camera captures video and forwards it to FFmpeg, which encodes the video and streams it to the designated RTMP server.

rpicam-vid -o - -t 0 --mode 1280:720:8:P --framerate 25 --codec mjpeg -n | \
ffmpeg -f mjpeg -i - -c:v libx264 -preset ultrafast -f flv rtmp://192.168.50.30:1950/rasp/live
example response
[1:18:35.843667339] [4439]  INFO Camera camera_manager.cpp:330 libcamera v0.5.2+99-bfd68f78
[1:18:35.856899321] [4443] INFO RPI pisp.cpp:720 libpisp version 1.3.0
[1:18:35.861008610] [4443] INFO IPAProxy ipa_proxy.cpp:180 Using tuning file /usr/share/libcamera/ipa/rpi/pisp/ov5647.json
[1:18:35.871729641] [4443] INFO Camera camera_manager.cpp:220 Adding camera '/base/axi/pcie@1000120000/rp1/i2c@88000/ov5647@36' for pipeline handler rpi/pisp
[1:18:35.871788604] [4443] INFO RPI pisp.cpp:1179 Registered camera /base/axi/pcie@1000120000/rp1/i2c@88000/ov5647@36 to CFE device /dev/media2 and ISP device /dev/media0 using PiSP variant BCM2712_C0
Mode selection for 1280:720:8:P(25)
SGBRG10_CSI2P,640x480/58.924 - Score: 4093.33
SGBRG10_CSI2P,1296x972/46.3371 - Score: 2400.33
SGBRG10_CSI2P,1920x1080/32.8052 - Score: 1250
SGBRG10_CSI2P,2592x1944/15.6335 - Score: 21700.2
Stream configuration adjusted
[1:18:35.894009204] [4439] INFO Camera camera.cpp:1215 configuring streams: (0) 640x480-YUV420/sYCC (1) 1920x1080-GBRG_PISP_COMP1/RAW
[1:18:35.894125149] [4443] INFO RPI pisp.cpp:1483 Sensor: /base/axi/pcie@1000120000/rp1/i2c@88000/ov5647@36 - Selected sensor format: 1920x1080-SGBRG10_1X10/RAW - Selected CFE format: 1920x1080-PC1g/RAW
ffmpeg version 7.1.3-0+deb13u1+rpt1 Copyright (c) 2000-2025 the FFmpeg developers
built with gcc 14 (Debian 14.2.0-19)
configuration: --prefix=/usr --extra-version=0+deb13u1+rpt1 --toolchain=hardened --incdir=/usr/include/aarch64-linux-gnu --enable-gpl --disable-stripping --disable-libmfx --disable-mmal --disable-omx --enable-gnutls --enable-libaom --enable-libass --enable-libbs2b --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libglslang --enable-libgme --enable-libgsm --enable-libharfbuzz --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-openal --enable-opencl --enable-opengl --disable-sndio --disable-libvpl --libdir=/usr/lib/aarch64-linux-gnu --arch=arm64 --enable-neon --enable-v4l2-request --enable-libudev --enable-epoxy --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-vout-drm --enable-chromaprint --enable-frei0r --enable-ladspa --enable-libbluray --enable-libcaca --enable-libdvdnav --enable-libdvdread --enable-libjack --enable-libpulse --enable-librabbitmq --enable-librist --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libx264 --enable-libzmq --enable-libzvbi --enable-lv2 --enable-sand --enable-sdl2 --enable-libplacebo --enable-librav1e --enable-pocketsphinx --enable-librsvg --enable-libjxl --enable-shared
libavutil 59. 39.100 / 59. 39.100
libavcodec 61. 19.101 / 61. 19.101
libavformat 61. 7.100 / 61. 7.100
libavdevice 61. 3.100 / 61. 3.100
libavfilter 10. 4.100 / 10. 4.100
libswscale 8. 3.100 / 8. 3.100
libswresample 5. 3.100 / 5. 3.100
libpostproc 58. 3.100 / 58. 3.100
Input #0, mjpeg, from 'fd:':
Duration: N/A, bitrate: N/A
Stream #0:0: Video: mjpeg (Baseline), yuvj420p(pc, bt470bg/unknown/unknown), 640x480 [SAR 1:1 DAR 4:3], 25 fps, 25 tbr, 1200k tbn
Stream mapping:
Stream #0:0 -> #0:0 (mjpeg (native) -> h264 (libx264))
[libx264 @ 0x5555ae3f79b0] using SAR=1/1
[libx264 @ 0x5555ae3f79b0] using cpu capabilities: ARMv8 NEON
[libx264 @ 0x5555ae3f79b0] profile Constrained Baseline, level 3.0, 4:2:0, 8-bit
[libx264 @ 0x5555ae3f79b0] 264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=250 keyint_min=25 scenecut=0 intra_refresh=0 rc=crf mbtree=0 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
Output #0, flv, to 'rtmp://192.168.50.30:1950/rasp/live':
Metadata:
encoder : Lavf61.7.100
Stream #0:0: Video: h264 ([7][0][0][0] / 0x0007), yuvj420p(pc, bt470bg/unknown/unknown, progressive), 640x480 [SAR 1:1 DAR 4:3], q=2-31, 25 fps, 1k tbn
Metadata:
encoder : Lavc61.19.101 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
frame= 4999 fps= 25 q=19.0 size= 26774KiB time=00:03:19.96 bitrate=1096.9kbits/s speed=1.01x

If you need other resolutions:

rpicam-vid --list-cameras
example response
Available cameras
-----------------
0 : ov5647 [2592x1944 10-bit GBRG] (/base/axi/pcie@1000120000/rp1/i2c@88000/ov5647@36)
Modes: 'SGBRG10_CSI2P' : 640x480 [58.92 fps - (16, 0)/2560x1920 crop]
1296x972 [46.34 fps - (0, 0)/2592x1944 crop]
1920x1080 [32.81 fps - (348, 434)/1928x1080 crop]
2592x1944 [15.63 fps - (0, 0)/2592x1944 crop]

Result

If you look at the running Eyeson session, you will see the "Observer", which has joined via GUI-Link and "RaspberryPI5" which is the Ghost Client via RTMP server.