Building a digital video recorder with FreeBSD

This shows how to set up a digital video recorder using webcamd, Tvheadend, nginx, and Samba on a FreeBSD box. I have been using an Elgato EyeTV Hybrid USB stick which seems to be compatible to some Terratec devices according to the LinuxTV V4L-DVB wiki. The em28xx driver of webcamd supports this stick. Thus, this howto should be applicable to any supported device.

  1. Just select DVB, INPUT, and RADIO when configuring and installing webcamd from the ports:
    # make -C /usr/ports/multimedia/webcamd config install clean
    
  2. For Tvheadend, I chose AVAHI, INOTIFY, and TRANSCODING:
    # make -C /usr/ports/multimedia/tvheadend config install clean
    
  3. I selected these when asked for build options for ffmpeg: If you like to convert recordings for mobile devices, you might want to enable additional codecs like X264, X265, and/or THEORA.
  4. Next is nginx:
    # make -C /usr/ports/www/nginx config install clean
    
    I have used nginx to serve recordings to my local PC via http. Therefore, just a few build options are needed:
  5. Samba is used to play recorded streams on a "smart" TV set:
    # make -C /usr/ports/net/samba411 config install clean
    
    I just selected these options:
  6. After compiling these ports, you might want to get rid of unused packages and downloaded source files:
    # pkg autoremove
    # rm -rv /usr/ports/distfiles/*
    
  7. Avahi and thus dbus will announce Tvheadend and Samba services to local clients. Thus, enable and start them both now:
    # sysrc dbus_enable=YES
    # sysrc avahi_daemon_enable=YES
    # service dbus start
    # service avahi-daemon start
    
  8. webcamd needs the cuse kernel module:
    # sysrc kld_list+=cuse
    
    Manually load it without rebooting the machine:
    # kldload cuse
    
  9. Optionally, download the firmware needed for your TV stick to /boot/modules, e.g.:
    # cd /boot/modules
    # fetch https://github.com/OpenELEC/dvb-firmware/raw/master/firmware/dvb-usb-terratec-htc-stick-drxk.fw
    
  10. Now enable webcamd during system boot:
    # sysrc webcamd_enable=YES
    
  11. I had some issues with webcamd being automatically started by devd, thus disabled this:
    # sysrc webcamd_devd_starts_unspecified=NO
    
  12. In order to start one instance of webcamd, first display all suitable devices:
    # webcamd -l
    
    The output should look like this:
    Available device(s):
    webcamd [-d ugen1.1] -N AMD-EHCI-root-HUB -S unknown -M 0
    webcamd [-d ugen0.1] -N 0x1022-XHCI-root-HUB -S unknown -M 0
    webcamd [-d ugen0.2] -N Elgato-EyeTV-Hybrid -S 081xxxxxxxxx -M 0
    Show webcamd usage:
    webcamd -h
    
  13. Add the desired device to /etc/rc.conf. If you have just one device of the same name and serial number, you can omit the -M parameter:
    # sysrc webcamd_0_flags="-N Elgato-EyeTV-Hybrid -S 081xxxxxxxxx"
    
  14. Your TV device should now be visible under /dev:
    # ls -Rl /dev/dvb
    total 1
    dr-xr-xr-x  2 root  wheel  512 Jul  7 16:55 adapter0
    
    /dev/dvb/adapter0:
    total 0
    crw-rw----  1 webcamd  webcamd  0x81 Jul  7 16:55 demux0
    crw-rw----  1 webcamd  webcamd  0x82 Jul  7 16:55 dvr0
    crw-rw----  1 webcamd  webcamd  0x83 Jul  7 16:55 frontend0
    
  15. Add the user tvheadend to the group webcamd:
    # pw groupmod webcamd -m tvheadend
    
  16. Since Tvheadend does not respond to SIGHUP and thus does not play nicely with newsyslog, we will use syslog:
    # cat <<EOF >/etc/syslog.d/tvheadend.conf
    !tvheadend
    daemon.*	/var/log/tvheadend.log
    EOF
    # cat <<EOF >/etc/newsyslog.conf.d/tvheadend.conf
    /var/log/tvheadend.log	644 14 * @T00 JC
    EOF
    
  17. Create this logfile and restart syslogd:
    # touch /var/log/tvheadend.log
    # service syslogd reload
    
  18. Redirect its usual logfile to /dev/null:
    # mkdir /var/log/tvheadend
    # ln -s /dev/null /var/log/tvheadend/tvheadend.log
    
  19. Enable Tvheadend, allow anonymous access, and let it connect to dbus:
    # sysrc tvheadend_enable=YES
    # sysrc tvheadend_flags="-C -U"
    
  20. Start Tvheadend:
    # service tvheadend start
    
  21. Create a directory where all recordings will be saved:
    # mkdir -m 0755 /tvheadend
    # chown tvheadend:tvheadend /tvheadend
    
    If you use ZFS, you may create a dedicated filesystem for this and disable compression and/or deduplication.
  22. Open a web browser and go to http://your-freebsd-box:9981/. Your TV device should appear on the tab Configuration -> DVB Inputs -> TV adapters.
  23. Switch to the Networks tab and create a network based on your input type, e.g. DVB-T or DVB-S. For Network discovery select New muxes + changed muxes, deselect Skip startup scan, make sure Idle scan muxes is disabled, and of course give it a meaningful Network name. Do not forget to save your changes.
  24. Switch back to the TV adapters tab, and select the adapter matching the network you just created.
  25. On the right side, under Basic Settings select these options: Click on Save.
  26. The Muxes and Services tab should get filled now.
  27. Open the Configuration -> Recording -> Digital Video Recorder Profiles tab, select the (Default profile), at least change the Recording system path to /tvheadend, and save your changes.
  28. Finally, limit access to Tvheadend. Navigate to Configuration -> Users -> Access Entries. Create a permissive access profiles for both 127.0.0.0/8 and your local network, e.g. 10.23.42.0/24. I used these values:
  29. Nginx expects its configuration in /usr/local/etc/nginx/nginx.conf. The FreeBSD port will install a default configuration file. Here is the difference between the default and my file:
    --- nginx.conf-dist     2020-06-13 10:44:52.850585000 +0200
    +++ nginx.conf  2020-07-07 20:44:47.072723000 +0200
    @@ -13,9 +13,11 @@
     
     #pid        logs/nginx.pid;
     
    +load_module /usr/local/libexec/nginx/ngx_http_fancyindex_module.so;
     
     events {
    -    worker_connections  1024;
    +    use kqueue;
    +    worker_connections 1024;
     }
     
     
    @@ -27,18 +29,21 @@
         #                  '$status $body_bytes_sent "$http_referer" '
         #                  '"$http_user_agent" "$http_x_forwarded_for"';
     
    -    #access_log  logs/access.log  main;
    +    access_log      off;
     
         sendfile        on;
    -    #tcp_nopush     on;
    +    tcp_nopush      on;
    +    tcp_nodelay     on;
     
         #keepalive_timeout  0;
         keepalive_timeout  65;
     
    -    #gzip  on;
    +    gzip     off;
    +    directio off;
    +    aio      on;
     
         server {
    -        listen       80;
    +        listen       80 accept_filter=httpready reuseport;
             server_name  localhost;
     
             #charset koi8-r;
    @@ -46,8 +51,15 @@
             #access_log  logs/host.access.log  main;
     
             location / {
    -            root   /usr/local/www/nginx;
    +            root   /tvheadend;
                 index  index.html index.htm;
    +            fancyindex             on;
    +            fancyindex_exact_size  off;
    +            fancyindex_localtime   on;
    +        }
    +
    +        location = /favicon.ico {
    +            root   /usr/local/www/nginx;
             }
     
             #error_page  404              /404.html;
    
  30. Load the accf_http kernel module and nginx during system boot:
    # sysrc kld_list+=accf_http
    # sysrc nginx_enable=YES
    
  31. For now, start nginx manually:
    # kldload accf_http
    # service nginx start
    
  32. For samba, create a simple /usr/local/etc/smb4.conf configuration file, e.g.:
    # cat <<EOF >/usr/local/etc/smb4.conf
    [global]
            interfaces = bridge0
    
            workgroup = WORKGROUP
            security = user
            netbios name = tvserver
            server string = tvserver.local
            hostname lookups = yes
    
            load printers = no
            show add printer wizard = no
            time server = yes
            map to guest = Bad User
            use mmap = yes
    
            dos charset = 850
            unix charset = UTF-8
            mangled names = no
    
            log level = 0
    
            server min protocol = LANMAN1
    
    [tvheadend]
            guest ok = yes
            hide dot files = no
            path = /tvheadend
            read only = yes
            use sendfile = yes
    EOF
    
  33. Enable and start samba:
    # sysrc samba_server_enable=YES
    # service samba_server start
    

Related files

check_tvh.py checks and restarts webcamd and Tvheadend if Tvheadend does not report any active TV device.