cd ../writing

Jul 28, 2026 · 9 min read

Roku-Proofing a Media Library: One Tdarr Flow for HEVC, Subtitles and Disk Space

Some files in my Emby library simply would not play on Roku, and the cause wasn't the video at all — it was three subtitle track flags. Here's the Tdarr flow that fixes them library-wide while transcoding to 10-bit HEVC, and only keeps the re-encode when it actually saves space.

  • Homelab
  • Automation
  • Troubleshooting
Terminal output of mkvmerge -J piped through jq, listing three SubRip subtitle tracks with forced false on every one and default true on exactly one English track.
What a Roku-safe file looks like: every subtitle SRT, nothing forced, exactly one default.

My home server runs Emby, and the clients that matter are Roku devices — a Roku Ultra and a couple of Roku TVs. Most of the library plays fine. But a stubborn minority of files behaved badly in ways that made no sense: subtitles that refused to appear, and some files that wouldn't play at all.

The obvious suspects were all wrong. It wasn't the codec, the bitrate, the container, or the network. The same files played perfectly in a browser and on a Chromecast. It was the subtitle track flags — metadata, not content — and once I understood that, the fix turned into an automation problem rather than a video problem.

What follows is the whole thing: the diagnosis, the standalone tool, and the Tdarr flow that now does it for every file that enters the library, alongside a 10-bit HEVC transcode that's careful about both quality and disk space.

The diagnosis, which I did not arrive at alone

I want to put the credits up front rather than at the bottom, because I didn't figure this out from first principles — I found people who'd already hit it and written it down.

The clearest statement of the problem comes from an Emby community thread about forced-display subtitles not showing on Roku TV and Roku Ultra, where an Emby team member lays it out directly:

Yes, it is an issue with the Roku player. What you want to make sure of is that no tracks are set as forced and default and that you don't have more than one track set as default. The Roku player has trouble if there is anything off in the track flags.

That is the entire bug in three clauses. A parallel thread on r/emby describes the same class of weirdness from the user side. And separately, an r/PleX thread on batch ASS/SSA → SRT conversion — and the script posted in it — covers the third failure mode, and is the direct ancestor of the tool I'll get to below. It even supplied the name.

So there are three distinct things that break Roku playback, and a file can have all three at once:

  1. The forced-display flag set on a subtitle track.
  2. More than one subtitle track marked default — or a track that's marked both forced and default.
  3. ASS/SSA (SubStation Alpha) subtitles, which Roku handles poorly at best.

None of these are corruption. Every one of these files is perfectly valid — MKVToolNix, VLC and ffmpeg are all completely happy with them. Roku's player just wants the flags tidy, and a surprising share of scene releases and anime rips arrive with them untidy.

Why the manual fix doesn't scale

The recommended cure is to open the file in MKVToolNix, untick "forced display," make sure exactly one subtitle is default, and re-mux. That works. It works exactly as well as you'd expect for a library of thousands of files, which is to say not at all.

My first instinct was to fix it inside Tdarr, since Tdarr already watches the library. Tdarr ships a native runMkvPropEdit flow node, which sounds precisely right — mkvpropedit is the tool for editing MKV track flags in place.

It isn't. Reading the plugin source settles it: its inputs array is empty, and the only operation it performs is a hardcoded --add-track-statistics-tags. There is no parameter for touching forced or default dispositions. A node that looks like the answer, isn't — which is worth checking early, because the name promises far more than it delivers.

So the flag fixes had to be expressed some other way. That turned out to be ffmpeg dispositions, and I'll come back to it.

bootylift: the standalone version

Before touching Tdarr I built a CLI to fix the library in place, because I had a backlog of existing media files that needed repairing regardless of what new arrivals did.

It's a Python 3.13 rewrite of that pastebin script, and I kept the original's name — bootylift, "it fixes ASS." The joke is the author's, and so is the core idea; the rewrite adds the Roku flag handling, safety guarantees, and idempotency. It walks a directory of MKVs and does three things:

  1. Converts every ASS/SSA track to SRT, re-muxing so the SubStation Alpha tracks are replaced while all other subtitle tracks — existing SRT, PGS — are preserved.
  2. Clears the forced-display flag on every subtitle track.
  3. Enforces at most one default subtitle, preferring English.

That third rule has more nuance in it than it first appears, and the nuance is where the bugs live. If a file has several defaults, it's reduced to one — and the winner is an English track if one exists, even when English wasn't originally the default. If there's no English track, the first originally-default track keeps the flag. And critically: a file that had no default subtitle keeps none. The tool never invents a default. Deciding to show subtitles on a file that previously showed none would be a worse outcome than the bug I was fixing.

Three properties made it safe enough to run across the whole library unattended:

Atomic in-place replacement. The re-mux writes a temp file; the original is only replaced via os.replace after the new file is complete. Any tool failure leaves the original untouched and reports the file as an error.

A fast path for flag-only fixes. When a file has no ASS/SSA subtitles but does have bad flags, mkvpropedit edits the flags in place — no re-mux at all. That's near-instant regardless of whether the file is 400 MB or 40 GB, and most files needing repair fall into this category.

Idempotency. A second run finds nothing to do. This matters more than it sounds, because it's what makes the same logic safe to run inside a continuously-scanning system.

Folding it into Tdarr

The CLI fixes the backlog. It doesn't help with everything that lands in the library tomorrow. For that, the logic had to live in Tdarr — which was already transcoding the library with an aging stack of classic plugins.

Since no native node can set dispositions, the subtitle work happens in a Run Custom Function node that appends ffmpeg arguments to the single pass Tdarr is already going to run. The whole fix is three arguments:

ff.overallOuputArguments.push('-c:s', 'srt');          // ASS/SSA/mov_text -> SRT
ff.overallOuputArguments.push('-disposition:s', '0');  // clear forced + default on all subs
ff.overallOuputArguments.push(`-disposition:s:${defaultIdx}`, 'default');

Clear every disposition bit on every subtitle track, then set default back on exactly one. That single line pairing is what satisfies all three of the Roku conditions at once.

Two things worth knowing if you write one of these yourself.

That misspelling is real. The field is overallOuputArguments — "Ouput". It's baked into Tdarr's own API, not a typo in my code, and using the correct spelling silently does nothing. I lost time to that one.

Guard it for idempotency. The node inspects the streams first and only pushes arguments when something actually needs fixing:

const anyNonSrt = keptSubs.some((s) => String(s.codec_name || '').toLowerCase() !== 'subrip');
const anyForced = keptSubs.some(isForced);
const wrongDefault = keptSubs.length > 0
  && (keptSubs.filter(isDefault).length !== 1 || !isDefault(keptSubs[defaultIdx]));

Without that check, a continuously-scanned library would re-mux files that are already correct, forever. The node also drops image-based subtitles (PGS, VobSub) up front, since they can't become SRT.

The other half: quality and disk space

The same flow replaced a classic plugin stack built around Migz1FFMPEG, whose quality model was bitrate-following: estimate the source's bitrate from file_size ÷ duration, target half of it, and clamp to ±30%. That was a reasonable approach in its day, but it takes the size of the file you already have as the definition of how good it should look. A badly-encoded source stays bad; a generously-encoded one gets punished.

I replaced it with a quality target instead of a size target: VBR constant-quality NVENC at -cq 23 with no bitrate cap, on an RTX A4000.

-rc:v vbr -cq:v 23 -b:v 0 -bf:v 4 -spatial_aq:v 1 -temporal_aq:v 1 -rc-lookahead:v 32

Some notes on why each piece is there, because the defaults will quietly let you down:

VBR-CQ rather than constant QP. Tdarr's native encoder node only offers constant-QP, which is why it's configured with quality off and the rate control supplied by a custom-arguments node instead. The reason matters: under constant QP, spatial/temporal AQ and lookahead don't meaningfully do their job. Under VBR constant-quality they do — bits get redistributed toward complex regions and frames, which is quality-per-byte you get for free.

Every flag is :v-scoped. Not cosmetic. The same ffmpeg pass also encodes SRT subtitles, and an unscoped -cq 23 is an invitation for the argument to bind somewhere you didn't intend. Scope them to the video stream and the ambiguity disappears.

B-frames (-bf 4). The native encoder node adds none at all, which is easy to miss — you'd get a working encode that's simply 5–10% larger than it needed to be. The old plugin asked for -bf 5; NVENC HEVC caps out around 4, so it was being clamped anyway.

10-bit (-profile:v main10 -pix_fmt p010le). Even for 8-bit SDR sources, encoding at 10 bits reduces banding in gradients and compresses slightly better. For any HDR source it's mandatory unless you enjoy destroying HDR. Main10 plays on every HEVC-capable 4K Roku.

The design decision I'd repeat: gate the transcode, not the flow

The old stack ended with a file-size check that passed or failed the whole job. If the HEVC encode came out bigger than the source, the file was rejected and nothing happened to it — including the subtitle fixes it still needed.

That's backwards. The transcode is an optimisation; the subtitle and container cleanup is a correctness fix. They shouldn't share a fate.

So in the new flow the size check gates only the video transcode. If the re-encode is smaller, it replaces the original. If it came out bigger — or suspiciously tiny, which means broken — the flow reverts to the original video and still re-muxes it to MKV with the reorder, data-stream removal and subtitle fixes applied. You always get a clean, Roku-safe MKV. You only keep the HEVC re-encode when it actually earns its place.

Twenty-two nodes, and the shape is easier to read as a diagram:

Diagram of the Tdarr flow. Input File feeds Begin Command, then a Check Video Codec decision. The 'not HEVC' branch goes to a second Check Video Codec testing for AV1; its 'not AV1' branch runs Set Video Encoder, VBR-CQ + B-frames and 10 Bit Video, then rejoins the main column of Reorder Streams, Remove Data Streams, Run Custom Function, Set Container and Execute, ending at a Compare File Size Ratio decision. From there, 'smaller' goes to Replace Original File and keeps the transcode, while 'bigger or broken' goes to Set Working File, which loops back into a second remux-only column. That second column — Begin Command, Reorder Streams, Remove Data Streams, Run Custom Function, Set Container, Execute, Replace Original File — is also fed directly by the 'already HEVC' and 'is AV1' branches, so those files are stream-copied and cleaned up without being re-encoded.
The flow redrawn in Tdarr's editor style — same 22 nodes, with the branch conditions labelled. Three paths converge on the remux-only column: already-HEVC, AV1, and any transcode that came out bigger.

Already-HEVC files, AV1 files, and any transcode that came out bigger all converge on the same bottom branch: stream-copy the video, fix everything else, replace. Nothing is re-encoded twice and nothing fails.

The AV1 branch deserves a note, because it's a workaround dressed as a feature. The A4000 can't hardware-transcode AV1, so I pass those files through untouched. The reason it's a separate codec check rather than letting them fail is subtle: a stream copy never decodes the video at all, so an AV1 file can never be marked as a failed transcode. It gets a clean MKV and fixed subtitles like everything else. It does not become AV1-playable on a Roku that can't decode AV1 — that's still the media server's problem at playback time. The flow is honest about what it can't do rather than erroring out.

Verify before you point it at everything

Flows replace originals. Test on copies, with a file chosen to be nasty: H.264, an ASS subtitle, a forced flag set, and a second non-English subtitle track. Add an already-HEVC file and a 4K file too.

Then check the generated ffmpeg command in the job report actually contains what you think it does — hevc_nvenc, -preset p6, -rc:v vbr -cq:v 23 -b:v 0, the AQ pair, -bf:v 4, p010le and main10, -c:s srt, and both disposition arguments. And verify the output rather than trusting the log:

mkvmerge -J out.mkv

You're looking for subtitles reported as S_TEXT/UTF8, forced=0 on every track, exactly one default=1 and ideally an English one, video as hevc, 10-bit. Then confirm the already-HEVC file was re-muxed and not re-encoded, and that a file where HEVC would have been larger kept its original video while still becoming a clean MKV.

What I'd take away from it

The bug is rarely where the symptom is. "This file won't play" points at video. The cause was three metadata flags, and no amount of re-encoding would have fixed it — in fact re-encoding while preserving the dispositions would have burned hours of GPU time and changed nothing.

Correctness and optimisation deserve separate fates. Bundling the subtitle fix behind the same pass/fail gate as the space-saving transcode meant the files that needed help most were the ones that silently didn't get it.

Search for the symptom before you build. The Emby forum thread had the answer, stated plainly by someone who knew, years before I hit it. The engineering that followed was real work — but the diagnosis, which is the part that would have taken longest, was a search away.

Sources

Credit where it's due — this was assembled on top of other people's debugging:

Cite this post

Anojh Thayaparan, “Roku-Proofing a Media Library: One Tdarr Flow for HEVC, Subtitles and Disk Space,” anojh.com, 2026. https://anojh.com/blog/tdarr-flow-roku-emby-hevc-subtitles

© 2026 Anojh Thayaparan. Licensed under CC BY 4.0. You may quote, translate, or build on this post — including citing it as a source in an AI-generated answer — as long as you credit Anojh Thayaparan and link back to the original. Training or fine-tuning a model on it is not licensed.