This guide will introduce you to the concept of shell pipelines and process substitution by playing youtube videos in your terminal.
We need to install ffmpeg, python3, and sox if not already installed. On OS X, you can use homebrew:
brew install ffmpeg brew install sox brew install python3 brew install pv brew install php # only necessary if you don't already have php installed
If you have older versions already installed, installing will fail as they already exist. I would recommend updating to the latest version though:
brew upgrade ffmpeg brew upgrade sox brew upgrade python3 brew upgrade pv brew upgrade php # you may or may not want to do this depending on if you're ready to upgrade to php8 on your laptop :)
Next, install youtube-dl. Note that if you want to run through this again at a future date you may need to update youtube-dl — old versions of it occasionally break as youtube updates their site.
sudo -H pip3 install --upgrade youtube-dl
Next, install yt-dlp, a fork of youtube-dl that has a workaround (for now) for an issue where youtube has been throttling youtube-dl’s download speed:
sudo -H pip3 install --upgrade yt-dlp
Next, run a couple more commands to install printvideo.com
:
mkdir -p ~/pipelines && cd ~/pipelines curl https://justine.lol/printvideo.com > printvideo.com && chmod a+x printvideo.com
Finally, exit and restart your shell to ensure all these recently installed programs are loaded.
Make sure your laptop is plugged into a power source. OS X may throttle your CPU if not, resulting in choppy video playback.
Download a video:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch?v=gAjR4_CbPpQ -o daft
Try playing it through your regular video player.
Try playing it with printvideo.com
:
./printvideo.com daft.webm zsh: exec format error: ./printvideo.com
If you get an error using zsh, try running it once in bash:
bash -c './printvideo.com daft.webm'
Then run it in zsh again:
./printvideo.com daft.webm
Hmm nothing happens. Turns out the printvideo.com
requires a video in mpeg format. So let’s convert out video:
ffmpeg -i daft.webm daft.mpg
Now try playing it again:
./printvideo.com daft.mpg
Pipes connect the stdout of the process on the left hand side to the stdin of the process on the right hand side.
echo -e "1\n2\n3" 1 2 3
echo -e "1\n2\n3" > echo_test.txt
wc -l echo_test.txt 3 echo_test.txt
A one liner:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=gAjR4_CbPpQ -o - | ffmpeg -i pipe:0 -f mpeg pipe:1 | ./printvideo.com -
Not bad! On my computer, the audio tends to cut out after about 15s or so. We will fix this later with Process Substitution…
To quit the video, run this in another terminal window:
pkill printvideo.com
What do you think will happen if you run the youtube-dl command above alone?
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=gAjR4_CbPpQ -o -
The below command allows us to stream videos while they are being downloaded, which is useful for long videos. For example, downloading a 10 hour fireplace video could take several hours, and it might take up a ton of disk space on our hard drive:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=L_LUpnjgPso [youtube] L_LUpnjgPso: Downloading webpage WARNING: Requested formats are incompatible for merge and will be merged into mkv. [download] Destination: Fireplace 10 hours full HD-L_LUpnjgPso.f137.mp4 [download] 1.2% of 17.79GiB at 3.36MiB/s ETA 01:29:12
But we can stream its playback with our pipeline in realtime. No waiting and no hard drive space required:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch?v=L_LUpnjgPso -o - | ffmpeg -i pipe:0 -f mpeg pipe:1 | ./printvideo.com -
no backpressure:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=L_LUpnjgPso -o - 2>/dev/null | pv >/dev/null 48.0MiB 0:00:05 [12.1MiB/s]
backpressure from ffmpeg:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=L_LUpnjgPso -o - 2>/dev/null | pv | ffmpeg -i pipe:0 -f mpeg pipe:1 >/dev/null 2>&1 106MiB 0:00:15 [7.44MiB/s]
backpressure from printvideo.com:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=L_LUpnjgPso -o - 2>/dev/null | pv | ffmpeg -i pipe:0 -f mpeg pipe:1 2>&1 | ./printvideo.com - >/dev/null 13.9MiB 0:00:08 [ 553KiB/s]
Process substitution allows the input or output of a command to appear as a file. This can generally be done anywhere that a command might expect a filename, even multiple times in one command.
wc -l <(echo -e "1\n2\n3") 3 /dev/fd/11
The output of echo
appears as a file to wc
.
echo -e "1\n2\n3" > >(wc -l) 3
The the input of wc
appears as a file to echo
Here we use process substitution to allow the input of ffmpeg and printvideo.com to appear as a file:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=gAjR4_CbPpQ -o - > >(ffmpeg -i pipe:0 -f mpeg pipe:1 > >(./printvideo.com -))
No pipes!
Simiarly, this should also work, inverting the process substitution order by allowing the output of ffmpeg and youtube-dl to appear as a file:
./printvideo.com <(ffmpeg -i <(yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=gAjR4_CbPpQ -o -) -f mpeg pipe:1)
But the above command fails because ffmpeg appears to be having trouble writing in the background.
On my computer, the audio tends to drop out after about 15 seconds or so of playing the video in my terminal. As a reminder, here is our original command for the video playing one-liner:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=gAjR4_CbPpQ -o - | ffmpeg -i pipe:0 -f mpeg pipe:1 | ./printvideo.com -
Let’s try to fix this with some process substitution combined with tee
:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=gAjR4_CbPpQ -o - | ffmpeg -i pipe:0 -f mpeg pipe:1 | tee >(./printvideo.com -) >(ffplay -vn -nodisp -infbuf -i pipe:0 -volume 20 2>/dev/null) >/dev/null
Hmm, the sound we teed to ffplay
is coming in a bit late. Probably due to some buffering that ffplay
is doing internally. We can try to delay the start of printvideo.com
to match ffplay
's delay. We’ll use delay_buffer
Let’s install delay_buffer
.
curl https://gist.githubusercontent.com/dasl-/eca8cca4228779f89d3ea16afaedfdbe/raw/509004c14e8f645f56eba6ab565202aa59781976/delay_buffer > delay_buffer && chmod a+x delay_buffer
See how it works:
echo 123 | ./delay_buffer -t 2.5 123
time echo 123 | ./delay_buffer -t 2.5 123 echo 123 0.00s user 0.00s system 38% cpu 0.003 total ./delay_buffer -t 2.5 1.81s user 0.71s system 98% cpu 2.545 total
Now let’s use delay_buffer
to try to delay the start of printvideo.com
so that it aligns with when ffplay
begins playing audio:
yt-dlp --extractor-args youtube:player_client=android https://www.youtube.com/watch\?v\=gAjR4_CbPpQ -o - | ffmpeg -i pipe:0 -f mpeg pipe:1 | tee >(./delay_buffer -t 2.4 | ./printvideo.com -) >(ffplay -vn -nodisp -infbuf -i pipe:0 -volume 20 2>/dev/null) >/dev/null
The delay won’t sync up correctly every time, due to "randomness" and "computers". But try running it a couple of times. On my computer, it usually syncs up. You may have to adjust the delay values on your computer.
You now have fully functional streaming video in your shell.