Skip to content

Instantly share code, notes, and snippets.

@rubikcuber
Last active January 6, 2026 15:51
Show Gist options
  • Select an option

  • Save rubikcuber/c1567c912e0fe0f215263689b1eb14e7 to your computer and use it in GitHub Desktop.

Select an option

Save rubikcuber/c1567c912e0fe0f215263689b1eb14e7 to your computer and use it in GitHub Desktop.
Spawn script for tvheadend to transcode live TV in real-time using an Intel / vaapi hardware device. Used
#!/bin/bash
# Spawn script for tvheadend to transcode live TV in real-time using an Intel / vaapi hardware device.
# Create a new Profile in tvheadend called "pass", disable the built-in pass profile, and set this one as
# the default.
#
# This is specifically designed for UK DVB-T and DVB-S processing for Plex DVR. The script transcodes
# the incoming TV stream to 50hz video using field based deinterlacing. Plex enforces frame based
# deinterlacing at all times, so if you have a client that cannot direct play recordings reliably
# (e.g. Samsung and probably others) you will only be able to get 25hz "filmic" recordings. This
# script deals with it at the source, so Plex never gets the interlaced video.
#
# The resulting MPEG-TS stream has errors and discontinuities fixed so TVs can Direct Stream reliably.
log_dir="/config/logs"
# Set RAW_CAPTURE_DIR to empty to disable raw capture.
RAW_CAPTURE_DIR="/recordings/raw_streams"
mkdir -p "${log_dir}"
logfile="${log_dir}/stream-stable.$(date +%Y%m%d).log"
ffmpeg=/usr/bin/ffmpeg
hwdevice=$(ls -l /sys/class/drm/renderD*/device/driver 2>/dev/null | grep i915 | sed 's/.*\(renderD[0-9]*\).*/\/dev\/dri\/\1/' | head -1)
if [ -z "${hwdevice}" ]; then
hwdevice="/dev/dri/renderD128"
fi
echo "$(date): starting stable stream transcode" >> "${logfile}"
echo "$(date): hwdevice=${hwdevice}" >> "${logfile}"
if [ -n "${RAW_CAPTURE_DIR}" ]; then
mkdir -p "${RAW_CAPTURE_DIR}"
raw_file="${RAW_CAPTURE_DIR}/stream-$(date +%Y%m%d-%H%M%S).ts"
echo "$(date): raw capture enabled at ${raw_file}" >> "${logfile}"
fi
# Always field-based deinterlace and 50fps output for stability.
cmd=(
"${ffmpeg}"
-hide_banner -nostdin
-fflags +genpts+discardcorrupt
-err_detect ignore_err
-avoid_negative_ts make_zero
-max_interleave_delta 0
-analyzeduration 2M -probesize 2M
-hwaccel vaapi -hwaccel_device "${hwdevice}" -hwaccel_output_format vaapi
-i pipe:0
)
cmd+=(
-map 0:v:0 -map 0:a:0 -map 0:s?
-vf "deinterlace_vaapi=rate=field:auto=1"
-r 50
-c:v h264_vaapi -b:v 6000k -maxrate 8000k -bufsize 12000k
-c:a aac -b:a 320k -ac 2
-c:s copy
-muxdelay 0 -muxpreload 0
-f mpegts -mpegts_flags +resend_headers+initial_discontinuity
pipe:1
)
if [ -n "${RAW_CAPTURE_DIR}" ]; then
tee "${raw_file}" | "${cmd[@]}" 2>> "${logfile}"
else
"${cmd[@]}" 2>> "${logfile}"
fi
exit_code=$?
echo "$(date): ffmpeg exited with code ${exit_code}" >> "${logfile}"
# If ffmpeg fails, fall back to passthrough to keep the stream alive.
if [ "${exit_code}" -ne 0 ]; then
echo "$(date): fallback to passthrough" >> "${logfile}"
tee
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment