Camera Streams with MediaMTX
MediaMTX serves both of the robot’s camera feeds as RTSP on port 8554. This
is the camera server the tablet’s camera view connects to, and it should be the
only process on the robot that opens the USB camera.
What the tablet expects
Section titled “What the tablet expects”| Path | Source | Delivered |
|---|---|---|
rtsp://<robot-ip>:8554/static |
USB camera | H.264, 1280x720 at 30 fps |
rtsp://<robot-ip>:8554/ptz |
PTZ camera relay | The camera’s own codec, unchanged |
The two paths stream differently, and the difference matters when you inspect the robot:
/staticpublishes from the moment MediaMTX starts and runs continuously while the service is up (runOnInit). The USB pipeline is always working, even with nobody watching./ptzis opened only while someone is watching (sourceOnDemand) and closed again afterwards.
The tablet dials the robot (the robot never needs the tablet’s address), and
RTSP is served on the single port 8554. The tablet derives both URLs from the
robot’s address; Application Architecture covers how
it uses them.
Before you start
Section titled “Before you start”Run these checks on the robot as its Linux user — normally robot. The group
list must contain video, or nothing can open the camera:
id # the group list must contain "video"v4l2-ctl --list-devices # the USB camera, normally /dev/video0The USB camera must offer MJPEG at 1280x720 and 30 fps:
v4l2-ctl -d /dev/video0 --list-formats-ext | grep -A4 "1280x720"Expect a 1280x720 block whose MJPG entry lists
Discrete 0.033s (30.000 fps). Prove the exact mode works by capturing 30
frames:
gst-launch-1.0 -q v4l2src device=/dev/video0 num-buffers=30 \ ! image/jpeg,width=1280,height=720,framerate=30/1 \ ! fakesinkSilence means it worked. If it fails with not-negotiated, the camera cannot
do that mode and the pipeline’s caps have to be changed to match what it can.
Check the codec nodes and the GStreamer elements the pipeline needs:
v4l2-ctl -d /dev/video10 --list-formats-out # decoder: H264 and MJPG must appearv4l2-ctl -d /dev/video11 --list-formats-out # encoder: raw formats (YU12, YV12, NV12, NV21, …)
gst-inspect-1.0 v4l2h264encgst-inspect-1.0 h264parsegst-inspect-1.0 jpegdecgst-inspect-1.0 rtspclientsink # if missing: sudo apt install -y gstreamer1.0-plugins-badConfirm the PTZ camera is reachable from the robot. Replace <PTZ-CAMERA-IP>
with the camera’s real address; this is the URL the config relays:
ffprobe -rtsp_transport tcp -show_streams rtsp://<PTZ-CAMERA-IP>/stream-1.sdp | grep -E "codec_name|width"The camera must not be in use elsewhere while you test.
The id check above is the user/group check that outlives the preflight: the
service unit runs as robot with SupplementaryGroups=video. If the robot’s
Linux user is not robot, adjust the unit’s User= and Group= and the
chown below accordingly before enabling it.
Install MediaMTX v1.21.0
Section titled “Install MediaMTX v1.21.0”Download the pinned arm64 release on the robot:
curl -fL -o /tmp/mediamtx.tar.gz \ https://github.com/bluenviron/mediamtx/releases/download/v1.21.0/mediamtx_v1.21.0_linux_arm64.tar.gzVerify the download, then install it:
echo "a8113b5928ba1a934b81557b61b8a07954b76921a4b567d54c7f086f8b39d9a2 /tmp/mediamtx.tar.gz" | sha256sum -c -Expect OK; if not, download it again.
sudo tar -xzf /tmp/mediamtx.tar.gz -C /usr/local/bin mediamtxsudo chmod 755 /usr/local/bin/mediamtx/usr/local/bin/mediamtx --version # expect: v1.21.0rm /tmp/mediamtx.tar.gzIf the robot has no internet access, download the same URL on a workstation,
verify the same checksum there, then
scp mediamtx_v1.21.0_linux_arm64.tar.gz robot@10.0.0.23:/tmp/ and run the
same extract commands on the robot.
Configure MediaMTX
Section titled “Configure MediaMTX”Write /etc/mediamtx/mediamtx.yml from the published template, then edit the
one placeholder:
sudo mkdir -p /etc/mediamtxsudo curl -fsSL -o /etc/mediamtx/mediamtx.yml https://hausbots-docs.celeriscode.com/downloads/mediamtx/mediamtx.ymlsudo nano /etc/mediamtx/mediamtx.yml # replace rtsp://<PTZ-CAMERA-IP>/stream-1.sdpIf the robot has no internet, download the template on the workstation and
scp it across, then sudo install -m 0644 mediamtx.yml /etc/mediamtx/mediamtx.yml.
Reference — the template as downloaded:
# HausBots robot camera server (MediaMTX v1.21.0).
# Replace the PTZ source below with the camera's real RTSP URL before starting.
# Verify it first from the robot: ffprobe -rtsp_transport tcp <url>
rtsp: yes
rtspAddress: :8554
# RTSP only: the other servers are off so MediaMTX never generates a TLS cert.
hls: no
webrtc: no
rtmp: no
moq: no
api: no
metrics: no
pprof: no
paths:
# USB camera, published as H.264. Must stay on one line.
# This pipeline publishes from MediaMTX start and runs continuously while
# the service is up (runOnInit) — not only while a client is watching.
# MJPEG decode is software here (~17% of one core at 720p30); the hardware
# JPEG decoder stalls on this kernel, so keep jpegdec.
static:
runOnInit: gst-launch-1.0 -e v4l2src device=/dev/video0 ! image/jpeg,width=1280,height=720,framerate=30/1 ! jpegdec ! videoconvert ! v4l2h264enc extra-controls="controls,h264_i_frame_period=30,video_bitrate=2500000" ! h264parse config-interval=1 ! rtspclientsink location=rtsp://127.0.0.1:8554/static
runOnInitRestart: yes
# PTZ camera, forwarded untouched (no decode, no re-encode).
# sourceOnDemand opens the camera only while someone is watching.
ptz:
source: rtsp://<PTZ-CAMERA-IP>/stream-1.sdp
sourceOnDemand: yes
sourceOnDemandCloseAfter: 30s
RTSP is the only server enabled: HLS, WebRTC, RTMP, the API, metrics, and pprof are off, so MediaMTX never generates the self-signed TLS pair it would otherwise need, and there is no second service to secure.
The two paths are built very differently:
/staticdecodes the camera’s MJPEG in software (jpegdec), converts it, and encodes H.264 on the VideoCore (v4l2h264enc), then publishes the result back to MediaMTX over loopback withrtspclientsink. Because it is arunOnInitcommand, it runs from service start — about 17% of one core at 720p30 — whether or not anyone is watching.runOnInitRestart: yesbrings the pipeline back if it exits./ptzis forwarded untouched: no decode, no re-encode, so the tablet gets the camera’s own codec.sourceOnDemandopens the camera only while a client is connected, and closes it 30 seconds after the last one leaves.
Replace the ptz source: URL — rtsp://<PTZ-CAMERA-IP>/stream-1.sdp — with
the address that answered the ffprobe above. Leave the static pipeline on
one line.
Run it as a service
Section titled “Run it as a service”Install the unit, create its working directory, and enable it:
sudo curl -fsSL -o /etc/systemd/system/mediamtx.service https://hausbots-docs.celeriscode.com/downloads/mediamtx/mediamtx.servicesudo mkdir -p /var/lib/mediamtx && sudo chown robot:robot /var/lib/mediamtxsudo systemctl daemon-reloadsudo systemctl enable --now mediamtxsystemctl is-active mediamtx # expect: activeIf the robot’s Linux user is not robot, adjust User=, Group=, and the
chown accordingly before enabling.
Reference — the unit as downloaded:
[Unit]
Description=MediaMTX RTSP server (HB2 cameras)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=robot
Group=robot
SupplementaryGroups=video
WorkingDirectory=/var/lib/mediamtx
ExecStart=/usr/local/bin/mediamtx /etc/mediamtx/mediamtx.yml
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
The unit runs MediaMTX as robot, adds the video group so the USB camera can
be opened, and restarts it after a failure.
Firewall
Section titled “Firewall”Only if ufw is active:
sudo ufw status | head -1 # only continue if it says "Status: active"sudo ufw allow 8554/tcpsudo ufw allow 8000:8001/udpMediaMTX answers RTSP on 8554/tcp and RTP/RTCP on UDP 8000–8001 for clients
that negotiate UDP transport. Clients kept on -rtsp_transport tcp carry the
media inside the TCP connection and only need the first rule.
Verify
Section titled “Verify”Confirm MediaMTX started listening:
journalctl -u mediamtx -n 20 --no-pager | grep RTSP # a [RTSP] line on :8554From another machine on the same network (the robot’s address, e.g.
10.0.0.23):
ffprobe -rtsp_transport tcp -show_streams rtsp://10.0.0.23:8554/static \ | grep -E "codec_name|width|height|r_frame_rate"Expect exactly:
codec_name=h264width=1280height=720r_frame_rate=30/1ffprobe -rtsp_transport tcp -show_streams rtsp://10.0.0.23:8554/ptz | grep -E "codec_name|width"Expect the PTZ camera’s own codec and size, unchanged from the check above.
Then watch both:
ffplay -rtsp_transport tcp rtsp://10.0.0.23:8554/staticffplay -rtsp_transport tcp rtsp://10.0.0.23:8554/ptzAcceptance checklist
Section titled “Acceptance checklist”| Command | Expect |
|---|---|
systemctl is-active mediamtx |
active |
pgrep -fa gst-launch while MediaMTX runs |
one gst-launch-1.0 process (the /static pipeline publishes from service start) |
sudo systemctl stop mediamtx, then pgrep -fa gst-launch |
no output |
journalctl -u mediamtx -n 50 | grep "path ptz" after watching /ptz, then leaving |
started on demand, then stopped: … not needed by anyone |
top -bn1 | head -12 while MediaMTX runs |
gst-launch-1.0 around 17% of one core |
sudo reboot, then ffprobe both paths again |
both play with no manual step |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Fix |
|---|---|
Pipeline exits at once, Device or resource busy |
Something else holds the camera — stop it, then sudo systemctl restart mediamtx. |
no element "rtspclientsink" |
sudo apt install -y gstreamer1.0-plugins-bad |
/ptz connects but no video |
The source: URL is wrong or unreachable; re-run the ffprobe against it from the robot, fix /etc/mediamtx/mediamtx.yml, then restart. |
not-negotiated from v4l2h264enc |
The camera cannot do the mode; re-check the MJPEG 720p30 check and adjust the caps. |
| Stream stutters | Lower video_bitrate to 1500000, and keep clients on -rtsp_transport tcp. |
| Whatever you changed, revert | Restore /etc/mediamtx/mediamtx.yml (reference above) and sudo systemctl restart mediamtx. |
What to send back
Section titled “What to send back”After commissioning, send the output of these commands:
/usr/local/bin/mediamtx --versionffprobe -rtsp_transport tcp -show_streams rtsp://127.0.0.1:8554/static | grep -E "codec_name|width|height|r_frame_rate"ffprobe -rtsp_transport tcp -show_streams rtsp://127.0.0.1:8554/ptz | grep -E "codec_name|width|height"systemctl is-active mediamtx