#!/usr/bin/env bash set -e function usage { cat <<EOF 1>&2 Usage: `basename $0` [-h] [-a AUDIO_STREAM] [-o OUTPUT_FILE] VIDEO_FILE [SUBTITLES_FILE] Create MKV file containing the video and audio streams from the given VIDEO_FILE without re-encoding. If -a is not used, the program will use stream 1 from VIDEO_FILE. If -o is not used, the default output file will be /tmp/watch.mkv. If passed as an additional argument, SUBTITLES_FILE will be included as an additional stream in the output MKV file and made active by default. Option -h prints this help message. EOF } function usage_and_abort { usage exit 1 } while getopts "a:o:h" OPTNAME; do case "$OPTNAME" in a) OPTARG_AUDIO_STREAM="$OPTARG" ;; o) OPTARG_OUTPUT_FILE="$OPTARG" ;; h) usage; exit 0 ;; *) usage_and_abort ;; esac done shift $((OPTIND-1)) if [ $# -lt 1 ] || [ $# -gt 2 ]; then usage_and_abort fi VIDEO_FILE="$1" SUBTITLES_FILE="$2" # May be empty. AUDIO_STREAM="${OPTARG_AUDIO_STREAM:-1}" OUTPUT_FILE="${OPTARG_OUTPUT_FILE:-/tmp/watch.mkv}" START_WEBSERVER="" WEBSERVER_PORT=64004 WEBSERVER_ROOT="$HOME/Public" declare -a FFMPEG_INPUTS declare -a FFMPEG_MAPS # Video file as input. FFMPEG_INPUTS+=("-i") FFMPEG_INPUTS+=("$VIDEO_FILE") # Pick first stream from it (video), and the selected audio stream (number 1 by default). FFMPEG_MAPS+=("-map") FFMPEG_MAPS+=("0:v:0") FFMPEG_MAPS+=("-map") FFMPEG_MAPS+=("0:$AUDIO_STREAM") if [ -n "$SUBTITLES_FILE" ]; then # Add subtitles file as second input. FFMPEG_INPUTS+=("-i") FFMPEG_INPUTS+=("$SUBTITLES_FILE") # Pick first subtitles stream from it and make them active by default. FFMPEG_MAPS+=("-map") FFMPEG_MAPS+=("1:s:0") FFMPEG_MAPS+=("-disposition:s:0") FFMPEG_MAPS+=("default") fi rm -f "$OUTPUT_FILE" ffmpeg "${FFMPEG_INPUTS[@]}" "${FFMPEG_MAPS[@]}" -c copy "$OUTPUT_FILE" read -n 1 -p "Start web server? [y/N] " REPLY echo case "$REPLY" in [yY]) START_WEBSERVER=1 ;; *) ;; esac if [ -n "$START_WEBSERVER" ]; then echo "Starting web server..." cd "$WEBSERVER_ROOT" busybox httpd -f -vv -p $WEBSERVER_PORT || true # || true helps because the web server is shut down with Ctrl+C and does not return 0. fi exit 0
Casting video files from Linux to AppleTV
A couple of weeks ago I received an AppleTV 4K as an early Christmas present. It’s a really nice device and it immediately replaced my Chromecast Ultra as the way to watch streaming content on my 15-year-old non-smart TV. Of course, the kids love it too!
Before I disconnected my Chromecast Ultra from the TV to put it back into its box, there was a small matter I needed to solve. Sometimes I watch content on my TV by casting it from my Linux PC. Most of that content are rips of my DVD collection, which is thankfully legal in Spain, as far as I know.
Using the Chromecast Ultra, I only had to configure catt once with the name given to the device and then launch catt cast VIDEO_FILE
from the command line on my PC.
Like magic, the video would start playing on the TV provided it had a video and audio format the Chromecast Ultra would understand.
I was looking for a similar experience on the AppleTV, which does not support the Chromecast protocol and uses Apple’s proprietary AirPlay protocol instead.
The Concept
A web search only provided some unconvincing results with old tools that (for the most part) didn’t work, or were not as straightforward, or involved media servers and more complex setups. Long story short, if you want something that works well and is relatively easy to set up and understand in concept for us Linux nerds, the most practical solution I’ve found is to install the VLC app on the AppleTV, which has a convenient feature to play content from a remote URL. This way, you only need to serve the video file from your Linux box using HTTP. Yes, technically this is not really “casting”.
Typing URLs on the VLC app is a pain in the neck unless you pair a Bluetooth mouse and keyboard to your AppleTV, but VLC remembers a history of recently played URLs, so my final setup involves serving the content I want to play from a fixed URL. This way, I can open the VLC app, scroll down to the last played URL and hit the remote button to start playing whatever I’ve chosen to watch at that moment in a matter of seconds.
The URL
My URL looks like http://192.168.68.202:64004/watch.mkv
.
Let’s take that bit by bit.
-
The protocol is HTTP since I’m serving content from the local network and there’s no SSL involved at all.
-
The host name is the local IP address of my Linux box. To make it fixed, I configured a static DHCP assignment in the router so my Linux PC always gets the same address. You only have to do that once. As an alternative, if you use a Pi-Hole as the DNS server and DHCP server, it usually makes your devices available under the name
HOSTNAME.lan
, whereHOSTNAME
is the Linux box host name (seehostnamectl --help
if your distribution usessystemd
). Using this method, you do not need a fixed local network IP address. Of course, a simple third alternative is using a static local IP address outside the DHCP address range, but that can be inconvenient if the Linux box is a laptop that you carry around to other networks and you prefer to use DHCP by default. -
For the port I’ve chosen 64004 as one of the ephemeral ports that’s normally always available on Linux and is easy to remember. You normally want a port that’s above 1024 so opening it for listening does not require root privileges. Another sensible classic choice is 8080, the typical alternative to HTTP port 80.
-
Finally, the file name I’ve chosen is
watch.mkv
and it should be available from the root of the served content. Directory indexing is not required. VLC will parse the file contents before starting to play it, so the file does not need to be an actual MKV file despite its name.
The HTTP Server
You probably want something simple for the HTTP server, and there are several options.
Most Linux distributions have Python installed, and the http.server
module includes a simple built-in HTTP server that can be used from the command line.
python3 -m http.server -d DIRECTORY PORT
will serve the contents of the DIRECTORY
directory using the given PORT
(e.g. 8080 as we mentioned earlier).
I’m also partial to Busybox’s httpd
server because it has a few extra capabilities and it allows you to customize directory indexing with a CGI script.
To use it, call busybox httpd -f -vv -p PORT -h DIRECTORY
.
For most people, the Python server is a more direct and convenient option.
A simple setup is serving the contents of a fixed directory in the file system where you place a symlink named watch.mkv
that points to the real file you want to serve, and you change the target of the symlink each time.
The directory could be a subdirectory of /tmp
, or perhaps $HOME/public_html
.
In my case I serve $HOME/Public
because I use that location for serving files to the local network with multiple tools.
Then, there’s the matter of making sure the AppleTV can connect to your Linux box on the chosen port.
If your distribution uses some kind of firewall, you may have to explicitly open the port.
For example, in Fedora Workstation you could use sudo firewall-cmd --permanent --add-port=PORT/tcp
as a one-time setup command with PORT
being the chosen port.
Subtitles
If the file you serve contains subtitle streams, VLC lets you choose the subtitle track when playing content, so that’s good enough.
The app also has an option to look for subtitles following the same name pattern as the file being played.
For example, placing a watch.srt
file next to the video file in the same directory should work.
I sometimes download SRT files from the web when the included DVD subtitles are subpar.
I’m looking at you, Paw Patrol 2-Movie Collection, and your subtitles that display too early, too late or too briefly!
Almost as disappointing as the quality of the toys.
In any case, I found it very convenient to create a proper MKV file on the fly whenever I want to watch something, embedding the subtitle stream in it if needed, and making it active by default for the most friction-less experience possible.
Note this is different from “burning” the subtitles into the video stream, which requires re-encoding.
When embedding subtitles this way, I don’t even have to bother activating them from VLC.
Creating a proper MKV file on /tmp
is easy and pretty fast (seconds) if you don’t re-encode the video or audio streams.
For that, I wrote the following script and named it ffmpeg-prepare-watch.sh
.
Note you can change WEBSERVER_PORT
, WEBSERVER_ROOT
and the way the HTTP server is launched as mentioned above.
Conclusion
The user experience is now precisely as I like it: simple and understandable, despite using the command line.
When I want to “cast” something from my Linux PC, I call ffmpeg-prepare-watch.sh VIDEO_FILE SUBTITLES_FILE
.
This will create /tmp/watch.mkv
very quickly, and $HOME/Public/watch.mkv
is a symlink to it.
Once ffmpeg
finishes, I answer "y" to the prompt to start the web server, which will serve the contents of $HOME/Public
over HTTP.
Finally, I start playing the last URL from the VLC AppleTV app.
The whole process takes a few seconds and is as convenient as using catt
was before.