clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / internal / tuner / tuner.go
tuner.go 4.5K
1// Package tuner shares the HDHomeRun's tuners among viewers, recordings2// and measuring (see Manager), and says how ffmpeg reads what they3// receive (Input).4package tuner56import (7 "context"8 "fmt"9 "hash/fnv"10 "io"1112 "airwaves/internal/cc"13 "airwaves/internal/ts"14)1516// Device describes the tuner.17type Device struct {18 ID string `json:"id"`19 Name string `json:"name"` // "HDHomeRun FLEX DUO"20 Kind string `json:"kind"` // "hdhomerun" or "none"21 Model string `json:"model,omitempty"`22 Tuners int `json:"tuners,omitempty"`23 Detail string `json:"detail,omitempty"`24 // Standards lists the broadcast standards the tuners receive ("ATSC25 // 1.0"); none without a tuner. ATSC3 is whether that includes ATSC 3.026 // (NextGen TV).27 Standards []string `json:"standards,omitempty"`28 ATSC3 bool `json:"atsc3"`29}3031// AudioTrack is one audio stream of an input.32type AudioTrack struct {33 Map string `json:"-"` // ffmpeg stream specifier, e.g. "0:a:1"34 Lang string `json:"lang"` // ISO 639-2 code from the broadcast, e.g. "spa"35 // Described marks a video description (DVS) track for blind viewers.36 Described bool `json:"described,omitempty"`37}3839// Input is how ffmpeg should read a channel or recording.40type Input struct {41 Args []string // input options ending with "-i <url>"42 // Video is the ffmpeg stream specifier of the picture.43 Video string44 // Audio lists the tracks to offer, main track first. Empty means no45 // audio (for example ATSC 3.0 AC-4, which ffmpeg cannot decode).46 Audio []AudioTrack47 // Broadcast marks a real over-the-air source: deinterlace it, and48 // expect CEA-608 closed captions in the picture.49 Broadcast bool50 // Captions marks CEA-608 closed captions in the picture of a source51 // that isn't a broadcast, such as a custom channel.52 Captions bool53 // Subtitles, when set, are the source's captions as text, timed from54 // its first frame, for a live stream to offer as WebVTT.55 Subtitles *cc.Script56 // Note is shown to the viewer, e.g. why audio is missing.57 Note string58 // VOD marks a recording: transcode as fast as possible and keep every59 // segment so the player can seek.60 VOD bool61 // Copy marks a picture that's H.264 already (a podcast's), copied as62 // it is rather than transcoded; its sound is still made AAC.63 Copy bool64 // Offset is where in the recording this input starts, in seconds.65 Offset float6466 // Source, when set, writes the channel's MPEG-TS for ffmpeg to read on67 // stdin (Args then end with "-i pipe:0"), until ctx ends.68 Source func(ctx context.Context, w io.Writer) error69}7071// AudioTracks turns a program's audio streams into tracks to offer, in72// broadcast order (main track first). AC-4 (ATSC 3.0) can't be decoded by73// ffmpeg and is left out, keeping the others' numbering, with a note when74// it leaves nothing to hear.75func AudioTracks(p ts.Program) ([]AudioTrack, string) {76 var out []AudioTrack77 skipped := false78 for n, s := range p.Audio() {79 if s.Codec() == "AC4" {80 skipped = true81 continue82 }83 out = append(out, AudioTrack{Map: fmt.Sprintf("0:a:%d", n), Lang: s.Lang, Described: s.Described})84 }85 if len(out) == 0 && skipped {86 return nil, "ATSC 3.0 AC-4 audio cannot be decoded; video only"87 }88 return out, ""89}9091var patterns = []string{92 "smptehdbars=s=1280x720:r=30",93 "testsrc2=s=1280x720:r=30",94 "mandelbrot=s=1280x720:r=30",95 "life=s=640x360:r=30:mold=10:ratio=0.12:life_color=#9fd8ff:death_color=#0b1530,scale=1280:720:flags=neighbor",96 "gradients=s=1280x720:r=30:speed=0.015:n=4",97 "zoneplate=s=1280x720:r=30:kt2=6:ku=64:kv=64",98 "rgbtestsrc=s=1280x720:r=30",99 "cellauto=s=640x360:r=30:rule=110:scroll=1,scale=1280:720:flags=neighbor",100}101102// TestPattern is a test pattern read straight from ffmpeg's generators,103// with an English and a Spanish tone, for testing streaming without a104// tuner.105func TestPattern(number string) Input {106 video, eng, spa := demoSources(number)107 return Input{108 Args: []string{109 "-re", "-f", "lavfi", "-i", video,110 "-re", "-f", "lavfi", "-i", eng,111 "-re", "-f", "lavfi", "-i", spa,112 },113 Video: "0:v:0",114 Audio: []AudioTrack{{Map: "1:a:0", Lang: "eng"}, {Map: "2:a:0", Lang: "spa"}},115 }116}117118// demoSources picks a stable pattern and two tones (main and second119// language) for a channel number.120func demoSources(number string) (video, main, second string) {121 h := fnv.New32a()122 h.Write([]byte(number))123 n := h.Sum32()124 tone := 220 + float64(n%12)*40125 sine := "sine=frequency=%.0f:sample_rate=48000,volume=0.05"126 return patterns[n%uint32(len(patterns))], fmt.Sprintf(sine, tone), fmt.Sprintf(sine, tone*1.5)127}