Broadcasting 4k TV over ATSC 1.0 -- How??
We didn’t say it’d be easy
IEEE Madison, Feb 25th, 2025
Anton Kapela, Channel 3, Eugene - K03IM-D
CONTEXT
This talk is not about a translator, it’s a LP-D
This talk is about how someone used mostly open source tools to get UltraHD programs working over ATSC 1.0
BUT REALLY…
This talk is about codecs
-
Our relationship with them
-
Human sense & perception
Glossary - Warning: buzzwords ahead!
Past
Present
Future
Live
Demo
Past: How’d we get here?
Journey Into Wireless
1996
Garage Sale
1999/00
Aironet 900Mhz
2001
Ooh!
WiFi
2002
Mesh
Madison
2006+
Wireless
for real
Past: But Who is this Anton guy?
But Who is this Anton guy?
In The Beginning
Worked in/near A/V/IPTV for much of my career; data centers, ISPs, microwave, mobility networks, you name it: if it moves bits, I touch(ed) it
…But Mostly Ignored Broadcast!
Everything was great, until one day:
Happened to buy a UHD TV in 2020; nothing on the air besides MPEG2…
No ATSC 3 in Madison, and this dumb TV didn’t even support it!
…couldn’t let this stand; had to get something working with UHD resolutions
But …
Can 19.3 megabits do anything visually interesting?
… with HEVC?
And: Can anything DECODE HEVC over a normal Transport Stream (TS) … over the air (or cable)?
Let’s get something working
A Starting Point for UHD - Rate Targets Circa 2020
Credit: https://codecalamity.com/encoding-settings-for-hdr-4k-videos-using-10-bit-x265/
Typical UHD rates
Bitrates we want
SSIM we want
19.39 mbit - ATSC 1.0 Phy Rate
Orienting the discussion - Human perception system
Let’s get something working
Oops
Oops
Real Lab: Figure It Out @ the datacenter
Setup a “personal cloud” to explore transcoding options in HEVC with x.265
~750 GHz lets you figure it out with brute-force
Write programs to run through variations of 1000’s of these, compare results:
Average Case: “Offline” transcode example - 10 bit, ~4.5mbit ABR/VBR:
$ ffmpeg -fflags +discardcorrupt -ec guess_mvs+deblock+favor_inter -i input.mkv -vf'scale=3840:2160:force_original_aspect_ratio=decrease,�pad=3840:2160:-1:-1:color=black'-c:a ac3 -ac 2 -ar 48000 -c:v libx265 -preset medium -x265-params 'hdr-opt=1:repeat-headers=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,50):max-cll=3201,386:keyint=72:ref=5:bframes=3:b-adapt=2:bitrate=4000:vbv-maxrate=4500:vbv-bufsize=8000:merange=256:me=hex:no-open-gop=1:hrd=1:aq-mode=3:pmode=1:rect=1:rc-lookahead=36' -map_metadata -1 -map_chapters -1 output.mkv
What does that look like?
~3 FPS
As good as it got in 2022/23: ~20-threads per node, latest-greatest x.265 library and Intel Xeon E5 v4 cpus, 3840x2160 main10
~70 watt/seconds per frame, per node
Result:
squeezing non-noisy things beyond the visual rate floor: ~4.5 Mbits coded rate @2160
Ok, what about live HEVC UHD transcoding?
Micro ITX 1RU half depth case
Tiny 300W PS
Nvidia GPUs (nvenc, CUDA, npp offload)
i9 10k Extreme CPU
10c/20t
Legit OOB
and IPMI!
Standard copper gige for IP video I/O
Quad channel, lowest-latency supported DDR4
Hot-swap SATA bays
Rude blue LED power light
Custom kapton PCI-e ribbon adapter
Magic: Live transcode NASA UHD to 10-bit SDR at ~4 Mbits VBR on GPU:
while :; do ffmpeg -fflags +discardcorrupt -i udp://238.1.1.1:2000?fifo_size=400000\?overrun_nonfatal=1\&buffer_size=10000000 -map 0:i:4161 -map 0:i:4164 -vf fps=fps=30000/1001 -pix_fmt p010le -c:v hevc_nvenc -g 90 -preset p7 -b:v 3.2M -maxrate 4.5M -bufsize 8M -bf 2 -refs 5 -rc-lookahead 27 -weighted_pred 0 -b_ref_mode each -nonref_p 0 -spatial_aq 1 -temporal_aq 1 -aq-strength 8 -forced-idr 1 -strict_gop 1 -c:a copy -program title="NASA-4K":st=0:st=1 -color_primaries bt709 -color_trc bt709 -colorspace bt709 -f mpegts -mpegts_start_pid 160 -max_interleave_delta 200000 -muxpreload 2 -flush_packets 0 udp://224.2.2.254:1466?pkt_size=1316\&bitrate=8121600 ; sleep 1 ; done
*take a breath
All of the books in the world contain no more information than is broadcast as video in a single large American city in a single year. Not all bits have equal value.
What’s in a CODEC, besides few bits?
Lies, approximations, exploits, and shortcuts.
Exploits
Exploits
Exploits
Q: What’s in a CODEC? A: Silencing (c. 2010)
Applied Silencing:
Motion Vectors
Exhaustive motion estimation, coded pict: <20 Kbytes
Exhaustive motion estimation, coded pict: <10 Kbytes
Summary of Findings
Enablers for ultra-low rate HD/UHD
Absolutely Essential: to reach the visual-silencing-floor in HEVC, we need:
Enablers for ultra-low rate HD/UHD
These algorithmic mechanisms (in previous slide) have substantial compute/power cost, generally not fully exploited by OTT/streamers, and impractical at YouTube/etc scale. YouTube ingests >30,000 hours per hour of video in 2022: no time & power for bespoke encoding of everything.
They are eminently practical for broadcast:
one screen target, one rate target, one program stream
Some Just Can’t Even (do one stream)
Everyone gets their own thing!
What does that look like, in 2025?
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
“LOL wut”
Thus, in OTA Broadcast:
No side-by-side ABX → “is it wrong?”
Don’t let perfect block good
Nitpicks
Nitpicks
Nitpicks
Nitpicks - or total breakdown? (NO FGS!)
Nitpicks - or total breakdown? (NO FGS!)
Nerdy Transport Stream Stuff
Statistics & Mux Handling
Coded UHD pictures on the wire:
I-frame: ~50 to 150 Kilobytes
B-frame: <1 up to ~17 kbytes
P-frame: ~30 to 70 Kilobytes
Viewed another way… Presentation order & Reference weights
Props to Elecard StreamEye! →
So what’s the problem challenge of VBR streams?
TS packet timestamps on the wire:
Present: What’s Channel 3 up to?
K03IM-D Rundown
If it works on DVB-C, maybe it’ll also work over ATSC 1.0
Just add: PSIP, TVCT, EAS switching
Channel 3 Broadcast System
Power Amp
Ant.
ADV-8300
ESXi VM host systems
Linux Guest OS
ffmpeg
MPTS mux TS shaping
Linux Guest OS
ffmpeg/vlc
playout/playlist
Linux Guest OS
OBS
Live input switching & mixing
Ethernet Switch
D2D 5220
Control systems & OAM terminals
ASI Monitoring
(Ineoquest IQ / ASI Cricket-A6x,
Nucomm ASI-IP)
Mask Filter
GPSDO - OXCO
NTPD
10MHz
Ref In
GPS ant.
MPEG TS Mux
8VSB modulator
Enter: D2D Tech
After semi-exhaustive research, determined D2D Technologies has best price/perf/hackability ratio* (Linux inside!) for muxing & “finishing” (ie. PSIP, EIT, ETT, other ATSC 1 adornments): https://d2dtechnologies.com/d2flex5220/
Got to know Steve Doll and Jessica Colyer, agreed to “try supporting” more codecs in VBR modes
With mux hardware in hand, I:
Caveats
Where we started - Broken VBR
Initially, mux logic would simply drop TS packets that weren’t “within CBR rate estimate window” - oops!
Where we started - Broken VBR handling
Tried to ‘better constrain’ HEVC code rate to be “almost CBR” – still insufficient: I-frame bursts created gaps in TS output cadence. PCR packets spaced too far apart for most receivers to tolerate.
How to support VBR in MPTS?
Where we are now - Robust VBR
Input Loss
Input Return
With new rate estimation logic, D2D mux properly transmits packets in a weighted-fair round robin scheme; PCR continuity within tolerances; perfect adaptation to input loss/restoration.
TS/PES pacing is preserved through mux: greater PES/NAL byte counts for I/IDR than B/P-frames reveals corresponding “gaps” of PTS in time, while retaining consistent PCR intervals: receiver remains phase locked + Rx slack buffer managed (ie. PCR < PTS/DTS)
Where we are now - Robust VBR
Input Loss
Input Return
Where we are now - Robust When Maxed Out
Oversubscribed mux rate is fine, within limits
If PTS/DTS greater than PCR timestamp value, we’re fine. Note PCR cleanly “linear” (despite interval variance) while DTS/PTS delta “wanders” due to momentary mux queue depth. Long term VBR encoder control ensures DTS/DTS > PCR (ie. pictures can’t “happen” before current time in PCR).
Where we are now - Maxed Out 4x UHD progs
Hurdle: Cleared, next:
Take this on the road:
https://www.rabbitears.info/market.php?request=print_station&facility_id=185855
https://enterpriseefiling.fcc.gov/dataentry/public/tv/publicFacilityDetails.html?facilityId=185855
Channel 3 Eugene Photo Gallery: https://imgur.com/a/IyNAxlz
Program Flow: ~2100 miles, ~53 Msec RTT
Madison Datacenter
K03IM-D Xmitter
Internets
Cool site pics
Cool site pics
Cool site pics
What about measurements/performance?
L/R & Contours
Mux Feb 2024, 14 programs
More Mux Pics
PCR Off Air Stats: Not bad!
PCR Off Air Stats: Not bad!
PCR From FFMPEG: Actually Perfect
Feedback from the field
Other subtle adjustments for “good display compatibility” with VBR-style AVC and HEVC:
Back To
The
Future!
What’s next?
Enablers for ultra-low rate HD/UHD Cont.
Channel 3 Refinement wishlist:
Does the world really need *another* mux/transcoder/router?
Questions?
tkapela@gmail.com
Appendix A
FFMPEG notes & etc
RX stream from an SRT source, relay back out to mcast, output connection stats every 4000 msec, using fairly robust options - 2000 msec ARQ time to live, 2100 msec fixed sender/receiver latency, sender buffer allocation 10 megabytes, and overhead bandwidth of 100%
while :; do srt-live-transmit -s 4000 "srt://source.host.net:1234?mode=caller&latency=2100&lossmaxttl=2000&&sndbuf=10000000&oheadbw=100" udp://233.65.202.50:1234 ; sleep 1 ; done
Pulling from YT live source, conforming to 720p at lowest-possible bcastable frame rate of 23.976, low-bitate target
while :; do yt-dlp --socket-timeout 6 https://www.youtube.com/watch?v=xxxx -f best -o - | mbuffer -q -W 32 -m 64k | ffmpeg -xerror -async 1 -re -i - -map 0:0 -map 0:1 -c:a ac3 -ab 96k -cutoff 18000 -ac 2 -ar 48000 -vf "minterpolate=fps=24000/1001:mi_mode=blend" -fps_mode cfr -c:v libx264 -g 72 -rc-lookahead 48 -preset veryslow -tune animation -b:v 400K -nal-hrd vbr -maxrate:v 1.5M -bufsize 1.5M -me_method umh -me_range 64 -refs 4 -bf 4 -aq-mode 3 -aq-strength 0.9 -qcomp .1 -flags +loop+qpel+cgop -map_metadata -1 -map_chapters -1 -f mpegts -flush_packets 0 udp://233.65.202.201:1234?pkt_size=1316 ; sleep 1 ; done
Listening to a mcast source, connecting to remote host via TCP, relaying A/V stream to target host
while :; do ffmpeg -i udp://233.65.202.201:1234 -c copy -f mpegts -flush_packets 0 tcp://10.0.3.2:1423?pkt_size=3008 ; sleep 1 ; done
Relaying a unicast TSoTCP stream in a CBR TS, to another TS mux, via unicast UDP
while :; do ffmpeg -fflags +genpts+discardcorrupt -i tcp://:1490?listen=1\&listen_timeout=10000\&timeout=20000000 -map 0:v -map 0:a -c:v copy -c:a copy -program title=Some Program Name:st=0:st=1 -f mpegts -mpegts_start_pid 300 -flush_packets 0 -muxdelay 1.7 -max_interleave_delta 200000 -muxrate 2000320 -pcr_period 70 udp://10.0.3.19:1402?pkt_size=1316\&bitrate=2000320\&fifo_size=40000\&overrun_nonfatal=1 ; sleep 1 ; done
Rx a slice-encoded (note: single decoder thread to avoid race conditions) 1080i program from mcast source, convert to 10 bit non-subsampled chroma, deinterlace/etc, convert back to yuv420 subsampled, force constant frame rate at 29.97, normalize audio level to ~-6 dBfs peak, various x.264 adjustments tuning encoder for “talking head” news programming; note custom quantizer matrix, output encoded stream towards mcast destination
while :; do ./ffmpeg-6.1-amd64-static/ffmpeg -xerror -async 1 -threads:v 1 -i udp://233.65.202.50:1234?fifo_size=200000\&overrun_nonfatal=1\&reuse=1\&timeout=900000000\&buffer_size=10000000 \
-vf format=yuv444p10le,yadif=mode=0:deint=all,format=yuv420p,fps=fps=30000/1001 \
-fps_mode cfr \
-map 0:v:0 -map 0:a:0 \
-af "dynaudnorm=p=.25" \
-c:a ac3 -ac 2 -ab 96k -ar 48000 -cutoff 18000 \
-c:v libx264 -g 90 -preset veryslow -b:v 1M -maxrate:v 2M -bufsize 2M -refs 4 -bf 4 \
-aq-mode 3 -aq-strength 0.90 -b_qfactor 1.0 -b_qoffset 0.0 -qcomp 0.3 \
-dc 9 -subq 10 -weightp 2 -weightb 1 -bidir_refine 4 -mixed-refs 1 \
-8x8dct 1 -partitions all -direct-pred auto -nal-hrd vbr \
-rc-lookahead 30 -me_range 64 -me_method umh -trellis 2 -b_strategy 2 -b-pyramid 2 \
-fast-pskip 0 -flags +qpel+loop+cgop \
-intra_matrix "8,8,9,9,10,10,11,11,8,9,9,10,10,11,11,12,9,9,10,10,11,11,12,12,9,10,10,11,11,12,13,13,10,10,11,11,12,13,13,14,10,11,11,12,13,13,14,15,11,11,12,13,13,14,15,15,11,12,12,13,14,15,15,16" \
-inter_matrix "8,8,9,9,10,10,11,11,8,9,9,10,10,11,11,12,9,9,10,10,11,11,12,12,9,10,10,11,11,12,13,13,10,10,11,11,12,13,13,14,10,11,11,12,13,13,14,15,11,11,12,13,13,14,15,15,11,12,12,13,14,15,15,16" \
-f mpegts -max_interleave_delta 0 -flush_packets 0 udp://233.65.202.15:1234?pkt_size=1316 ; sleep 1 ; done
Pick a random file from a list of files matching a pattern, transcode this file at native playback rate, keeping multichannel audio intact (or conform to 6 chan/5.1 layout); conform all output to 1440x1080 at 4:3 aspect ratio, stop muxing when audio or video inputs have no more data (-shortest), to ensure output a/v pids of matching lengths; restart a/v PTS’s from zero, and signal an explicit TS discontinuity to inform receivers that a new TS is starting
IFS=$'\n' ; while :; do for i in `find "/mnt/space3/blah/" -name "*.mkv" -type f |shuf` ; do \
./ffmpeg-6.1-amd64-static/ffmpeg -fflags +discardcorrupt \
-ec guess_mvs+deblock+favor_inter -err_detect ignore_err \
-readrate_initial_burst 4 -async 1 -re -i "$i" \
-fps_mode cfr -map 0:v -map 0:a \
-ar 48000 -c:a eac3 -b:a 192k -cutoff 18000 -ac 6 -channel_layout "5.1" \
-color_primaries 1 -color_trc 1 -colorspace 1 \
-c:v libx264 -g 72 -preset veryslow -b:v 0.9M -maxrate:v 2M -bufsize 2M -refs 4 -bf 4 \
-psy-rd 1.1:0.5 -deblock -5:-3 -tune grain -b_qfactor 1.0 -b_qoffset 0.0 \
-dc 9 -subq 7 -weightp 2 -weightb 1 -aq-mode 3 -aq-strength 0.80 -qcomp 0.4 \
-mixed-refs 1 -8x8dct 1 -partitions all -direct-pred auto -nal-hrd vbr \
-rc-lookahead 36 -me_range 64 -me_method umh -trellis 2 -b_strategy 2 \
-b-pyramid 2 -bidir_refine 2 -fast-pskip 0 \
-flags +qpel+loop \
-intra_matrix "8,8,8,9,8,9,11,11,11,11,11,11,13,12,13,13,13,13,13,13,13,13,13,13,13,14,14,14,17,17,17,13,13,13,14,13,15,15,16,16,17,17,18,16,18,17,17,17,17,19,19,20,20,20,24,24,23,23,28,28,29,34,34,41" \
-inter_matrix "8,8,9,9,10,10,11,11,8,9,9,10,10,11,11,12,9,9,10,10,11,11,12,12,9,10,10,11,11,12,13,13,10,10,11,11,12,13,13,14,10,11,11,12,13,13,14,15,11,11,12,13,13,14,15,15,11,12,12,13,14,15,15,16" \
-vf "fps=24000/1001,format=yuv444p10le,atadenoise,setpts=PTS-STARTPTS,\
scale=iw*sar:ih:flags=lanczos,setsar=1,\
scale=1440:1080:force_original_aspect_ratio=decrease:flags=lanczos,\
pad=1440:1080:-1:-1:color=black,setsar=1,setdar=4/3,format=yuv420p" \
-af "asetpts=PTS-STARTPTS,volume=+3dB" \
-map_metadata -1 -map_chapters -1 \
-f mpegts -flush_packets 0 -shortest -mpegts_flags initial_discontinuity udp://233.6.2.9:1090?pkt_size=1316 \
; done ; done
Ultra-low-rate mpeg2 video encoding, targeting 1.1 mbits/sec for 16:9 720x480p - note for super-low rates, variable length coding table 0 (vlc) is recommended. We convert to non-subsampled 10 bit 4:4:4 chroma for deinterlacing, noise reduction, and scaling - ie. example assumes input is 1080i, lowrez progressive out. Max motion vector search area at 511 pixels. Set spatial and temporal masking to bias quantizer upwards for higher motion and higher complexity areas, leaving more bits for gradients/smooth areas. Less banding, more reduction on details that won’t be visible to most viewers.
while :; do ./ffmpeg-6.1-amd64-static/ffmpeg -threads 16 -fflags +discardcorrupt -ec guess_mvs+deblock+favor_inter -async 1 \
-i http://208.66.132.245:5004/auto/v15.1?overrun_nonfatal=1 \
-map 0:v:0 -map 0:a:0 \
-c:a ac3 -ac 2 -ab 96k -ar 48000 -cutoff 18000 \
-vf "format=yuv444p10le,yadif=mode=0:deint=all,fps=fps=30000/1001,\
hqdn3d=3:3:3:3,scale=720x480:flags=lanczos+accurate_rnd,\
hqdn3d=2:2:2:2,format=yuv420p" \
-fps_mode cfr \
-c:v mpeg2video -threads 16 -me_range 511 -intra_vlc 0 \
-tcplx_mask 0.3 -scplx_mask 0.2 -qcomp 0.06 \
-cmp satd -subcmp satd -mbcmp satd -dct int -idct int \
-qmin 2 -qmax 200 \
-trellis 0 -keyint_min 8 -g 60 -bf 3 -b_strategy 2 -brd_scale 2 -bidir_refine 2 -border_mask 2.0 \
-b:v 1.1M -maxrate 4M -bufsize 4M -dc 9 \
-intra_matrix "8,8,8,9,8,9,11,11,11,11,11,11,13,12,13,13,13,13,13,13,13,13,13,13,13,14,14,14,\
17,17,17,13,13,13,14,13,15,15,16,16,17,17,18,16,18,17,17,17,17,19,19,20,20,20,24,24,23,23,28,28,29,34,34,41" \
-inter_matrix "8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,\
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,12" \
-f mpegts -flush_packets 0 udp://233.65.202.151:1234?overrun_nonfatal=1\&pkt_size=1316 ; sleep 1 ; done