Streaming from Linux to a Chromecast

The Google Chromecast is
an impressive little device. If you haven't encountered one already,
it's a small HDMI dongle which, when connected to a TV screen, allows to
play audio, video, or visual content of a compatible webapp from a
computer or mobile device.

However, it is primarily designed to only stream content from the Web,
and not from your computer itself, which follows the current trend that
everything should be "in the cloud" and is infuriatingly limiting. As
you can guess, that dubious ideology is not my cup of tea.

Luckily, the excellent library
PyChromecast allows to
control the device from a Python program. Yet the issue is that it only
works for codecs the Chromecast is able to decode natively, i.e.,
H.264 and VP8. Besides, the Chromecast is only able to handle a few
containers like MP4 and WebM. What if you want to stream other video
formats ? Besides, what if you want to stream dynamically-generated
content, for instance your screen or a live video from a camera ?

In this example, ffmpeg reads test.avi, recodes the video stream
as VP8 and the audio stream as Vorbis, encapsulates the streams in WebM
format and outputs in out.webm.

We can enhance this command for streaming to the Chromecast. In
particular, ffmpeg allows video filters with -vf, and also
various parameters to tune the VP8 codec. Here, we want constant-bitrate
realtime encoding with a bound on 50% CPU (0 is 100% and 15 is minimum
here). Here, the target bitrate is set to 4Mbps so it fits a crappy Wifi
link, but you could set it higher, to 8Mbps for instance.

The video filter is a bit obscure to read but it boils down to two
actions:

Scale the video uniformly until either its width fits the width of
the screen or its height fits the height of the screen

Pad the scaled video with black so it is centered and the output size
is the size of the screen

However, this creates a new problem. What about subtitles ? If a video
has a subtitles track, they are now ignored and you can't see them. The
simplest solution is to just hardcode subtitles in the streamed video,
whether the video uses a subtitles track or an external subtitles file.

You might have been wondering, why bother with padding ? That's your
answer: since we hardcode subtitles, we want them to take advantage of
the padding so they cover the image as little as possible.

We can call ffmpeg from Python code with the subprocess module,
while directing its output to the standard output (with -). It is
good practice to use arrays to specify arguments rather than passing the
command as a string with shell=True. The latter can be a security
hazard since it allows shell injection.

Finally, we need a small command-line client with PyChromecast to start
the video.

#!/usr/bin/env python3importtimeimportsysimportloggingimportsubprocessimportpychromecastimportoptparseimportjsondefaultRootUrl='http://192.168.0.X:8888/'# Address of the serverparser=optparse.OptionParser()parser.add_option("-d","--device",dest="name",help="send to NAME",metavar="NAME")parser.add_option("-t","--type",dest="type",default="BUFFERED",help="set stream to TYPE",metavar="TYPE")parser.add_option("-l","--list",action="store_true",dest="list",default=False,help="list names")(options,args)=parser.parse_args()ifoptions.list:print(json.dumps({'devices':list(pychromecast.get_chromecasts_as_dict().keys())}));exit(0)ifoptions.name:cast=pychromecast.get_chromecast(friendly_name=options.name);else:cast=pychromecast.get_chromecast();cast.wait()ifnotcast.is_idle:cast.quit_app()time.sleep(1)iflen(args)==0:print(json.dumps({'device':cast.device.friendly_name}))exit(0)if"://"inargs[0]:url=args[0]else:url=defaultRootUrl+args[0]print(json.dumps({'device':cast.device.friendly_name,'url':url}))cast.play_media(url,"video/webm",stream_type=options.type)cast.media_controller.enable_subtitle(0);exit(0)