Streaming and recording IP TV

Sport is better on a big screen and in my case that means a project screen in the wintergarden. I have a Chromecast and a laptop connected to the projector and previously I have been using the laptop with Kodi for movies and various web based streaming services. The Chromecast was basically only used for guests that wanted to stream something from their mobile phones via the guest wifi.

If you have an IPTV provider that provides an unencrypted and available stream, and there are many such, there is nobody stopping us from developing a solution with focus on user friendliness with a fairly low investment of time and hardware which is better than what the various out-of-the-box solutions can provide. I will point out the key points from my solutions below so you can stitch together something for yourself quickly if you find yourself with similar needs at some point.

I use and like Home Assistant, and have created a view there where the sport channels can be watched, also the last recorded event. Nothing fancy but the actions are available with a single finger tap which was my first priority. I did not want to fiddle with a computer or start streaming from the phone when it is time to watch something. There are obviously some steps involved so let’s take a look at what we need.

For a starter we need a Home Assistant installation and I run mine on a RPI4 which is sufficient for my needs (and it is doing a fair bit nowadays). To go into details on HA is beyond the scope of this blog post. (In theory we could maybe get away with letting the same RPI4 be the server that acts as streaming proxy and recorder as well if we avoid the transcoding but that is less robust and I have other servers around at home so there is no need to stretch the boundaries of the RPI4’s capacity.) I have another server, the stream server, running the scripts for exposing the stream to the Chromecast and to do the stream recording and hosting the small web site to do the recording.

In Home Assistant I have a Home Assistant script that invokes a shell_command which executes a script on the “stream server” via non-interactive ssh. The script on the stream server uses VLC to point the Chromecast to the stream corresponding to the channel:

/usr/bin/cvlc "http://stream-url-here" --sout="#chromecast{ip=X.X.X.X}" --demux-filter=demux_chromecast --sout="#transcode{venc=x264{preset=ultrafast},vcodec=h264,threads=1}:chromecast{ip=X.X.X.X,conversion-quality=0}" --loop

Why transcoding with VLC we might ask ourselves, would it not work to just hand over the stream url to the Chromecast with something like go-chromecast or pyChromecast? I did tests with all three and VLC is clearly more fault tolerant due to the transcoding it is doing on the fly, meanwhile the other two relies on the stream being compatible and relies on the Chromecast to handle it. VLC is able to handle a much broader range of formats so your chances are must better that way. The stream from my provider did not work when Chromecast tries to interpret it “natively”. Your success may vary… The “–loop” part is essential since it will make vlc re-try in case the connection is interrupted.

That was the “live streaming part”, now over to the recording. I use the same mechanism (with VLC) when playing already recorded streams and go-chromecast for controling the stream (play/pause/rewind/forward/seek). As an example, let’s say I want to record an NHL game at 2 am in my time zone, we would need a way to specify the channel, time and duration. I made a rudimentary web ui for this purpose. The only interesting feature there is probably the possibility to look up matching events based on a team. Since I anyway source all sports events from a sports TV site and have the future events represented by ical files on a reachable filesystem, I can simply rgrep among all the ical files and find info about an upcoming event matching for example “Colorado” and prepopulate the channel and time fields. The biggest hurdle was to transform datetime stamps in the formats used in ical/the html component datetime-local and something that “at” understands. Fortunately this is all pretty simple with date which understands the datetime from the html component and can give us the format that at prefers, date -d "${STARTTIME}" +"%H:%M %Y-%m-%d"

When the form is submitted, a record job is placed on the stream server with “at”, which is a convenient tool to schedule ad-hoc jobs which should run at a specific time. The job is simply to let ffmpeg record the stream url matching the channel and save it to the disk, followed by a command to let ffmpeg switch container format (from mkv to mp4, the Chromecast was doing better with mp4) but avoiding re-encoding video and audio streams (still with h264) just to avoid any irregularities caused when saving the stream which most likely will prevent proper navigating in the file (relative and absolute positioning). Such a “container switch” is fast since the streams are left as is. The last thing the scheduled job does is to update the link to the “last recording” to point to the new one.

ffmpeg -i "stream-url-here" -y -err_detect ignore_err -c:v copy -c:a copy -t $SEC_LENGTH /home/username/public_html/record/record_${STARTTIME//:}${CHANNELNO}.mkv
ffmpeg -i /home/username/public_html/record/record${STARTTIME//:}${CHANNELNO}.mkv -c:a copy -c:v copy /home/username/public_html/record/record${STARTTIME//:}${CHANNELNO}.mp4
ln -sf /home/username/public_html/record/record${STARTTIME//:}${CHANNELNO}.mp4 /home/username/public_html/record/latest.mp4
rm /home/username/public_html/record/record${STARTTIME//:}_${CHANNELNO}.mkv

I should point out one thing which caused my a bit of head ache. The scheduling of the at job needs to be done differently when scripting compared to in an interactive shell. I first tried to pipe the job to at but that did not work because the standard input in the script is different. To avoid that, simply use a “here document” (from the Advanced Bash scripting guide: “A here document is a special-purpose code block. It uses a form of I/O redirection to feed a command list to an interactive program or a command, such as ftpcat, or the ex text editor.”)

command <<EOF-MARKER
input
more input
EOF-MARKER
nextCommand

For me that meant:

at -m ${FORMATTED_STARTTIME} <<!
the commands to run in at job
multiple commands can be used, one per line
!

A typical scenario when it comes to actually watching recordings, is to simply play the latest recording, for example an NHL game that took place during the night. One tap in Home Assistant is enough since a symbolic link points to the latest recording, and we can use the same setup as outlined above, but point Chromecast to the url for the recording on the stream server (it saved the recording to a directory in the user’s public_html exposed via Apache2 (a2enmod userdir). I did some experiments with dbus-send to VLC, and meanwhile that works, using go-chromecast for the navigation turned out more convinient. With that utility you can simply do things like “/usr/bin/go-chromecast seek 60 -a X.X.X.X” to fast forward a minute (a commercial break…) or using seek-to to go to an absolute position.

You might have noticed I have been referring to the Chromecast by IP address. Usually the tools make it possible to call it by its friendly name but I figure the name is more likely to change (for example when resetting the device) than the IP address due to the static IP address allocation based on the MAC address (this would only need a config change when router changes which is less frequent).

This entry was posted in sport, tv, webbprojekt, webbservern and tagged , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *