TV Tuner

By Connor Taffe | Published .

I recently stumbled upon a boxed Hauppauge WinTV-HVR-1600 PCI TV Tuner card at Goodwill, and wondered if I could use it broadcast live TV over my network. I installed it into an available PCI slot on an old Dell Optiplex 755, and attached an antenna.

Hauppauge WinTV-HVR 1600 box
Hauppauge WinTV-HVR 1600 box

We can see the card is recognized over PCI:

; lspci | grep video
03:00.0 Multimedia video controller: Conexant Systems, Inc. CX23418 Single-Chip MPEG-2 Encoder with Integrated Analog Video/Broadcast Audio Decoder

We can then see the cx18 driver for that chip loaded on boot:

; sudo dmesg | grep -i CX23418
[   24.373431] cx18-0: cx23418 revision 01010000 (B)
[   24.623610] tveeprom: audio processor is CX23418 (idx 38)
[   24.623612] tveeprom: decoder processor is CX23418 (idx 31)
[   26.275958] cx18-0: loaded v4l-cx23418-cpu.fw firmware (158332 bytes)
[   26.482992] cx18-0: loaded v4l-cx23418-apu.fw firmware V00120000 (141200 bytes)
[   27.601932] cx18-0 843: loaded v4l-cx23418-dig.fw firmware (16382 bytes)
[   27.621767] cx18-0 843: verified load of v4l-cx23418-dig.fw firmware (16382 bytes)

To test scanning available channels, install w-scan2 on Ubuntu via the PPA:

; sudo add-apt-repository ppa:w-scan2/stable
; sudo apt update
; sudo apt install w-scan2

Then do a scan, in this case outputting a channel list in VLC playlist format:

; sudo w_scan2 -c US -L > chans.xspf

TVHeadEnd

Tvheadend is an open source TV streaming and recording service. To install tvheadend on Ubuntu, follow the Linux Install Documentation, which amounts to running:

curl -1sLf 'https://dl.cloudsmith.io/public/tvheadend/tvheadend/setup.deb.sh' | sudo -E bash

Inspect the script to ensure it hasn't been tampered with. It adds the tvheadend/tvheadend repository at /etc/apt/sources.list.d/tvheadend-tvheadend.list, installs their GPG signing key, and ensures some dpkg components like apt-transport-https are installed.

Then install tvheadend:

sudo apt install tvheadend

You will be prompted for a superuser username and password for the web interface. The tvheadend service is started by default:

systemctl status tvheadend.service

To see the full logs:

journalctl -u tvheadend.service

Which reminds us of the port tvheadend is listening on:

Jan 15 01:10:52 typhoon systemd[1]: Started tvheadend.service - Tvheadend - a TV streaming server and DVR.
Jan 15 01:10:52 typhoon tvheadend[14972]: config: Using configuration from '/var/lib/tvheadend'
Jan 15 01:10:52 typhoon tvheadend[14972]: http: Starting HTTP server 0.0.0.0:9981
Jan 15 01:10:52 typhoon tvheadend[14972]: htsp: Starting HTSP server 0.0.0.0:9982

To allow apps which don't support our self-signed CA which we'll use with a reverse proxy below, we need to enable listening on 0.0.0.0, and add a firewall rule. Define a new application profile in /etc/ufw/applications.d/tvheadend:

[Tvheadend]
title=Tvheadend TV streaming server
description=Tvheadend is the leading TV streaming server for Linux.
# HTTP, HTSP
ports=9981,9982/tcp

Then enable the rule:

; sudo ufw app update tvheadend
; sudo ufw allow tvheadend
Rule added
Rule added (v6)

Now, the mDNS record generated by the Avahi integration for autodiscovery will match the available ports. Apps like TvhClient on iOS can auto-discover the server using this record. Ideally we would load the self-signed certificate onto the iOS device, then modify the Avahi configuration to reflect the reverse proxied ports 80 and 443 with _http._tcp and _https._tcp. I'm not certain how to handle the HTSP port.

Continue through the wizard's setup process, associate your card (mine is detected as a Samsung S5H1409 QAM/8VSB) with an ATSC-T network, then associate that network with the us-ATSC-center-frequencies-8VSB mux. Once the setup is complete, a scan will commence which takes several minutes. I don't have cable, so the ATSC-C (cable) network is not useful.

From the UI, under the Configuration tab, you can see the relevant objects:

The files under /var/lib/tvheadend/ represent each of these objects.

The Web UI uses a transcoding profile, webtv-h264-aac-matroska, to transform the raw stream from the tuner into a stream playable in the browser. This extra processing lead to stuttering and audio issues. Using a client like TvhClient on iOS which uses VLC to process the raw stream on-device provides the best experience.

Tvheadend UI displaying a broadcast channel stream of Forensic Files
Tvheadend UI displaying a broadcast channel stream of Forensic Files

Unfortunately, Tvheadend no longer supports analog channels, which limits our card's utility to ATSC-T instead of NTSC (for instance, from a VCR) or FM radio.

IPTV

Tvheadend supports MPEG-TS encoded over-the-network streams, however guide (EPG) information must be provided by another source, e.g. XMLTV. See an example of some of these channels at tv.garden. Some IPTV channels are documented at iptv-org/iptv, available as m3u files. Each m3u file contains URLs which point to yet more m3u or m3u8 (UTF-8) files, this continues recursively until a file contains a sequence of .ts (MPEG-TS) files. Tvheadend supports some of these streams by default, but can support more with the help of ffmpeg using pipe:// URIs:

The /var/lib/tvheadend/ffmpeg-wrapper-m3u.sh script to rewrite m3u files:

#!/usr/bin/env bash
set -euo pipefail

curl -sSL "$1" | awk '/^#/ { print; next } { print "pipe:///var/lib/tvheadend/ffmpeg-wrapper.sh", $0 }'

And the /var/lib/tvheadend/ffmpeg-wrapper.sh script which re-encodes MPEG-TS streams:

#!/usr/bin/env bash
set -euo pipefail

ffmpeg -loglevel fatal -i $1 -vcodec copy -acodec copy -f mpegts pipe:1

To add channels like these to Tvheadend:

  1. Under Configuration, DVB Inputs, Networks, choose Add and then select IPTV Automatic Network. Name the network, e.g. iptv-org/pbs, toggle Enabled and Create Bouquet, set the maximum number of input streams to 20 to prevent over-loading the system with scans (or uncheck scan after creation), in the URL field place pipe:///var/lib/tvheadend/ffmpeg-wrapper-m3u.sh followed by a space and the URL of your m3u file, for example the raw link to the Github us_pbs.m3u file, https://raw.githubusercontent.com/iptv-org/iptv/refs/heads/master/streams/us_pbs.m3u.
  2. The system will pipe the m3u file through our ffmpeg-wrapper-m3u.sh script, which will rewrite each m3u8 URL entry with a pipe:///var/lib/tvheadend/ffmpeg-wrapper.sh URI to reformat the stream. Then, the system scans each channel and determines if it contains a broadcast. If it does, a mux and a corresponding service are created. Under Configuration, DVB Inputs, Services select Map Services to create channels from each service.

Now, these additional IPTV channels will be available as channels in clients, but without guide information.

Reverse Proxy

For my purposes, I want the HTTP interface to listen on localhost and to proxy to it from nginx listening on the HTTP standard port 80. That way, I can assign dvr.home.arpa to this machine and have that host route to the tvheadend web interface. To do that, we can edit $OPTIONS used by the systemd service at /etc/default/tvheadend and add the --bindaddr option specified in man tvheadend to look as below:

OPTIONS="-u hts -g video --bindaddr 127.0.0.1"

Then edit the config at /var/lib/tvheadend/config, gleaning some documentation from config.c. Enabling proxy allows for X-Forwarded-For support.

        "proxy": true,
        "cors_origin": "https://dvr.home.arpa",

Now we can restart tvheadend:

sudo systemctl restart tvheadend.service

Reverse Proxy

To serve HTTPS on 443 via nginx, we need certificates. I use cfssl as described in my post on PKI, after adding servers/typhoon/typhoon.home.arpa.json and adding the expected cert and key files as make targets, we simply run make to construct the certs and back them up.

Remember to add the intermediate and root certificates to form the full chain:

cat servers/typhoon/typhoon.home.arpa-server.pem intermediate-ca.pem ca.pem > servers/typhoon/typhoon.home.arpa-server-chain.pem
; scp servers/typhoon/typhoon.home.arpa-server{-chain,-key}.pem typhoon.home.arpa:.
typhoon.home.arpa-server-key.pem                                                                                          100% 1679   490.6KB/s   00:00
typhoon.home.arpa-server-chain.pem

Then on typhoon (our desktop we are installing tvheadened on):

sudo mv typhoon.home.arpa-server-chain.pem /etc/ssl/certs/typhoon.home.arpa-server.pem
sudo chown root:root /etc/ssl/certs/typhoon.home.arpa-server.pem
sudo chmod 777 /etc/ssl/certs/typhoon.home.arpa-server.pem
sudo chmod 644 /etc/ssl/certs/typhoon.home.arpa-server.pem
sudo mv typhoon.home.arpa-server-key.pem /etc/ssl/private/
sudo chown root:ssl-cert /etc/ssl/private/typhoon.home.arpa-server-key.pem
sudo chmod 640 /etc/ssl/private/typhoon.home.arpa-server-key.pem

Now we can install nginx:

sudo apt install nginx

The nginx config at /etc/nginx/conf.d/dvr.conf looks like:

server {
    listen       80;
    listen       [::]:80;
    server_name  dvr.home.arpa;
    root         /usr/share/nginx/html;

    return 301 https://$host$request_uri;
}

# Settings for a TLS enabled server.
server {
    listen       443 ssl http2;
    listen       [::]:443 ssl http2;
    server_name  dvr.home.arpa;
    root         /usr/share/nginx/html;

    ssl_certificate "/etc/ssl/certs/typhoon.home.arpa-server.pem";
    ssl_certificate_key "/etc/ssl/private/typhoon.home.arpa-server-key.pem";
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  10m;

    location / {
        proxy_pass http://127.0.0.1:9981/;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }

    error_page 404 /404.html;
    location = /404.html {
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    }
}

The Upgrade and Connection headers are required to enable proxying websockets.

And it passes testing:

; sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Now we can enable nginx:

sudo systemctl enable nginx.service --now

And we can allow HTTP and HTTPS traffic through the ufw firewall:

; sudo ufw allow https
Rule added
Rule added (v6)
; sudo ufw allow http
Rule added
Rule added (v6)

After adding a host override in pfSense to point dvr.home.arpa at the IP statically assigned through DHCP to our desktop, we can navigate to dvr.home.arpa.