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 / ts / psi.go
psi.go 11.0K
1package ts23import (4 "cmp"5 "slices"6 "strings"7 "unicode/utf16"8)910// maxSection bounds a section being collected; ATSC's private sections are11// at most 4096 bytes.12const maxSection = 40961314// Stream is one elementary stream of a program.15type Stream struct {16 PID int17 // Type is the PMT's stream_type: 0x02 MPEG-2 video, 0x1b H.264, 0x2418 // HEVC, 0x81 AC-3 and 0x87 E-AC-3 (ATSC), 0x0f AAC, 0x03 and 0x0419 // MPEG audio, 0x06 private data its descriptors name.20 Type byte21 // Lang is the ISO 639-2 language code ("eng", "spa") from the ISO 63922 // language descriptor or ATSC's AC-3 audio descriptor; empty when the23 // broadcast gives none.24 Lang string25 // Described marks a video description track for blind viewers.26 Described bool2728 codec string // named by a descriptor, for private data29}3031var codecs = map[byte]string{32 0x01: "MPEG1VIDEO", 0x02: "MPEG2VIDEO", 0x1b: "H264", 0x24: "HEVC",33 0x03: "MPEG2AUDIO", 0x04: "MPEG2AUDIO", 0x0f: "AAC", 0x11: "AAC", 0x81: "AC3", 0x87: "EAC3",34}3536// Codec names the stream's coding ("MPEG2VIDEO", "H264", "HEVC", "AC3",37// "EAC3", "AAC", "MPEG2AUDIO", "AC4"); "" for data.38func (s Stream) Codec() string { return cmp.Or(s.codec, codecs[s.Type]) }3940// Video reports whether the stream is a picture.41func (s Stream) Video() bool {42 switch s.Codec() {43 case "MPEG1VIDEO", "MPEG2VIDEO", "H264", "HEVC":44 return true45 }46 return false47}4849// Audio reports whether the stream is sound.50func (s Stream) Audio() bool {51 switch s.Codec() {52 case "AC3", "EAC3", "AAC", "MPEG2AUDIO", "AC4":53 return true54 }55 return false56}5758// Program is one program of a multiplex, as its PMT describes it.59type Program struct {60 Number int61 PMTPID int62 PCRPID int63 Streams []Stream // in PMT order64}6566// Audio lists the program's audio streams in PMT order: the main track67// first, as broadcasters list them.68func (p Program) Audio() []Stream {69 var out []Stream70 for _, s := range p.Streams {71 if s.Audio() {72 out = append(out, s)73 }74 }75 return out76}7778// VirtualChannel is a channel of ATSC's terrestrial or cable virtual79// channel table.80type VirtualChannel struct {81 Major, Minor int82 Name string // the short name, "KWGN-DT"83 Program int // its program number in the multiplex84 TSID int // the transport stream it's in85 Hidden bool86 // ServiceType is 0x02 for digital TV, 0x03 audio only, 0x04 data.87 ServiceType int88}8990// Demux follows a multiplex's tables: the PAT, each program's PMT and the91// VCT. Feed it every packet.92type Demux struct {93 tables map[int]*assembler // by PID94 tsid int95 patVer int // -1 before a PAT96 pmts map[int]int // PMT PID by program number, from the PAT97 progs map[int]Program98 pmtVer map[int]int99 vct vctState100 // gen counts changes to the PAT and PMTs, for Filter.101 gen int102}103104type vctState struct {105 version int // of parts, -1 for none106 parts map[int][]VirtualChannel107 done []VirtualChannel108 doneVer int109}110111// NewDemux returns a Demux that has seen nothing yet.112func NewDemux() *Demux {113 return &Demux{114 tables: map[int]*assembler{0: newAssembler(0), psipPID: newAssembler(psipPID)},115 patVer: -1, pmts: map[int]int{}, progs: map[int]Program{}, pmtVer: map[int]int{},116 vct: vctState{version: -1, doneVer: -1},117 }118}119120// Packet reads pkt's part of any table. Packets the demodulator flagged as121// damaged are skipped.122func (d *Demux) Packet(pkt []byte) {123 if len(pkt) < PacketSize || pkt[0] != syncByte || pkt[1]&0x80 != 0 {124 return125 }126 if a := d.tables[PID(pkt)]; a != nil {127 a.packet(pkt, d)128 }129}130131// TSID is the multiplex's transport stream ID, from its PAT; ok is false132// before one.133func (d *Demux) TSID() (int, bool) { return d.tsid, d.patVer >= 0 }134135// Program returns a program by number; ok is false until its PMT is read.136func (d *Demux) Program(number int) (Program, bool) {137 p, ok := d.progs[number]138 return p, ok139}140141// Programs lists the programs whose PMT has been read, by number.142func (d *Demux) Programs() []Program {143 out := make([]Program, 0, len(d.progs))144 for _, p := range d.progs {145 out = append(out, p)146 }147 slices.SortFunc(out, func(a, b Program) int { return cmp.Compare(a.Number, b.Number) })148 return out149}150151// Channels lists the virtual channels of the latest complete VCT; nil152// before one.153func (d *Demux) Channels() []VirtualChannel { return slices.Clone(d.vct.done) }154155// Ready reports whether the PAT and every PMT it lists have been read.156func (d *Demux) Ready() bool {157 if d.patVer < 0 {158 return false159 }160 for n := range d.pmts {161 if _, ok := d.progs[n]; !ok {162 return false163 }164 }165 return true166}167168// section takes a whole section that arrived on pid.169func (d *Demux) section(pid int, s []byte) {170 // The sections followed all have the long form, with a CRC.171 if len(s) < 12 || s[1]&0x80 == 0 || s[5]&1 == 0 || CRC32(s) != 0 {172 return173 }174 switch {175 case s[0] == 0x00 && pid == 0:176 d.pat(s)177 case s[0] == 0x02 && pid != 0 && pid != psipPID:178 d.pmt(pid, s)179 case (s[0] == 0xc8 || s[0] == 0xc9) && pid == psipPID:180 d.vctSection(s)181 }182}183184func (d *Demux) pat(s []byte) {185 tsid, ver := int(s[3])<<8|int(s[4]), int(s[5]>>1&0x1f)186 if ver == d.patVer && tsid == d.tsid {187 return188 }189 pmts := map[int]int{}190 body := s[8 : len(s)-4]191 for i := 0; i+4 <= len(body); i += 4 {192 if n := int(body[i])<<8 | int(body[i+1]); n != 0 {193 pmts[n] = int(body[i+2]&0x1f)<<8 | int(body[i+3])194 }195 }196 d.tsid, d.patVer, d.pmts = tsid, ver, pmts197 // Programs gone, or whose PMT moved, are read again.198 for n, p := range d.progs {199 if pid, ok := pmts[n]; !ok || pid != p.PMTPID {200 delete(d.progs, n)201 delete(d.pmtVer, n)202 }203 }204 keep := map[int]bool{0: true, psipPID: true}205 for _, pid := range pmts {206 keep[pid] = true207 if d.tables[pid] == nil {208 d.tables[pid] = newAssembler(pid)209 }210 }211 for pid := range d.tables {212 if !keep[pid] {213 delete(d.tables, pid)214 }215 }216 d.gen++217}218219func (d *Demux) pmt(pid int, s []byte) {220 if len(s) < 16 {221 return222 }223 number, ver := int(s[3])<<8|int(s[4]), int(s[5]>>1&0x1f)224 if d.pmts[number] != pid {225 return // not a program the PAT lists here226 }227 if v, ok := d.pmtVer[number]; ok && v == ver {228 return229 }230 p := Program{Number: number, PMTPID: pid, PCRPID: int(s[8]&0x1f)<<8 | int(s[9])}231 info := int(s[10]&0x0f)<<8 | int(s[11])232 body := s[12 : len(s)-4]233 if info > len(body) {234 return235 }236 for body = body[info:]; len(body) >= 5; {237 n := int(body[3]&0x0f)<<8 | int(body[4])238 if 5+n > len(body) {239 return240 }241 st := Stream{PID: int(body[1]&0x1f)<<8 | int(body[2]), Type: body[0]}242 describe(&st, body[5:5+n])243 p.Streams = append(p.Streams, st)244 body = body[5+n:]245 }246 d.progs[number], d.pmtVer[number] = p, ver247 d.gen++248}249250// describe reads what a stream's descriptors say of its language, its251// audience and, for private data, its coding.252func describe(s *Stream, desc []byte) {253 for len(desc) >= 2 {254 tag, n := desc[0], int(desc[1])255 if 2+n > len(desc) {256 return257 }258 b := desc[2 : 2+n]259 switch tag {260 case 0x0a: // ISO 639 language261 if n >= 4 {262 s.Lang = cmp.Or(s.Lang, language(b[:3]))263 s.Described = s.Described || b[3] == 3 // visual impaired commentary264 }265 case 0x81: // ATSC AC-3 audio266 ac3(s, b)267 case 0x05: // registration268 if s.Type == 0x06 && n >= 4 {269 switch string(b[:4]) {270 case "AC-3":271 s.codec = "AC3"272 case "EAC3":273 s.codec = "EAC3"274 case "AC-4":275 s.codec = "AC4"276 }277 }278 case 0x6a, 0x7a: // DVB AC-3, E-AC-3279 if s.Type == 0x06 {280 s.codec = map[byte]string{0x6a: "AC3", 0x7a: "EAC3"}[tag]281 }282 case 0x7f: // DVB extension: AC-4283 if s.Type == 0x06 && n >= 1 && b[0] == 0x15 {284 s.codec = "AC4"285 }286 }287 desc = desc[2+n:]288 }289}290291// ac3 reads ATSC's AC-3 audio descriptor (A/52 Annex A): its bit stream292// mode, where 2 is for the visually impaired, and the ISO 639 language at293// its end, past the optional fields before it.294func ac3(s *Stream, b []byte) {295 if len(b) < 3 {296 return297 }298 s.Described = s.Described || b[2]>>5 == 2299 i := 4 // past langcod300 if b[2]>>1&0x0f == 0 {301 i++ // 1+1 mode: langcod2302 }303 i++ // mainid or asvcflags304 if i >= len(b) {305 return306 }307 i += 1 + int(b[i]>>1) // textlen and the text308 if i >= len(b) {309 return310 }311 if b[i]&0x80 != 0 && i+4 <= len(b) {312 s.Lang = cmp.Or(s.Lang, language(b[i+1:i+4]))313 }314}315316// language is a three-letter ISO 639 code in lower case, "" for anything317// else.318func language(b []byte) string {319 l := strings.ToLower(string(b))320 for _, r := range l {321 if r < 'a' || r > 'z' {322 return ""323 }324 }325 return l326}327328func (d *Demux) vctSection(s []byte) {329 if len(s) < 16 {330 return331 }332 ver, num, last := int(s[5]>>1&0x1f), int(s[6]), int(s[7])333 if ver == d.vct.doneVer {334 return335 }336 if ver != d.vct.version {337 d.vct.version, d.vct.parts = ver, map[int][]VirtualChannel{}338 }339 body := s[10 : len(s)-4]340 chans := []VirtualChannel{}341 for range int(s[9]) {342 if len(body) < 32 {343 return344 }345 n := int(body[30]&0x03)<<8 | int(body[31])346 if 32+n > len(body) {347 return348 }349 chans = append(chans, VirtualChannel{350 Name: shortName(body[:14]),351 Major: int(body[14]&0x0f)<<6 | int(body[15]>>2),352 Minor: int(body[15]&0x03)<<8 | int(body[16]),353 TSID: int(body[22])<<8 | int(body[23]),354 Program: int(body[24])<<8 | int(body[25]),355 Hidden: body[26]&0x10 != 0,356 ServiceType: int(body[27] & 0x3f),357 })358 body = body[32+n:]359 }360 d.vct.parts[num] = chans361 var all []VirtualChannel362 for i := 0; i <= last; i++ {363 p, ok := d.vct.parts[i]364 if !ok {365 return // more sections to come366 }367 all = append(all, p...)368 }369 d.vct.done, d.vct.doneVer = all, ver370}371372// shortName decodes a VCT short name: seven UTF-16 code units, padded with373// zeros.374func shortName(b []byte) string {375 u := make([]uint16, 0, 7)376 for i := 0; i+1 < len(b); i += 2 {377 c := uint16(b[i])<<8 | uint16(b[i+1])378 if c == 0 {379 break380 }381 u = append(u, c)382 }383 return strings.TrimSpace(string(utf16.Decode(u)))384}385386// assembler collects one PID's sections from its packets.387type assembler struct {388 pid int389 cc int // the last packet's continuity counter, -1 before one390 buf []byte391 open bool // collecting a section whose start arrived392}393394func newAssembler(pid int) *assembler { return &assembler{pid: pid, cc: -1} }395396func (a *assembler) packet(pkt []byte, d *Demux) {397 pl := payload(pkt)398 if len(pl) == 0 {399 return400 }401 cc := int(pkt[3] & 0x0f)402 if cc == a.cc {403 return // a duplicate404 }405 lost := a.cc >= 0 && cc != (a.cc+1)&0x0f406 a.cc = cc407 if pkt[1]&0x40 != 0 {408 ptr := int(pl[0])409 if 1+ptr > len(pl) {410 a.open, a.buf = false, a.buf[:0]411 return412 }413 if a.open && !lost {414 a.buf = append(a.buf, pl[1:1+ptr]...)415 a.drain(d)416 }417 a.buf, a.open = append(a.buf[:0], pl[1+ptr:]...), true418 } else {419 if !a.open || lost {420 a.open, a.buf = false, a.buf[:0]421 return422 }423 a.buf = append(a.buf, pl...)424 }425 a.drain(d)426}427428// drain hands d each whole section collected, stopping at stuffing.429func (a *assembler) drain(d *Demux) {430 for a.open && len(a.buf) >= 3 {431 if a.buf[0] == 0xff {432 a.open, a.buf = false, a.buf[:0]433 return434 }435 n := 3 + (int(a.buf[1]&0x0f)<<8 | int(a.buf[2]))436 if n > maxSection {437 a.open, a.buf = false, a.buf[:0]438 return439 }440 if len(a.buf) < n {441 return442 }443 d.section(a.pid, a.buf[:n])444 a.buf = a.buf[:copy(a.buf, a.buf[n:])]445 }446}