I like to watch broadcast TV from time to time, as it is free and local, but I'm used to the usual affordances of the streaming era like the ability to pause or watch on any of my devices, and to be able to record TV, so when my wife and I started to watch Jeopardy! more religiously I decided to investigate getting a TV tuner so I could record episodes.
TV tuners have been supported in the Linux kernel officially for a really long time at this point, and I was vaguely aware of this from high school when I used a TV tuner card to play GameCube on my desktop monitor which was quite some time ago.
The state of Linux TV today is.. weird. When I bought a tuner a few years ago, I wound up buying a Hauppauge WinTV-dualHD USB device. Hauppauge makes a whole bunch of different tuners, and as far as I could tell, is basically the only manufacturer around. Their marketing copy is confusing and their website kind of sketches me out, and the firmware is shitty: tuning to a frequency takes a hysterically long time and the process is poorly behaved once it's started.
The documentation for using TV tuners on Linux is all super old and if you search online for how to install the device, you'll find firmware install instructions that have probably not been necessary in a decade and that you shouldn't follow, but the support is all at the kernel level so when you plug in the tuner, if you aren't looking at dmesg
you'd have no idea.
In practice, I mainly watch broadcast TV through Plex, and that works for the most part with only minor jank. Despite the lack of documentation, none is really needed; you just plug it in and Plex detects it. After setup, Plex lets me manage my DVR schedule through a sane interface, and integrates Electronic Programming Guide data into the interface so that you can have a guide. EPG data is only available through a paid subscription, so this interface and the EPG data are something I'm happy to get through my lifetime Plex Pass.
However, I recently moved, and in my new market I'm not sure if the EPG data from Plex is correct. This would be a problem, because Plex will only display channels that are known to the EPG database. It does not allow me to simply type in the channel number and tune the card.
I looked around to see if I could find different software to control the tuner. kaffeine
crashes after successfully scanning for channels, and every other piece of software for viewing streams from the tuner was abandonware, except for the commands bundled with v4l
, in the package v4l-utils
on Arch.
v4l-utils
gives us dvb-fe-tool
which shows what dvb
devices are available. From there I was able to determine that my tuner card creates two virtual tuners at /dev/dvb/adapter0
and /dev/dvb/adapter1
. OK, that's straightforward.
I figured out that dvbv5-zap
lets you tune, but it requires a configuration file that I don't have, to tell it what channels exist. I tried to use dvbv5-scan
to find channels, only to discover that it.. can't? You have to have "scan tables" from.. somewhere..
After a few more searches I found that "the community" maintains some scan tables, and in Arch they can be installed from the AUR package dtv-scan-tables-dvbv5-git
. Obviously.
Now I can scan for channels and save them in the file dvbv5-zap
expects:
dvbv5-scan /usr/share/dvb/atsc/us-ATSC-center-frequencies-8VSB -a /dev/dvb/adapter1 -o dvb_channel.conf
This takes awhile, so I went to bed. When I woke up, it had produced a file full of entries like this:
[BUZZR ]
VCHANNEL = 13.4
SERVICE_ID = 6
VIDEO_PID = 97
AUDIO_PID = 100
FREQUENCY = 521028615
MODULATION = VSB/8
DELIVERY_SYSTEM = ATSC
Sure, why not? Now I can finally tune to them, by browsing this file and then running a command like the following:
dvbv5-zap -c dvb_channel.conf -p -r BUZZR
and … okay that just outputs some stuff on the command line and doesn't give the prompt back. After a little while, if the signal is strong enough, it tells me /dev/dvb/adapter0/dvr0
is ready.
Now, finally, it is possible to watch the channel, by firing up mplayer
with some options I chose through trial and error:
mplayer /dev/dvb/adapter0/dvr0 -framedrop -cache 4000
At this point mplayer
is displaying the raw stream from the OTA broadcast and I'm pretty pleased. It's not pretty, but it works (unlike kaffeine
), and doesn't require a web UI like Plex or Jellyfin. I suspect VLC can play these streams too but it wasn't obvious to me how.
Now I wanted to browse the channels and compare to the ones Plex's scanner found, as this expedition was initially sparked by my desire to see if Plex's EPG data was incorrect, since it is missing my local PBS channel. Perhaps I can tune the channel, but the wrong data is displayed?
Looking through dvb_channel.conf
I didn't see the channel I was looking for, so I began to tune them using the dvbv5-zap
command. Cumbersome.
I decided it'd be fun to build a channel browser in Tcl/Tk, so I threw one together:
I love Tcl for this kind of thing, because I can easily write a shell script with a GUI. Writing a proper program to combine these steps with dvbv5-zap
and mplayer
would be a huge undertaking, especially if I didn't want to simply instrument those commands, as I would have to build upon lower-level primitives.
But especially with the help of an LLM to scaffold the boilerplate for the UI, I can whip up a usable interface in much less time than it takes me to write about it afterwards. I love the directness of it. Sure, this is hardly production software that I would or could distribute. I have hard-coded the path to the channels file, which I manually generated earlier. This is not software that is ready to distribute. It is very home-cooked.
But with frankly very little code, I can now surf the channels on my desktop just by clicking around in the list element. It's much more ergonomic than a CLI, does exactly what I want, and it doesn't need to do anything else. Likely nobody else will ever have any use for it – but nor would they if it was better built. That's noteworthy. There's something to do be said for tools that let us quickly arrive at solutions that are simply good enough; worse really can be better.
In any case, here's the code:
#!/usr/bin/env wish
package require Tk
wm title . "TV Tuner"
wm geometry . 400x300
frame .mainFrame -borderwidth 2 -relief groove
pack .mainFrame -fill both -expand 1 -padx 10 -pady 10
listbox .mainFrame.list -height 10 -width 40
pack .mainFrame.list -fill both -expand 1
set channelFile [open "~/dvb_channel.conf"]
set tunerPid -1
while {[gets $channelFile line] >= 0} {
if {[regexp {^\[[A-Za-z0-9-]+\]$} $line]} {
set line [string range $line 1 end-1]
.mainFrame.list insert end $line
}
}
close $channelFile
set mplayerPid [exec mplayer /dev/dvb/adapter0/dvr0 -framedrop -cache 4000 &]
proc tuneChannel {} {
set selection [.mainFrame.list curselection]
global tunerPid mplayerPid
if {$selection ne -1} {
set channel [.mainFrame.list get $selection]
if {$tunerPid ne -1} {
catch {
exec kill -9 $tunerPid
exec killall dvbv5-zap
}
while {![catch {exec kill -0 $tunerPid 2>@1}]} {
after 10
}
set tunerPid -1
catch {
exec kill $mplayerPid
}
set mplayerPid [exec mplayer /dev/dvb/adapter0/dvr0 -framedrop -cache 4000 &]
}
set tunerPid [exec dvbv5-zap -c dvb_channel.conf -p -r $channel &]
}
}
bind .mainFrame.list <<ListboxSelect>> tuneChannel
proc cleanup {} {
global mplayerPid tunerPid
catch {
exec kill $mplayerPid
}
catch {
if {$tunerPid ne -1} {
exec kill -9 $tunerPid
}
exec killall dvbv5-zap
}
exit
}
wm protocol . WM_DELETE_WINDOW cleanup
bind . <Destroy> {+if {"%W" eq "."} {cleanup}}
The end product is janky, but usable. It's now slightly more apparent to me why tuning takes so long in Plex; it takes forever at the lower level, as well. And sometimes SIGTERM
doesn't properly terminate the process holding the tuner. That explains why every so often I need to restart Plex to change the channel. This is made abundantly clear in my crappy cleanup
function above and my dubious use of killall
. It's okay, it's home-cooked, remember?
At the end of the journey, PBS is still missing, so my quest has been unsuccessful. Now that I've done all this digging, I have to guess that the scan tables must be out of date and Plex shares a data source with.. uh.. whoever puts them in the AUR. Or maybe I just need a roof antenna? I will have to continue to investigate.