It’s been a while since I had to face off against my favorite nemesis – the MKV file. Specifically, the sort of MKV files that come out of well-intentioned anime distributors and that make my life hard when what I really want to do is stream them to an Apple TV using the built-in “Computers” app.
This app is firmly grounded in “so all of your media is perfectly legitimate, RIGHT?” and only plays a very small subset of the kinds of video files that are out there. No MKV containers, no obscure codecs, certainly no support for soft subtitles.
To make this work, I need h.264 or possibly h.265 video and aac or AC-3 audio in an mp4 or m4v container and any subtitles need to be burned into the video, and since the average anime release these days is something like “AV1 video and opus-encoded audio with seven different subtitle languages”, this typically means loading up a folder of files in Handbrake and manually choosing language and subtitle options for every episode before pushing the “start queue” button and walking away.
(Yes, technically I could run VLC or DS video on the ATV, but then I wouldn’t see content I had purchased from iTunes in the same place as the less-reputable content)
And, I mean, manually processing a folder of video files in Handbrake works. It’s a really solid encoder and I have been using it for years. It’s pretty much muscle memory by this point.
On the other hand, my workflow here is a bit labor-intensive. There’s a lot of setting options on a per-episode basis and real potential for human error on my part. It would be better if I could throw the whole folder at the CLI version of Handbrake, like this, and this DOES work – but it’s quite fragile. It always uses the first audio track and first subtitle track:
#!/bin/bash
mkdir out
for filename in *.mkv
do
filenamenoext=${filename%.*}
handbrakecli -i "$filenamenoext".mkv -o out/"$filenamenoext".mp4 -Z "Apple 1080p60 Surround" --subtitle 1 --subtitle-burned --crop-mode none
done
(As always, my scripts come with no guarantees. Use at own risk)
For those cases where you have video files and external srt-format subs, and assuming that the srt files are in the same folder and have the same name as the video files, you can use something like this:
handbrakecli -i "$filenamenoext".mkv --srt-file "$filenamenoext".srt -o out/"$filenamenoext".mp4 -Z "Apple 1080p60 Surround" --subtitle 1 --subtitle-burned --crop-mode none
As a side note, I’m using the “Apple 1080p60 Surround” preset here because it gives me a highly-compatible video file and does it reasonably quickly. Using the “Apple 2160p60 4K HEVC Surround” preset would give me a file that is consistently at least 30% smaller but also takes four times as long to encode.
So, in order to get good output from simple conversion script like this, I need to make sure that the input is good. That is to say, it needs an mkv file where the first audio and subtitle tracks are the ones I want… which is not often the case. It’s very common for mkv files to have the first audio track be the English dub and the first subtitle track be the “Signs and Songs” subtitle track.
I had a very judgmental comment here about dub fans, but I have omitted it in the interest of world peace.
I have, however, been lazy enough to not really delve into this too much.
Recently, however, I was presented with an interesting set of video files that had Japanese, Spanish, and Catalan dubs, along with several different English and non-English subtitle options, and this got me interested in digging into how ffmpeg can work with content streams – based on the metadata associated with the streams, rather than just their index in the file.
My first attempt looked something like this:
ffmpeg -i infile.mkv -codec:a:m:title:Japanese copy -vcodec copy -codec:s:m:title:English copy outfile.mkv
This actually worked quite well. It produced an output file with the original video, the Japanese audio track, and only those subtitle tracks with a title of “English”. I didn’t realize at this point that the “title” metadata was separate from the “language” metadata, so the only reason this was working was because my command matched the titles for these audio and subtitle tracks.
What didn’t work well is that mkv files can have attached fonts, and this command doesn’t copy the fonts. So the subtitles that referenced the fonts in the original mkv file just rendered in the default Handbrake font. This needed tweaking.
After some digging, it appears that the only way to get ffmpeg to copy attachments from input file to output file is to use the -map option, specifically “-map 0″to tell it to copy everything from the input file followed by a number of “-map -0” commands to tell ffmpeg which to drop. This gets interesting fast.
The command that did the trick for these specific files wound up looking like this, but made me wondering if there wasn’t a better way to accomplish it.
ffmpeg -i file.mkv -map 0 -map -0:m:title:"Catalan" -map -0:m:title:"Spanish 1" -map -0:m:title:"Spanish 2" -map -0:m:title:"Spanish 3" -map -0:m:title:"CrabSubs" -c copy outfile.mkv
Note that this worked because both audio and subtitle streams had a title set to the language. I was still working off the title field without realizing that there was a better way to do things. Also, “-c copy” was necessary because otherwise ffmpeg wants to re-encode all of the streams and this dies because of subtitles. We don’t want it to re-encode regardless, but even if I had wanted to it would not be possible with a subtitle track present.
After a bit more experimenting, I came up with this ffmpeg command that gives me only Japanese language audio and English language subtitles, ASSUMING that the audio and subtitle streams are correctly flagged for language:
ffmpeg -i infile.mkv -map 0 -map -0:a -map -0:s -map 0:s:m:language:eng -map 0:a:m:language:jpn -c copy outfile.mkv
Let’s break this down:
-map 0 : include everything from the original file. Streams and attachments (fonts)
-map -0:a : don’t include any audio tracks
-map -0:s : don’t include any subtitle tracks
-map 0:s:m:language:eng : do include any subtitle tracks with language eng
-map 0:a:m:language:jpn : do include any audio tracks with language jpn
-c copy : copy all streams, don’t re-encode.
This is almost perfect. It’s dependent on the original muxer to flag their streams properly, but this is often the case. The only problem is that you get ALL English subtitle tracks, including the “Signs and Songs” track which is often the first subtitle track. In this case I got around it with a two pass method.
ffmpeg -i outfile.mkv -map 0 -map -0:s -map 0:s:m:title:Dialogue -c copy outfile2.mkv
This just takes the output from the first command and applies these rules to it before writing it out to a new output file:
-map 0 : include everything from the original file. Streams and attachments (fonts)
-map -0:s : don’t include any subtitle tracks
-map 0:s:m:title:Dialogue : do include any subtitle tracks where the “title” field is “Dialogue”
-c copy : copy all streams, don’t re-encode.
There’s almost certainly a better way to do this. “map” commands are evaluated sequentially so there is probably a sequence to put these in which would do it in one pass. Sadly,
-map 0:s:m:language:jpn:m:title:Dialogue
…does not work. You can’t specify two metadata comparisons in a single map command.
If you don’t want to go to the effort of massaging the input file TOO much, you can always just specify the subtitle track to use when crafting your HandbrakeCLI command line:
handbrakecli -i file.mkv -o file.mp4 -Z "Apple 1080p60 Surround" --subtitle 2 --subtitle-burned --crop-mode none
Anyway, I ended my latest exploration of ffmpeg feeling slightly more accomplished. I even whipped up a little script to handle the .webm files that my PS5 likes to generate when I do screen recordings:
#!/bin/bash
mkdir out
for filename in *.webm
do
filenamenoext=${filename%.*}
handbrakecli -i "$filenamenoext".webm -o out/"$filenamenoext".mp4 -Z "Apple 1080p60 Surround" --crop-mode none
done