Last year I started a Bluray collection after watching a Linus Tech Tips video about it. I’m frustrated about the rapidly rising cost of streaming services. For 4K ad-free service, between 2019 and 2026, Disney+ went from $7/mo to $19/mo, and Netflix went from $14/mo to $25/mo. On my “ad-free” subscriptions, sometimes I see ads anyway. I’ve read horror stories about losing “bought” media if the service cancels your account someday.
I knew I wanted convenience and quality at least as good as streaming. This meant digital copies, easily browseable, that play on my Apple TV. Even though Bluray (non-UHD) discs aren’t 4K, the quality looks similar to 4K streaming.
Here are six months worth of lessons I learned the slow way:
Start Small
Make sure ripping works and playback looks good before you spend a lot on hardware and content. You can start with just a Bluray drive, MakeMKV ripping software trial, a few Bluray movies, and an old computer running Jellyfin media server.
Get an HDD, Keep the Originals
Watching ripped Blurays as-is is easy, and the video looks great - as good as any 4K stream on my TV. Getting transcoded movies to be small and look good is a lot harder, with lots of knobs to play with.
I bought a 4 TB NVMe drive ($250) originally, planning to transcode down to 5 GB per movie and discard the 30 GB originals. Six months later, I’m still iterating on my settings and not completely happy. My NVMe drive was 3/4 full, so I got a 22 TB HDD (also $250) to ensure I don’t have to delete the originals and risk having to redo the ripping work. I should’ve just gotten the HDD to start with.
Find a Small Desktop to Host
I initially ran Jellyfin on an old laptop with the NVMe drive, the Bluray drive, and an Ethernet dongle hanging off of USB ports. It was fragile and sporadically unreliable. I eventually got a MiniPC with built-in ethernet and room to put the NVMe drive inside. Since then, everything has been completely reliable.
A small used desktop with onboard Ethernet and bays for the Bluray and HDD drives would’ve been great. Even a ~2020 desktop is more than enough. I don’t need hardware transcoding support, because my clients can all natively play my H264 originals or H265 transcodes.
On Playback
Jellyfin is a great, free server to host your media.
- Learn Jellyfin’s folder and file naming pattern before you start ripping.
- ex: media/Movies/Jurassic Park (1993)/Jurassic Park (1993) - H265v3.mkv
- ex: media/Shows/Adventure Time (2010)/Season 01/Adventure Time (2010) S01E01 - AV1v2.mkv
Dashboard > Libraries > Display > Group films into collectionswill make your library easier to browse.Dashboard > Users > Viewer > Allow Transcoding = Offto ensure the copy on the server is playing correctly as-is, not being converted to play.
On OLED TVs, stutter (occasional jerky motion) is a problem.
- Settings > Video > Clarity > Motion Smoothing = Smooth (LG TVs) seems to look best.
- Players need to ‘Match Content Framerate’ (24 Hz) or run at a Refresh Rate of 24 Hz to look as smooth as possible.
Swiftfin is a good Jellyfin client for Apple TV.
- To get Framerate Matching, in Settings:
- Use Native Player = On
- Use fmp4 with HLS = On
- Force Direct Play = On
On Transcoding
H.265 is my preferred video codec.
- All of my devices (iPad 9th, iPhone 13, Apple TV 4K, and newer) support hardware decoding for it.
- The encoder is mature, running quickly and getting good quality even for challenging scenes.
- AV1 looks promising, but my devices can’t all play it, I struggle to get good quality in dark scenes, and with quality settings high, it’s barely smaller than my H265 copies.
E-AC3 (Dolby Digital Plus) is my preferred audio codec.
- It supports surround sound (5.1, 7.1).
- It is supported on all of my devices.
- It’s much smaller than AC3 (384 kbps for excellent 5.1 audio, vs. 640 kbps AC3)
- You don’t also need AAC Stereo. My tablets and phones all play the audio fine with only an E-AC3 5.1 channel stream.
- Bluray Audio is big. AC3, 5.1 channel, 640 kbps uses 575 MB for two hours - small in a 30 GB Bluray original, huge in a 2-3 GB compact transcode.
- AAC Stereo 128 kbps is all you need for “portable” files for watching on iPads or phones on the go.
MKV as the container file format.
- Needed to support subtitles that are not ‘burned-in’ (always on).
- Seems to work with Apple TV and Swiftfin with the ‘Native Player’ and Framerate Matching, even though the docs say it shouldn’t.
Script your transcoding.
You will iterate often on settings. Automate it so it’s easy to update your whole library.
Use HandBrake to experiment.
- The built-in presets are good defaults.
- It’s easy to find and adjust settings in the user interface.
- It’s easy to get the latest version of HandBrake.
- New AV1 encoding libraries were much, much faster than old ones.
- Once your settings are good, make a custom preset, and export it as JSON, then:
Automate with HandBrakeCLI
FFMPEG is often recommended, but…
- It’s hard to find arguments, get them right, and combine them successfully for different encoding options.
- It’s hard to get recent builds with recent libraries on Ubuntu LTS without breaking stuff.
Here’s my current transcode-incremental bash script.
Test your transcoding first.
- It is so, so much faster to transcode a 10 minute clip than a whole movie.
- You need short snippets (10-30 seconds) played back-to-back to compare audio and video. Choose a scene with challenging details.
- For video, screenshots of single frames are easiest to compare (note: you’ll be pickier than when watching the video).
- Once you see settings you like, objective measures are great for ensuring other content is similar.
- VMAF seems to correspond well to my assessment of how movies look.
On Settings
So far, I’ve settled on:
- Video: H.265, CR 20, Preset Slow. No cropping. SDR (8-bit color).
- Audio: E-AC3 5.1 channel, 384 kbps. No second stereo stream.
- Subtitles: ‘Foreign Audio Search’ on, and copy all subtitle tracks.
- HandBrake Preset JSON
VMAF scores are around ~95, and movies are around 4.5 GB (3-6 GB) transcoded.
Portable/Travel Settings:
- Video: H.265, CR 25, Preset Slow.
- CR 25 is around half the size of CR20
- CR 27 also interesting. ~20% smaller than CR25, VMAF ~90.
- Audio: AAC Stereo 128 kbps.
- Subtitles only for your language.
VMAF scores are around ~92, movies 1-2 GB transcoded.
On Buying Media
- Make a list and keep an eye out. You have to look for deals. It’s not as convenient as just renting them for $4 each anytime.
- Blurays (1080p) are much cheaper than UHD Blurays (4K). They are smaller and still look excellent on my TV.
- Collections are much cheaper per movie. (Terminator 1-6 for $15 from Amazon)
- eBay “Choose Your Own Lots”, Goodwill stores, and Half Price Books are good sources.
- Amazon collections on sale are sometimes surprisingly cheap.
Conclusions
I wish I could say that my “Own Media” setup has perfect playback, my transcoding settings are settled, and my library is complete, but I’m still learning a lot. I’m still tweaking to see perfectly smooth playback like I do from the physical discs in a drive. Video transcoding is much more complicated than music, which I converted to MP3 ages ago and haven’t thought about since.
Still, I’m pleased overall. I’m happy to have many movies and shows that I know I’ll always be able to watch. No subscriptions to pay. No ads. Offline copies that never expire. I have the space to start my own photo and file backups and expand into other projects.
My Stack
HandBrake/FFMPEG Cheat Sheet
# HandBrakeCLI: Transcode using a JSON Preset File
# &> to redirect all output except encoding progress.
HandBrakeCLI
--preset-import-file "$handBrakePresetJsonFilePath" -Z "$handBrakePresetName" \
--quality $quality --encoder-preset $preset \
-i "$inFilePath" -o "$outFilePath" \
&> "$outdir/CR$quality P$preset.log"
# FFMPEG: Compute VMAF Score of clip
# outFilePath MUST BE FIRST to compute correctly.
# `-r 24` and `setpts` needed to compare the same frames consistently.
# `-an -sn` to exclude audio and subtitles (a bit faster?).
# n_threads=12 to parallelize.
# n_subsample=6 to check every 6th frame (~4 per sec, ~2x faster)
# log_fmt=csv:log_path=... to log VMAF of each analyzed frame
# Can plot and check for minimums to compare screenshots.
ffmpeg
-r 24 -i "$outFilePath" -r 24 -i "$inFilePath" \
-an -sn -map 0:V -map 1:V \
-lavfi "[0:v]setpts=PTS-STARTPTS[dist];[1:v]setpts=PTS-STARTPTS[ref];[dist][ref]libvmaf=n_threads=12:n_subsample=6:log_fmt=csv:log_path=$vmafLogFolderPath/$fileName.csv" \
-f null - &> "$outdir/VMAF CR$quality P$preset.log"
# FFMPEG: Extract Clip for Testing
# `-ss` is start time (00:05:00 is five minutes after video start)
# `-to` is end time (15m after start; 10m clip)
ffmpeg -ss 00:05:00 -to 00:15:00 -i "$inFilePath" -c:s copy -c copy "$outClipPath"
# FFMPEG: Extract frame screenshot by exact 0-based frame number (slow)
# NOTE: JPG shows details that PNG didn't have
# -qscale:v 2 to request very high quality image (need screenshot not to be compressed again)
# -vframes 1 to get one frame; `-vframes 24 Frame_%04d.jpg` to get 24 frame images.
ffmpeg -i "$inFilePath$" \
-vf "format=yuv420p,setpts=PTS-STARTPTS,select=eq(n\,$frameNumber)" \
-qscale:v 2 -vframes 1 $outScreenShotPath.jpg
# FFMPEG: Extract frame screenshot by seconds into video (fast, may be inexact)
# -ss must be *before* -i to seek quickly.
# $secondsIntoClipFloat is $frameNumber / 23.976. (ex: 132822 / 23.975 = 5529.7897)
ffmpeg -ss $secondsIntoClipFloat -i "$inFilePath$" \
-vf "format=yuv420p"
-qscale:v 2 -vframes 1 $outScreenShotPath.jpg