void@hr54:~/DIRECTV-HR54-700$ cat README.md

I Turned a DIRECTV HR54-700 Into a Standalone Jellyfin Box

I got a free HR54 on Saturday. By Sunday night it was playing Jellyfin through the stock Broadcom video stack. Then I kept going until the box did the whole job itself.

September 2026 DIRECTV HR54-700 Broadcom BCM7346B2 / MIPS32

My friend Hector gave me this thing for free on Saturday. Thanks, Hector.

It was a DIRECTV HR54-700 Genie DVR. I do not have any use for DIRECTV service, but the hardware is decent and it runs Linux, so obviously I was going to screw with it.

By Sunday night I had Jellyfin video and audio playing through the receiver's own Broadcom decoder and HDMI output.

After that the job was mostly making the proof usable: persistent root, a TV UI, remote navigation, search, auth, transport controls, Wi-Fi, reboot persistence, and finally removing the development PC from the runtime path.

The finished box boots on its own, joins Wi-Fi, talks directly to Jellyfin, and plays media from the DIRECTV remote. No dish. No helper PC.

Hardware

DIRECTV HR54-700
Broadcom BCM7346B2
2x BMIPS5000 ~1.3 GHz
MIPS32 big-endian o32
1 GiB physical RAM
Linux 3.3.8
BusyBox 1.16.1
uClibc 0.9.32.1
1 TB WD AV-class hard drive

The root filesystem is read-only SquashFS. The hard drive has normal Linux storage, a huge recording partition, and a firmware staging partition.

The useful part is that DIRECTV already shipped all the hard media stuff:

networking
WebKit / ITV
remote input
MPEG transport handling
H.264 decode
AC3 decode
HDMI output

I had no reason to replace Linux or write a video driver. I wanted to reuse all of that.

Persistent root came from the plugin loader

The receiver has a boot-time plugin mechanism that eventually runs a deployment path as root.

The useful bug was simple: one part of the loader verified a genuine signed image, while a later part resolved what it mounted by filename. Those were not bound tightly enough to the same file.

So a real signed image could satisfy verification while a different same-basename image was the one that actually got mounted.

No signing key. No forged signature. The loader just disagreed with itself about which file it had approved.

I wrapped the vendor indexer, ran my setup first, then exec'd the original vendor binary so the stock system kept working.

boot
 |
 v
vendor plugin deploy
 |
 v
asset-7 wrapper
 |
 +--> persistent setup
 +--> root shell / helpers
 `--> original vendor indexer

Everything I added lives under /var. I did not modify flash.

Once I had root I started using the crap already in the box

I exposed a root shell on TCP 5777, used the receiver's existing transfer service, and started inventorying the middleware.

The stock app is a proprietary pile of Druid UI code, Siege VM classes, ITV/WebKit, Broadcom media plumbing, and satellite-service logic.

The satellite-service logic was immediately annoying because there was no dish attached.

Getting rid of the 775 blocker

The main no-satellite blocker was OSD 36, extension 775.

SWM not detected
    |
    v
SignalManager NOT_DETECTED
    |
    v
Aggregate775TestResult
    |
    v
OSD 36 / extension 775

The receiver already had a diagnostic command to remove it:

dt removeOsd -osd 36 -session 0

That got the menus usable again. I wasted some time looking at fake satellite state, then quit caring. I did not need satellite state for what I was trying to do.

playURL was the important piece

I first poked at the stock DVR playback path. That immediately dragged in access-card and DVR-subscriber state, which was pointless for Jellyfin.

The middleware also had a generic URL playback command:

<com.ucentric.pvruconnect.DirectTest
    command="playURL"
    url="http://host/media.ts"/>

I gave it an MPEG transport stream. It fetched the URL and fed it into the stock media pipeline:

HTTP
 |
 v
MediaPlayer.watchRemoteStream()
 |
 v
dvr_core LocalPlayer
 |
 v
RemoteMediaStream
 |
 v
TS demux
 |
 v
Broadcom CDI/NEXUS
 |
 v
HDMI

H.264 video and AC3 audio decoded normally. First I got color bars and a test tone, then I moved to actual media.

At that point I had arbitrary HTTP media going through the receiver's real hardware decoder without touching the DIRECTV recording-entitlement path.

Jellyfin only needed to feed it the right format

The safe playback profile was:

container: MPEG-TS
video:     H.264
audio:     AC3
channels:  2
bitrate:   conservative

I asked Jellyfin for PlaybackInfo, forced transcoding, took the MPEG-TS URL, and relayed it to the HR54.

The first real movie I used was A Trip to the Moon. The source itself was not receiver-friendly, which was fine. Jellyfin transcoded it and the HR54 played it with picture and sound.

That happened Sunday night.

Pause was fake, so I did it in the stream

The vendor API has commands named pause, resume, stop, and so on. They returned success and did nothing useful for plain playURL.

The logs explained why:

Cannot pause, no vodcapture created

The normal DIRECTV transport path expects a VODCapture. playURL does not create one.

So transport moved into the HTTP relay:

pause  -> stop sending MPEG-TS bytes
resume -> start sending again
stop   -> close the stream
seek   -> restart the Jellyfin transcode at StartTimeTicks

Short pauses can keep the same socket open. After roughly twenty seconds the receiver decides the stream starved, so a long-pause resume starts a new transcode at the saved position.

Ugly enough. Works.

The UI had to run on the DVR

The early prototype used a browser on another machine. Fine for debugging. Not acceptable as the actual interface.

The HR54 already has an ITV/WebKit runtime. I got it loading my HTML, CSS, and JavaScript, and XHR back to the backend worked.

The first version was running but invisible. WebKit had loaded everything, the key dispatcher was active, and the TV was still showing DIRECTV.

The problem was Druid/app ownership, not HTTP.

The receiver-native launcher now clears the blocking OSD, walks the stock UI back to the right base screen, then launches the ITV app so WebKit actually owns the visible surface.

clear blocking OSD
    |
normalize Druid screen state
    |
launch ITV app
    |
fullscreen Jellyfin

That fixed the overlay garbage and gave me a real full-screen TV client.

The TV client is intentionally dumb

This WebKit is old. I wrote the frontend for that instead of pretending it is Chrome.

ES5 JavaScript
large text
large focus targets
UP / DOWN / LEFT / RIGHT
SELECT
BACK / EXIT
six-item paging
on-screen keyboard
search
saved browse state

No React. No giant framework. No mouse. The DIRECTV remote moves a deterministic focus model around the page.

Search uses an on-screen keyboard, and the backend filters Jellyfin virtual items so folders do not show up pretending to be playable files.

Quick Connect got rid of the development API key

The first version used a pre-provisioned Jellyfin API key. That was just scaffolding.

The final client uses Jellyfin Quick Connect. The TV shows the six-character code, I approve it from another Jellyfin client, and the native backend stores the resulting user token under persistent /var storage.

TV: GET CODE
    |
    v
six-character code
    |
    v
approve in Jellyfin
    |
    v
HR54 stores user token

The Quick Connect secret and access token stay in the backend. They are not exposed to the old WebKit page.

The helper PC had to go

The development version used another Linux box at .25. It handled Jellyfin API calls, auth, search, artwork, playback state, and stream relay.

That worked, but it meant the HR54 was not standalone. If .25 was off, Jellyfin was dead.

I rewrote the required backend in C and ran it directly on the receiver.

The production service is hr54-jf, a static MIPS32 binary listening on port 8130. It only implements what this TV client needs:

/api/status
/api/auth/status
/api/auth/start
/api/auth/poll
/api/auth/logout
/api/libraries
/api/items
/api/play
/api/transport
/api/seek
/api/tv/state
/art/<item>
/play/<opaque-token>.ts

It talks directly to Jellyfin, handles Quick Connect, stores local state, gets PlaybackInfo, relays the transcode, and starts local playURL.

The build target matters:

zig cc \
  -target mips-linux-musleabi \
  -mcpu=mips32 \
  -static -O2 \
  -o hr54-jf \
  hr54_jf.c

mips32r2 produced an illegal instruction on this box. Plain mips32 works.

The final runtime path is:

DIRECTV remote
      |
      v
HR54 ITV/WebKit UI
      |
      v
hr54-jf :8130
      |
      v
Jellyfin :8096
      |
      v
MPEG-TS / H.264 / AC3
      |
      v
HR54 playURL
      |
      v
Broadcom decoder
      |
      v
HDMI

.25 can be shut off. The receiver does not care.

Wi-Fi was annoyingly proprietary

The receiver has Wi-Fi hardware but none of the normal Linux setup tools I expected. No wpa_supplicant, no useful iw workflow, no NetworkManager.

It uses a vendor daemon called wlanmanager and stores the saved profile through the receiver's NVRAM manager.

The profile was object type 84 in /dev/nds/nvram0. I mapped the SSID and credential fields and figured out the checksum, which turned out to be a plain 32-bit big-endian byte sum.

/dev/nds/nvram0
  |
  +-- object 84
        |
        +-- SSID
        +-- credential
        `-- big-endian byte-sum checksum

I built the modified image offline, verified the exact changed bytes, then used the vendor NVWriteBytesToEEPROM primitive to write only the needed ranges.

I kept Ethernet up the whole time. I was not going to break the one management path I knew worked while changing Wi-Fi config.

After reboot, wl0 associated and got DHCP normally.

Boot persistence is one wrapper

The same asset-7 boot wrapper restores the useful changes every boot. It starts hr54-jf, restores the firewall rule, applies UI overlays, and leaves the stock system alone otherwise.

/var/hr54-persist/jellyfin/
    bin/
    config/
    state/
    cache/
    log/
    ui/
    www/

The stock SquashFS stays read-only. Persistent replacements live under /var and are bind-mounted where needed. Rollback copies are kept.

The actual standalone test

I eventually stopped accepting "basically standalone" and just killed the development backend:

192.168.88.25:8130 DOWN

Then I tested the box again:

cold boot
Wi-Fi
MENU
fullscreen Jellyfin
Quick Connect
libraries
search
artwork
play
picture + audio
pause
resume
seek
stop
return to Jellyfin
saved UI state

All of it worked directly between the HR54 and the Jellyfin server.

What I kept

I reused almost everything that was already good at being a set-top box:

DIRECTV Linux
Druid UI shell
ITV/WebKit
MediaPlayer
dvr_core
Broadcom demux/decoders
remote plumbing
HDMI path

I replaced the parts that were blocking what I wanted:

boot-time control
775 blocking state
media source
TV application
Jellyfin backend
transport behavior
auth state
Wi-Fi profile
helper-PC dependency

There was no reason to rebuild the whole OS just to play a movie.

Remaining DIRECTV junk

The receiver still has some stock crap I am cleaning up. Error 755 text can still show old card-service wording in places, and the bouncing screensaver is still using a DIRECTV logo resource that turned out not to be the file I first replaced.

Neither affects Jellyfin. I am changing the 755 strings to useful "wait / press GUIDE" instructions and tracing the actual screensaver asset instead of guessing at filenames.

Where it is now

I can unplug Ethernet, reboot the HR54, wait for Wi-Fi, press the remote, and use Jellyfin.

Search works. Quick Connect works. Playback works. Pause, resume, seek, and stop work. When playback ends I go back to the Jellyfin UI instead of the DIRECTV menus.

No satellite connection. No helper PC. Just the HR54 and the Jellyfin server.

HR54-700
  |
  +-- persistent root via boot plugin wrapper
  |
  +-- receiver-native hr54-jf :8130
  |      |
  |      +-- Quick Connect
  |      +-- Jellyfin API
  |      +-- artwork
  |      +-- PlaybackInfo
  |      +-- stream relay
  |      +-- pause / seek / stop
  |      `-- TV state
  |
  +-- ITV/WebKit Jellyfin UI
  |
  +-- playURL
  |
  +-- Broadcom H.264 / AC3 decode
  |
  `-- HDMI

        Wi-Fi
          |
          v
   Jellyfin server

Free DVR on Saturday. Jellyfin Sunday night. Standalone after I got tired of the helper box being in the way.