Skip to content

Commit b11e61e

Browse files
committed
feat: change output to JSON
1 parent 541b2bb commit b11e61e

10 files changed

+292
-244
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ data from MPEG-2 TS streams.
1414
## ts-info
1515

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

cmd/ts-info/app/parser.go

+46-88
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,59 @@ package app
33
import (
44
"bufio"
55
"context"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"strings"
910

10-
"github.com/Eyevinn/mp4ff/avc"
11-
"github.com/Eyevinn/mp4ff/sei"
1211
"github.com/asticode/go-astits"
1312
)
1413

1514
type Options struct {
15+
MaxNrPictures int
1616
ParameterSets bool
1717
Version bool
18-
MaxNrPictures int
18+
Indent bool
1919
}
2020

2121
const (
2222
packetSize = 188
2323
)
2424

25+
type elementaryStream struct {
26+
PID uint16 `json:"pid"`
27+
Codec string `json:"codec"`
28+
Type string `json:"type"`
29+
}
30+
31+
type jsonPrinter struct {
32+
w io.Writer
33+
indent bool
34+
accError error
35+
}
36+
37+
func (p *jsonPrinter) print(data any) {
38+
var out []byte
39+
var err error
40+
if p.accError != nil {
41+
return
42+
}
43+
if p.indent {
44+
out, err = json.MarshalIndent(data, "", " ")
45+
} else {
46+
out, err = json.Marshal(data)
47+
}
48+
if err != nil {
49+
p.accError = err
50+
return
51+
}
52+
_, p.accError = fmt.Fprintln(p.w, string(out))
53+
}
54+
55+
func (p *jsonPrinter) error() error {
56+
return p.accError
57+
}
58+
2559
func Parse(ctx context.Context, w io.Writer, f io.Reader, o Options) error {
2660
rd := bufio.NewReaderSize(f, 1000*packetSize)
2761
dmx := astits.NewDemuxer(ctx, rd)
@@ -30,6 +64,7 @@ func Parse(ctx context.Context, w io.Writer, f io.Reader, o Options) error {
3064
sdtPrinted := false
3165
esKinds := make(map[uint16]string)
3266
avcPSs := make(map[uint16]*avcPS)
67+
jp := &jsonPrinter{w: w, indent: o.Indent}
3368
dataLoop:
3469
for {
3570
d, err := dmx.NextData()
@@ -58,14 +93,18 @@ dataLoop:
5893
if pmtPID < 0 && d.PMT != nil {
5994
// Loop through elementary streams
6095
for _, es := range d.PMT.ElementaryStreams {
96+
var e *elementaryStream
6197
switch es.StreamType {
6298
case astits.StreamTypeH264Video:
63-
fmt.Fprintf(w, "H264 video detected on PID: %d\n", es.ElementaryPID)
99+
e = &elementaryStream{PID: es.ElementaryPID, Codec: "AVC", Type: "video"}
64100
esKinds[es.ElementaryPID] = "AVC"
65101
case astits.StreamTypeAACAudio:
66-
fmt.Fprintf(w, "AAC audio detected on PID: %d\n", es.ElementaryPID)
102+
e = &elementaryStream{PID: es.ElementaryPID, Codec: "AAC", Type: "audio"}
67103
esKinds[es.ElementaryPID] = "AAC"
68104
}
105+
if e != nil {
106+
jp.print(e)
107+
}
69108
}
70109
pmtPID = int(d.PID)
71110
}
@@ -79,7 +118,7 @@ dataLoop:
79118
switch esKinds[d.PID] {
80119
case "AVC":
81120
avcPS := avcPSs[d.PID]
82-
avcPS, err = parseAVCPES(w, d, avcPS, o.ParameterSets)
121+
avcPS, err = parseAVCPES(jp, d, avcPS, o.ParameterSets)
83122
if err != nil {
84123
return err
85124
}
@@ -95,86 +134,5 @@ dataLoop:
95134
}
96135
}
97136
}
98-
return nil
99-
}
100-
101-
func parseAVCPES(w io.Writer, d *astits.DemuxerData, ps *avcPS, verbose bool) (*avcPS, error) {
102-
pid := d.PID
103-
pes := d.PES
104-
fp := d.FirstPacket
105-
if pes.Header.OptionalHeader.PTS == nil {
106-
return nil, fmt.Errorf("no PTS in PES")
107-
}
108-
outText := fmt.Sprintf("PID: %d, ", pid)
109-
if fp != nil {
110-
af := fp.AdaptationField
111-
if af != nil {
112-
outText += fmt.Sprintf("RAI: %t, ", af.RandomAccessIndicator)
113-
}
114-
}
115-
pts := *pes.Header.OptionalHeader.PTS
116-
data := pes.Data
117-
outText += fmt.Sprintf("PTS: %d, ", pts.Base)
118-
119-
dts := pes.Header.OptionalHeader.DTS
120-
if dts != nil {
121-
outText += fmt.Sprintf("DTS: %d, ", dts.Base)
122-
}
123-
nalus := avc.ExtractNalusFromByteStream(data)
124-
firstPS := false
125-
outText += "NALUs: "
126-
for _, nalu := range nalus {
127-
var seiMsg string
128-
naluType := avc.GetNaluType(nalu[0])
129-
switch naluType {
130-
case avc.NALU_SPS:
131-
if ps == nil && !firstPS {
132-
ps = &avcPS{}
133-
err := ps.setSPS(nalu)
134-
if err != nil {
135-
return nil, fmt.Errorf("cannot set SPS")
136-
}
137-
firstPS = true
138-
}
139-
case avc.NALU_PPS:
140-
if firstPS {
141-
err := ps.setPPS(nalu)
142-
if err != nil {
143-
return nil, fmt.Errorf("cannot set PPS")
144-
}
145-
}
146-
case avc.NALU_SEI:
147-
var sps *avc.SPS
148-
if ps != nil {
149-
sps = ps.getSPS()
150-
}
151-
msgs, err := avc.ParseSEINalu(nalu, sps)
152-
if err != nil {
153-
return nil, err
154-
}
155-
seiTexts := make([]string, 0, len(msgs))
156-
for _, msg := range msgs {
157-
if msg.Type() == sei.SEIPicTimingType {
158-
pt := msg.(*sei.PicTimingAvcSEI)
159-
seiTexts = append(seiTexts, fmt.Sprintf("Type 1: %s", pt.Clocks[0]))
160-
}
161-
}
162-
seiMsg = strings.Join(seiTexts, ", ")
163-
seiMsg += " "
164-
}
165-
outText += fmt.Sprintf("[%s %s%dB]", naluType, seiMsg, len(nalu))
166-
}
167-
if ps == nil {
168-
return nil, nil
169-
}
170-
if firstPS {
171-
for i := range ps.spss {
172-
printPS(w, fmt.Sprintf("PID %d, SPS", pid), i, ps.spsnalu, ps.spss[i], verbose)
173-
}
174-
for i := range ps.ppss {
175-
printPS(w, fmt.Sprintf("PID %d, PPS", pid), i, ps.ppsnalus[i], ps.ppss[i], verbose)
176-
}
177-
}
178-
fmt.Fprintln(w, outText)
179-
return ps, nil
137+
return jp.error()
180138
}

cmd/ts-info/app/parser_test.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package app_test
33
import (
44
"bytes"
55
"context"
6+
"flag"
67
"os"
78
"strings"
89
"testing"
@@ -11,6 +12,10 @@ import (
1112
"github.com/stretchr/testify/require"
1213
)
1314

15+
var (
16+
update = flag.Bool("update", false, "update the golden files of this test")
17+
)
18+
1419
func TestParseFile(t *testing.T) {
1520
cases := []struct {
1621
name string
@@ -20,6 +25,7 @@ func TestParseFile(t *testing.T) {
2025
}{
2126
{"avc_with_time", "../testdata/avc_with_time.ts", app.Options{ParameterSets: true}, "testdata/golden_avc_with_time.txt"},
2227
{"bbb_1s", "testdata/bbb_1s.ts", app.Options{MaxNrPictures: 15}, "testdata/golden_bbb_1s.txt"},
28+
{"bbb_1s_indented", "testdata/bbb_1s.ts", app.Options{MaxNrPictures: 2, Indent: true}, "testdata/golden_bbb_1s_indented.txt"},
2329
}
2430
for _, c := range cases {
2531
t.Run(c.name, func(t *testing.T) {
@@ -29,8 +35,7 @@ func TestParseFile(t *testing.T) {
2935
require.NoError(t, err)
3036
err = app.Parse(ctx, &buf, f, c.options)
3137
require.NoError(t, err)
32-
expected_output := getExpectedOutput(t, c.expected_output_file)
33-
require.Equal(t, expected_output, buf.String(), "should produce expected output")
38+
compareUpdateGolden(t, buf.String(), c.expected_output_file, *update)
3439
})
3540
}
3641
}
@@ -42,3 +47,20 @@ func getExpectedOutput(t *testing.T, file string) string {
4247
expected_output_str := strings.ReplaceAll(string(expected_output), "\r\n", "\n")
4348
return expected_output_str
4449
}
50+
51+
func compareUpdateGolden(t *testing.T, actual string, goldenFile string, update bool) {
52+
t.Helper()
53+
if update {
54+
err := os.WriteFile(goldenFile, []byte(actual), 0644)
55+
require.NoError(t, err)
56+
} else {
57+
expected := getExpectedOutput(t, goldenFile)
58+
require.Equal(t, expected, actual, "should produce expected output")
59+
}
60+
}
61+
62+
// TestMain is to set flags for tests. In particular, the update flag to update golden files.
63+
func TestMain(m *testing.M) {
64+
flag.Parse()
65+
os.Exit(m.Run())
66+
}
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,4 @@
1-
H264 video detected on PID: 512
2-
PID 512, SPS 0 len 36B: 27640020ac2b402802dd80880000030008000003032742001458000510edef7c1da1c32a
3-
{
4-
"Profile": 100,
5-
"ProfileCompatibility": 0,
6-
"Level": 32,
7-
"ParameterID": 0,
8-
"ChromaFormatIDC": 1,
9-
"SeparateColourPlaneFlag": false,
10-
"BitDepthLumaMinus8": 0,
11-
"BitDepthChromaMinus8": 0,
12-
"QPPrimeYZeroTransformBypassFlag": false,
13-
"SeqScalingMatrixPresentFlag": false,
14-
"SeqScalingLists": null,
15-
"Log2MaxFrameNumMinus4": 4,
16-
"PicOrderCntType": 2,
17-
"Log2MaxPicOrderCntLsbMinus4": 0,
18-
"DeltaPicOrderAlwaysZeroFlag": false,
19-
"OffsetForNonRefPic": 0,
20-
"OffsetForTopToBottomField": 0,
21-
"RefFramesInPicOrderCntCycle": null,
22-
"NumRefFrames": 1,
23-
"GapsInFrameNumValueAllowedFlag": false,
24-
"FrameMbsOnlyFlag": true,
25-
"MbAdaptiveFrameFieldFlag": false,
26-
"Direct8x8InferenceFlag": true,
27-
"FrameCroppingFlag": false,
28-
"FrameCropLeftOffset": 0,
29-
"FrameCropRightOffset": 0,
30-
"FrameCropTopOffset": 0,
31-
"FrameCropBottomOffset": 0,
32-
"Width": 1280,
33-
"Height": 720,
34-
"NrBytesBeforeVUI": 10,
35-
"NrBytesRead": 36,
36-
"VUI": {
37-
"SampleAspectRatioWidth": 1,
38-
"SampleAspectRatioHeight": 1,
39-
"OverscanInfoPresentFlag": false,
40-
"OverscanAppropriateFlag": false,
41-
"VideoSignalTypePresentFlag": false,
42-
"VideoFormat": 0,
43-
"VideoFullRangeFlag": false,
44-
"ColourDescriptionFlag": false,
45-
"ColourPrimaries": 0,
46-
"TransferCharacteristics": 0,
47-
"MatrixCoefficients": 0,
48-
"ChromaLocInfoPresentFlag": false,
49-
"ChromaSampleLocTypeTopField": 0,
50-
"ChromaSampleLocTypeBottomField": 0,
51-
"TimingInfoPresentFlag": true,
52-
"NumUnitsInTick": 1,
53-
"TimeScale": 100,
54-
"FixedFrameRateFlag": true,
55-
"NalHrdParametersPresentFlag": true,
56-
"NalHrdParameters": {
57-
"CpbCountMinus1": 0,
58-
"BitRateScale": 4,
59-
"CpbSizeScale": 2,
60-
"CpbEntries": [
61-
{
62-
"BitRateValueMinus1": 2603,
63-
"CpbSizeValueMinus1": 20749,
64-
"CbrFlag": true
65-
}
66-
],
67-
"InitialCpbRemovalDelayLengthMinus1": 23,
68-
"CpbRemovalDelayLengthMinus1": 23,
69-
"DpbOutputDelayLengthMinus1": 23,
70-
"TimeOffsetLength": 24
71-
},
72-
"VclHrdParametersPresentFlag": false,
73-
"VclHrdParameters": null,
74-
"LowDelayHrdFlag": false,
75-
"PicStructPresentFlag": true,
76-
"BitstreamRestrictionFlag": true,
77-
"MotionVectorsOverPicBoundariesFlag": true,
78-
"MaxBytesPerPicDenom": 2,
79-
"MaxBitsPerMbDenom": 1,
80-
"Log2MaxMvLengthHorizontal": 13,
81-
"Log2MaxMvLengthVertical": 11,
82-
"MaxNumReorderFrames": 0,
83-
"MaxDecFrameBuffering": 1
84-
}
85-
}
86-
PID 512, PPS 0 len 4B: 28ee3cb0
87-
{
88-
"PicParameterSetID": 0,
89-
"SeqParameterSetID": 0,
90-
"EntropyCodingModeFlag": true,
91-
"BottomFieldPicOrderInFramePresentFlag": false,
92-
"NumSliceGroupsMinus1": 0,
93-
"SliceGroupMapType": 0,
94-
"RunLengthMinus1": null,
95-
"TopLeft": null,
96-
"BottomRight": null,
97-
"SliceGroupChangeDirectionFlag": false,
98-
"SliceGroupChangeRateMinus1": 0,
99-
"PicSizeInMapUnitsMinus1": 0,
100-
"SliceGroupID": null,
101-
"NumRefIdxI0DefaultActiveMinus1": 0,
102-
"NumRefIdxI1DefaultActiveMinus1": 0,
103-
"WeightedPredFlag": false,
104-
"WeightedBipredIDC": 0,
105-
"PicInitQpMinus26": 0,
106-
"PicInitQsMinus26": 0,
107-
"ChromaQpIndexOffset": 0,
108-
"DeblockingFilterControlPresentFlag": true,
109-
"ConstrainedIntraPredFlag": false,
110-
"RedundantPicCntPresentFlag": false,
111-
"Transform8x8ModeFlag": true,
112-
"PicScalingMatrixPresentFlag": false,
113-
"PicScalingLists": null,
114-
"SecondChromaQpIndexOffset": 0
115-
}
116-
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]
1+
{"pid":512,"codec":"AVC","type":"video"}
2+
{"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}}}
3+
{"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}}
4+
{"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}]}

0 commit comments

Comments
 (0)