Skip to content

Commit

Permalink
feat: change output to JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
tobbee committed Jan 15, 2024
1 parent 541b2bb commit b11e61e
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 244 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ data from MPEG-2 TS streams.
## ts-info

`ts-info` is a tool that parses a TS file, or a stream on stdin, and prints
information about the video streams.
information about the video streams in JSON format.
For AVC (H.264), it shows information about
PTS/DTS, PicTiming SEI, SPS and PPS, and NAL units.

Expand Down
134 changes: 46 additions & 88 deletions cmd/ts-info/app/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,59 @@ package app
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"strings"

"github.com/Eyevinn/mp4ff/avc"
"github.com/Eyevinn/mp4ff/sei"
"github.com/asticode/go-astits"
)

type Options struct {
MaxNrPictures int
ParameterSets bool
Version bool
MaxNrPictures int
Indent bool
}

const (
packetSize = 188
)

type elementaryStream struct {
PID uint16 `json:"pid"`
Codec string `json:"codec"`
Type string `json:"type"`
}

type jsonPrinter struct {
w io.Writer
indent bool
accError error
}

func (p *jsonPrinter) print(data any) {
var out []byte
var err error
if p.accError != nil {
return
}
if p.indent {
out, err = json.MarshalIndent(data, "", " ")
} else {
out, err = json.Marshal(data)
}
if err != nil {
p.accError = err
return
}
_, p.accError = fmt.Fprintln(p.w, string(out))
}

func (p *jsonPrinter) error() error {
return p.accError
}

func Parse(ctx context.Context, w io.Writer, f io.Reader, o Options) error {
rd := bufio.NewReaderSize(f, 1000*packetSize)
dmx := astits.NewDemuxer(ctx, rd)
Expand All @@ -30,6 +64,7 @@ func Parse(ctx context.Context, w io.Writer, f io.Reader, o Options) error {
sdtPrinted := false
esKinds := make(map[uint16]string)
avcPSs := make(map[uint16]*avcPS)
jp := &jsonPrinter{w: w, indent: o.Indent}
dataLoop:
for {
d, err := dmx.NextData()
Expand Down Expand Up @@ -58,14 +93,18 @@ dataLoop:
if pmtPID < 0 && d.PMT != nil {
// Loop through elementary streams
for _, es := range d.PMT.ElementaryStreams {
var e *elementaryStream
switch es.StreamType {
case astits.StreamTypeH264Video:
fmt.Fprintf(w, "H264 video detected on PID: %d\n", es.ElementaryPID)
e = &elementaryStream{PID: es.ElementaryPID, Codec: "AVC", Type: "video"}
esKinds[es.ElementaryPID] = "AVC"
case astits.StreamTypeAACAudio:
fmt.Fprintf(w, "AAC audio detected on PID: %d\n", es.ElementaryPID)
e = &elementaryStream{PID: es.ElementaryPID, Codec: "AAC", Type: "audio"}
esKinds[es.ElementaryPID] = "AAC"
}
if e != nil {
jp.print(e)
}
}
pmtPID = int(d.PID)
}
Expand All @@ -79,7 +118,7 @@ dataLoop:
switch esKinds[d.PID] {
case "AVC":
avcPS := avcPSs[d.PID]
avcPS, err = parseAVCPES(w, d, avcPS, o.ParameterSets)
avcPS, err = parseAVCPES(jp, d, avcPS, o.ParameterSets)
if err != nil {
return err
}
Expand All @@ -95,86 +134,5 @@ dataLoop:
}
}
}
return nil
}

func parseAVCPES(w io.Writer, d *astits.DemuxerData, ps *avcPS, verbose bool) (*avcPS, error) {
pid := d.PID
pes := d.PES
fp := d.FirstPacket
if pes.Header.OptionalHeader.PTS == nil {
return nil, fmt.Errorf("no PTS in PES")
}
outText := fmt.Sprintf("PID: %d, ", pid)
if fp != nil {
af := fp.AdaptationField
if af != nil {
outText += fmt.Sprintf("RAI: %t, ", af.RandomAccessIndicator)
}
}
pts := *pes.Header.OptionalHeader.PTS
data := pes.Data
outText += fmt.Sprintf("PTS: %d, ", pts.Base)

dts := pes.Header.OptionalHeader.DTS
if dts != nil {
outText += fmt.Sprintf("DTS: %d, ", dts.Base)
}
nalus := avc.ExtractNalusFromByteStream(data)
firstPS := false
outText += "NALUs: "
for _, nalu := range nalus {
var seiMsg string
naluType := avc.GetNaluType(nalu[0])
switch naluType {
case avc.NALU_SPS:
if ps == nil && !firstPS {
ps = &avcPS{}
err := ps.setSPS(nalu)
if err != nil {
return nil, fmt.Errorf("cannot set SPS")
}
firstPS = true
}
case avc.NALU_PPS:
if firstPS {
err := ps.setPPS(nalu)
if err != nil {
return nil, fmt.Errorf("cannot set PPS")
}
}
case avc.NALU_SEI:
var sps *avc.SPS
if ps != nil {
sps = ps.getSPS()
}
msgs, err := avc.ParseSEINalu(nalu, sps)
if err != nil {
return nil, err
}
seiTexts := make([]string, 0, len(msgs))
for _, msg := range msgs {
if msg.Type() == sei.SEIPicTimingType {
pt := msg.(*sei.PicTimingAvcSEI)
seiTexts = append(seiTexts, fmt.Sprintf("Type 1: %s", pt.Clocks[0]))
}
}
seiMsg = strings.Join(seiTexts, ", ")
seiMsg += " "
}
outText += fmt.Sprintf("[%s %s%dB]", naluType, seiMsg, len(nalu))
}
if ps == nil {
return nil, nil
}
if firstPS {
for i := range ps.spss {
printPS(w, fmt.Sprintf("PID %d, SPS", pid), i, ps.spsnalu, ps.spss[i], verbose)
}
for i := range ps.ppss {
printPS(w, fmt.Sprintf("PID %d, PPS", pid), i, ps.ppsnalus[i], ps.ppss[i], verbose)
}
}
fmt.Fprintln(w, outText)
return ps, nil
return jp.error()
}
26 changes: 24 additions & 2 deletions cmd/ts-info/app/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app_test
import (
"bytes"
"context"
"flag"
"os"
"strings"
"testing"
Expand All @@ -11,6 +12,10 @@ import (
"github.com/stretchr/testify/require"
)

var (
update = flag.Bool("update", false, "update the golden files of this test")
)

func TestParseFile(t *testing.T) {
cases := []struct {
name string
Expand All @@ -20,6 +25,7 @@ func TestParseFile(t *testing.T) {
}{
{"avc_with_time", "../testdata/avc_with_time.ts", app.Options{ParameterSets: true}, "testdata/golden_avc_with_time.txt"},
{"bbb_1s", "testdata/bbb_1s.ts", app.Options{MaxNrPictures: 15}, "testdata/golden_bbb_1s.txt"},
{"bbb_1s_indented", "testdata/bbb_1s.ts", app.Options{MaxNrPictures: 2, Indent: true}, "testdata/golden_bbb_1s_indented.txt"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand All @@ -29,8 +35,7 @@ func TestParseFile(t *testing.T) {
require.NoError(t, err)
err = app.Parse(ctx, &buf, f, c.options)
require.NoError(t, err)
expected_output := getExpectedOutput(t, c.expected_output_file)
require.Equal(t, expected_output, buf.String(), "should produce expected output")
compareUpdateGolden(t, buf.String(), c.expected_output_file, *update)
})
}
}
Expand All @@ -42,3 +47,20 @@ func getExpectedOutput(t *testing.T, file string) string {
expected_output_str := strings.ReplaceAll(string(expected_output), "\r\n", "\n")
return expected_output_str
}

func compareUpdateGolden(t *testing.T, actual string, goldenFile string, update bool) {
t.Helper()
if update {
err := os.WriteFile(goldenFile, []byte(actual), 0644)
require.NoError(t, err)
} else {
expected := getExpectedOutput(t, goldenFile)
require.Equal(t, expected, actual, "should produce expected output")
}
}

// TestMain is to set flags for tests. In particular, the update flag to update golden files.
func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
}
120 changes: 4 additions & 116 deletions cmd/ts-info/app/testdata/golden_avc_with_time.txt
Original file line number Diff line number Diff line change
@@ -1,116 +1,4 @@
H264 video detected on PID: 512
PID 512, SPS 0 len 36B: 27640020ac2b402802dd80880000030008000003032742001458000510edef7c1da1c32a
{
"Profile": 100,
"ProfileCompatibility": 0,
"Level": 32,
"ParameterID": 0,
"ChromaFormatIDC": 1,
"SeparateColourPlaneFlag": false,
"BitDepthLumaMinus8": 0,
"BitDepthChromaMinus8": 0,
"QPPrimeYZeroTransformBypassFlag": false,
"SeqScalingMatrixPresentFlag": false,
"SeqScalingLists": null,
"Log2MaxFrameNumMinus4": 4,
"PicOrderCntType": 2,
"Log2MaxPicOrderCntLsbMinus4": 0,
"DeltaPicOrderAlwaysZeroFlag": false,
"OffsetForNonRefPic": 0,
"OffsetForTopToBottomField": 0,
"RefFramesInPicOrderCntCycle": null,
"NumRefFrames": 1,
"GapsInFrameNumValueAllowedFlag": false,
"FrameMbsOnlyFlag": true,
"MbAdaptiveFrameFieldFlag": false,
"Direct8x8InferenceFlag": true,
"FrameCroppingFlag": false,
"FrameCropLeftOffset": 0,
"FrameCropRightOffset": 0,
"FrameCropTopOffset": 0,
"FrameCropBottomOffset": 0,
"Width": 1280,
"Height": 720,
"NrBytesBeforeVUI": 10,
"NrBytesRead": 36,
"VUI": {
"SampleAspectRatioWidth": 1,
"SampleAspectRatioHeight": 1,
"OverscanInfoPresentFlag": false,
"OverscanAppropriateFlag": false,
"VideoSignalTypePresentFlag": false,
"VideoFormat": 0,
"VideoFullRangeFlag": false,
"ColourDescriptionFlag": false,
"ColourPrimaries": 0,
"TransferCharacteristics": 0,
"MatrixCoefficients": 0,
"ChromaLocInfoPresentFlag": false,
"ChromaSampleLocTypeTopField": 0,
"ChromaSampleLocTypeBottomField": 0,
"TimingInfoPresentFlag": true,
"NumUnitsInTick": 1,
"TimeScale": 100,
"FixedFrameRateFlag": true,
"NalHrdParametersPresentFlag": true,
"NalHrdParameters": {
"CpbCountMinus1": 0,
"BitRateScale": 4,
"CpbSizeScale": 2,
"CpbEntries": [
{
"BitRateValueMinus1": 2603,
"CpbSizeValueMinus1": 20749,
"CbrFlag": true
}
],
"InitialCpbRemovalDelayLengthMinus1": 23,
"CpbRemovalDelayLengthMinus1": 23,
"DpbOutputDelayLengthMinus1": 23,
"TimeOffsetLength": 24
},
"VclHrdParametersPresentFlag": false,
"VclHrdParameters": null,
"LowDelayHrdFlag": false,
"PicStructPresentFlag": true,
"BitstreamRestrictionFlag": true,
"MotionVectorsOverPicBoundariesFlag": true,
"MaxBytesPerPicDenom": 2,
"MaxBitsPerMbDenom": 1,
"Log2MaxMvLengthHorizontal": 13,
"Log2MaxMvLengthVertical": 11,
"MaxNumReorderFrames": 0,
"MaxDecFrameBuffering": 1
}
}
PID 512, PPS 0 len 4B: 28ee3cb0
{
"PicParameterSetID": 0,
"SeqParameterSetID": 0,
"EntropyCodingModeFlag": true,
"BottomFieldPicOrderInFramePresentFlag": false,
"NumSliceGroupsMinus1": 0,
"SliceGroupMapType": 0,
"RunLengthMinus1": null,
"TopLeft": null,
"BottomRight": null,
"SliceGroupChangeDirectionFlag": false,
"SliceGroupChangeRateMinus1": 0,
"PicSizeInMapUnitsMinus1": 0,
"SliceGroupID": null,
"NumRefIdxI0DefaultActiveMinus1": 0,
"NumRefIdxI1DefaultActiveMinus1": 0,
"WeightedPredFlag": false,
"WeightedBipredIDC": 0,
"PicInitQpMinus26": 0,
"PicInitQsMinus26": 0,
"ChromaQpIndexOffset": 0,
"DeblockingFilterControlPresentFlag": true,
"ConstrainedIntraPredFlag": false,
"RedundantPicCntPresentFlag": false,
"Transform8x8ModeFlag": true,
"PicScalingMatrixPresentFlag": false,
"PicScalingLists": null,
"SecondChromaQpIndexOffset": 0
}
PID: 512, RAI: true, PTS: 5508000, NALUs: [AUD_9 2B][SPS_7 36B][PPS_8 4B][SEI_6 Type 1: 13:40:57:15 offset=0 29B][IDR_5 2096B]
{"pid":512,"codec":"AVC","type":"video"}
{"pid":512,"parameterSet":"SPS","nr":0,"hex":"27640020ac2b402802dd80880000030008000003032742001458000510edef7c1da1c32a","length":36,"details":{"Profile":100,"ProfileCompatibility":0,"Level":32,"ParameterID":0,"ChromaFormatIDC":1,"SeparateColourPlaneFlag":false,"BitDepthLumaMinus8":0,"BitDepthChromaMinus8":0,"QPPrimeYZeroTransformBypassFlag":false,"SeqScalingMatrixPresentFlag":false,"SeqScalingLists":null,"Log2MaxFrameNumMinus4":4,"PicOrderCntType":2,"Log2MaxPicOrderCntLsbMinus4":0,"DeltaPicOrderAlwaysZeroFlag":false,"OffsetForNonRefPic":0,"OffsetForTopToBottomField":0,"RefFramesInPicOrderCntCycle":null,"NumRefFrames":1,"GapsInFrameNumValueAllowedFlag":false,"FrameMbsOnlyFlag":true,"MbAdaptiveFrameFieldFlag":false,"Direct8x8InferenceFlag":true,"FrameCroppingFlag":false,"FrameCropLeftOffset":0,"FrameCropRightOffset":0,"FrameCropTopOffset":0,"FrameCropBottomOffset":0,"Width":1280,"Height":720,"NrBytesBeforeVUI":10,"NrBytesRead":36,"VUI":{"SampleAspectRatioWidth":1,"SampleAspectRatioHeight":1,"OverscanInfoPresentFlag":false,"OverscanAppropriateFlag":false,"VideoSignalTypePresentFlag":false,"VideoFormat":0,"VideoFullRangeFlag":false,"ColourDescriptionFlag":false,"ColourPrimaries":0,"TransferCharacteristics":0,"MatrixCoefficients":0,"ChromaLocInfoPresentFlag":false,"ChromaSampleLocTypeTopField":0,"ChromaSampleLocTypeBottomField":0,"TimingInfoPresentFlag":true,"NumUnitsInTick":1,"TimeScale":100,"FixedFrameRateFlag":true,"NalHrdParametersPresentFlag":true,"NalHrdParameters":{"CpbCountMinus1":0,"BitRateScale":4,"CpbSizeScale":2,"CpbEntries":[{"BitRateValueMinus1":2603,"CpbSizeValueMinus1":20749,"CbrFlag":true}],"InitialCpbRemovalDelayLengthMinus1":23,"CpbRemovalDelayLengthMinus1":23,"DpbOutputDelayLengthMinus1":23,"TimeOffsetLength":24},"VclHrdParametersPresentFlag":false,"VclHrdParameters":null,"LowDelayHrdFlag":false,"PicStructPresentFlag":true,"BitstreamRestrictionFlag":true,"MotionVectorsOverPicBoundariesFlag":true,"MaxBytesPerPicDenom":2,"MaxBitsPerMbDenom":1,"Log2MaxMvLengthHorizontal":13,"Log2MaxMvLengthVertical":11,"MaxNumReorderFrames":0,"MaxDecFrameBuffering":1}}}
{"pid":512,"parameterSet":"PPS","nr":0,"hex":"28ee3cb0","length":4,"details":{"PicParameterSetID":0,"SeqParameterSetID":0,"EntropyCodingModeFlag":true,"BottomFieldPicOrderInFramePresentFlag":false,"NumSliceGroupsMinus1":0,"SliceGroupMapType":0,"RunLengthMinus1":null,"TopLeft":null,"BottomRight":null,"SliceGroupChangeDirectionFlag":false,"SliceGroupChangeRateMinus1":0,"PicSizeInMapUnitsMinus1":0,"SliceGroupID":null,"NumRefIdxI0DefaultActiveMinus1":0,"NumRefIdxI1DefaultActiveMinus1":0,"WeightedPredFlag":false,"WeightedBipredIDC":0,"PicInitQpMinus26":0,"PicInitQsMinus26":0,"ChromaQpIndexOffset":0,"DeblockingFilterControlPresentFlag":true,"ConstrainedIntraPredFlag":false,"RedundantPicCntPresentFlag":false,"Transform8x8ModeFlag":true,"PicScalingMatrixPresentFlag":false,"PicScalingLists":null,"SecondChromaQpIndexOffset":0}}
{"pid":512,"rai":true,"pts":5508000,"nalus":[{"type":"AUD_9","len":2},{"type":"SPS_7","len":36},{"type":"PPS_8","len":4},{"type":"SEI_6","len":29,"data":"Type 1: 13:40:57:15 offset=0"},{"type":"IDR_5","len":2096}]}
Loading

0 comments on commit b11e61e

Please sign in to comment.