shantanu.tech
← Writing
Nov 15, 2026·9 min read

Turning a Fire TV Stick and a Raspberry Pi Into a Camera Screen

An IP camera feed that opens itself on the TV and survives power cuts: MediaMTX on a Pi 5 re-serving one RTSP connection, a health check that won't boot-loop, network ADB driving the Firestick with no remote, and a freeze watchdog that catches a paused VLC.

raspberry-pihome-labrtsp

The setup we wanted

This week we turned a TV into a full-time camera screen. An IP camera's feed now plays on a Fire TV Stick whenever the TV is on, and nobody has to touch a remote to get it there. A Raspberry Pi 5 sits in the middle and does the real work.

There were two rules:

  1. If the TV is on, the camera is on screen. No opening VLC, no typing URLs, no “can someone fix the camera”.
  2. After a power cut, everything comes back by itself, with nobody touching anything.

Pointing VLC on the Firestick straight at the camera gets you most of the way. It works right up until the stream hiccups, the camera reboots, or the power goes, and then someone has to fix it by hand. Everything in this post is about removing that someone.

Why put a Pi in the middle

Many IP cameras only allow a handful of RTSP sessions at once, and every viewer is one more stream the camera has to push. So the Pi holds a single connection to the camera and re-serves it to everything else on the network:

IP camera ──RTSP/TCP──▶ Raspberry Pi 5 (MediaMTX) ──RTSP / HLS / WebRTC──▶ Firestick, phones, laptops

The server is MediaMTX. It only remuxes: the H.264 coming out of the camera goes back out untouched, so the CPU barely notices. A Pi 3 or a Pi Zero 2 W could do this job too.

The parts of the config that matter:

rtspTransports: [tcp]        # serve over TCP only

pathDefaults:
  rtspTransport: tcp         # pull from the camera over TCP too
  sourceOnDemand: false      # keep the camera connection open 24/7

paths:
  cam1:
    source: rtsp://USER:PASSWORD@<camera-ip>:554/<stream-path>
  • TCP everywhere. UDP over Wi-Fi drops packets, and dropped packets show up as grey, smeared frames. Players that try UDP first get told to retry over TCP.
  • sourceOnDemand: false. The Pi stays connected to the camera even when nobody is watching, so the TV gets a picture the moment it asks. If the camera drops or reboots, MediaMTX keeps retrying until it's back.
  • Three outputs for free. The same stream is also on HLS (:8888) for players that are bad at RTSP, and on WebRTC (:8889), which plays in the Firestick's Silk browser with no app installed.
  • Locked down by default. Anyone on the LAN can watch, nobody can push a stream into the Pi, and the API only listens on 127.0.0.1. Nothing is port-forwarded.

Surviving a power cut

A power cut is the hardest case, because everything comes back at once and in the wrong order. The Pi usually boots before the router. The camera comes up whenever it feels like it. And the SD card just lost power in the middle of who knows what. Here's what handles each piece:

  • Camera not ready yet: MediaMTX keeps retrying until it answers. Nothing else to do.
  • MediaMTX crashes or hangs: the container has restart: always. On top of that, a systemd timer runs a health check every minute that calls the MediaMTX API and restarts it after two failures in a row. It also runs docker compose up -d, which does nothing if the container is fine and recreates it if it's gone.
  • Network doesn't come back: the same check pings the gateway. Every 3 minutes of failure it restarts networking. After 20 minutes it reboots the Pi, but only if the Pi has been up for more than 30 minutes.
  • Pi freezes hard: the Broadcom hardware watchdog reboots it within about 15 seconds, and a kernel panic reboots after 10.
  • SD card corruption: logs go to RAM, the swap file is off, and fsck.repair=yes lets the boot-time check fix errors without a keyboard. Wi-Fi power saving is off too, since it's the classic cause of random stream drops on a Pi.

The uptime guard on the reboot is the line I'm happiest with. After a power cut the router can take several minutes to come back. Without the guard, the Pi would reboot itself again and again while it waits.

UPTIME=$(cut -d. -f1 /proc/uptime)
if (( FAILS >= 20 && UPTIME > 1800 )); then
  logger -t rtsp-proxy-health "Network down 20 min, rebooting"
  systemctl reboot
elif (( FAILS % 3 == 0 )); then
  logger -t rtsp-proxy-health "Restarting networking"
  systemctl restart NetworkManager
fi

Docker or native?

The installer can run MediaMTX either way, and the stream behaves the same. The real difference is power-cut protection. In native mode you can turn on Raspberry Pi OS's overlay filesystem: the SD card becomes read-only and every write goes to RAM, so a power cut can't corrupt anything. Docker can't run on top of that overlay.

We went with Docker anyway. Protection there means writing as little as possible and recovering on its own. The container keeps no data, so if Docker's own storage ever gets corrupted, you delete /var/lib/docker and run docker compose up -d again. The image tag is pinned to bluenviron/mediamtx:1.21.3, so a rebuild can never quietly pull a different version. Booting from an NVMe SSD and adding a small UPS are the next upgrades.

Getting the Firestick to open the stream by itself

This is the fun part. Fire OS has no boot scripts and no “open this app on start” setting. But a Fire TV is Android underneath, and Android has network ADB. So the Pi drives the Firestick over the network.

One-time setup on the Firestick:

  1. Install VLC from the Appstore and open it once to get past its welcome screens.
  2. Go to Settings → My Fire TV → About and press OK on the device name 7 times to unlock developer options.
  3. Turn on Developer options → ADB debugging.
  4. Give it a DHCP reservation in the router so the IP never changes.

Then, from the Pi:

adb connect <firestick-ip>:5555

adb shell am start -a android.intent.action.VIEW \
  -d "'rtsp://<pi-ip>:8554/cam1'" \
  -n org.videolan.vlc/org.videolan.vlc.gui.video.VideoPlayerActivity

That one am start is the whole trick. VLC opens straight into the player with the stream. The rest of the firestick-autoplay service, a bash loop under systemd, is about deciding when to send it.

The remote was missing

Small problem: the Firestick's remote had gone missing. The first ADB connection from a new machine pops up “Allow USB debugging?” on the TV, and you need to press OK.

There are two ways around it:

  • The Amazon Fire TV phone app works as a remote.
  • Reuse a key the Firestick already trusts. If another computer already connected with ADB, copy its adbkey into the project folder. The setup script moves it into the service's home (moves, not copies, so the private key doesn't sit around) and no prompt appears.

One gotcha with the second option: if the trusted connection came from an ADB app on a phone, that key lives inside the app. Copying ~/.android from a laptop won't help. The setup script now says exactly that when it sees an unauthorized state with a copied key.

Boot, wake or always

The service has three modes:

  • boot: open the stream once each time the Firestick starts. Good for a TV you also use for other things.
  • wake: also open it every time the Firestick wakes up, so turning on the TV shows the camera.
  • always: a dedicated camera monitor. Whenever the Firestick is awake and the stream isn't on screen, reopen it. This is what we run.

The tricky part is telling a Firestick restart apart from a Pi restart. If only the Pi reboots while someone is watching something else, it shouldn't take over the TV. So the service reads the Firestick's /proc/sys/kernel/random/boot_id and uptime over ADB. A boot ID it hasn't seen, with the stick already up for more than 15 minutes, means the Pi is the one that restarted, so it leaves the screen alone.

A few more checks keep it polite:

  • It waits until sys.boot_completed is set, then another 30 seconds for the home screen to settle.
  • It never wakes a sleeping Firestick. It checks dumpsys power for mWakefulness=Awake first.
  • It checks dumpsys window to see whether VLC's VideoPlayerActivity is in focus before doing anything.

The freeze watchdog

VLC has one weakness for this job: when the stream drops, it sits on the last frame forever. always mode reopens VLC when it isn't on screen, but a frozen VLC is on screen. It looks fine to every check above.

The first idea was to ask MediaMTX. Its API lists every RTSP session with the bytes sent, so the Pi can see exactly how much video has gone to the Firestick's IP:

curl -fsS "$MEDIAMTX_API/v3/rtsp/sessions/list?itemsPerPage=1000" |
  jq -r --arg ip "$FIRESTICK_IP" --arg p "$STREAM_PATH" '
    [ .items[]
      | select(.path == $p and .state == "read"
               and (.remoteAddr | startswith($ip + ":")))
      | .outboundBytes ]
    | if length == 0 then empty else add end'

If that number stops growing, VLC is stuck. That catches a dead stream. It doesn't catch a paused one. A paused VLC keeps reading the live RTSP stream, so the byte count keeps climbing while the picture stands still.

So there's a second signal: VLC's own media session. dumpsys media_session reports a PlaybackState for each app: 3 is playing, 2 is paused, 6 is buffering, 7 is an error. A small awk script pulls VLC's state out of that output.

The rule ended up as:

  • Video is flowing when the byte count grew since the last check and VLC says it's playing.
  • If nothing has flowed for 20 seconds while the camera is fine, force-stop VLC and open the stream again.
  • If the camera itself is down (MediaMTX says the path isn't available), do nothing. Restarting VLC can't fix a camera. Wait for it instead.
  • After 3 restarts in a row that don't help, slow down to one every 5 minutes.

To test it, we paused VLC with adb shell input keyevent 127. About 22 seconds later the Pi restarted it and the live picture was back.

Small things that cost time

  • Use the Pi's IP, not raspberrypi.local. Many Firestick apps can't resolve .local names.
  • Older Firesticks only decode H.264. The Lite and 2nd gen can't play H.265. If you get sound but no picture, switch the camera's stream to H.264. Don't transcode on the Pi.
  • Vega OS sticks are out. Newer Fire TV models on Amazon's Vega OS, like the Fire TV Stick 4K Select, don't support ADB or VLC. Every Fire OS model does.
  • ADB debugging can switch itself off after a Fire OS update. The setup script checks port 5555 first and says so when nothing answers.
  • Special characters in the camera password need URL-encoding (@ becomes %40), or you get 401 Unauthorized.
  • A space in the folder name. The project lives in ~/Docker Application/… on the Pi. Every script quotes it, and scp needs it with no inner quotes, because SFTP takes the path literally.

What I learned this week

  • Remux, don't transcode. Passing the camera's video through untouched is why a Pi can do this job with almost no CPU.
  • Plan for the boot order after a power cut. Everything comes back at once, in the wrong order. Retry forever, escalate slowly, and never let a recovery step loop on itself.
  • One signal isn't enough to say “it's working”. The byte count said everything was fine while VLC sat paused. It took the network view and the app's own state together to get it right.
  • ADB turns a Fire TV Stick into a display you can script. No root, no custom launcher. One am start and a loop around it.
  • Knowing when not to act matters as much as acting. Camera down? Wait. Only the Pi rebooted? Leave the TV alone. Sleeping stick? Don't wake it.

Next up, maybe: a second camera in a grid, or a small UPS so a power blip doesn't even reach the Pi.