How to use your Raspberry Pi Camera in Apple Homekit with Scrypted (Bookworm, Trixie)

Hardware needed
- Raspberry Pi Model A or B, gen 2, 3, 4 or 5
- Raspberry Pi OS 11 Bullseye (libcamera) or newer. (Tested on Bookworm and Trixie)
- SD Card with capacity of 4GB or higher
- Raspberry Pi compatible camera with flatband cable
- Raspberry Pi Camera Module v1, v2 or v3
or
- Raspberry Pi HQ Camera
or
- ArduCam 16MP Autofocus
- Raspberry Pi Enclosure
- Poolside Factory Camera Mount
- Tilt-Adjustable Mount for Camera Module v1/v2/PiNoir
or
- Universal Tilt-Adjustable Mount for v1/v2/PiNoir/ArduCam 16MP Autofocus or HQ camera
Note: For the Raspberry Pi OS 11 (Bullseye) or earlier, you can also follow this earlier tutorial that was based on Homebridge and the ffmpeg plugin.:
Instructions
If you already have an SD card with a working Raspberry Pi image, you can use that one, otherwise you can follow these steps to create a fresh one without needing a screen or keyboard for your Raspberry Pi.
Log in to your Raspberry Pi.
If you are using an official Raspberry Pi camera, you can skip this paragraph:
For an Arducam 16MP with Autofocus, if you don't care about autofocus, it's sufficient to add dtoverlay=imx519,vcm=off under the [all] section in /boot/firmware/config.txt and reboot. For getting autofocus to work, you'll have to install the Arducam camera drivers and libcamera apps by following these instructions.
Now run the following block (it will take several minutes). It will also make Wi-Fi reliable for a camera without a screen: it turns off Wi-Fi power saving, installs a small watchdog that reconnects the Pi if it drops off Wi-Fi, and keeps the system log across reboots so problems can be diagnosed.
sudo apt install -y ffmpeg wget curl libcamera-apps-lite vim
# Newest MediaMTX release, and the right build for this Pi's processor
MTX_VER=$(curl -sI https://github.com/bluenviron/mediamtx/releases/latest | awk -F/ 'tolower($1) ~ /^location:/ {print $NF}' | tr -d '\r')
case "$(uname -m)" in aarch64) MTX_ARCH=arm64 ;; armv7l) MTX_ARCH=armv7 ;; armv6l) MTX_ARCH=armv6 ;; esac
echo "Installing MediaMTX $MTX_VER for $MTX_ARCH"
wget -q -O /tmp/mediamtx.tar.gz "https://github.com/bluenviron/mediamtx/releases/download/${MTX_VER}/mediamtx_${MTX_VER}_linux_${MTX_ARCH}.tar.gz"
tar -zxf /tmp/mediamtx.tar.gz
sudo mkdir -p /opt/mediamtx
sudo mv mediamtx mediamtx.yml /opt/mediamtx/
sudo chmod +x /opt/mediamtx/mediamtx
cat << EOF | sudo tee /opt/mediamtx/mediamtx.yml
# Global settings
rtspAddress: :8554
# rtpAddress: :8000
webrtcAddress: :8889
rtspTransports: [tcp, multicast, udp]
paths:
cam:
# source: publisher/rpi
runOnInit: ""
runOnInitRestart: yes
all_others:
# readTimeout: 10s
EOF
cat << EOF | sudo tee /opt/mediamtx/stream.sh
#!/bin/bash
# Stream RPi Camera Module 3 via TCP to MediaMTX
export LIBCAMERA_LOG_LEVELS="*:WARN"
rpicam-vid \
--nopreview --verbose 0 \
--autofocus-mode auto \
--autofocus-range normal \
--width 1920 --height 1080 \
--framerate 10 \
--bitrate 1000000 \
--inline \
--flush \
--listen \
-t 0 \
--codec h264 \
-o tcp://0.0.0.0:8000 & \
sleep 2 # Wait for rpicam-vid to start TCP server
ffmpeg -hide_banner -nostats -loglevel warning \
-re \
-fflags +genpts+discardcorrupt \
-flags low_delay \
-avioflags direct \
-i tcp://127.0.0.1:8000 \
-c:v copy \
-f rtsp \
rtsp://localhost:8554/cam
EOF
sudo chmod +x /opt/mediamtx/stream.sh
cat << EOF | sudo tee /etc/systemd/system/mediamtx.service
[Unit]
Description=MediaMTX RTSP Server
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/mediamtx
ExecStart=/opt/mediamtx/mediamtx /opt/mediamtx/mediamtx.yml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
cat << EOF | sudo tee /etc/systemd/system/rpi-stream.service
[Unit]
Description=RPi Camera Stream to MediaMTX
Wants=network-online.target mediamtx.service
After=network-online.target mediamtx.service
[Service]
Type=simple
User=root
ExecStart=/opt/mediamtx/stream.sh
ExecStop=/bin/kill -TERM \$MAINPID
ExecStopPost=/usr/bin/pkill -f rpicam-vid
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable mediamtx
sudo systemctl start mediamtx
sudo systemctl enable rpi-stream
sudo systemctl start rpi-stream
# --- Keep Wi-Fi reliable: power save off, never give up reconnecting ---
WIFI=$(nmcli -t -f NAME,TYPE con show | awk -F: '$2=="802-11-wireless"{print $1; exit}')
sudo nmcli con modify "$WIFI" connection.auth-retries 0 connection.autoconnect-retries 0 802-11-wireless.powersave 2
# --- Keep the system log across reboots (max 200 MB) ---
sudo mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nStorage=persistent\nSystemMaxUse=200M\n' | sudo tee /etc/systemd/journald.conf.d/50-persistent.conf > /dev/null
sudo systemctl restart systemd-journald
# --- Wi-Fi watchdog: reconnects if the router stops answering ---
sudo tee /usr/local/sbin/wifi-watchdog.sh > /dev/null <<'EOF'
#!/bin/bash
# Runs every 2 min. If the router does not answer: reconnect (twice),
# then reload the Wi-Fi driver, then reboot (max once per 6 h, not in the first 30 min).
# Events: /var/log/wifi-watchdog.log
IFACE=wlan0
STATE=/run/wifi-watchdog.strikes
LAST=/var/lib/wifi-watchdog/last-reboot
LOG=/var/log/wifi-watchdog.log
log() { echo "$(date '+%F %T') $*" >> "$LOG"; logger -t wifi-watchdog "$*"; }
[ -f "$LOG" ] && [ "$(stat -c %s "$LOG")" -gt 1048576 ] && mv -f "$LOG" "$LOG.old"
CON=$(nmcli -t -f NAME,TYPE con show | awk -F: '$2=="802-11-wireless"{print $1; exit}')
[ -z "$CON" ] && exit 0
GW=$(ip -4 route show default dev "$IFACE" 2>/dev/null | awk '{print $3; exit}')
if [ -n "$GW" ] && ping -c3 -W2 -I "$IFACE" "$GW" >/dev/null 2>&1; then
[ -f "$STATE" ] && log "recovered after strike $(cat "$STATE")"
rm -f "$STATE"; exit 0
fi
n=$(( $(cat "$STATE" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$STATE"
log "router ${GW:-(no route)} unreachable on $IFACE - strike $n, NetworkManager: $(nmcli -t -f GENERAL.STATE dev show "$IFACE" 2>/dev/null | cut -d: -f2-)"
reconnect() { log "reconnecting: nmcli connection up '$CON'"; nmcli -w 60 con up "$CON" >/dev/null 2>&1; }
case $n in
1|2) reconnect ;;
3) log "reloading Wi-Fi driver"
modprobe -r $(lsmod | awk '$1 ~ /^brcmfmac_/{print $1}') brcmfmac 2>/dev/null
sleep 3; modprobe brcmfmac; sleep 10; reconnect ;;
*) up=$(cut -d. -f1 /proc/uptime); last=$(cat "$LAST" 2>/dev/null || echo 0); now=$(date +%s)
if [ "$up" -gt 1800 ] && [ $((now - last)) -gt 21600 ]; then
mkdir -p "$(dirname "$LAST")"; echo "$now" > "$LAST"; log "rebooting"; sync; systemctl reboot
else
reconnect
fi ;;
esac
EOF
sudo chmod 755 /usr/local/sbin/wifi-watchdog.sh
printf '[Unit]\nDescription=Wi-Fi watchdog\nAfter=NetworkManager.service\n\n[Service]\nType=oneshot\nExecStart=/usr/local/sbin/wifi-watchdog.sh\nTimeoutStartSec=5min\n' | sudo tee /etc/systemd/system/wifi-watchdog.service > /dev/null
printf '[Unit]\nDescription=Run the Wi-Fi watchdog every 2 minutes\n\n[Timer]\nOnBootSec=5min\nOnUnitActiveSec=2min\nAccuracySec=10s\n\n[Install]\nWantedBy=timers.target\n' | sudo tee /etc/systemd/system/wifi-watchdog.timer > /dev/null
sudo systemctl daemon-reload
sudo systemctl enable --now wifi-watchdog.timer
# Install Scrypted
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-scrypted-docker-compose.sh > ~/install-scrypted-docker-compose.sh
sudo SERVICE_USER=$USER bash ~/install-scrypted-docker-compose.sh
rm ~/install-scrypted-docker-compose.sh
This needs a couple of "yes" confirmations.
When your Apple device requests the video stream from your scrypted camera, the resolution that is requested depends on the device that is requesting the stream:
iPad requests are 1920 x 1080, 802kbps
iPhone requests are 1280 x 720, 15fps, 299kbps
So no matter what settings you configure in the configuration above, you can't go beyond these limits. It does make a lot of sense though to reduce the frame rate for some camera placements, which will result in the video feed being much sharper, as it doesn't need to be compressed as much.
I have had mixed results opening the scrypted url in the browser with using hostname.local, so more reliable to look up the current ip address of your Raspberry Pi with
sudo ifconfig wlan0 or in color: ip addr show wlan0 and open in your browser:
https://your-ip-addr-from-above:10443/endpoint/@scrypted/core/public/#/
Your browser will likely complain, but click on Advanced and then on "proceed". Here's an example how this looks in a Chrome browser:

Create a new login on the following page:

Then click Management Console.

Then click on INSTALL PLUGIN.

Install the RTSP plugin.

Click ADD RTSP CAMERA.

Give it a name with which to identify the camera in Scrypted. Later, when adding to Homekit, you will once more be asked for a name that is then used in Homekit.

And fill in the following rtsp stream url:
rtsp://localhost:8554/cam

Then go to the plugin install page again and also install the homekit plugin:

Enable the switch for the camera you just created (very bottom of this picture).

Go back to the RTSP plugin, select the camera that you previously added, and go on the now appearing homekit tab, then pairing, then enable Standalone Accessory Mode. 
Then go to the "General" tab and scan the homekit QR code with your iPhone to add the camera to Homekit and give it its final name.

Note that if you don't enable the camera as a standalone camera, you can also scan the bridge QR code to add, and then also scan the camera QR code, and also get the camera working that way. The proposed way with standalone mode makes it so you just need to scan one QR code.
On your phone in the Home app, click the + and then 'Add Accessory' and scan the 2D barcode displayed in the browser.

Accept the homekit warning that this is an uncertified product.
Done, the Raspberry Pi camera should now be visible in your Home app.
Attach your Raspberry Pi camera module or HQ camera directly to the Raspberry Pi case using one the Poolside Factory mounts:
Tilt-Adjustable Camera Mount for Raspberry Pi Camera Modules
Universal-Tilt-Adjustable Camera Mount for all Raspberry Pi Cameras
Additional helpful commands:
See the output of the services we created (for debugging)
sudo systemctl status mediamtx
sudo systemctl status rpi-stream
# or
journalctl -u mediamtx -u rpi-stream
journalctl -u rpi-stream # or individually
# or
journalctl -u rpi-stream -f
# and see the mediamtx port
sudo netstat -tulnp | grep 8000
cat /var/log/wifi-watchdog.log # Lists every wifi drop
journalctl -b -1 # System log of the boot before the last reboot
If you want to access the camera for other tasks than homekit from the Raspberry Pi, you need to stop the streaming service, and then restart it again for Homekit afterwards:
sudo systemctl stop rpi-stream
# for instance, use the rpicam tools
rpicam-hello
rpicam-still --list-cameras
rpicam-still -o test.jpg
rpicam-vid -t 5000 -o test.h264
sudo systemctl start rpi-stream # re-enable
Updating an install made before the November 2026 instructions
Earlier versions of this guide wrote a log line for every video frame to /var/log/stream.log, which can fill the SD card over months. They also had no protection against the Pi dropping off Wi-Fi for good. To bring an existing install up to date, run these commands once:
# 1. Quiet stream.sh and remove the old log (run once only)
sudo sed -i -e '/tee -a \/var\/log\/stream.log/d' \
-e 's/^rpicam-vid /rpicam-vid --nopreview --verbose 0 /' \
-e 's/^ffmpeg /ffmpeg -hide_banner -nostats -loglevel warning /' \
-e '1a export LIBCAMERA_LOG_LEVELS="*:WARN"' /opt/mediamtx/stream.sh
sudo systemctl restart rpi-stream
sudo rm -f /var/log/stream.log
# 2. Switch Scrypted's auto-updater to the maintained version
cd ~/.scrypted
sed -i 's#image: *containrrr/watchtower#image: nickfedor/watchtower#' docker-compose.yml
sudo docker compose up -d watchtower
cd ~
Then run the Wi-Fi and log command section from the big block above starting with # --- Keep Wi-Fi reliable, stopping before # Install Scrypted
