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> |
