Skip to content

Commit

Permalink
feat: add parser for SCTE (#16)
Browse files Browse the repository at this point in the history
* feat: add parser for SCTE35 markers
  • Loading branch information
Wkkkkk committed Jan 25, 2024
1 parent 2f00fbf commit 7aef2ea
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 3 deletions.
21 changes: 18 additions & 3 deletions cmd/mp2ts-info/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ var usg = `Usage of %s:
func parseOptions() internal.Options {
opts := internal.Options{ShowStreamInfo: true, Indent: true}
flag.BoolVar(&opts.ShowService, "service", false, "show service information")
flag.BoolVar(&opts.ShowSCTE35, "scte35", true, "show SCTE35 information")
flag.BoolVar(&opts.Indent, "indent", true, "indent JSON output")
flag.BoolVar(&opts.Version, "version", false, "print version")

flag.Usage = func() {
Expand All @@ -34,13 +36,26 @@ func parseOptions() internal.Options {
return opts
}

func parseInfo(ctx context.Context, w io.Writer, f io.Reader, o internal.Options) error {
return internal.ParseInfo(ctx, w, f, o)
func parse(ctx context.Context, w io.Writer, f io.Reader, o internal.Options) error {
// Parse either general information, or scte35 (by default)
if o.ShowService {
err := internal.ParseInfo(ctx, w, f, o)
if err != nil {
return err
}
} else if o.ShowSCTE35 {
err := internal.ParseSCTE35(ctx, w, f, o)
if err != nil {
return err
}
}

return nil
}

func main() {
o, inFile := internal.ParseParams(parseOptions)
err := internal.Execute(os.Stdout, o, inFile, parseInfo)
err := internal.Execute(os.Stdout, o, inFile, parse)
if err != nil {
log.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/Eyevinn/mp2ts-tools
go 1.19

require (
github.com/Comcast/gots/v2 v2.2.1
github.com/Eyevinn/mp4ff v0.41.1-0.20240123164056-bbd4656aecc0
github.com/asticode/go-astits v1.13.0
github.com/stretchr/testify v1.8.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
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.41.1-0.20240123164056-bbd4656aecc0 h1:TVO5vjF4CEAB6OKR0RWMQFvHXtZ+UdKCsp+awECiIVE=
github.com/Eyevinn/mp4ff v0.41.1-0.20240123164056-bbd4656aecc0/go.mod h1:w/6GSa5ghZ1VavzJK6McQ2/flx8mKtcrKDr11SsEweA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
Expand Down
78 changes: 78 additions & 0 deletions internal/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"fmt"
"io"

"github.com/Comcast/gots/v2/packet"
"github.com/Comcast/gots/v2/psi"
"github.com/Comcast/gots/v2/scte35"
"github.com/asticode/go-astits"
)

Expand Down Expand Up @@ -155,6 +158,8 @@ dataLoop:
streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "AAC", Type: "audio"}
case astits.StreamTypeH265Video:
streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "HEVC", Type: "video"}
case astits.StreamTypeSCTE35:
streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "SCTE35", Type: "cue"}
}

if streamInfo != nil {
Expand Down Expand Up @@ -183,3 +188,76 @@ dataLoop:

return jp.Error()
}

func ParseSCTE35(ctx context.Context, w io.Writer, f io.Reader, o Options) error {
reader := bufio.NewReader(f)
_, err := packet.Sync(reader)
if err != nil {
return fmt.Errorf("syncing with reader %w", err)
}
pat, err := psi.ReadPAT(reader)
if err != nil {
return fmt.Errorf("reading PAT %w", err)
}

var pmts []psi.PMT
pm := pat.ProgramMap()
for _, pid := range pm {
pmt, err := psi.ReadPMT(reader, pid)
if err != nil {
return fmt.Errorf("reading PMT %w", err)
}
pmts = append(pmts, pmt)
}

jp := &JsonPrinter{W: w, Indent: o.Indent}
scte35PIDs := make(map[int]bool)
for _, pmt := range pmts {
for _, es := range pmt.ElementaryStreams() {
pid := uint16(es.ElementaryPid())
var streamInfo *ElementaryStreamInfo
switch es.StreamType() {
case psi.PmtStreamTypeMpeg4VideoH264:
streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "AVC", Type: "video"}
case psi.PmtStreamTypeAac:
streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "AAC", Type: "audio"}
case psi.PmtStreamTypeMpeg4VideoH265:
streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "HEVC", Type: "video"}
case psi.PmtStreamTypeScte35:
streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "SCTE35", Type: "cue"}
scte35PIDs[es.ElementaryPid()] = true
}

if streamInfo != nil {
jp.Print(streamInfo, o.ShowStreamInfo)
}
}
}

// Print SCTE35
for {
var pkt packet.Packet
if _, err := io.ReadFull(reader, pkt[:]); err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
break
}
return fmt.Errorf("reading Packet %w", err)
}

currPID := packet.Pid(&pkt)
if scte35PIDs[currPID] {
pay, err := packet.Payload(&pkt)
if err != nil {
return fmt.Errorf("cannot get payload for packet on PID %d Error=%s\n", currPID, err)
}
msg, err := scte35.NewSCTE35(pay)
if err != nil {
return fmt.Errorf("cannot parse SCTE35 Error=%v\n", err)
}
scte35 := toSCTE35(uint16(currPID), msg)
jp.Print(scte35, o.ShowSCTE35)
}
}

return jp.Error()
}
3 changes: 3 additions & 0 deletions internal/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestParseFile(t *testing.T) {
fullOptionsWith35PicWithoutNALUSEI.ShowSEIDetails = false

parseInfoFunc := ParseInfo
parseSCTE35Func := ParseSCTE35
parseAllFunc := ParseAll

cases := []struct {
Expand All @@ -35,6 +36,8 @@ func TestParseFile(t *testing.T) {
}{
{"avc", "testdata/avc_with_time.ts", Options{MaxNrPictures: 10, Indent: true, ShowStreamInfo: true, ShowPS: true, ShowStatistics: true}, "testdata/golden_avc.txt", parseAllFunc},
{"avc_without_ps", "testdata/avc_with_time.ts", Options{MaxNrPictures: 10, ShowStreamInfo: true}, "testdata/golden_avc_without_ps.txt", parseInfoFunc},
{"avc_with_service", "testdata/80s_with_ad.ts", Options{MaxNrPictures: 0, ShowStreamInfo: true, ShowService: true}, "testdata/golden_avc_with_service.txt", parseInfoFunc},
{"avc_with_scte35", "testdata/80s_with_ad.ts", Options{MaxNrPictures: 0, ShowStreamInfo: true, ShowSCTE35: true}, "testdata/golden_avc_with_scte35.txt", parseSCTE35Func},
{"bbb_1s", "testdata/bbb_1s.ts", fullOptionsWith35Pic, "testdata/golden_bbb_1s.txt", parseAllFunc},
{"bbb_1s_indented", "testdata/bbb_1s.ts", fullOptionsWith2Pic, "testdata/golden_bbb_1s_indented.txt", parseAllFunc},
{"bbb_1s_no_nalu_no_sei", "testdata/bbb_1s.ts", fullOptionsWith35PicWithoutNALUSEI, "testdata/golden_bbb_1s_no_nalu(no_sei).txt", parseAllFunc},
Expand Down
80 changes: 80 additions & 0 deletions internal/scte35.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package internal

import (
"github.com/Comcast/gots/v2/scte35"
)

type SCTE35Info struct {
PID uint16 `json:"pid"`
SpliceCommand SpliceCommand `json:"spliceCommand"`
SegDesc []SegmentationDescriptor `json:"segmentationDes,omitempty"`
}

type SpliceCommand struct {
Type string `json:"type"`
EventId uint32 `json:"eventId"`
PTS uint64 `json:"pts"`
Duration uint64 `json:"duration,omitempty"`
Out bool `json:"outOfNetwork,omitempty"`
Immediate bool `json:"immediate,omitempty"`
}

type SegmentationDescriptor struct {
SegmentNumber uint8 `json:"segmentNumber"`
EventId uint32 `json:"eventId"`
Type string `json:"type"`
Duration uint64 `json:"duration,omitempty"`
}

func toSCTE35(pid uint16, msg scte35.SCTE35) SCTE35Info {
scte35Info := SCTE35Info{PID: pid, SpliceCommand: toSpliceCommand(msg.CommandInfo())}

if insert, ok := msg.CommandInfo().(scte35.SpliceInsertCommand); ok {
scte35Info.SpliceCommand = toSpliceInsertCommand(insert)
}
for _, desc := range msg.Descriptors() {
segDesc := toSegmentationDescriptor(desc)
scte35Info.SegDesc = append(scte35Info.SegDesc, segDesc)
}

return scte35Info
}

func toSpliceCommand(spliceCommand scte35.SpliceCommand) SpliceCommand {
spliceCmd := SpliceCommand{Type: getCommandType(spliceCommand)}
if spliceCommand.HasPTS() {
spliceCmd.PTS = uint64(spliceCommand.PTS())
}

return spliceCmd
}

func toSegmentationDescriptor(segdesc scte35.SegmentationDescriptor) SegmentationDescriptor {
segDesc := SegmentationDescriptor{}
segDesc.EventId = segdesc.EventID()
segDesc.Type = scte35.SegDescTypeNames[segdesc.TypeID()]
segDesc.SegmentNumber = segdesc.SegmentNumber()
if segdesc.HasDuration() {
segDesc.Duration = uint64(segdesc.Duration())
}
return segDesc
}

func toSpliceInsertCommand(spliceCommand scte35.SpliceInsertCommand) SpliceCommand {
spliceCmd := SpliceCommand{Type: getCommandType(spliceCommand)}
spliceCmd.EventId = spliceCommand.EventID()
spliceCmd.Immediate = spliceCommand.SpliceImmediate()
spliceCmd.Out = spliceCommand.IsOut()
if spliceCommand.HasPTS() {
spliceCmd.PTS = uint64(spliceCommand.PTS())
}
if spliceCommand.HasDuration() {
spliceCmd.Duration = uint64(spliceCommand.Duration())
}

return spliceCmd
}

func getCommandType(spliceCommand scte35.SpliceCommand) string {
return scte35.SpliceCommandTypeNames[spliceCommand.CommandType()]
}
Binary file added internal/testdata/80s_with_ad.ts
Binary file not shown.
4 changes: 4 additions & 0 deletions internal/testdata/golden_avc_with_scte35.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"pid":256,"codec":"AVC","type":"video"}
{"pid":257,"codec":"AAC","type":"audio"}
{"pid":1001,"codec":"SCTE35","type":"cue"}
{"pid":1001,"spliceCommand":{"type":"SpliceInsert","eventId":255,"pts":1032000,"duration":1800000,"outOfNetwork":true}}
4 changes: 4 additions & 0 deletions internal/testdata/golden_avc_with_service.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"pid":256,"codec":"AVC","type":"video"}
{"pid":257,"codec":"AAC","type":"audio"}
{"pid":1001,"codec":"SCTE35","type":"cue"}
{"SDT":[{"serviceId":1,"descriptors":[{"serviceName":"Service01","providerName":"FFmpeg"}]}]}
1 change: 1 addition & 0 deletions internal/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Options struct {
VerbosePSInfo bool
ShowNALU bool
ShowSEIDetails bool
ShowSCTE35 bool
ShowStatistics bool
}

Expand Down

0 comments on commit 7aef2ea

Please sign in to comment.