For a long time now I've wanted the ability to record over-the-air TV and watch it later, either on my computer, TV, or phone from somewhere else.
Great, you say! Products abound to serve this specific need. Just go buy something for 10's to 100' of dollars and move on with your life.
However, for various reasons the things I tried all came up lacking. First of all, years ago I'd bought a few USB TV tuner sticks online for this purpose, all of which professed to come with various TV recording programs. There was one that worked well, but it only ran on a Mac. All the others had the unhelpful habit of crashing and failing to record a program, typically the one I really wanted to watch.
After those failed attempts, I then set up a PC running Windows Media Center, which could utilize those USB TV tuner sticks. It worked well enough, but had a few problems, not the least of which was that it's deprecated and no longer runs on Windows 10 without hacks. However even leaving that aside, it recorded into this proprietary format that was difficult to something you could watch on your phone on the go. Furthermore, it also had the habit of crashing from time to time and failing to record my shows.
At this point it was becoming clear that either the USB sticks or their drivers were just flakey, and I should have just bought a more professional piece of hardware. However, I'm stubborn, and besides, restarting the program always made it work again: it seems like these programs could automatically try that instead of silently failing.
All of this culminated with this year's Winter Olympics. I wanted to be able to watch sports I was interested in, later when I got home from work or on the bus home, and skip the commercials. Also, the Olympics were already underway and I needed something fast.
Hence, the hack written up in this blogpost for how to cobble together a makeshift setup using only the free software tools VLC and ffmpeg.
There are practically innumerable programs for receiving this data from the TV tuner USB sticks, but for this article I'll talk about using VLC on Windows. VLC media player is an open-source Swiss army knife of a video player which can also transcode video and stream it across a network or to a file. Of particular relevance to this article, it can also open TV tuners on Windows and decode + display the video and audio contained in the transport streams they present.
So that's step one. Install VLC. Unfortunately, there's a bug in VLC versions 2.0 and greater where it no longer opens TV tuners, at least for my USB TV tuner sticks. As a result, you'll need something earlier; I use version 1.9.
Launch VLC, then select Open Capture Device from the Media menu. Change the capture device to TV - digital, and choose the transmission format used in your country. Here in the USA, this is ATSC. Finally, pick your TV channel.
This is where things get a little interesting. This isn't as simple as typing channel "5" into the frequency box, for two reasons. The first is that when the US transitioned to digital TV, most stations changed the channel they broadcast on, because reasons. In order to avoid having to teach people new channel names for their favorite stations, along with the fact that these numbers are much less memorable than the original ones (channel 5 in my area became channel 48 -- doesn't quite roll off tongue), the new standard allows stations to advertise "virtual" channels which are shown to the user. When the user selects virtual channel 5, the TV actually tunes to channel 48. In order to find the actual channels, the first time you set up a TV in a new location, it performs a time-consuming scan of every single possible channel to see if there's anybody there. Hacky, but such is the story of backwards compatibility.
Normally, your recording software does exactly the same thing for you, but with this setup you'll have to do it manually. If you know the call letters of the station you want to watch, the easiest way is to search for call-letters-TV in Google and look at their Wikipedia page. For example, the aforementioned channel 5 in my area is KING. So, searching for KING-TV brings up their Wikipedia page. On the sidebar, the digital channel (the actual channel) will be listed.
If, on the other hand, you don't know your station's call letters, you've got a few choices.
One is to replace your city in this Wikipedia template and see if they have a list of stations compiled for your area. Alternately, go to TV Fool and put in your address, and it'll tell you the list of stations you can receive in your area, both the real channel and the virtual one.
With the real channel of your station known, there's one additional step. You still can't just type that channel in the box, because a "channel" is actually just another affordance for people to remember. Each channel actually maps to specific (in the USA) 6 Mhz wide swath of frequencies. Luckily, VLC knows this mapping, but you have to do a little work to get it. Click Advanced Options button and scroll all the way to the bottom, and put this channel in the box for Physical Channel.
That's it. Click Play and within a few seconds hopefully you're watching TV.
If nothing shows, up, there's a variety of things that could be going wrong. The first thing to try is to go to the Tools menu and choose Messages. Change the Verbosity to 2. Then, click the stop button, wait a few seconds, then click play again. Look through the log for any errors and see if any of them seem actionable. Admittedly, this is probably unlikely, but you never know. At this point, there's basically 3 things that can be going wrong:
That done, see if the problem is a weak signal. Try another strong channel, and fiddle with the antenna. If that doesn't work, and a nearby TV can receive the signal just fine while your stick can't, I don't know how to help you. :-/
Now that you've verified you can receive data from the tuner, it's time to figure out which sub-channel you're interested in. You've interacted with sub-channels when watching regular TV, but might not have known that they were called that. They're when you have channel 5.1 which is what you wanted to watch, and 5.2 and 5.3 which show old movies you're not interested in or infomercials.
Most probably, VLC will have chosen the first sub-channel when it started playing from the tuner. You can change channels from the Playback menu under the Programs submenu.
Unfortunately, in the MPEG standard, these sub-channels, like everything else, just another user-friendly mapping to an underlying technical value, and VLC deals only with the technical value. In this case, these are specified in a set of tables collectively called the program-specific information (also discussed by the standards committee). There are too many details not relevant to this discussion to go into deeply, but at a high level, a Virtual Channel Table associates a sub-channel (called the minor channel) with another arbitrary number, known as the program number. This number, in turn, is used to identify the video, audio, and closed caption (CC) streams of data associated with that sub-channel through yet more tables of indirection.
We need to determine the program number associated with our desired sub-channel (probably channel .1). Luckily this is easy: it's just what VLC lists as the number when you change programs via the aforementioned menu. Change to the program you wanted to watch (the lowest number probably) and remember it.
With that information, we now have what we need to have VLC record a single channel to disk for later watching. To do so, we'll use the lightly documented various magic boxes. In particular, data comes into VLC from the TV tuner. From there, we'll use the duplicate module. Not to duplicate anything, but because as part of duplicating it contains functionality to select only a single program from the transport stream. Then, we'll use the file output method of the dst option of the std output module to save that data to a file.As part of saving, VLC needs to know how to wrap up the various video, audio and CC data we've selected into a single file. The default option is to have VLC wrap it up in another transport stream, just like it came to us off the air, and that's fine for our purposes so we'll set the mux option of the std output module to ts.
Put together, this looks like:
Finally, we need to tell VLC to use the TV tuner, and what channel to tune it to, which we can do like so:
Try running this command on your channel from earlier for a few minutes before exiting VLC, and you should see several megabytes of data in the file specified:
So now we have the ability to record some show from over the air and convert it to something that the phone will play. Now what's needed is making it available remotely for watching on the go. Since we're using a Windows machine in this guide, the easiest way to do this is to use the built-in Internet Information Server to host a web site on this computer. There are various guides, but I couldn't find anything that listed all the steps required in a single step. Consequently, take a look at a previous post I wrote up on how to do this.
Running the actual encoding is done with a batch file, which just contains the live above directing ffmpeg to transcode the file.
Great, you say! Products abound to serve this specific need. Just go buy something for 10's to 100' of dollars and move on with your life.
However, for various reasons the things I tried all came up lacking. First of all, years ago I'd bought a few USB TV tuner sticks online for this purpose, all of which professed to come with various TV recording programs. There was one that worked well, but it only ran on a Mac. All the others had the unhelpful habit of crashing and failing to record a program, typically the one I really wanted to watch.
After those failed attempts, I then set up a PC running Windows Media Center, which could utilize those USB TV tuner sticks. It worked well enough, but had a few problems, not the least of which was that it's deprecated and no longer runs on Windows 10 without hacks. However even leaving that aside, it recorded into this proprietary format that was difficult to something you could watch on your phone on the go. Furthermore, it also had the habit of crashing from time to time and failing to record my shows.
At this point it was becoming clear that either the USB sticks or their drivers were just flakey, and I should have just bought a more professional piece of hardware. However, I'm stubborn, and besides, restarting the program always made it work again: it seems like these programs could automatically try that instead of silently failing.
All of this culminated with this year's Winter Olympics. I wanted to be able to watch sports I was interested in, later when I got home from work or on the bus home, and skip the commercials. Also, the Olympics were already underway and I needed something fast.
Hence, the hack written up in this blogpost for how to cobble together a makeshift setup using only the free software tools VLC and ffmpeg.
Acquiring the stream
Over the air TV broadcast is sent as a MPEG2 transport stream, which is a container format holding one or more video, audio and text data. This stream of bits is wrapped in various error correction and synchronization bits, then sent out over the air using one of a variety of transmission formats. The exact details aren't particularly important, because the TV tuner and its driver handles decoding those signals from the air and giving you back the original bits of the transport stream.There are practically innumerable programs for receiving this data from the TV tuner USB sticks, but for this article I'll talk about using VLC on Windows. VLC media player is an open-source Swiss army knife of a video player which can also transcode video and stream it across a network or to a file. Of particular relevance to this article, it can also open TV tuners on Windows and decode + display the video and audio contained in the transport streams they present.
So that's step one. Install VLC. Unfortunately, there's a bug in VLC versions 2.0 and greater where it no longer opens TV tuners, at least for my USB TV tuner sticks. As a result, you'll need something earlier; I use version 1.9.
Launch VLC, then select Open Capture Device from the Media menu. Change the capture device to TV - digital, and choose the transmission format used in your country. Here in the USA, this is ATSC. Finally, pick your TV channel.
This is where things get a little interesting. This isn't as simple as typing channel "5" into the frequency box, for two reasons. The first is that when the US transitioned to digital TV, most stations changed the channel they broadcast on, because reasons. In order to avoid having to teach people new channel names for their favorite stations, along with the fact that these numbers are much less memorable than the original ones (channel 5 in my area became channel 48 -- doesn't quite roll off tongue), the new standard allows stations to advertise "virtual" channels which are shown to the user. When the user selects virtual channel 5, the TV actually tunes to channel 48. In order to find the actual channels, the first time you set up a TV in a new location, it performs a time-consuming scan of every single possible channel to see if there's anybody there. Hacky, but such is the story of backwards compatibility.
Normally, your recording software does exactly the same thing for you, but with this setup you'll have to do it manually. If you know the call letters of the station you want to watch, the easiest way is to search for call-letters-TV in Google and look at their Wikipedia page. For example, the aforementioned channel 5 in my area is KING. So, searching for KING-TV brings up their Wikipedia page. On the sidebar, the digital channel (the actual channel) will be listed.
If, on the other hand, you don't know your station's call letters, you've got a few choices.
One is to replace your city in this Wikipedia template and see if they have a list of stations compiled for your area. Alternately, go to TV Fool and put in your address, and it'll tell you the list of stations you can receive in your area, both the real channel and the virtual one.
With the real channel of your station known, there's one additional step. You still can't just type that channel in the box, because a "channel" is actually just another affordance for people to remember. Each channel actually maps to specific (in the USA) 6 Mhz wide swath of frequencies. Luckily, VLC knows this mapping, but you have to do a little work to get it. Click Advanced Options button and scroll all the way to the bottom, and put this channel in the box for Physical Channel.
That's it. Click Play and within a few seconds hopefully you're watching TV.
If nothing shows, up, there's a variety of things that could be going wrong. The first thing to try is to go to the Tools menu and choose Messages. Change the Verbosity to 2. Then, click the stop button, wait a few seconds, then click play again. Look through the log for any errors and see if any of them seem actionable. Admittedly, this is probably unlikely, but you never know. At this point, there's basically 3 things that can be going wrong:
- Your TV tuner doesn't have drivers installed.
- The TV signal is too weak to be received properly.
- VLC doesn't support your TV tuner.
That done, see if the problem is a weak signal. Try another strong channel, and fiddle with the antenna. If that doesn't work, and a nearby TV can receive the signal just fine while your stick can't, I don't know how to help you. :-/
Now that you've verified you can receive data from the tuner, it's time to figure out which sub-channel you're interested in. You've interacted with sub-channels when watching regular TV, but might not have known that they were called that. They're when you have channel 5.1 which is what you wanted to watch, and 5.2 and 5.3 which show old movies you're not interested in or infomercials.
Most probably, VLC will have chosen the first sub-channel when it started playing from the tuner. You can change channels from the Playback menu under the Programs submenu.
Unfortunately, in the MPEG standard, these sub-channels, like everything else, just another user-friendly mapping to an underlying technical value, and VLC deals only with the technical value. In this case, these are specified in a set of tables collectively called the program-specific information (also discussed by the standards committee). There are too many details not relevant to this discussion to go into deeply, but at a high level, a Virtual Channel Table associates a sub-channel (called the minor channel) with another arbitrary number, known as the program number. This number, in turn, is used to identify the video, audio, and closed caption (CC) streams of data associated with that sub-channel through yet more tables of indirection.
We need to determine the program number associated with our desired sub-channel (probably channel .1). Luckily this is easy: it's just what VLC lists as the number when you change programs via the aforementioned menu. Change to the program you wanted to watch (the lowest number probably) and remember it.
With that information, we now have what we need to have VLC record a single channel to disk for later watching. To do so, we'll use the lightly documented various magic boxes. In particular, data comes into VLC from the TV tuner. From there, we'll use the duplicate module. Not to duplicate anything, but because as part of duplicating it contains functionality to select only a single program from the transport stream. Then, we'll use the file output method of the dst option of the std output module to save that data to a file.As part of saving, VLC needs to know how to wrap up the various video, audio and CC data we've selected into a single file. The default option is to have VLC wrap it up in another transport stream, just like it came to us off the air, and that's fine for our purposes so we'll set the mux option of the std output module to ts.
Put together, this looks like:
:sout=#duplicate{dst=std{access=file,dst=c:\\tv\\tv.ts,mux=ts},select="program=3"}
Finally, we need to tell VLC to use the TV tuner, and what channel to tune it to, which we can do like so:
atsc://frequency=0000 :dvb-physical-channel=48
Try running this command on your channel from earlier for a few minutes before exiting VLC, and you should see several megabytes of data in the file specified:
vlc.exe atsc://frequency=0000 :dvb-physical-channel=48 :sout=#duplicate{dst=std{access=file,dst=d:\\tv\\tv.ts,mux=ts},select="program=3"}
Transcoding
If the only goal is to be able to watch a show later on the same computer from which you recorded it, you're done. Well, not completely done in that it doesn't automatically start or stop recording, but VLC will happily play back what it just saved. However, one of my main goals of this hack was to be able to watch what was recorded later on the go, in particular on my iPhone. For that to work, the information we just saved needs to be transcoded, for two reasons.
First, it's way to big. ATSC allows up to TV stations to transmit data at around 20 Mbps, faster than most phones will stream while on a cellular network or someone's slow WiFi. We need to make it smaller. Second, and more troubling is that at least in countries which broadcast with the ATSC standard, the video and audio are encoded in a format that most mobile phones don't support. That's because what's broadcast is MPEG2 video and Dolby AC3 audio, whereas most phones can only decode MPEG4 H264 video and AAC audio. Furthermore, many channels broadcast interlaced video (if you've see 1080i as the resolution of a channel on your TV, the I stands for interlaced), and phones can typically only play progressive video. Consequently, the original video also needs to be deinterlaced, at which point it'll need to be re-compressed anyway, so we might as well transcode it into a newer smaller format.
To do all of this, we'll use another open-source tool called ffmpeg. If VLC is swiss army knife of playing back and saving video, ffmpeg is the equivalent knife for transcoding video. It can read almost any video or audio format and the same for writing to them. (Actually, both VLC and ffmpeg share a common library, libavcodec, from whence they obtain most of their functionality.) The upshot of this flexibility is some complexity, combined with at times missing documentation. To be fair to the ffmpeg team, this has improved greatly in recent years.
FFmpeg has an informative page on the various options for H264 compression, including a list of which profiles and levels are supported by which mobile devices. These days if you're targeting iPhones, there's no need to use anything but the highest level, as phones which only support lower levels are all no longer supported by Apple. Also important is the option to use the +faststart option, which stores metadata about the file at the beginning so that playback can begin as soon as it starts downloading. Another important point to note is that iPhone doesn't support interlaced playback, so the raw data from over the air will need to be deinterlaced through some mechanism before the phone will play it back. Finally, I was targeting watching the result on a regular iPhone which has effectively 720p resolution. Since the signal comes in at 1080i , we could resize it to match the iPhone's screen before transcoding it to make a smaller file.
In addition to ensuring the file is as compact as possible and matched to playback on the iPhone, there was also the desire that it could be encoded fast enough that it was done by the time the next TV program came on. For simplicity I decided to just let the machine stream the NBC channel 24 hours a day so that I didn't have to manage figuring out exactly when what was worth watching was on, so this was needed to ensure there wasn't an endless stream of files backing up never finishing encoding. However, there was also the desire to be able to watch whatever was happening relatively close to live, which is easier the faster the recording encodes.
For managing that, there's the preset option which trades off speed for file size, for a given quality. I found that the default medium worked well enough for my relatively older AMD desktop machine, but on the lower powered Atom PC I wanted to use for encoding, I needed to change it to veryfast to keep up.
Finally, I found the default quality factor was a little too conservative, and I could reduce the quality to 26 without noticeable degradation watching on my phone.
Audio is relatively simpler (or at least, I didn't investigate it as much). There's also a page on this but I mostly stuck with the default options and left it be.
Putting all this together, the FFMPEG command line to accomplish all this comes out to be:
-i tells ffmpeg the input file, and comes before any of the conversion options.
Format Choice
The main goal of this project was to make it such that I and my friends could watch the Olympics from our phones or laptops at work on the commute there and back. As it happens, I have an iPhone, and most of my friends and family only have Mac laptops. As it goes with user-friendly design, I wanted to minimize the friction for them to use whatever I came up with, which to me meant absolutely no installing any apps, either on the phone or the laptop. Also, no requirements to download an entire video file before watching. I wanted it be as simple as streaming a video on YouTube, which also meant that the file size needed to be small enough that it would reliably stream on a mobile network and on mediocre internet connections. In particular, the requirement to be able to stream to an iPhone limits the choice of video and audio compression formats to effectively H264 and AAC audio. Theoretically since last year one can also use H265 video compression, but I didn't explore this.FFmpeg has an informative page on the various options for H264 compression, including a list of which profiles and levels are supported by which mobile devices. These days if you're targeting iPhones, there's no need to use anything but the highest level, as phones which only support lower levels are all no longer supported by Apple. Also important is the option to use the +faststart option, which stores metadata about the file at the beginning so that playback can begin as soon as it starts downloading. Another important point to note is that iPhone doesn't support interlaced playback, so the raw data from over the air will need to be deinterlaced through some mechanism before the phone will play it back. Finally, I was targeting watching the result on a regular iPhone which has effectively 720p resolution. Since the signal comes in at 1080i , we could resize it to match the iPhone's screen before transcoding it to make a smaller file.
In addition to ensuring the file is as compact as possible and matched to playback on the iPhone, there was also the desire that it could be encoded fast enough that it was done by the time the next TV program came on. For simplicity I decided to just let the machine stream the NBC channel 24 hours a day so that I didn't have to manage figuring out exactly when what was worth watching was on, so this was needed to ensure there wasn't an endless stream of files backing up never finishing encoding. However, there was also the desire to be able to watch whatever was happening relatively close to live, which is easier the faster the recording encodes.
For managing that, there's the preset option which trades off speed for file size, for a given quality. I found that the default medium worked well enough for my relatively older AMD desktop machine, but on the lower powered Atom PC I wanted to use for encoding, I needed to change it to veryfast to keep up.
Finally, I found the default quality factor was a little too conservative, and I could reduce the quality to 26 without noticeable degradation watching on my phone.
Audio is relatively simpler (or at least, I didn't investigate it as much). There's also a page on this but I mostly stuck with the default options and left it be.
Putting all this together, the FFMPEG command line to accomplish all this comes out to be:
ffmpeg -i recording.ts -vcodec libx264 -preset veryfast -profile:v high -level 4.2 -s hd720 -acodec aac -ac 2 -ar 48000 -ab 196k -deinterlace -movflags +faststart encoded.mp4The general ffmpeg documentation is pretty good at explaining the various options, but a little overwhelming, so breaking down each section:
-i tells ffmpeg the input file, and comes before any of the conversion options.
-vcodec libx264 -preset veryfast -profile:v high -level 4.2comes from the H264 encoder page following the instructions for quality, phone profile support and encoding speed.
-s hd720tells ffmpeg to resize the video to regular HD (720p) size
-acodec aac -ac 2 -ar 48000 -ab 196kalso comes from the AAC page. I change the sample rate to 48000 since that matches the incoming stream, and the bitrate to 196k as I found I could hear compression artifacts using 128k.
-deinterlacetells ffmpeg to use its default deinterlacer to convert the incoming video to progressive video before resizing, compressing, etc, and is needed because the video comes in as interlaced.
As an aside, if you leave out the deinterlace option, ffmpeg will still run and produce a video. It's just that in scenes with motion, you'll end up with interlacing artifacts in the output file. That's because without being told otherwise, ffmpeg will just smush the two fields of the interlaced video one after another into a single frame of progressive video. This ends up working fine when presented with a still image, but in a scene with lots of motion, the two fields will contain very different pictures and the smushed frame will be full of scissor lines.
Finally, -movflags +faststart
This adds to the flags ffmpeg uses for post-processing and tells it to move the metadata about the file to the beginning so the phone can immediately start decoding the video as it downloads. This is required because normally this information is written to the end, after all the file is encoded.
Watching Remotely
Putting It All Together
With all the basic pieces in place, the final step is to automate it so that you're not sitting there manually clicking record, running ffmpeg, and copying the files to the web server directory.
I set this up using two PCs. One, an old laptop, sits near the window with an unobstructed view of the TV tower. There, I used a small Python script to start a new recording every half an hour. This PC also hosts the web server which serves out the encoded files. I configured Windows to share the volume which stores these two folders over the network.
On a second PC, I used another Python script to watch for new files and launch ffmpeg to encode them.
On the recording PC, the script looks like this:
On the encoding PC, the script looks like this:
Then, on a second PC (my main desktop).
Running the actual encoding is done with a batch file, which just contains the live above directing ffmpeg to transcode the file.







