Casting video files from Linux to AppleTV

Posted on . Updated on .

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, where HOSTNAME is the Linux box host name (see hostnamectl --help if your distribution uses systemd). 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.

#!/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

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.

Load comments