diff --git a/CHANGELOG.md b/CHANGELOG.md index fd88a01..041d042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet +### Added + +- SMPTE-2038 data option to mp2ts-tools ## [0.2.1] - 2024-01-25 diff --git a/cmd/mp2ts-nallister/main.go b/cmd/mp2ts-nallister/main.go index 9c9ce9f..b30f5a9 100644 --- a/cmd/mp2ts-nallister/main.go +++ b/cmd/mp2ts-nallister/main.go @@ -14,13 +14,15 @@ import ( var usg = `Usage of %s: -%s generates a list of nalus with information about timestamps, rai, SEI etc. +%s generates a list of AVC/HEVC nalus with information about timestamps, rai, SEI etc. +It can further be used to generate a list of SMPTE-2038 data. ` func parseOptions() internal.Options { opts := internal.Options{ShowStreamInfo: true, ShowService: false, ShowPS: false, ShowNALU: true, ShowSEIDetails: false, ShowStatistics: true} flag.IntVar(&opts.MaxNrPictures, "max", 0, "max nr pictures to parse") flag.BoolVar(&opts.ShowSEIDetails, "sei", false, "print detailed sei message information") + flag.BoolVar(&opts.ShowSMPTE2038, "smpte2038", false, "print details about SMPTE-2038 data") flag.BoolVar(&opts.Indent, "indent", false, "indent JSON output") flag.BoolVar(&opts.Version, "version", false, "print version") diff --git a/go.mod b/go.mod index 085d0e7..dc9734a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/Comcast/gots/v2 v2.2.1 - github.com/Eyevinn/mp4ff v0.42.0 + github.com/Eyevinn/mp4ff v0.42.1-0.20240221161741-c9bb3c122204 github.com/asticode/go-astits v1.13.0 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20240119083558-1b970713d09a diff --git a/go.sum b/go.sum index 2180aff..462d6c1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Comcast/gots/v2 v2.2.1 h1:LU/SRg7p2KQqVkNqInV7I4MOQKAqvWQP/PSSLtygP2s= github.com/Comcast/gots/v2 v2.2.1/go.mod h1:firJ11on3eUiGHAhbY5cZNqG0OqhQ1+nSZHfsEEzVVU= -github.com/Eyevinn/mp4ff v0.42.0 h1:I85b/EDTkP77GsoBL8nRV6sfFKZhAXoP6oJHU8fv6kM= -github.com/Eyevinn/mp4ff v0.42.0/go.mod h1:w/6GSa5ghZ1VavzJK6McQ2/flx8mKtcrKDr11SsEweA= +github.com/Eyevinn/mp4ff v0.42.1-0.20240221161741-c9bb3c122204 h1:hxaiwOKm+ooV4lH/UIfgBzi8/CGj3pRIYXnVU3uQQpc= +github.com/Eyevinn/mp4ff v0.42.1-0.20240221161741-c9bb3c122204/go.mod h1:w/6GSa5ghZ1VavzJK6McQ2/flx8mKtcrKDr11SsEweA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w= github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= diff --git a/internal/avc.go b/internal/avc.go index 04aeb94..73cf7d6 100644 --- a/internal/avc.go +++ b/internal/avc.go @@ -156,6 +156,9 @@ func ParseAVCPES(jp *JsonPrinter, d *astits.DemuxerData, ps *AvcPS, o Options) ( }) } + if jp == nil { + return ps, nil + } if firstPS { for nr := range ps.spss { jp.PrintPS(pid, "SPS", nr, ps.spsnalu, ps.spss[nr], o.VerbosePSInfo, o.ShowPS) diff --git a/internal/hevc.go b/internal/hevc.go index 007a01a..dfa8362 100644 --- a/internal/hevc.go +++ b/internal/hevc.go @@ -141,6 +141,9 @@ func ParseHEVCPES(jp *JsonPrinter, d *astits.DemuxerData, ps *HevcPS, o Options) Data: nil, }) } + if jp == nil { + return ps, nil + } if firstPS { jp.PrintPS(pid, "VPS", 0, ps.vpsnalu, nil, o.VerbosePSInfo, o.ShowPS) diff --git a/internal/parser.go b/internal/parser.go index 2a521cf..253f9c3 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -51,20 +51,9 @@ dataLoop: if pmtPID < 0 && d.PMT != nil { // Loop through elementary streams for _, es := range d.PMT.ElementaryStreams { - var streamInfo *ElementaryStreamInfo - switch es.StreamType { - case astits.StreamTypeH264Video: - streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "AVC", Type: "video"} - esKinds[es.ElementaryPID] = "AVC" - case astits.StreamTypeAACAudio: - streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "AAC", Type: "audio"} - esKinds[es.ElementaryPID] = "AAC" - case astits.StreamTypeH265Video: - streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "HEVC", Type: "video"} - esKinds[es.ElementaryPID] = "HEVC" - } - + streamInfo := ParseAstitsElementaryStreamInfo(es) if streamInfo != nil { + esKinds[es.ElementaryPID] = streamInfo.Codec jp.Print(streamInfo, o.ShowStreamInfo) } } @@ -107,6 +96,10 @@ dataLoop: } nrPics++ statistics[d.PID] = &hevcPS.Statistics + case "SMPTE-2038": + if o.ShowSMPTE2038 { + ParseSMPTE2038(jp, d, o) + } default: // Skip unknown elementary streams continue diff --git a/internal/smpte2038.go b/internal/smpte2038.go new file mode 100644 index 0000000..4603d76 --- /dev/null +++ b/internal/smpte2038.go @@ -0,0 +1,104 @@ +package internal + +import ( + "bytes" + "fmt" + "io" + "log" + + "github.com/Eyevinn/mp4ff/bits" + "github.com/asticode/go-astits" +) + +// didMap maps SMPTE-20348 [did] values registered by SMPTE +// [dids]: https://smpte-ra.org/smpte-ancillary-data-smpte-st-291 +type SMPTE291Identifier struct { + did, sdid byte +} + +var SMPTE291Map = map[SMPTE291Identifier]string{ + {0x41, 0x7}: "ANSI/SCTE 104 messages", + {0x41, 0x5}: "AFD and Bar Data", + {0x41, 0x8}: "DVB/SCTE VBI data", + {0x61, 0x1}: "EIA 708B Data mapping into VANC space", + {0x61, 0x2}: "EIA 608 Data mapping into VANC space", +} + +type smpte2038Data struct { + PID uint16 `json:"pid"` + PTS int64 `json:"pts"` + Entries []smpte2038Entry +} + +type smpte2038Entry struct { + LineNr byte `json:"lineNr"` + HorOffset byte `json:"horOffset"` + DID byte `json:"did"` + SDID byte `json:"sdid"` + DataCount byte `json:"dataCount"` + Type string `json:"type"` +} + +func ParseSMPTE2038(jp *JsonPrinter, d *astits.DemuxerData, o Options) { + pl := d.PES.Data + pdtDtsIndicator := d.PES.Header.OptionalHeader.PTSDTSIndicator + if pdtDtsIndicator != 2 { + fmt.Printf("SMPTE-2038: invalid PDT_DTS_Indicator=%d\n", pdtDtsIndicator) + } + pts := d.PES.Header.OptionalHeader.PTS + rd := bytes.NewBuffer(pl) + r := bits.NewReader(rd) + smpteData := smpte2038Data{PID: d.PID, PTS: pts.Base} + for { + z := r.Read(6) + if r.AccError() == io.EOF { + break + } + if z == 0xffffffffffff { + z2 := r.Read(2) + if z2 != 0x3 { + log.Printf("SMPTE-2038: invalid stuffing\n") + return + } + _ = r.ReadRemainingBytes() + } + if z != 0 { + log.Printf("SMPTE-2038: reserved bits not zero %x\n", z) + return + } + _ = r.Read(1) // cNotYChFlag + lineNr := r.Read(11) + horOffset := r.Read(12) + did := r.Read(10) + did = did & 0xff // 8 bits + sdid := r.Read(10) + sdid = sdid & 0xff // 8 bits + didStr := SMPTE291Map[SMPTE291Identifier{byte(did), byte(sdid)}] + if didStr == "" { + didStr = "unknown SID/DID" + } + dataCount := int(r.Read(10)) & 0xff // 8 bits + for j := 0; j < dataCount; j++ { + _ = r.Read(10) + } + _ = r.Read(10) // checkSumWord + if r.NrBitsReadInCurrentByte() != 8 { + _ = r.Read(8 - r.NrBitsReadInCurrentByte()) + } + if r.AccError() != nil { + fmt.Printf("SMPTE-2038: read error\n") + return + } + smpteData.Entries = append(smpteData.Entries, smpte2038Entry{ + LineNr: byte(lineNr), + HorOffset: byte(horOffset), + DID: byte(did), + SDID: byte(sdid), + DataCount: byte(dataCount), + Type: didStr, + }) + } + if jp != nil { + jp.Print(smpteData, true) + } +} diff --git a/internal/utils.go b/internal/utils.go index 03c249f..27f4261 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -2,6 +2,7 @@ package internal import ( "context" + "encoding/hex" "errors" "flag" "fmt" @@ -30,6 +31,7 @@ type Options struct { VerbosePSInfo bool ShowNALU bool ShowSEIDetails bool + ShowSMPTE2038 bool ShowSCTE35 bool ShowStatistics bool FilterPids bool @@ -38,9 +40,14 @@ type Options struct { } func CreateFullOptions(max int) Options { - return Options{MaxNrPictures: max, ShowStreamInfo: true, ShowService: true, ShowPS: true, ShowNALU: true, ShowSEIDetails: true, ShowStatistics: true} + return Options{MaxNrPictures: max, ShowStreamInfo: true, ShowService: true, ShowPS: true, ShowNALU: true, ShowSEIDetails: true, ShowSMPTE2038: true, ShowStatistics: true} } +const ( + ANC_REGISTERED_IDENTIFIER = 0x56414E43 + ANC_DESCRIPTOR_TAG = 0xC4 +) + type OptionParseFunc func() Options type RunableFunc func(ctx context.Context, w io.Writer, f io.Reader, o Options) error @@ -127,6 +134,36 @@ func ParseAstitsElementaryStreamInfo(es *astits.PMTElementaryStream) *Elementary streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "HEVC", Type: "video"} case astits.StreamTypeSCTE35: streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "SCTE35", Type: "cue"} + case astits.StreamTypePrivateData: + streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "PrivateData", Type: "data"} + default: + return nil + } + for _, d := range es.ElementaryStreamDescriptors { + switch d.Tag { + case astits.DescriptorTagISO639LanguageAndAudioType: + l := d.ISO639LanguageAndAudioType + fmt.Printf("Language: %s\n", l.Language) + case astits.DescriptorTagDataStreamAlignment: + a := d.DataStreamAlignment + log.Printf("PID %d: Descriptor Data stream alignment: %d\n", es.ElementaryPID, a.Type) + case astits.DescriptorTagRegistration: + r := d.Registration + switch r.FormatIdentifier { + case ANC_REGISTERED_IDENTIFIER: + streamInfo.Codec = "SMPTE-2038" + streamInfo.Type = "ANC" + } + case ANC_DESCRIPTOR_TAG: + if streamInfo.Type != "ANC" { + log.Printf("PID %d: bad combination of descriptor 0xc4 and no preceding ANC", es.ElementaryPID) + continue + } + u := d.UserDefined + log.Printf("PID %d: Got ancillary descriptor with data: %q\n", es.ElementaryPID, hex.EncodeToString(u)) + default: + // Nothing + } } return streamInfo