The Issue

When casting from YouTube to a Chromecast, sometimes the audio playback will skip and stutter. This issue is independent of the quality of the video, the FPS, or the internet connection. Trying to watch certain videos will reliably cause playback issues on the Chromecast.

According to this thread, the playback issue only appeared in the latest and final firmware update for the Chromecast. Successful playback of the videos was possible in the past, and casting the downloaded YouTube videos eliminates the problem entirely. The problem exists solely when casting certain videos using the YouTube app.

A Solution

Given that the problem only occurs with the YouTube app, you can download a video and cast it via castnow or catt, skipping the YouTube app entirely.

You could do something like:

#!/usr/bin/env bash

export url="https://youtu.be/Kas0tIxDvrg"

function cast() {
    url="$1"
    
    filename=$(youtube-dl --get-filename "$url")
    youtube-dl "$url"
    
    # wait a bit to download the file
    castnow "$filename"
    rm "$filename"
}

cast "$url"

But having to skip using the YouTube app to cast is already a clunky solution, and downloading every video before playing them is an even worse user experience. Instant playback and ephemeral streams are what make for a pleasant video streaming experience in 2020, and this solution implements neither of them

A Slightly Better Solution

Since youtube-dl allows us to output to stdout, if we can hook its stdout to a casting app, we could emulate the instant playback and ephemeral videos we expect because we don’t have to wait for an entire file to download.

Unfortunately, castnow and catt won’t cast from stdin. You’re expected to pass it file locations to cast from.

This is where one of my favorite shell features really shines: process substitution.

With process substitution, Bash gives us a convenient way to make ephemeral anonymous pipes. This method is both efficient and concurrent, making this primitive an apt choice to build a solution to the problem at hand. A process reading the pipe blocks until the pipe is opened by another process for writing. A process writing to the pipe will suspend until the pipe’s buffer is read by another process. The anonymous pipe will automatically remove itself, and when it is manually removed, its dependent processes will be terminated.

When using process substitution, a process’ stdout is hooked up to an anonymous pipe. That pipe can be accessed from a file descriptor, and the location of the pipe is given to the calling process.

$ echo <(echo "Content sent to pipe")
/dev/fd/63

$ cat <(echo "Content sent to pipe")
Content sent to pipe

In the example above, the <(command) syntax is how we invoke process substitution in Bash. The output of command is written to an anonymous pipe, and the calling process is given the location of the file descriptor to access that pipe.

Using that example, we can take advantage of process substitution:

vlc <(youtube-dl -q -o - "$url")

The command above will play the YouTube video locally with VLC, and illustrates that process substitution can work for our use case.

However, when we try to use castnow, we can’t cast from the pipe:

$ castnow <(youtube-dl -q -o - "$url")
Error: Load failed

Nor can we cast with catt:

$ catt cast <(youtube-dl -q -o - "$url")
Error: The chosen file does not exist.

We know we can use VLC locally, and VLC also lets you cast to a Chromecast using its IP address.

Let’s try that again using VLC:

function cast_vlc() {
    path="$1"

    # get the ip address for chromecast.lan host
    ip=$(dig +short chromecast.lan | tail -n 1)

    vlc -I ncurses \
      --sout '#chromecast' \
      --sout-chromecast-ip="$ip" \
      --demux-filter=demux_chromecast \
      "$path" < /dev/tty
}

cast_vlc <(youtube-dl -q -o - "$url")

That works.

As an aside, we hook VLC’s stdin to /dev/tty so that we can use the ncurses interface even if we invoke the function from a script.

Let’s look at the ncurses interface.

It only displays the file descriptor, and very little about the video itself. I’m not a fan of that.

A Better Solution

Instead of using anonymous pipes, we can use named pipes. Named pipes are like anonymous pipes, except they are not anonymous (they have a name) nor are they ephemeral. Named pipes still give us the efficiency and concurrency benefits that anonymous pipes give us, but Bash lacks the syntactic sugar it has for process substitution when it comes to named pipes.

This is how we create named pipes, write to them, read from them and remove them.

$ mkfifo ourpipe
$ echo "Content in pipe" > ourpipe &
$ cat ourpipe
Content in pipe
$ rm ourpipe

Not as pretty as <(command), but it gets the job done.

We can give a named pipe the same name as our YouTube video, and that way, the VLC interface will show the name of what we’re watching.

function cast_ytdl() {
  url="$1"

  # create a temporary named pipe
  # why? because vlc will show the file descriptor path if we just use process substitution
  filename=$(youtube-dl --get-filename "$url")
  path="/tmp/$filename"
  mkfifo "$path"

  # download in background, push to named pipe
  youtube-dl -q -o - "$url" > "$path" &
  pid="$!"
  disown $pid

  # cast from named pipe
  cast_vlc "$path"

  # cleanup process and named pipe
  kill -9 "$pid" &> /dev/null
}

cast_ytdl "$url"

This works, too.

We need to manually create a named pipe with mkfifo, redirect youtube-dl’s stdout to the named pipe while running the process in the background, and then cleanup the process after casting from it via VLC, otherwise it might linger in the background. Each of those tasks would have been handled for us automatically using process substitution.

But it does look a bit better:

Caveats

Pipes, anonymous pipes and named pipes are also known by another name because of the way they behave: FIFOs, or first in, first out. What’s written to the pipe is read from the pipe in first in, first out order. This behavior maps well to video streaming.

While you can interact with anonymous and named pipes like you would a file, the interface isn’t 1:1 with a standard file. You cannot seek() forward or backward in pipe, you can just read the next forward values. For our use case, that means we cannot skip forward or backward in our streaming videos. We can only play, pause or stop the video.

That’s not a problem for me, however it’s something that can be mitigated if it’s a problem for you. The first solution I can think of would be to write to a temporary file via youtube-dl and read from it. Or perhaps a temporary spooled file can act as a buffer for the pipe, such that you can seek() through the buffer, but the buffer itself is ephemeral unlike a normal file.