diff --git a/format/format.go b/format/format.go index 219b23b43c..0fa3c36a64 100644 --- a/format/format.go +++ b/format/format.go @@ -106,6 +106,10 @@ const ( MPEG_PES_PACKET = "mpeg_pes_packet" MPEG_SPU = "mpeg_spu" MPEG_TS = "mpeg_ts" + MPEG_TS_PACKET = "mpeg_ts_packet" + MPEG_TS_PAT = "mpeg_ts_pat" + MPEG_TS_PMT = "mpeg_ts_pmt" + MPEG_TS_SDT = "mpeg_ts_sdt" MSGPACK = "msgpack" OGG = "ogg" OGG_PAGE = "ogg_page" @@ -327,3 +331,35 @@ type CSVLIn struct { type BitCoinBlockIn struct { HasHeader bool `doc:"Has blkdat header"` } + +type MpegTsStream struct { + ProgramPid int + Type int +} + +type MpegTsProgram struct { + Number int + Pid int + StreamPids []int +} + +type MpegTsPacketIn struct { + ProgramMap map[int]MpegTsProgram + StreamMap map[int]MpegTsStream +} + +type MpegTsPacketOut struct { + Pid int + ContinuityCounter int + TransportScramblingControl int + PayloadUnitStart bool + Payload []byte +} + +type MpegTsPatOut struct { + PidMap map[int]int // pid to program number that has pmt +} + +type MpegTsPmtOut struct { + Streams map[int]MpegTsStream +} diff --git a/format/mpeg/mpeg_pes.go b/format/mpeg/mpeg_pes.go index 6554aae826..5447ee53aa 100644 --- a/format/mpeg/mpeg_pes.go +++ b/format/mpeg/mpeg_pes.go @@ -5,6 +5,8 @@ package mpeg // http://dvdnav.mplayerhq.hu/dvdinfo/mpeghdrs.html import ( + "log" + "github.com/wader/fq/format" "github.com/wader/fq/pkg/bitio" "github.com/wader/fq/pkg/decode" @@ -19,8 +21,8 @@ func init() { Name: format.MPEG_PES, Description: "MPEG Packetized elementary stream", DecodeFn: pesDecode, - RootArray: true, - RootName: "packets", + // RootArray: true, + RootName: "packets", Dependencies: []decode.Dependency{ {Names: []string{format.MPEG_PES_PACKET}, Group: &pesPacketFormat}, {Names: []string{format.MPEG_SPU}, Group: &spuFormat}, @@ -45,36 +47,39 @@ func pesDecode(d *decode.D, _ any) any { spuD := d.FieldArrayValue("spus") - for d.NotEnd() { - dv, v, err := d.TryFieldFormat("packet", pesPacketFormat, nil) - if dv == nil || err != nil { - break - } - - switch dvv := v.(type) { - case subStreamPacket: - s, ok := substreams[dvv.number] - if !ok { - s = &subStream{} - substreams[dvv.number] = s + d.FieldArray("packets", func(d *decode.D) { + for d.NotEnd() { + dv, v, err := d.TryFieldFormat("packet", pesPacketFormat, nil) + if dv == nil || err != nil { + log.Printf("err: %#+v\n", err) + break } - s.b = append(s.b, dvv.buf...) - if s.l == 0 && len(s.b) >= 2 { - s.l = int(s.b[0])<<8 | int(s.b[1]) - // TODO: zero l? + switch dvv := v.(type) { + case subStreamPacket: + s, ok := substreams[dvv.number] + if !ok { + s = &subStream{} + substreams[dvv.number] = s + } + s.b = append(s.b, dvv.buf...) + + if s.l == 0 && len(s.b) >= 2 { + s.l = int(s.b[0])<<8 | int(s.b[1]) + // TODO: zero l? + } + + // TODO: is this how spu end is signalled? + if s.l == len(s.b) { + spuD.FieldFormatBitBuf("spu", bitio.NewBitReader(s.b, -1), spuFormat, nil) + s.b = nil + s.l = 0 + } } - // TODO: is this how spu end is signalled? - if s.l == len(s.b) { - spuD.FieldFormatBitBuf("spu", bitio.NewBitReader(s.b, -1), spuFormat, nil) - s.b = nil - s.l = 0 - } + i++ } - - i++ - } + }) return nil } diff --git a/format/mpeg/mpeg_pes_packet.go b/format/mpeg/mpeg_pes_packet.go index 26c030e82a..11be0282d3 100644 --- a/format/mpeg/mpeg_pes_packet.go +++ b/format/mpeg/mpeg_pes_packet.go @@ -201,9 +201,9 @@ func pesPacketDecode(d *decode.D, _ any) any { // nop } - if d.BitsLeft() > 0 { - d.FieldRawLen("data", d.BitsLeft()) - } + // if d.BitsLeft() > 0 { + // d.FieldRawLen("data", d.BitsLeft()) + // } return v } diff --git a/format/mpeg/mpeg_ts.go b/format/mpeg/mpeg_ts.go index 896a42d41b..cb6fd422cb 100644 --- a/format/mpeg/mpeg_ts.go +++ b/format/mpeg/mpeg_ts.go @@ -1,33 +1,300 @@ package mpeg +// TODO: dump bug: array with only sub buffers show wrong summary bytes +// TODO: dump idea: array with only value scalars, collapse? +// TODO: split into generic table decoder or helper function? and use table_id to select? pass on args and return out value? switch on return type? +// TODO: probe, count? +// TODO: check crc +// TODO: mpeg_pes, share code? +// TODO: mpeg_pes_packet, length 0 for video? +// TODO: dup start? + +// ffmpeg $(for i in $(seq 0 50); do echo "-f lavfi -i sine"; done) -t 100ms $(for i in $(seq 0 50); do echo "-map $i:0"; done) test2.ts + +// ISO/IEC 13818-1 - Generic coding of moving pictures and associated audio information: Systems +// https://tsduck.io/download/docs/mpegts-introduction.pdf + import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/wader/fq/format" + "github.com/wader/fq/pkg/bitio" "github.com/wader/fq/pkg/decode" "github.com/wader/fq/pkg/interp" "github.com/wader/fq/pkg/scalar" ) +var mpegTsFormatMpegTsPacket decode.Group +var mpegTsFormatMpegTsPat decode.Group +var mpegTsFormatMpegTsPmt decode.Group +var mpegTsFormatMpegPesPacket decode.Group + func init() { interp.RegisterFormat(decode.Format{ Name: format.MPEG_TS, - ProbeOrder: format.ProbeOrderBinFuzzy, // make sure to be after gif, both start with 0x47 Description: "MPEG Transport Stream", Groups: []string{format.PROBE}, - DecodeFn: tsDecode, + Dependencies: []decode.Dependency{ + {Names: []string{format.MPEG_TS_PACKET}, Group: &mpegTsFormatMpegTsPacket}, + {Names: []string{format.MPEG_TS_PAT}, Group: &mpegTsFormatMpegTsPat}, + {Names: []string{format.MPEG_TS_PMT}, Group: &mpegTsFormatMpegTsPmt}, + {Names: []string{format.MPEG_PES_PACKET}, Group: &mpegTsFormatMpegPesPacket}, + }, + DecodeFn: tsDecode, }) } -// TODO: ts_packet +const ( + adaptationFieldControlPayloadOnly = 0b01 + adaptationFieldControlAdaptationFieldOnly = 0b10 + adaptationFieldControlAdaptationFieldAndPayload = 0b11 +) + +type tsBuffer struct { + length int + buf bytes.Buffer + packetIndexes []int +} + +func (tb *tsBuffer) Reset() { + tb.length = -1 + // new bytes buffer to not share byte slice + tb.buf = bytes.Buffer{} + tb.packetIndexes = nil + +} + +type tsContinuityMap map[int]int + +func (tcm tsContinuityMap) Update(pid int, n int) bool { + current, currentOk := tcm[pid] + tcm[pid] = n + if currentOk { + return (current+1)&0xf == n + } + return n == 0 +} + +func tsPesDecode(d *decode.D, pid int, programPid int, streamType int, pesBuf *tsBuffer) { + d.FieldValueUint("pid", uint64(pid), tsPidMap, scalar.UintHex) // TODO: more things? or less? + d.FieldValueUint("program", uint64(programPid), scalar.UintHex) // TODO: more things? or less? + d.FieldValueUint("stream_type", uint64(streamType), tsStreamTypeMap, scalar.UintHex) // TODO: more things? or less? + d.FieldArray("indexes", func(d *decode.D) { + for _, i := range pesBuf.packetIndexes { + d.FieldValueUint("index", uint64(i)) + } + }) + d.FieldRawLen("payload", d.BitsLeft()) + // d.TryFieldFormatBitBuf("payload", bitio.NewBitReader(b.Bytes(), -1), mpegTsFormatMpegPesPacket, nil) +} func tsDecode(d *decode.D, _ any) any { - d.FieldU8("sync", d.UintAssert(0x47), scalar.UintHex) - d.FieldBool("transport_error_indicator") - d.FieldBool("payload_unit_start") - d.FieldBool("transport_priority") - d.FieldU13("pid") - d.FieldU2("transport_scrambling_control") - d.FieldU2("adaptation_field_control") - d.FieldU4("continuity_counter") + var tableReassemble = map[int]*tsBuffer{} + var pesReassemble = map[int]*tsBuffer{} + pidProgramMap := map[int]format.MpegTsProgram{} + pidStreamMap := map[int]format.MpegTsStream{} + continuityMap := tsContinuityMap(map[int]int{}) + packetIndex := 0 + + tablesD := d.FieldArrayValue("tables") + pesD := d.FieldArrayValue("pes") + + d.FieldArray("packets", func(d *decode.D) { + for !d.End() { + _, v, err := d.TryFieldFormat( + "packet", + mpegTsFormatMpegTsPacket, + format.MpegTsPacketIn{ + ProgramMap: pidProgramMap, + StreamMap: pidStreamMap, + }, + ) + if err != nil { + // TODO: malformted packet, how? + d.FieldRawLen("packet", tsPacketLength) + continue + } + mtpo, mtpoOk := v.(format.MpegTsPacketOut) + if !mtpoOk { + panic("packet is not a MpegTsPacketOut") + } + + isContinous := continuityMap.Update(mtpo.Pid, mtpo.ContinuityCounter) + isTable := tsPidIsTable(mtpo.Pid, pidProgramMap) + stream, isStream := pidStreamMap[mtpo.Pid] + + // log.Printf("mtpo.Pid: %x isContinous=%t isTable=%t isStream=%t", mtpo.Pid, isContinous, isTable, isStream) + + switch { + case isTable: + if !isContinous { + // TODO: reset + } + + // TODO: version, current section etc + tableBuf, tableBufOk := tableReassemble[mtpo.Pid] + if !tableBufOk { + tableBuf = &tsBuffer{length: -1} + tableReassemble[mtpo.Pid] = tableBuf + } + tableBuf.packetIndexes = append(tableBuf.packetIndexes, packetIndex) + b := &tableBuf.buf + b.Write(mtpo.Payload) + + // log.Printf(" b.Len() 1: %p %#+v %d\n", &b, b.Len(), tableBuf.length) + + const sectionHeaderLength = 3 + if tableBuf.length == -1 && b.Len() >= sectionHeaderLength { + // length is BE 10 bits of byte 1 and 2 and add header length to know expected full length + tableBuf.length = int(binary.BigEndian.Uint16(b.Bytes()[1:])&0b0000_0011_1111_1111) + sectionHeaderLength + } + + // log.Printf(" b.Len() 2: %#+v %d\n", b.Len(), tableBuf.length) + + if b.Len() >= tableBuf.length { + program, isPMT := pidProgramMap[mtpo.Pid] + + tablesD.FieldStructRootBitBufFn("table", bitio.NewBitReader(b.Bytes(), -1), func(d *decode.D) { + d.FieldValueUint("pid", uint64(mtpo.Pid), tsPidMap, scalar.UintHex) // TODO: more things? or less? + d.FieldArray("indexes", func(d *decode.D) { + for _, i := range tableBuf.packetIndexes { + d.FieldValueUint("index", uint64(i)) + } + }) + + switch { + case mtpo.Pid == pidPAT: + _, v, err := d.TryFieldFormat("payload", mpegTsFormatMpegTsPat, nil) + if err != nil { + // TODO: malformted table, how? + d.FieldRawLen("payload", tsPacketLength) + } else { + mtpo, mtpoOk := v.(format.MpegTsPatOut) + if !mtpoOk { + panic(fmt.Sprintf("expected MpegTsPatOut got %#+v", v)) + } + + // TODO: correct? remove streams for program? + for mapPid, mapNum := range mtpo.PidMap { + if prevProgram, ok := pidProgramMap[mapPid]; ok { + for _, streamPid := range prevProgram.StreamPids { + delete(pidStreamMap, streamPid) + } + } + pidProgramMap[mapPid] = format.MpegTsProgram{ + Number: mapNum, + Pid: mapPid, + } + } + } + + case isPMT: + _, v, err := d.TryFieldFormat("payload", mpegTsFormatMpegTsPmt, nil) + if err != nil { + // TODO: malformted table, how? + d.FieldRawLen("packet", tsPacketLength) + } else { + + mtpo, mtpoOk := v.(format.MpegTsPmtOut) + if !mtpoOk { + panic(fmt.Sprintf("expected MpegTsPmtOut got %#+v", v)) + } + + // TODO: correct? replace streams? + for _, streamPid := range program.StreamPids { + delete(pidStreamMap, streamPid) + } + for streamPid, stream := range mtpo.Streams { + program.StreamPids = append(program.StreamPids, streamPid) + pidStreamMap[streamPid] = format.MpegTsStream{ + ProgramPid: program.Pid, + Type: stream.Type, + } + } + } + + default: + // TODO: raw table decoder? + d.FieldRawLen("payload", d.BitsLeft()) + } + + tableBuf.Reset() + }) + } + + case isStream: + if !isContinous { + // TODO: reset + } + + pesBuf, pesBufOk := pesReassemble[mtpo.Pid] + if !pesBufOk { + pesBuf = &tsBuffer{length: -1} + pesReassemble[mtpo.Pid] = pesBuf + } + b := &pesBuf.buf + + pesFn := func(d *decode.D) { + tsPesDecode(d, mtpo.Pid, stream.ProgramPid, stream.Type, pesBuf) + pesBuf.Reset() + } + + // TODO: when to reset if wrong? + if mtpo.PayloadUnitStart && pesBuf.length == 0 && b.Len() > 0 { + // TODO: only video? + + pesD.FieldStructRootBitBufFn("pes", bitio.NewBitReader(b.Bytes(), -1), pesFn) + } + + pesBuf.packetIndexes = append(pesBuf.packetIndexes, packetIndex) + b.Write(mtpo.Payload) + + const pesHeaderLength = 6 // 3 sync, 1 stream id, 2 length + if pesBuf.length == -1 && b.Len() >= pesHeaderLength { + // length is BE bytes 4 and 5 and add ad header length to know expected full length + length := int(binary.BigEndian.Uint16(b.Bytes()[4:])) + if length == 0 { + pesBuf.length = 0 + } else { + pesBuf.length = length + pesHeaderLength + } + } + + // log.Printf(" b.Len() 1: %p %#+v %d\n", &b, b.Len(), pesBuf.length) + + // TODO: zero length, start flag? + if pesBuf.length > 0 && b.Len() >= pesBuf.length { + pesD.FieldStructRootBitBufFn("pes", bitio.NewBitReader(b.Bytes(), -1), pesFn) + } + + default: + // unknown ts packet payload + } + + packetIndex++ + } + }) + + // TODO: + // add possible partial pes + for pid, pesBuf := range pesReassemble { + if pesBuf.buf.Len() == 0 { + continue + } + + // TODO: can we assume there is a stream? + stream, isStream := pidStreamMap[pid] + if !isStream { + continue + } + + pesD.FieldStructRootBitBufFn("pes", bitio.NewBitReader(pesBuf.buf.Bytes(), -1), func(d *decode.D) { + tsPesDecode(d, pid, stream.ProgramPid, stream.Type, pesBuf) + }) + + } return nil } diff --git a/format/mpeg/mpeg_ts_packet.go b/format/mpeg/mpeg_ts_packet.go new file mode 100644 index 0000000000..6dc2c31050 --- /dev/null +++ b/format/mpeg/mpeg_ts_packet.go @@ -0,0 +1,138 @@ +package mpeg + +import ( + "github.com/wader/fq/format" + "github.com/wader/fq/pkg/decode" + "github.com/wader/fq/pkg/interp" + "github.com/wader/fq/pkg/scalar" +) + +func init() { + interp.RegisterFormat(decode.Format{ + Name: format.MPEG_TS_PACKET, + Description: "MPEG Transport Stream Packet", + DecodeFn: tsPacketDecode, + }) +} + +func tsPacketDecode(d *decode.D, v any) any { + mtpi, mtpiOk := v.(format.MpegTsPacketIn) + if !mtpiOk { + mtpi.ProgramMap = map[int]format.MpegTsProgram{} + mtpi.StreamMap = map[int]format.MpegTsStream{} + } + + var mtpo format.MpegTsPacketOut + + d.FramedFn(tsPacketLength, func(d *decode.D) { + d.FieldU8("sync", scalar.UintHex) // TODO: sometimes not 0x47? d.UintAssert(0x47) + d.FieldBool("transport_error_indicator") + mtpo.PayloadUnitStart = d.FieldBool("payload_unit_start") + d.FieldBool("transport_priority") + pid := d.FieldU13("pid", tsPidMap, scalar.UintHex) + if p, ok := mtpi.ProgramMap[int(pid)]; ok { + d.FieldValueUint("program", uint64(p.Number), scalar.UintHex) + } else if s, ok := mtpi.StreamMap[int(pid)]; ok { + if p, ok := mtpi.ProgramMap[s.ProgramPid]; ok { + d.FieldValueUint("program", uint64(p.Number), scalar.UintHex) + } + d.FieldValueUint("stream_type", uint64(s.Type), tsStreamTypeMap) + } + mtpo.Pid = int(pid) + mtpo.TransportScramblingControl = int(d.FieldU2("transport_scrambling_control", scalar.UintMapSymStr{ + 0b00: "not_scrambled", + 0b01: "reserved", + 0b10: "even_key", + 0b11: "odd_key", + })) + adaptationFieldControl := d.FieldU2("adaptation_field_control", scalar.UintMapSymStr{ + 0b00: "reserved", + adaptationFieldControlPayloadOnly: "payload_only", + adaptationFieldControlAdaptationFieldOnly: "adaptation_field_only", + adaptationFieldControlAdaptationFieldAndPayload: "adaptation_and_payload", + }) + mtpo.ContinuityCounter = int(d.FieldU4("continuity_counter")) + + switch adaptationFieldControl { + case adaptationFieldControlAdaptationFieldOnly, + adaptationFieldControlAdaptationFieldAndPayload: + d.FieldStruct("adaptation_field", func(d *decode.D) { + length := d.FieldU8("length") // Number of bytes in the adaptation field immediately following this byte + d.FramedFn(int64(length)*8, func(d *decode.D) { + d.FieldBool("discontinuity_indicator") // Set if current TS packet is in a discontinuity state with respect to either the continuity counter or the program clock reference + d.FieldBool("random_access_indicator") // Set when the stream may be decoded without errors from this point + d.FieldBool("elementary_stream_priority_indicator") // Set when this stream should be considered "high priority" + pcrPresent := d.FieldBool("pcr_present") // Set when PCR field is present + opcrPresent := d.FieldBool("opcr_present") // Set when OPCR field is present + splicingPointPresent := d.FieldBool("splicing_point_present") // Set when splice countdown field is present + transportPrivatePresent := d.FieldBool("transport_private_present") // Set when transport private data is present + adaptationFieldExtensionPresent := d.FieldBool("adaptation_field_extension_present") // Set when adaptation extension data is present + if pcrPresent { + d.FieldU("pcr", 48) + } + if opcrPresent { + d.FieldU("opcr", 48) + } + if splicingPointPresent { + d.FieldU8("splicing_point") + } + if transportPrivatePresent { + d.FieldStruct("transport_private", func(d *decode.D) { + length := d.FieldU8("length") + d.FieldRawLen("data", int64(length)*8) + }) + } + if adaptationFieldExtensionPresent { + d.FieldStruct("adaptation_extension", func(d *decode.D) { + length := d.FieldU8("length") + d.FramedFn(int64(length)*8, func(d *decode.D) { + d.FieldBool("legal_time_window") + d.FieldBool("piecewise_rate") + d.FieldBool("seamless_splice") + d.FieldU5("reserved", scalar.UintHex) + d.FieldRawLen("data", d.BitsLeft()) + }) + }) + + // Optional fields + // LTW flag set (2 bytes) + // LTW valid flag 1 0x8000 + // LTW offset 15 0x7fff Extra information for rebroadcasters to determine the state of buffers when packets may be missing. + // Piecewise flag set (3 bytes) + // Reserved 2 0xc00000 + // Piecewise rate 22 0x3fffff The rate of the stream, measured in 188-byte packets, to define the end-time of the LTW. + // Seamless splice flag set (5 bytes) + // Splice type 4 0xf000000000 Indicates the parameters of the H.262 splice. + // DTS next access unit 36 0x0efffefffe The PES DTS of the splice point. Split up as multiple fields, 1 marker bit (0x1), 15 bits, 1 marker bit, 15 bits, and 1 marker bit, for 33 data bits total. + } + if d.BitsLeft() > 0 { + d.FieldRawLen("stuffing", d.BitsLeft()) + } + }) + }) + } + + isTable := tsPidIsTable(mtpo.Pid, mtpi.ProgramMap) + if isTable { + var payloadPointer uint64 + if mtpo.PayloadUnitStart { + payloadPointer = d.FieldU8("payload_pointer") + } + if payloadPointer > 0 { + d.FieldRawLen("stuffing", int64(payloadPointer)*8) + } + } + + switch adaptationFieldControl { + case adaptationFieldControlPayloadOnly, + adaptationFieldControlAdaptationFieldAndPayload: + payload := d.FieldRawLen("payload", d.BitsLeft()) + mtpo.Payload = d.ReadAllBits(payload) + default: + // TODO: unknown adaption control flags + d.FieldRawLen("unknown", d.BitsLeft()) + } + }) + + return mtpo +} diff --git a/format/mpeg/mpeg_ts_pat.go b/format/mpeg/mpeg_ts_pat.go new file mode 100644 index 0000000000..961b6e4c69 --- /dev/null +++ b/format/mpeg/mpeg_ts_pat.go @@ -0,0 +1,58 @@ +package mpeg + +import ( + "github.com/wader/fq/format" + "github.com/wader/fq/pkg/decode" + "github.com/wader/fq/pkg/interp" + "github.com/wader/fq/pkg/scalar" +) + +func init() { + interp.RegisterFormat(decode.Format{ + Name: format.MPEG_TS_PAT, + Description: "MPEG TS Program Association Table", + Groups: []string{format.PROBE}, + DecodeFn: mpegTsPatDecode, + }) +} + +func mpegTsPatDecode(d *decode.D, _ any) any { + mtpo := format.MpegTsPatOut{ + PidMap: map[int]int{}, + } + + d.FieldU8("table_id") + d.FieldU1("syntax_indicator") + d.FieldU3("reserved0", scalar.UintHex) + length := d.FieldU12("section_length") + d.FramedFn(int64(length-4)*8, func(d *decode.D) { + d.FieldU16("transport_stream_id") + d.FieldU2("reserved1", scalar.UintHex) + d.FieldU5("version_number") // TODO: output? + d.FieldU1("current_next_indicator") + d.FieldU8("section_number") + d.FieldU8("last_section_number") + d.FieldArray("programs", func(d *decode.D) { + for !d.End() { + d.FieldStruct("program", func(d *decode.D) { + programNumber := d.FieldU16("program_number") + d.FieldU3("reserved", scalar.UintHex) + switch programNumber { + case 0: + d.FieldU13("network_pid", scalar.UintHex) + default: + programPid := d.FieldU13("program_map_pid", scalar.UintHex) + mtpo.PidMap[int(programPid)] = int(programNumber) + } + }) + } + }) + }) + // TODO: move + d.FieldU32("crc", scalar.UintHex) + if d.BitsLeft() > 0 { + d.FieldRawLen("stuffing", d.BitsLeft()) + } + + return mtpo +} diff --git a/format/mpeg/mpeg_ts_pmt.go b/format/mpeg/mpeg_ts_pmt.go new file mode 100644 index 0000000000..8c28eb8bea --- /dev/null +++ b/format/mpeg/mpeg_ts_pmt.go @@ -0,0 +1,75 @@ +package mpeg + +import ( + "github.com/wader/fq/format" + "github.com/wader/fq/pkg/decode" + "github.com/wader/fq/pkg/interp" + "github.com/wader/fq/pkg/scalar" +) + +func init() { + interp.RegisterFormat(decode.Format{ + Name: format.MPEG_TS_PMT, + Description: "MPEG TS Program Map Table", + Groups: []string{format.PROBE}, + DecodeFn: mpegTsPmtDecode, + }) +} + +func mpegTsPmtDecode(d *decode.D, _ any) any { + mtpo := format.MpegTsPmtOut{ + Streams: map[int]format.MpegTsStream{}, + } + + d.FieldU8("table_id") + d.FieldU1("syntax_indicator") + d.FieldU3("reserved0", scalar.UintHex) + length := d.FieldU12("section_length") + d.FramedFn(int64(length-4)*8, func(d *decode.D) { + d.FieldU16("program_number") + d.FieldU2("reserved1", scalar.UintHex) + d.FieldU5("version_number") + d.FieldU1("current_next_indicator") + d.FieldU8("section_number") + d.FieldU8("last_section_number") + d.FieldU3("reserved2", scalar.UintHex) + d.FieldU13("pcr_pid") + d.FieldU4("reserved3", scalar.UintHex) + programInfoLength := d.FieldU12("program_info_length") + d.FramedFn(int64(programInfoLength)*8, func(d *decode.D) { + d.FieldArray("decriptors", func(d *decode.D) { + for !d.End() { + d.FieldStruct("decriptor", func(d *decode.D) { + d.FieldU8("tag", tsStreamTagMap, scalar.UintHex) + length := d.FieldU8("length") + // TODO: + d.FieldRawLen("data", int64(length)*8) + }) + } + }) + }) + d.FieldArray("streams", func(d *decode.D) { + for !d.End() { + d.FieldStruct("stream", func(d *decode.D) { + streamType := d.FieldU8("stream_type", scalar.UintHex, tsStreamTypeMap) + d.FieldU3("reserved0", scalar.UintHex) + streamPid := d.FieldU13("elementary_pid", scalar.UintHex) + d.FieldU4("reserved1", scalar.UintHex) + length := d.FieldU12("es_info_length") + d.FramedFn(int64(length)*8, func(d *decode.D) { + // TODO: + d.FieldRawLen("data", d.BitsLeft()) + }) + + mtpo.Streams[int(streamPid)] = format.MpegTsStream{Type: int(streamType)} + }) + } + }) + }) + d.FieldU32("crc", scalar.UintHex) + if d.BitsLeft() > 0 { + d.FieldRawLen("stuffing", d.BitsLeft()) + } + + return mtpo +} diff --git a/format/mpeg/mpeg_ts_sdt.go b/format/mpeg/mpeg_ts_sdt.go new file mode 100644 index 0000000000..3d6bdeb9f4 --- /dev/null +++ b/format/mpeg/mpeg_ts_sdt.go @@ -0,0 +1,57 @@ +package mpeg + +import ( + "github.com/wader/fq/format" + "github.com/wader/fq/pkg/decode" + "github.com/wader/fq/pkg/interp" + "github.com/wader/fq/pkg/scalar" +) + +func init() { + interp.RegisterFormat(decode.Format{ + Name: format.MPEG_TS_SDT, + Description: "MPEG TS Service Description Table", + Groups: []string{format.PROBE}, + DecodeFn: mpegTsSdtDecode, + }) +} + +func mpegTsSdtDecode(d *decode.D, _ any) any { + d.FieldU8("table_id", tsTableMap, scalar.UintHex) + d.FieldU1("syntax_indicator") + d.FieldU3("reserved0", scalar.UintHex) + + length := d.FieldU12("section_length") + d.FramedFn(int64(length-4)*8, func(d *decode.D) { + d.FieldU16("transport_stream_id") + d.FieldU2("reserved1", scalar.UintHex) + d.FieldU5("version_number") + d.FieldU1("current_next_indicator") + d.FieldU8("section_number") + d.FieldU8("last_section_number") + d.FieldU16("original_network_id", scalar.UintHex) + d.FieldU8("reserved3", scalar.UintHex) + d.FieldArray("services", func(d *decode.D) { + for !d.End() { + d.FieldStruct("stream", func(d *decode.D) { + d.FieldU16("service_id") + d.FieldU6("reserved0", scalar.UintHex) + d.FieldBool("eit_schedule_flag") + d.FieldBool("present_following_flag") + d.FieldU3("running_status") + d.FieldBool("free_ca_mode") + descriptorsLoopLength := d.FieldU12("descriptors_loop_length") + d.FramedFn(int64(descriptorsLoopLength)*8, func(d *decode.D) { + // TODO: + d.FieldRawLen("descriptor", d.BitsLeft()) + }) + }) + } + }) + }) + d.FieldU32("crc", scalar.UintHex) + if d.BitsLeft() > 0 { + d.FieldRawLen("stuffing", d.BitsLeft()) + } + return nil +} diff --git a/format/mpeg/shared.go b/format/mpeg/shared.go index 73843daf8f..f93de14284 100644 --- a/format/mpeg/shared.go +++ b/format/mpeg/shared.go @@ -4,9 +4,11 @@ import ( "bytes" "io" + "github.com/wader/fq/format" "github.com/wader/fq/pkg/bitio" "github.com/wader/fq/pkg/decode" "github.com/wader/fq/pkg/interp" + "github.com/wader/fq/pkg/scalar" ) func init() { @@ -111,3 +113,149 @@ func (r nalUnescapeReader) Read(p []byte) (n int, err error) { return n, err } + +const tsPacketLength = 188 * 8 + +const ( + pidPAT = 0 +) + +func tsPidIsTable(pid int, pmt map[int]format.MpegTsProgram) bool { + // pid 0x0-0x1f seems to all be tables + if pid >= 0 && pid <= 0x1f { + return true + } + _, isPMT := pmt[pid] + return isPMT +} + +var tsStreamTagMap = scalar.UintRangeToScalar{ + {Range: [2]uint64{0, 0}, S: scalar.Uint{Description: "Reserved"}}, + {Range: [2]uint64{1, 1}, S: scalar.Uint{Description: "Reserved"}}, + {Range: [2]uint64{2, 2}, S: scalar.Uint{Description: "video_stream_descriptor"}}, + {Range: [2]uint64{3, 3}, S: scalar.Uint{Description: "audio_stream_descriptor"}}, + {Range: [2]uint64{4, 4}, S: scalar.Uint{Description: "hierarchy_descriptor"}}, + {Range: [2]uint64{5, 5}, S: scalar.Uint{Description: "registration_descriptor"}}, + {Range: [2]uint64{6, 6}, S: scalar.Uint{Description: "data_stream_alignment_descriptor"}}, + {Range: [2]uint64{7, 7}, S: scalar.Uint{Description: "target_background_grid_descriptor"}}, + {Range: [2]uint64{8, 8}, S: scalar.Uint{Description: "video_window_descriptor"}}, + {Range: [2]uint64{9, 9}, S: scalar.Uint{Description: "CA_descriptor"}}, + {Range: [2]uint64{10, 10}, S: scalar.Uint{Description: "ISO_639_language_descriptor"}}, + {Range: [2]uint64{11, 11}, S: scalar.Uint{Description: "system_clock_descriptor"}}, + {Range: [2]uint64{12, 12}, S: scalar.Uint{Description: "multiplex_buffer_utilization_descriptor"}}, + {Range: [2]uint64{13, 13}, S: scalar.Uint{Description: "copyright_descriptor"}}, + {Range: [2]uint64{14, 14}, S: scalar.Uint{Description: "maximum_bitrate_descriptor"}}, + {Range: [2]uint64{15, 15}, S: scalar.Uint{Description: "private_data_indicator_descriptor"}}, + {Range: [2]uint64{16, 16}, S: scalar.Uint{Description: "smoothing_buffer_descriptor"}}, + {Range: [2]uint64{17, 17}, S: scalar.Uint{Description: "STD_descriptor"}}, + {Range: [2]uint64{18, 18}, S: scalar.Uint{Description: "IBP_descriptor"}}, + {Range: [2]uint64{19, 26}, S: scalar.Uint{Description: "Defined in ISO/IEC 13818-6"}}, + {Range: [2]uint64{27, 27}, S: scalar.Uint{Description: "MPEG-4_video_descriptor"}}, + {Range: [2]uint64{28, 28}, S: scalar.Uint{Description: "MPEG-4_audio_descriptor"}}, + {Range: [2]uint64{29, 29}, S: scalar.Uint{Description: "IOD_descriptor"}}, + {Range: [2]uint64{30, 30}, S: scalar.Uint{Description: "SL_descriptor"}}, + {Range: [2]uint64{31, 31}, S: scalar.Uint{Description: "FMC_descriptor"}}, + {Range: [2]uint64{32, 32}, S: scalar.Uint{Description: "external_ES_ID_descriptor"}}, + {Range: [2]uint64{33, 33}, S: scalar.Uint{Description: "MuxCode_descriptor"}}, + {Range: [2]uint64{34, 34}, S: scalar.Uint{Description: "FmxBufferSize_descriptor"}}, + {Range: [2]uint64{35, 35}, S: scalar.Uint{Description: "multiplexbuffer_descriptor"}}, + {Range: [2]uint64{36, 36}, S: scalar.Uint{Description: "content_labeling_descriptor"}}, + {Range: [2]uint64{37, 37}, S: scalar.Uint{Description: "metadata_pointer_descriptor"}}, + {Range: [2]uint64{38, 38}, S: scalar.Uint{Description: "metadata_descriptor"}}, + {Range: [2]uint64{39, 39}, S: scalar.Uint{Description: "metadata_STD_descriptor"}}, + {Range: [2]uint64{40, 40}, S: scalar.Uint{Description: "AVC video descriptor"}}, + {Range: [2]uint64{41, 41}, S: scalar.Uint{Description: "IPMP_descriptor (defined in ISO/IEC 13818-11, MPEG-2 IPMP)"}}, + {Range: [2]uint64{42, 42}, S: scalar.Uint{Description: "AVC timing and HRD descriptor"}}, + {Range: [2]uint64{43, 43}, S: scalar.Uint{Description: "MPEG-2_AAC_audio_descriptor"}}, + {Range: [2]uint64{44, 44}, S: scalar.Uint{Description: "FlexMuxTiming_descriptor"}}, + {Range: [2]uint64{45, 63}, S: scalar.Uint{Description: "ITU-T Rec. H.222.0 | ISO/IEC 13818-1 Reserved"}}, + {Range: [2]uint64{64, 255}, S: scalar.Uint{Description: "User Private"}}, +} + +var tsStreamTypeMap = scalar.UintRangeToScalar{ + {Range: [2]uint64{0x00, 0x00}, S: scalar.Uint{Description: "Reserved"}}, + {Range: [2]uint64{0x01, 0x01}, S: scalar.Uint{Sym: "video", Description: "ISO/IEC 11172-2 Video"}}, // TODO: video_mpeg? codec? + {Range: [2]uint64{0x02, 0x02}, S: scalar.Uint{Description: "ISO/IEC 13818-2 or ISO/IEC 11172-2"}}, + {Range: [2]uint64{0x03, 0x03}, S: scalar.Uint{Sym: "audio_mpeg1", Description: "ISO/IEC 11172-3 Audio"}}, + {Range: [2]uint64{0x04, 0x04}, S: scalar.Uint{Sym: "audio_mpeg2", Description: "ISO/IEC 13818-3 Audio"}}, + {Range: [2]uint64{0x05, 0x05}, S: scalar.Uint{Description: "ISO/IEC 13818-1 private_sections"}}, + {Range: [2]uint64{0x06, 0x06}, S: scalar.Uint{Description: "ISO/IEC 13818-1 PES packets containing private data"}}, + {Range: [2]uint64{0x07, 0x07}, S: scalar.Uint{Description: "ISO/IEC 13522 MHEG"}}, + {Range: [2]uint64{0x08, 0x08}, S: scalar.Uint{Description: "ISO/IEC 13818-1 Annex A DSM-CC"}}, + {Range: [2]uint64{0x09, 0x09}, S: scalar.Uint{Description: "ITU-T Rec. H.222.1"}}, + {Range: [2]uint64{0x0a, 0x0a}, S: scalar.Uint{Description: "ISO/IEC 13818-6 type A"}}, + {Range: [2]uint64{0x0b, 0x0b}, S: scalar.Uint{Description: "ISO/IEC 13818-6 type B"}}, + {Range: [2]uint64{0x0c, 0x0c}, S: scalar.Uint{Description: "ISO/IEC 13818-6 type C"}}, + {Range: [2]uint64{0x0d, 0x0d}, S: scalar.Uint{Description: "ISO/IEC 13818-6 type D"}}, + {Range: [2]uint64{0x0e, 0x0e}, S: scalar.Uint{Description: "ISO/IEC 13818-1 auxiliary"}}, + {Range: [2]uint64{0x0f, 0x0f}, S: scalar.Uint{Sym: "audio_adts", Description: "ISO/IEC 13818-7 Audio with ADTS transport syntax"}}, + {Range: [2]uint64{0x10, 0x10}, S: scalar.Uint{Description: "ISO/IEC 14496-2 Visual"}}, + {Range: [2]uint64{0x11, 0x11}, S: scalar.Uint{Sym: "audio_latm", Description: "ISO/IEC 14496-3 Audio with the LATM"}}, + {Range: [2]uint64{0x12, 0x12}, S: scalar.Uint{Description: "ISO/IEC 14496-1 SL-packetized stream or FlexMux stream carried in PES packets"}}, + {Range: [2]uint64{0x13, 0x13}, S: scalar.Uint{Description: "ISO/IEC 14496-1 SL-packetized stream or FlexMux stream carried in ISO/IEC 14496_sections"}}, + {Range: [2]uint64{0x14, 0x14}, S: scalar.Uint{Description: "ISO/IEC 13818-6 Synchronized Download Protocol"}}, + {Range: [2]uint64{0x15, 0x15}, S: scalar.Uint{Description: "Metadata carried in PES packets"}}, + {Range: [2]uint64{0x16, 0x16}, S: scalar.Uint{Description: "Metadata carried in metadata_sections"}}, + {Range: [2]uint64{0x17, 0x17}, S: scalar.Uint{Description: "Metadata carried in ISO/IEC 13818-6 Data Carousel"}}, + {Range: [2]uint64{0x18, 0x18}, S: scalar.Uint{Description: "Metadata carried in ISO/IEC 13818-6 Object Carousel"}}, + {Range: [2]uint64{0x19, 0x19}, S: scalar.Uint{Description: "Metadata carried in ISO/IEC 13818-6 Synchronized Download Protocol"}}, + {Range: [2]uint64{0x1a, 0x1a}, S: scalar.Uint{Description: "IPMP stream (defined in ISO/IEC 13818-11, MPEG-2 IPMP)"}}, + {Range: [2]uint64{0x1b, 0x1b}, S: scalar.Uint{Sym: "video_avc", Description: "AVC video stream as defined in ITU-T Rec. H.264 | ISO/IEC 14496-10 Video"}}, + {Range: [2]uint64{0x1c, 0x7e}, S: scalar.Uint{Description: "ITU-T Rec. H.222.0 | ISO/IEC 13818-1 Reserved"}}, + {Range: [2]uint64{0x7f, 0x7f}, S: scalar.Uint{Description: "IPMP stream"}}, + {Range: [2]uint64{0x80, 0xff}, S: scalar.Uint{Description: "User Private"}}, +} + +var tsPidMap = scalar.UintRangeToScalar{ + {Range: [2]uint64{pidPAT, pidPAT}, S: scalar.Uint{Sym: "pat", Description: "Program association table"}}, + {Range: [2]uint64{0x0001, 0x0001}, S: scalar.Uint{Sym: "cat", Description: "Conditional access table"}}, + {Range: [2]uint64{0x0002, 0x0002}, S: scalar.Uint{Description: "Transport stream description table"}}, + {Range: [2]uint64{0x0003, 0x0003}, S: scalar.Uint{Description: "IPMP control information table"}}, + {Range: [2]uint64{0x0004, 0x000f}, S: scalar.Uint{Description: "Reserved for future use"}}, + {Range: [2]uint64{0x0010, 0x001f}, S: scalar.Uint{Description: "DVB metadata"}}, + {Range: [2]uint64{0x0010, 0x0010}, S: scalar.Uint{Sym: "nit", Description: "NIT, ST"}}, + {Range: [2]uint64{0x0011, 0x0011}, S: scalar.Uint{Sym: "sdt", Description: "SDT, BAT, ST"}}, + {Range: [2]uint64{0x0012, 0x0012}, S: scalar.Uint{Sym: "eit", Description: "EIT, ST, CIT"}}, + {Range: [2]uint64{0x0013, 0x0013}, S: scalar.Uint{Sym: "rst", Description: "RST, ST"}}, + {Range: [2]uint64{0x0014, 0x0014}, S: scalar.Uint{Sym: "tdt", Description: "TDT, TOT, ST"}}, + {Range: [2]uint64{0x0015, 0x0015}, S: scalar.Uint{Description: "Network synchronization"}}, + {Range: [2]uint64{0x0016, 0x0016}, S: scalar.Uint{Sym: "rnt", Description: "RNT"}}, + {Range: [2]uint64{0x0017, 0x001b}, S: scalar.Uint{Description: "Reserved for future use"}}, + {Range: [2]uint64{0x001c, 0x001c}, S: scalar.Uint{Description: "Inband signalling"}}, + {Range: [2]uint64{0x001d, 0x001d}, S: scalar.Uint{Description: "Measurement"}}, + {Range: [2]uint64{0x001e, 0x001e}, S: scalar.Uint{Sym: "dit", Description: "DIT"}}, + {Range: [2]uint64{0x001f, 0x001f}, S: scalar.Uint{Sym: "sit", Description: "SIT"}}, + {Range: [2]uint64{0x0020, 0x1ffa}, S: scalar.Uint{Description: "Program maps, elementary streams and data"}}, + {Range: [2]uint64{0x1ffb, 0x1ffb}, S: scalar.Uint{Description: "DigiCipher 2/ATSC MGT metadata"}}, + {Range: [2]uint64{0x1ffc, 0x1ffe}, S: scalar.Uint{Description: "Program association table assigned"}}, + {Range: [2]uint64{0x1fff, 0x1fff}, S: scalar.Uint{Description: "Null packet (padding)"}}, +} + +var tsTableMap = scalar.UintRangeToScalar{ + {Range: [2]uint64{0x00, 0x00}, S: scalar.Uint{Description: "program_association_section"}}, + {Range: [2]uint64{0x01, 0x01}, S: scalar.Uint{Description: "conditional_access_section"}}, + {Range: [2]uint64{0x02, 0x02}, S: scalar.Uint{Description: "program_map_section"}}, + {Range: [2]uint64{0x03, 0x03}, S: scalar.Uint{Description: "transport_stream_description_section"}}, + {Range: [2]uint64{0x04, 0x3f}, S: scalar.Uint{Description: "reserved"}}, + {Range: [2]uint64{0x40, 0x40}, S: scalar.Uint{Description: "network_information_section - actual_network"}}, + {Range: [2]uint64{0x41, 0x41}, S: scalar.Uint{Description: "network_information_section - other_network"}}, + {Range: [2]uint64{0x42, 0x42}, S: scalar.Uint{Sym: "sdt", Description: "service_description_section - actual_transport_stream"}}, + {Range: [2]uint64{0x43, 0x45}, S: scalar.Uint{Description: "reserved for future use"}}, + {Range: [2]uint64{0x46, 0x46}, S: scalar.Uint{Description: "service_description_section - other_transport_stream"}}, + {Range: [2]uint64{0x47, 0x47}, S: scalar.Uint{Description: "to 0x49 reserved for future use"}}, + {Range: [2]uint64{0x4a, 0x4a}, S: scalar.Uint{Description: "bouquet_association_section"}}, + {Range: [2]uint64{0x4b, 0x4d}, S: scalar.Uint{Description: "reserved for future use"}}, + {Range: [2]uint64{0x4e, 0x4e}, S: scalar.Uint{Description: "event_information_section - actual_transport_stream, present/following"}}, + {Range: [2]uint64{0x4f, 0x4f}, S: scalar.Uint{Description: "event_information_section - other_transport_stream, present/following"}}, + {Range: [2]uint64{0x50, 0x5f}, S: scalar.Uint{Description: "event_information_section - actual_transport_stream, schedule"}}, + {Range: [2]uint64{0x60, 0x6f}, S: scalar.Uint{Description: "event_information_section - other_transport_stream, schedule"}}, + {Range: [2]uint64{0x70, 0x70}, S: scalar.Uint{Description: "time_date_section"}}, + {Range: [2]uint64{0x71, 0x71}, S: scalar.Uint{Description: "running_status_section"}}, + {Range: [2]uint64{0x72, 0x72}, S: scalar.Uint{Description: "stuffing_section"}}, + {Range: [2]uint64{0x73, 0x73}, S: scalar.Uint{Description: "time_offset_section"}}, + {Range: [2]uint64{0x74, 0x7d}, S: scalar.Uint{Description: "reserved for future use"}}, + {Range: [2]uint64{0x7e, 0x7e}, S: scalar.Uint{Description: "discontinuity_information_section"}}, + {Range: [2]uint64{0x7f, 0x7f}, S: scalar.Uint{Description: "selection_information_section"}}, + {Range: [2]uint64{0x80, 0xfe}, S: scalar.Uint{Description: "user defined"}}, + {Range: [2]uint64{0xff, 0xff}, S: scalar.Uint{Description: "reserved"}}, +} diff --git a/pkg/scalar/scalar.go b/pkg/scalar/scalar.go index 7a404ca97c..d3b3177285 100644 --- a/pkg/scalar/scalar.go +++ b/pkg/scalar/scalar.go @@ -121,13 +121,13 @@ func StrSymParseFloat(base int) StrMapper { return strMapToSym(func(s string) (any, error) { return strconv.ParseFloat(s, base) }, false) } -type URangeEntry struct { +type UintRangeEntry struct { Range [2]uint64 S Uint } // UintRangeToScalar maps uint64 ranges to a scalar, first in range is chosen -type UintRangeToScalar []URangeEntry +type UintRangeToScalar []UintRangeEntry func (rs UintRangeToScalar) MapUint(s Uint) (Uint, error) { n := s.Actual @@ -143,13 +143,13 @@ func (rs UintRangeToScalar) MapUint(s Uint) (Uint, error) { } // SRangeToScalar maps ranges to a scalar, first in range is chosen -type SRangeEntry struct { +type SintRangeEntry struct { Range [2]int64 S Sint } // SRangeToScalar maps sint64 ranges to a scalar, first in range is chosen -type SRangeToScalar []SRangeEntry +type SRangeToScalar []SintRangeEntry func (rs SRangeToScalar) MapSint(s Sint) (Sint, error) { n := s.Actual