I had a Hikvision IPC-B120 that provides a simple RTSP stream, which I could view in VLC. I also wanted to see my Logitech BRIO’s feed in a browser—without having to start a Video Call session.
To do this, I pull the RTSP (video/audio) feed from each camera, pipe it through FFmpeg to convert it into an MPEG-TS stream, and then serve it over WebSocket using JSMpeg. On my Raspberry Pi 5, I run two systemd services (one per camera) that use websocat to listen for WebSocket connections. When a client connects, websocat launches FFmpeg to start the conversion and forwards the stream; when the client disconnects, FFmpeg automatically stops.
# /etc/systemd/system/rtsp-ws.service
[Unit]
Description=On-demand WebSocket→FFmpeg bridge for JSMpeg
After=network.target
[Service]
User=pi
Group=pi
ExecStart=/usr/local/bin/websocat \
--binary \
--exit-on-eof \
-s 127.0.0.1:10000 \
sh-c:'/usr/bin/ffmpeg -hide_banner -loglevel error -rtsp_transport tcp -i "rtsp://user:pass@Local.IP.Camera:Localport/URI/channels/101" -f mpegts -codec:v mpeg1video -bf 0 -r 25 pipe:1'
Restart=on-failure
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/webcam-ws.service
[Unit]
Description=On-demand WebSocket→FFmpeg bridge for Brio webcam
After=network.target
[Service]
User=pi
ExecStart=/usr/local/bin/websocat \
--binary \
--exit-on-eof \
-s 127.0.0.1:10001 \
sh-c:'WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 /usr/bin/ffmpeg -hide_banner -loglevel error \
-f v4l2 -framerate 30 -video_size 1920x1080 -i /dev/video0 \
-f alsa -ac 2 -ar 44100 -i default \
-f mpegts \
-codec:v mpeg1video -bf 0 -r 30 \
-codec:a mp2 \
pipe:1'
Restart=on-failure
[Install]
WantedBy=multi-user.target
<!doctype html> |
<html lang="en"> |
<head> |
<meta charset="utf-8"> |
<title>Live Camera (JSMpeg)</title> |
<style> |
/* fullscreen black background, centered content */ |
html, body { |
margin: 0; padding: 0; |
width: 100vw; height: 100vh; |
background: #000; |
display: flex; |
align-items: center; |
justify-content: center; |
} |
/* wrapper to constrain size, max 90% of viewport */ |
.player-container { |
width: 90vw; |
max-width: 1280px; /* or whatever max you like */ |
max-height: 90vh; |
} |
/* canvas will fill the container but keep its own aspect‐ratio */ |
.player-container canvas { |
width: 100%; |
height: auto; |
display: block; |
} |
</style> |
</head> |
<body> |
<div class="player-container"> |
<canvas id="videoCanvas"></canvas> |
</div> |
<script src="/jsmpeg.min.js"></script> |
<script> |
const url = (location.protocol==='https:'?'wss://':'ws://') |
+ location.host + '/my-ws-prefix'; |
new JSMpeg.Player(url, { |
canvas: document.getElementById('videoCanvas'), |
autoplay: true, |
audio: false |
}); |
</script> |
</body> |
</html> |