├── .github └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── mp2ts-info │ └── main.go ├── mp2ts-nallister │ └── main.go ├── mp2ts-pidfilter │ └── main.go └── mp2ts-pslister │ └── main.go ├── go.mod ├── go.sum └── internal ├── avc.go ├── const.go ├── hevc.go ├── nalu.go ├── parameter_set.go ├── parser.go ├── parser_test.go ├── printer.go ├── scte35.go ├── sdt.go ├── smpte2038.go ├── statistics.go ├── step_calculation_test.go ├── testdata ├── 80s_with_ad.ts ├── avc_with_time.ts ├── bbb_1s.ts ├── golden_avc.txt ├── golden_avc_with_scte35.txt ├── golden_avc_with_service.txt ├── golden_avc_without_ps.txt ├── golden_bbb_1s.txt ├── golden_bbb_1s_indented.txt ├── golden_bbb_1s_no_nalu(no_sei).txt ├── golden_obs_hevc_aac.txt ├── golden_obs_hevc_aac_indented.txt ├── golden_obs_hevc_aac_no_nalu(no_sei).txt └── obs_hevc_aac.ts ├── utils.go └── version.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | platform: [macos-latest, ubuntu-latest, windows-latest] 16 | go-version: ["1.19"] 17 | runs-on: ${{ matrix.platform }} 18 | steps: 19 | - name: Setup Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Download Go dependencies 28 | run: go mod download 29 | env: 30 | GOPROXY: "https://proxy.golang.org" 31 | 32 | - name: Build 33 | run: go build -v ./... 34 | 35 | - name: Test 36 | run: go test -v ./... 37 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | workflow_dispatch: 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v3 19 | with: 20 | args: --skip-files=xml 21 | only-new-issues: true 22 | 23 | - name: go-report-card 24 | uses: creekorful/goreportcard-action@v1.0 25 | with: 26 | only-new-issues: true 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the make coverage target 12 | coverage.* 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # Configuration from VSCode 18 | *.DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD024": { 4 | "siblings_only": true 5 | }, 6 | "MD041": false, 7 | "MD046": false 8 | }, 9 | "cSpell.words": [ 10 | "asticode", 11 | "astits", 12 | "AVCPES", 13 | "Demuxer", 14 | "Eyevinn", 15 | "golangci", 16 | "NALU", 17 | "nalus", 18 | "PPSNAL", 19 | "ppsnalus", 20 | "spsnalu", 21 | "spss", 22 | "stretchr" 23 | ] 24 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - SMPTE-2038 data option to mp2ts-tools 13 | 14 | ## [0.2.1] - 2024-01-25 15 | 16 | ### Fixed 17 | 18 | - Updated Makefile to new project names 19 | 20 | ## [0.2.0] - 2024-01-23 21 | 22 | ### Added 23 | 24 | - Print image type (I, P, B) for AVC streams 25 | - Calculate GoP duration based on RAI-marker or IDR distance 26 | - Calculate frame-rate based on DTS/PTS and print out in JSON format 27 | - Enable NALU/SEI printing by option 28 | - Print SDT in JSON format 29 | - Support for HEVC PicTiming SEI message 30 | - SEI message data is now also printed as JSON 31 | 32 | ## Changed 33 | 34 | - mp2ts-info and mp2ts-pslister now always print indented output 35 | - mp2ts-nallister -sei option now turns on details. 36 | 37 | ## [0.1.0] - 2024-01-15 38 | 39 | ### Added 40 | 41 | - initial version of the repo 42 | - ts-info tool 43 | 44 | [Unreleased]: https://github.com/Eyevinn/mp2ts-tools/releases/tag/v0.2.1...HEAD 45 | [0.2.1]: https://github.com/Eyevinn/mp2ts-tools/releases/tag/v0.2.0...v0.2.1 46 | [0.2.0]: https://github.com/Eyevinn/mp2ts-tools/releases/tag/v0.1.0...v0.2.0 47 | [0.1.0]: https://github.com/Eyevinn/mp2ts-tools/releases/tag/v0.1.0 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Eyevinn Technology AB. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: test check coverage build 3 | 4 | .PHONY: build 5 | build: mp2ts-info mp2ts-nallister mp2ts-pslister 6 | 7 | .PHONY: prepare 8 | prepare: 9 | go mod tidy 10 | 11 | mp2ts-info mp2ts-nallister mp2ts-pslister: 12 | go build -ldflags "-X github.com/Eyevinn/mp2ts-tools/internal.commitVersion=$$(git describe --tags HEAD) -X github.com/Eyevinn/mp2ts-tools/internal.commitDate=$$(git log -1 --format=%ct)" -o out/$@ ./cmd/$@/main.go 13 | 14 | .PHONY: test 15 | test: prepare 16 | go test ./... 17 | 18 | .PHONY: coverage 19 | coverage: 20 | # Ignore (allow) packages without any tests 21 | go test ./... -coverprofile coverage.out 22 | go tool cover -html=coverage.out -o coverage.html 23 | go tool cover -func coverage.out -o coverage.txt 24 | tail -1 coverage.txt 25 | 26 | .PHONY: check 27 | check: prepare 28 | golangci-lint run 29 | 30 | .PHONY: update 31 | update: 32 | go get -t -u ./... 33 | 34 | clean: 35 | rm -f out/* 36 | 37 | install: all 38 | cp out/* $(GOPATH)/bin/ 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Test](https://github.com/Eyevinn/mp2ts-tools/workflows/Go/badge.svg) 2 | [![golangci-lint](https://github.com/Eyevinn/mp2ts-tools/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/Eyevinn/mp2ts-tools/actions/workflows/golangci-lint.yml) 3 | [![GoDoc](https://godoc.org/github.com/Eyevinn/mp2ts-tools?status.svg)](http://godoc.org/github.com/Eyevinn/mp2ts-tools) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/Eyevinn/mp2ts-tools)](https://goreportcard.com/report/github.com/Eyevinn/mp2ts-tools) 5 | [![license](https://img.shields.io/github/license/Eyevinn/mp2ts-tools.svg)](https://github.com/Eyevinn/mp2ts-tools/blob/master/LICENSE) 6 | 7 | # mp2ts-tools - A collection of tools for MPEG-2 TS 8 | 9 | MPEG-2 Transport Stream is a very wide spread format for transporting media. 10 | 11 | This repo provides some tools to facilitate analysis and extraction of 12 | data from MPEG-2 TS streams. 13 | 14 | ## mp2ts-info 15 | 16 | `mp2ts-info` is a tool that parses a TS file, or a stream on stdin, and prints 17 | information about the video streams in JSON format. 18 | 19 | ## mp2ts-pslister 20 | 21 | `mp2ts-pslister` is a tool that shows information about SPS, PPS (and VPS for HEVC) in a TS file. 22 | 23 | ## mp2ts-nallister 24 | 25 | `mp2ts-nallister` is a tool that shows information about PTS/DTS, PicTiming SEI, and NAL units. 26 | 27 | ## How to run 28 | 29 | You can download and install any tool directly using 30 | 31 | ```sh 32 | > go install github.com/Eyevinn/mp2ts-tools/cmd/mp2ts-info@latest 33 | ``` 34 | 35 | If you have the source code you should be able to run a tool like 36 | 37 | ```sh 38 | > cd cmd/mp2ts-info 39 | > go mod tidy 40 | > go run . h 41 | ``` 42 | 43 | Alternatively, you can use the Makefile to build the tools 44 | or make a coverage check. The Makefile will set the version depending 45 | on the Git commit used. 46 | 47 | ## Commits and ChangeLog 48 | 49 | This project aims to follow Semantic Versioning and 50 | [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 51 | There is a manual [ChangeLog](CHANGELOG.md). 52 | 53 | ## License 54 | 55 | MIT, see [LICENSE](LICENSE). 56 | 57 | ## Support 58 | 59 | Join our [community on Slack](http://slack.streamingtech.se) where you can post any questions regarding any of our open source projects. Eyevinn's consulting business can also offer you: 60 | 61 | * Further development of this component 62 | * Customization and integration of this component into your platform 63 | * Support and maintenance agreement 64 | 65 | Contact [sales@eyevinn.se](mailto:sales@eyevinn.se) if you are interested. 66 | 67 | ## About Eyevinn Technology 68 | 69 | [Eyevinn Technology](https://www.eyevinntechnology.se) is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor. As our way to innovate and push the industry forward we develop proof-of-concepts and tools. The things we learn and the code we write we share with the industry in [blogs](https://dev.to/video) and by open sourcing the code we have written. 70 | 71 | Want to know more about Eyevinn and how it is to work here. Contact us at ! 72 | -------------------------------------------------------------------------------- /cmd/mp2ts-info/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/Eyevinn/mp2ts-tools/internal" 13 | ) 14 | 15 | var usg = `Usage of %s: 16 | 17 | %s lists information about TS files, e.g. pids, bitrates, service, etc 18 | ` 19 | 20 | func parseOptions() internal.Options { 21 | opts := internal.Options{ShowStreamInfo: true, Indent: true} 22 | flag.BoolVar(&opts.ShowService, "service", false, "show service information") 23 | flag.BoolVar(&opts.ShowSCTE35, "scte35", true, "show SCTE35 information") 24 | flag.BoolVar(&opts.Indent, "indent", true, "indent JSON output") 25 | flag.BoolVar(&opts.Version, "version", false, "print version") 26 | 27 | flag.Usage = func() { 28 | parts := strings.Split(os.Args[0], "/") 29 | name := parts[len(parts)-1] 30 | fmt.Fprintf(os.Stderr, usg, name, name) 31 | fmt.Fprintf(os.Stderr, "\nRun as: %s [options] file.ts (- for stdin) with options:\n\n", name) 32 | flag.PrintDefaults() 33 | } 34 | 35 | flag.Parse() 36 | return opts 37 | } 38 | 39 | func parse(ctx context.Context, w io.Writer, f io.Reader, o internal.Options) error { 40 | // Parse either general information, or scte35 (by default) 41 | if o.ShowService { 42 | err := internal.ParseInfo(ctx, w, f, o) 43 | if err != nil { 44 | return err 45 | } 46 | } else if o.ShowSCTE35 { 47 | err := internal.ParseSCTE35(ctx, w, f, o) 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func main() { 57 | o, inFile := internal.ParseParams(parseOptions) 58 | err := internal.Execute(os.Stdout, o, inFile, parse) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cmd/mp2ts-nallister/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/Eyevinn/mp2ts-tools/internal" 13 | ) 14 | 15 | var usg = `Usage of %s: 16 | 17 | %s generates a list of AVC/HEVC nalus with information about timestamps, rai, SEI etc. 18 | It can further be used to generate a list of SMPTE-2038 data. 19 | ` 20 | 21 | func parseOptions() internal.Options { 22 | opts := internal.Options{ShowStreamInfo: true, ShowService: false, ShowPS: false, ShowNALU: true, ShowSEIDetails: false, ShowStatistics: true} 23 | flag.IntVar(&opts.MaxNrPictures, "max", 0, "max nr pictures to parse") 24 | flag.BoolVar(&opts.ShowSEIDetails, "sei", false, "print detailed sei message information") 25 | flag.BoolVar(&opts.ShowSMPTE2038, "smpte2038", false, "print details about SMPTE-2038 data") 26 | flag.BoolVar(&opts.Indent, "indent", false, "indent JSON output") 27 | flag.BoolVar(&opts.Version, "version", false, "print version") 28 | 29 | flag.Usage = func() { 30 | parts := strings.Split(os.Args[0], "/") 31 | name := parts[len(parts)-1] 32 | fmt.Fprintf(os.Stderr, usg, name, name) 33 | fmt.Fprintf(os.Stderr, "\nRun as: %s [options] file.ts (- for stdin) with options:\n\n", name) 34 | flag.PrintDefaults() 35 | } 36 | 37 | flag.Parse() 38 | return opts 39 | } 40 | 41 | func parseNALUInfo(ctx context.Context, w io.Writer, f io.Reader, o internal.Options) error { 42 | return internal.ParseAll(ctx, w, f, o) 43 | } 44 | 45 | func main() { 46 | o, inFile := internal.ParseParams(parseOptions) 47 | err := internal.Execute(os.Stdout, o, inFile, parseNALUInfo) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cmd/mp2ts-pidfilter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/Eyevinn/mp2ts-tools/internal" 13 | ) 14 | 15 | var usg = `Usage of %s: 16 | 17 | %s filters out some chosen pids from the ts packet. 18 | Drop nothing and list all PIDs if empty pids list is specified (by default). 19 | However, PAT(0) and PMT must not be dropped. 20 | ` 21 | 22 | func parseOptions() internal.Options { 23 | opts := internal.Options{ShowStreamInfo: true, Indent: true, FilterPids: true} 24 | flag.StringVar(&opts.PidsToDrop, "drop", "", "pids to drop in the PMT (split by space), e.g. \"256 257\"") 25 | flag.StringVar(&opts.OutPutTo, "output", "", "save the TS packets into the given file (filepath) or stdout (-)") 26 | flag.BoolVar(&opts.Indent, "indent", true, "indent JSON output") 27 | flag.BoolVar(&opts.Version, "version", false, "print version") 28 | 29 | flag.Usage = func() { 30 | parts := strings.Split(os.Args[0], "/") 31 | name := parts[len(parts)-1] 32 | fmt.Fprintf(os.Stderr, usg, name, name) 33 | fmt.Fprintf(os.Stderr, "\nRun as: %s [options] file.ts (- for stdin) with options:\n\n", name) 34 | flag.PrintDefaults() 35 | } 36 | 37 | flag.Parse() 38 | return opts 39 | } 40 | 41 | func filter(ctx context.Context, w io.Writer, f io.Reader, o internal.Options) error { 42 | outPutToFile := o.OutPutTo != "-" 43 | var textOutput io.Writer 44 | var tsOutput io.Writer 45 | // If we output to ts files, print analysis to stdout 46 | if outPutToFile { 47 | // Remove existing output file 48 | if err := internal.RemoveFileIfExists(o.OutPutTo); err != nil { 49 | return err 50 | } 51 | file, err := internal.OpenFileAndAppend(o.OutPutTo) 52 | if err != nil { 53 | return err 54 | } 55 | tsOutput = file 56 | textOutput = w 57 | defer file.Close() 58 | } else { // If we output to stdout, print analysis to stderr 59 | tsOutput = w 60 | textOutput = os.Stderr 61 | } 62 | 63 | return internal.FilterPids(ctx, textOutput, tsOutput, f, o) 64 | } 65 | 66 | func main() { 67 | o, inFile := internal.ParseParams(parseOptions) 68 | err := internal.Execute(os.Stdout, o, inFile, filter) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cmd/mp2ts-pslister/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/Eyevinn/mp2ts-tools/internal" 13 | ) 14 | 15 | var usg = `Usage of %s: 16 | 17 | %s lists parameter sets in TS files 18 | ` 19 | 20 | func parseOptions() internal.Options { 21 | opts := internal.Options{ShowStreamInfo: true, ShowService: false, ShowPS: true, Indent: true, ShowNALU: false, ShowSEIDetails: false, ShowStatistics: false} 22 | flag.IntVar(&opts.MaxNrPictures, "max", 0, "max nr pictures to parse") 23 | flag.BoolVar(&opts.VerbosePSInfo, "ps", false, "show verbose information") 24 | flag.BoolVar(&opts.Version, "version", false, "print version") 25 | 26 | flag.Usage = func() { 27 | parts := strings.Split(os.Args[0], "/") 28 | name := parts[len(parts)-1] 29 | fmt.Fprintf(os.Stderr, usg, name, name) 30 | fmt.Fprintf(os.Stderr, "\nRun as: %s [options] file.ts (- for stdin) with options:\n\n", name) 31 | flag.PrintDefaults() 32 | } 33 | 34 | flag.Parse() 35 | return opts 36 | } 37 | 38 | func parsePSInfo(ctx context.Context, w io.Writer, f io.Reader, o internal.Options) error { 39 | return internal.ParseAll(ctx, w, f, o) 40 | } 41 | 42 | func main() { 43 | o, inFile := internal.ParseParams(parseOptions) 44 | err := internal.Execute(os.Stdout, o, inFile, parsePSInfo) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Eyevinn/mp2ts-tools 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Comcast/gots/v2 v2.2.1 7 | github.com/Eyevinn/mp4ff v0.42.1-0.20240221161741-c9bb3c122204 8 | github.com/asticode/go-astits v1.13.0 9 | github.com/stretchr/testify v1.8.4 10 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a 11 | ) 12 | 13 | require ( 14 | github.com/asticode/go-astikit v0.42.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-test/deep v1.1.0 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Comcast/gots/v2 v2.2.1 h1:LU/SRg7p2KQqVkNqInV7I4MOQKAqvWQP/PSSLtygP2s= 2 | github.com/Comcast/gots/v2 v2.2.1/go.mod h1:firJ11on3eUiGHAhbY5cZNqG0OqhQ1+nSZHfsEEzVVU= 3 | github.com/Eyevinn/mp4ff v0.42.1-0.20240221161741-c9bb3c122204 h1:hxaiwOKm+ooV4lH/UIfgBzi8/CGj3pRIYXnVU3uQQpc= 4 | github.com/Eyevinn/mp4ff v0.42.1-0.20240221161741-c9bb3c122204/go.mod h1:w/6GSa5ghZ1VavzJK6McQ2/flx8mKtcrKDr11SsEweA= 5 | github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= 6 | github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w= 7 | github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= 8 | github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= 9 | github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 14 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 15 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 16 | github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 21 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 22 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 23 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= 24 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | -------------------------------------------------------------------------------- /internal/avc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Eyevinn/mp4ff/avc" 7 | "github.com/Eyevinn/mp4ff/sei" 8 | "github.com/asticode/go-astits" 9 | ) 10 | 11 | type AvcPS struct { 12 | spss map[uint32]*avc.SPS 13 | ppss map[uint32]*avc.PPS 14 | spsnalu []byte 15 | ppsnalus map[uint32][]byte 16 | Statistics StreamStatistics 17 | } 18 | 19 | func (a *AvcPS) getSPS() *avc.SPS { 20 | if len(a.spss) == 0 { 21 | return nil 22 | } 23 | for _, sps := range a.spss { 24 | return sps 25 | } 26 | // Not reachable 27 | return nil 28 | } 29 | 30 | func (a *AvcPS) setSPS(nalu []byte) error { 31 | if a.spss == nil { 32 | a.spss = make(map[uint32]*avc.SPS, 1) 33 | a.ppss = make(map[uint32]*avc.PPS, 1) 34 | a.ppsnalus = make(map[uint32][]byte) 35 | } 36 | sps, err := avc.ParseSPSNALUnit(nalu, true) 37 | if err != nil { 38 | return err 39 | } 40 | a.spsnalu = nalu 41 | a.spss[sps.ParameterID] = sps 42 | if len(a.spss) > 1 { 43 | return fmt.Errorf("more than one SPS") 44 | } 45 | return nil 46 | } 47 | 48 | func (a *AvcPS) setPPS(nalu []byte) error { 49 | pps, err := avc.ParsePPSNALUnit(nalu, a.spss) 50 | if err != nil { 51 | return err 52 | } 53 | a.ppss[pps.PicParameterSetID] = pps 54 | a.ppsnalus[pps.PicParameterSetID] = nalu 55 | return nil 56 | } 57 | 58 | func ParseAVCPES(jp *JsonPrinter, d *astits.DemuxerData, ps *AvcPS, o Options) (*AvcPS, error) { 59 | pid := d.PID 60 | pes := d.PES 61 | fp := d.FirstPacket 62 | if pes.Header.OptionalHeader.PTS == nil { 63 | return nil, fmt.Errorf("no PTS in PES") 64 | } 65 | nfd := NaluFrameData{ 66 | PID: pid, 67 | } 68 | if ps == nil { 69 | // return empty PS to count picture numbers correctly 70 | // even if we are not printing NALUs 71 | ps = &AvcPS{} 72 | } 73 | pts := *pes.Header.OptionalHeader.PTS 74 | nfd.PTS = pts.Base 75 | ps.Statistics.Type = "AVC" 76 | ps.Statistics.Pid = pid 77 | if fp != nil && fp.AdaptationField != nil { 78 | nfd.RAI = fp.AdaptationField.RandomAccessIndicator 79 | if nfd.RAI { 80 | ps.Statistics.RAIPTS = append(ps.Statistics.IDRPTS, pts.Base) 81 | } 82 | } 83 | 84 | dts := pes.Header.OptionalHeader.DTS 85 | if dts != nil { 86 | nfd.DTS = dts.Base 87 | } else { 88 | // Use PTS as DTS in statistics if DTS is not present 89 | nfd.DTS = pts.Base 90 | } 91 | ps.Statistics.TimeStamps = append(ps.Statistics.TimeStamps, nfd.DTS) 92 | 93 | data := pes.Data 94 | nalus := avc.ExtractNalusFromByteStream(data) 95 | firstPS := false 96 | for _, nalu := range nalus { 97 | var data any 98 | naluType := avc.GetNaluType(nalu[0]) 99 | switch naluType { 100 | case avc.NALU_SPS: 101 | if !firstPS { 102 | err := ps.setSPS(nalu) 103 | if err != nil { 104 | return nil, fmt.Errorf("cannot set SPS") 105 | } 106 | firstPS = true 107 | } 108 | case avc.NALU_PPS: 109 | if firstPS { 110 | err := ps.setPPS(nalu) 111 | if err != nil { 112 | return nil, fmt.Errorf("cannot set PPS") 113 | } 114 | } 115 | case avc.NALU_SEI: 116 | sps := ps.getSPS() 117 | msgs, err := avc.ParseSEINalu(nalu, sps) 118 | if err != nil { 119 | return nil, err 120 | } 121 | parts := make([]SeiOut, 0, len(msgs)) 122 | for _, msg := range msgs { 123 | t := sei.SEIType(msg.Type()) 124 | if t == sei.SEIPicTimingType { 125 | pt := msg.(*sei.PicTimingAvcSEI) 126 | if o.ShowSEIDetails && sps != nil { 127 | parts = append(parts, SeiOut{ 128 | Msg: t.String(), 129 | Payload: pt, 130 | }) 131 | } else { 132 | parts = append(parts, SeiOut{Msg: t.String()}) 133 | } 134 | } else { 135 | if o.ShowSEIDetails { 136 | parts = append(parts, SeiOut{Msg: t.String(), Payload: msg}) 137 | } else { 138 | parts = append(parts, SeiOut{Msg: t.String()}) 139 | } 140 | } 141 | } 142 | data = parts 143 | case avc.NALU_IDR, avc.NALU_NON_IDR: 144 | if naluType == avc.NALU_IDR { 145 | ps.Statistics.IDRPTS = append(ps.Statistics.IDRPTS, pts.Base) 146 | } 147 | sliceType, err := avc.GetSliceTypeFromNALU(nalu) 148 | if err == nil { 149 | nfd.ImgType = fmt.Sprintf("[%s]", sliceType) 150 | } 151 | } 152 | nfd.NALUS = append(nfd.NALUS, NaluData{ 153 | Type: naluType.String(), 154 | Len: len(nalu), 155 | Data: data, 156 | }) 157 | } 158 | 159 | if jp == nil { 160 | return ps, nil 161 | } 162 | if firstPS { 163 | for nr := range ps.spss { 164 | jp.PrintPS(pid, "SPS", nr, ps.spsnalu, ps.spss[nr], o.VerbosePSInfo, o.ShowPS) 165 | } 166 | for nr := range ps.ppss { 167 | jp.PrintPS(pid, "PPS", nr, ps.ppsnalus[nr], ps.ppss[nr], o.VerbosePSInfo, o.ShowPS) 168 | } 169 | } 170 | 171 | jp.Print(nfd, o.ShowNALU) 172 | return ps, jp.Error() 173 | } 174 | -------------------------------------------------------------------------------- /internal/const.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | const ( 4 | PacketSize = 188 5 | PtsWrap = 1 << 33 6 | PcrWrap = PtsWrap * 300 7 | TimeScale = 90000 8 | ) 9 | 10 | func SignedPTSDiff(p2, p1 int64) int64 { 11 | return (p2-p1+3*PtsWrap/2)%PtsWrap - PtsWrap/2 12 | } 13 | 14 | func UnsignedPTSDiff(p2, p1 int64) int64 { 15 | return (p2 - p1 + 2*PtsWrap) % PtsWrap 16 | } 17 | 18 | func AddPTS(p1, p2 int64) int64 { 19 | return (p1 + p2) % PtsWrap 20 | } 21 | -------------------------------------------------------------------------------- /internal/hevc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Eyevinn/mp4ff/avc" 7 | "github.com/Eyevinn/mp4ff/hevc" 8 | "github.com/Eyevinn/mp4ff/sei" 9 | "github.com/asticode/go-astits" 10 | ) 11 | 12 | type HevcPS struct { 13 | spss map[uint32]*hevc.SPS 14 | ppss map[uint32]*hevc.PPS 15 | vpsnalu []byte 16 | spsnalu []byte 17 | ppsnalus map[uint32][]byte 18 | Statistics StreamStatistics 19 | } 20 | 21 | func (a *HevcPS) setSPS(nalu []byte) error { 22 | if a.spss == nil { 23 | a.spss = make(map[uint32]*hevc.SPS, 1) 24 | a.ppss = make(map[uint32]*hevc.PPS, 1) 25 | a.ppsnalus = make(map[uint32][]byte, 1) 26 | } 27 | sps, err := hevc.ParseSPSNALUnit(nalu) 28 | if err != nil { 29 | return err 30 | } 31 | a.spsnalu = nalu 32 | a.spss[uint32(sps.SpsID)] = sps 33 | if len(a.spss) > 1 { 34 | return fmt.Errorf("more than one SPS") 35 | } 36 | return nil 37 | } 38 | 39 | func (a *HevcPS) setPPS(nalu []byte) error { 40 | pps, err := hevc.ParsePPSNALUnit(nalu, a.spss) 41 | if err != nil { 42 | return err 43 | } 44 | a.ppss[pps.PicParameterSetID] = pps 45 | a.ppsnalus[pps.PicParameterSetID] = nalu 46 | return nil 47 | } 48 | 49 | func ParseHEVCPES(jp *JsonPrinter, d *astits.DemuxerData, ps *HevcPS, o Options) (*HevcPS, error) { 50 | pid := d.PID 51 | pes := d.PES 52 | fp := d.FirstPacket 53 | if pes.Header.OptionalHeader.PTS == nil { 54 | return nil, fmt.Errorf("no PTS in PES") 55 | } 56 | nfd := NaluFrameData{ 57 | PID: pid, 58 | } 59 | if ps == nil { 60 | // return empty PS to count picture numbers correctly 61 | // even if we are not printing NALUs 62 | ps = &HevcPS{} 63 | } 64 | pts := *pes.Header.OptionalHeader.PTS 65 | nfd.PTS = pts.Base 66 | ps.Statistics.Type = "HEVC" 67 | ps.Statistics.Pid = pid 68 | if fp != nil && fp.AdaptationField != nil { 69 | nfd.RAI = fp.AdaptationField.RandomAccessIndicator 70 | if nfd.RAI { 71 | ps.Statistics.RAIPTS = append(ps.Statistics.IDRPTS, pts.Base) 72 | } 73 | } 74 | 75 | dts := pes.Header.OptionalHeader.DTS 76 | if dts != nil { 77 | nfd.DTS = dts.Base 78 | } else { 79 | // Use PTS as DTS in statistics if DTS is not present 80 | nfd.DTS = pts.Base 81 | } 82 | ps.Statistics.TimeStamps = append(ps.Statistics.TimeStamps, nfd.DTS) 83 | 84 | data := pes.Data 85 | firstPS := false 86 | for _, nalu := range avc.ExtractNalusFromByteStream(data) { 87 | naluType := hevc.GetNaluType(nalu[0]) 88 | switch naluType { 89 | case hevc.NALU_VPS: 90 | ps.vpsnalu = nalu 91 | firstPS = true 92 | case hevc.NALU_SPS: 93 | if firstPS { 94 | err := ps.setSPS(nalu) 95 | if err != nil { 96 | return nil, fmt.Errorf("cannot set SPS") 97 | } 98 | firstPS = true 99 | } 100 | case hevc.NALU_PPS: 101 | if firstPS { 102 | err := ps.setPPS(nalu) 103 | if err != nil { 104 | return nil, fmt.Errorf("cannot set PPS") 105 | } 106 | } 107 | case hevc.NALU_SEI_PREFIX, hevc.NALU_SEI_SUFFIX: 108 | 109 | var seiData any 110 | if o.ShowSEIDetails && len(ps.spss) > 0 { 111 | sps := ps.spss[0] 112 | seiMessages, err := hevc.ParseSEINalu(nalu, sps) 113 | if err != nil { 114 | return nil, fmt.Errorf("cannot parse SEI NALU") 115 | } 116 | parts := make([]SeiOut, 0, len(seiMessages)) 117 | for _, seiMsg := range seiMessages { 118 | var payload any 119 | switch seiMsg.Type() { 120 | case sei.SEIPicTimingType: 121 | payload = seiMsg.(*sei.PicTimingHevcSEI) 122 | } 123 | parts = append(parts, SeiOut{Msg: sei.SEIType(seiMsg.Type()).String(), Payload: payload}) 124 | } 125 | seiData = parts 126 | } else { 127 | seiData = nil // hex.EncodeToString(nalu) 128 | } 129 | nfd.NALUS = append(nfd.NALUS, NaluData{ 130 | Type: naluType.String(), 131 | Len: len(nalu), 132 | Data: seiData, 133 | }) 134 | continue 135 | case hevc.NALU_IDR_W_RADL, hevc.NALU_IDR_N_LP: 136 | ps.Statistics.IDRPTS = append(ps.Statistics.IDRPTS, pts.Base) 137 | } 138 | nfd.NALUS = append(nfd.NALUS, NaluData{ 139 | Type: naluType.String(), 140 | Len: len(nalu), 141 | Data: nil, 142 | }) 143 | } 144 | if jp == nil { 145 | return ps, nil 146 | } 147 | 148 | if firstPS { 149 | jp.PrintPS(pid, "VPS", 0, ps.vpsnalu, nil, o.VerbosePSInfo, o.ShowPS) 150 | for nr := range ps.spss { 151 | jp.PrintPS(pid, "SPS", nr, ps.spsnalu, ps.spss[nr], o.VerbosePSInfo, o.ShowPS) 152 | } 153 | for nr := range ps.ppss { 154 | jp.PrintPS(pid, "PPS", nr, ps.ppsnalus[nr], ps.ppss[nr], o.VerbosePSInfo, o.ShowPS) 155 | } 156 | } 157 | 158 | jp.Print(nfd, o.ShowNALU) 159 | return ps, jp.Error() 160 | } 161 | -------------------------------------------------------------------------------- /internal/nalu.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type NaluFrameData struct { 4 | PID uint16 `json:"pid"` 5 | RAI bool `json:"rai"` 6 | PTS int64 `json:"pts"` 7 | DTS int64 `json:"dts,omitempty"` 8 | ImgType string `json:"imgType,omitempty"` 9 | NALUS []NaluData `json:"nalus,omitempty"` 10 | } 11 | 12 | type NaluData struct { 13 | Type string `json:"type"` 14 | Len int `json:"len"` 15 | Data any `json:"data,omitempty"` 16 | } 17 | 18 | type SeiOut struct { 19 | Msg string `json:"msg"` 20 | Payload any `json:"payload,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /internal/parameter_set.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "encoding/hex" 4 | 5 | type ElementaryStreamInfo struct { 6 | PID uint16 `json:"pid"` 7 | Codec string `json:"codec"` 8 | Type string `json:"type"` 9 | } 10 | 11 | type PsInfo struct { 12 | PID uint16 `json:"pid"` 13 | ParameterSet string `json:"parameterSet"` 14 | Nr uint32 `json:"nr"` 15 | Hex string `json:"hex"` 16 | Length int `json:"length"` 17 | Details any `json:"details,omitempty"` 18 | } 19 | 20 | func (jp *JsonPrinter) PrintPS(pid uint16, psKind string, nr uint32, ps []byte, details any, verbose bool, show bool) { 21 | hexStr := hex.EncodeToString(ps) 22 | length := len(hexStr) / 2 23 | psInfo := PsInfo{ 24 | PID: pid, 25 | ParameterSet: psKind, 26 | Nr: nr, 27 | Hex: hexStr, 28 | Length: length, 29 | } 30 | if verbose { 31 | psInfo.Details = details 32 | } 33 | jp.Print(psInfo, show) 34 | } 35 | -------------------------------------------------------------------------------- /internal/parser.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/Comcast/gots/v2/packet" 10 | "github.com/Comcast/gots/v2/psi" 11 | "github.com/Comcast/gots/v2/scte35" 12 | "github.com/asticode/go-astits" 13 | slices "golang.org/x/exp/slices" 14 | ) 15 | 16 | func ParseAll(ctx context.Context, w io.Writer, f io.Reader, o Options) error { 17 | rd := bufio.NewReaderSize(f, 1000*PacketSize) 18 | dmx := astits.NewDemuxer(ctx, rd) 19 | pmtPID := -1 20 | nrPics := 0 21 | sdtPrinted := false 22 | esKinds := make(map[uint16]string) 23 | avcPSs := make(map[uint16]*AvcPS) 24 | hevcPSs := make(map[uint16]*HevcPS) 25 | jp := &JsonPrinter{W: w, Indent: o.Indent} 26 | statistics := make(map[uint16]*StreamStatistics) 27 | dataLoop: 28 | for { 29 | // Check if context was cancelled 30 | select { 31 | case <-ctx.Done(): 32 | break dataLoop 33 | default: 34 | } 35 | 36 | d, err := dmx.NextData() 37 | if err != nil { 38 | if err.Error() == "astits: no more packets" { 39 | break dataLoop 40 | } 41 | return fmt.Errorf("reading next data %w", err) 42 | } 43 | 44 | // Print service information 45 | if d.SDT != nil && !sdtPrinted { 46 | jp.PrintSdtInfo(d.SDT, o.ShowService) 47 | sdtPrinted = true 48 | } 49 | 50 | // Print PID information 51 | if pmtPID < 0 && d.PMT != nil { 52 | // Loop through elementary streams 53 | for _, es := range d.PMT.ElementaryStreams { 54 | streamInfo := ParseAstitsElementaryStreamInfo(es) 55 | if streamInfo != nil { 56 | esKinds[es.ElementaryPID] = streamInfo.Codec 57 | jp.Print(streamInfo, o.ShowStreamInfo) 58 | } 59 | } 60 | pmtPID = int(d.PID) 61 | } 62 | if pmtPID == -1 { 63 | continue 64 | } 65 | pes := d.PES 66 | if pes == nil { 67 | continue 68 | } 69 | 70 | switch esKinds[d.PID] { 71 | case "AVC": 72 | avcPS := avcPSs[d.PID] 73 | avcPS, err = ParseAVCPES(jp, d, avcPS, o) 74 | if err != nil { 75 | return err 76 | } 77 | if avcPS == nil { 78 | continue 79 | } 80 | if avcPSs[d.PID] == nil { 81 | avcPSs[d.PID] = avcPS 82 | } 83 | nrPics++ 84 | statistics[d.PID] = &avcPS.Statistics 85 | case "HEVC": 86 | hevcPS := hevcPSs[d.PID] 87 | hevcPS, err = ParseHEVCPES(jp, d, hevcPS, o) 88 | if err != nil { 89 | return err 90 | } 91 | if hevcPS == nil { 92 | continue 93 | } 94 | if hevcPSs[d.PID] == nil { 95 | hevcPSs[d.PID] = hevcPS 96 | } 97 | nrPics++ 98 | statistics[d.PID] = &hevcPS.Statistics 99 | case "SMPTE-2038": 100 | if o.ShowSMPTE2038 { 101 | ParseSMPTE2038(jp, d, o) 102 | } 103 | default: 104 | // Skip unknown elementary streams 105 | continue 106 | } 107 | 108 | // Keep looping if MaxNrPictures equals 0 109 | if o.MaxNrPictures > 0 && nrPics >= o.MaxNrPictures { 110 | break dataLoop 111 | } 112 | } 113 | 114 | for _, s := range statistics { 115 | jp.PrintStatistics(*s, o.ShowStatistics) 116 | } 117 | 118 | return jp.Error() 119 | } 120 | 121 | func ParseInfo(ctx context.Context, w io.Writer, f io.Reader, o Options) error { 122 | rd := bufio.NewReaderSize(f, 1000*PacketSize) 123 | dmx := astits.NewDemuxer(ctx, rd) 124 | pmtPID := -1 125 | jp := &JsonPrinter{W: w, Indent: o.Indent} 126 | dataLoop: 127 | for { 128 | // Check if context was cancelled 129 | select { 130 | case <-ctx.Done(): 131 | break dataLoop 132 | default: 133 | } 134 | 135 | d, err := dmx.NextData() 136 | if err != nil { 137 | if err.Error() == "astits: no more packets" { 138 | break dataLoop 139 | } 140 | return fmt.Errorf("reading next data %w", err) 141 | } 142 | 143 | // Print PID information 144 | if pmtPID < 0 && d.PMT != nil { 145 | // Loop through elementary streams 146 | for _, es := range d.PMT.ElementaryStreams { 147 | streamInfo := ParseAstitsElementaryStreamInfo(es) 148 | if streamInfo != nil { 149 | jp.Print(streamInfo, o.ShowStreamInfo) 150 | } 151 | } 152 | pmtPID = int(d.PID) 153 | } 154 | if pmtPID == -1 { 155 | continue 156 | } 157 | 158 | // Exit imediately if we don't want service information 159 | if !o.ShowService { 160 | break dataLoop 161 | } 162 | 163 | // Print service information 164 | if d.SDT != nil { 165 | jp.PrintSdtInfo(d.SDT, o.ShowService) 166 | break dataLoop 167 | } 168 | 169 | // Loop until we have printed service information 170 | } 171 | 172 | return jp.Error() 173 | } 174 | 175 | func ParseSCTE35(ctx context.Context, w io.Writer, f io.Reader, o Options) error { 176 | reader := bufio.NewReader(f) 177 | _, err := packet.Sync(reader) 178 | if err != nil { 179 | return fmt.Errorf("syncing with reader %w", err) 180 | } 181 | pat, err := psi.ReadPAT(reader) 182 | if err != nil { 183 | return fmt.Errorf("reading PAT %w", err) 184 | } 185 | 186 | var pmts []psi.PMT 187 | pm := pat.ProgramMap() 188 | for _, pid := range pm { 189 | pmt, err := psi.ReadPMT(reader, pid) 190 | if err != nil { 191 | return fmt.Errorf("reading PMT %w", err) 192 | } 193 | pmts = append(pmts, pmt) 194 | } 195 | 196 | jp := &JsonPrinter{W: w, Indent: o.Indent} 197 | scte35PIDs := make(map[int]bool) 198 | for _, pmt := range pmts { 199 | for _, es := range pmt.ElementaryStreams() { 200 | streamInfo := ParseElementaryStreamInfo(es) 201 | if streamInfo != nil { 202 | if streamInfo.Codec == "SCTE35" { 203 | scte35PIDs[es.ElementaryPid()] = true 204 | } 205 | 206 | jp.Print(streamInfo, o.ShowStreamInfo) 207 | } 208 | } 209 | } 210 | 211 | // Print SCTE35 212 | for { 213 | var pkt packet.Packet 214 | if _, err := io.ReadFull(reader, pkt[:]); err != nil { 215 | if err == io.EOF || err == io.ErrUnexpectedEOF { 216 | break 217 | } 218 | return fmt.Errorf("reading Packet %w", err) 219 | } 220 | 221 | currPID := packet.Pid(&pkt) 222 | if scte35PIDs[currPID] { 223 | pay, err := packet.Payload(&pkt) 224 | if err != nil { 225 | return fmt.Errorf("cannot get payload for packet on PID %d Error=%s\n", currPID, err) 226 | } 227 | msg, err := scte35.NewSCTE35(pay) 228 | if err != nil { 229 | return fmt.Errorf("cannot parse SCTE35 Error=%v\n", err) 230 | } 231 | scte35 := toSCTE35(uint16(currPID), msg) 232 | jp.Print(scte35, o.ShowSCTE35) 233 | } 234 | } 235 | 236 | return jp.Error() 237 | } 238 | 239 | func FilterPids(ctx context.Context, textWriter io.Writer, tsWriter io.Writer, f io.Reader, o Options) error { 240 | pidsToDrop := ParsePidsFromString(o.PidsToDrop) 241 | if slices.Contains(pidsToDrop, 0) { 242 | return fmt.Errorf("filtering out PAT is not allowed") 243 | } 244 | 245 | reader := bufio.NewReader(f) 246 | _, err := packet.Sync(reader) 247 | if err != nil { 248 | return fmt.Errorf("syncing with reader %w", err) 249 | } 250 | 251 | jp := &JsonPrinter{W: textWriter, Indent: o.Indent} 252 | statistics := PidFilterStatistics{PidsToDrop: pidsToDrop, TotalPackets: 0, FilteredPackets: 0, PacketsBeforePAT: 0} 253 | 254 | var pkt packet.Packet 255 | var pat psi.PAT 256 | foundPAT := false 257 | hasShownStreamInfo := false 258 | // Skip packets until PAT 259 | for { 260 | // Read packet 261 | if _, err := io.ReadFull(reader, pkt[:]); err != nil { 262 | if err == io.EOF || err == io.ErrUnexpectedEOF { 263 | break 264 | } 265 | return fmt.Errorf("reading Packet %w", err) 266 | } 267 | if packet.IsPat(&pkt) { 268 | // Found first PAT packet 269 | foundPAT = true 270 | } 271 | 272 | // Count PAT packet and non-PMT packets 273 | statistics.TotalPackets = statistics.TotalPackets + 1 274 | if !foundPAT { 275 | // packets before PAT 276 | statistics.PacketsBeforePAT = statistics.PacketsBeforePAT + 1 277 | continue 278 | } 279 | 280 | if packet.IsPat(&pkt) { 281 | // Parse PAT packet 282 | pat, err = ParsePacketToPAT(&pkt) 283 | if err != nil { 284 | return err 285 | } 286 | 287 | // Save PAT packet 288 | if err = WritePacket(&pkt, tsWriter); err != nil { 289 | return err 290 | } 291 | 292 | // Handle PMT packet(s) 293 | pm := pat.ProgramMap() 294 | for _, pid := range pm { 295 | if slices.Contains(pidsToDrop, pid) { 296 | return fmt.Errorf("filtering out PMT is not allowed") 297 | } 298 | 299 | packets, pmt, err := ReadPMTPackets(reader, pid) 300 | if err != nil { 301 | return err 302 | } 303 | // Count PMT packets 304 | statistics.TotalPackets = statistics.TotalPackets + uint32(len(packets)) 305 | 306 | // 1. Print stream info only once 307 | if o.ShowStreamInfo && !hasShownStreamInfo { 308 | for _, es := range pmt.ElementaryStreams() { 309 | streamInfo := ParseElementaryStreamInfo(es) 310 | if streamInfo != nil { 311 | jp.Print(streamInfo, true) 312 | } 313 | } 314 | hasShownStreamInfo = true 315 | } 316 | 317 | // 2. Drop pids if exist 318 | isFilteringOutPids := IsTwoSlicesOverlapping(pmt.Pids(), pidsToDrop) 319 | pkts := []*packet.Packet{} 320 | for i := range packets { 321 | pkts = append(pkts, &packets[i]) 322 | } 323 | if isFilteringOutPids { 324 | pidsToKeep := GetDifferenceOfTwoSlices(pmt.Pids(), pidsToDrop) 325 | pkts, err = psi.FilterPMTPacketsToPids(pkts, pidsToKeep) 326 | if err != nil { 327 | return fmt.Errorf("filtering pids %w", err) 328 | } 329 | 330 | statistics.FilteredPackets = statistics.FilteredPackets + uint32(len(pkts)) 331 | } 332 | 333 | // 3. Save PMT packets 334 | for _, p := range pkts { 335 | if err = WritePacket(p, tsWriter); err != nil { 336 | return err 337 | } 338 | } 339 | } 340 | 341 | // Move on to next packet 342 | continue 343 | } 344 | 345 | // Save non-PAT/PMT packets 346 | if err = WritePacket(&pkt, tsWriter); err != nil { 347 | return err 348 | } 349 | } 350 | 351 | jp.PrintFilter(statistics, true) 352 | return nil 353 | } 354 | -------------------------------------------------------------------------------- /internal/parser_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | var ( 15 | update = flag.Bool("update", false, "update the golden files of this test") 16 | ) 17 | 18 | func TestParseFile(t *testing.T) { 19 | fullOptionsWith2Pic := CreateFullOptions(2) 20 | fullOptionsWith2Pic.Indent = true 21 | fullOptionsWith35Pic := CreateFullOptions(35) 22 | fullOptionsWith35PicWithoutNALUSEI := CreateFullOptions(35) 23 | fullOptionsWith35PicWithoutNALUSEI.ShowNALU = false 24 | fullOptionsWith35PicWithoutNALUSEI.ShowSEIDetails = false 25 | 26 | parseInfoFunc := ParseInfo 27 | parseSCTE35Func := ParseSCTE35 28 | parseAllFunc := ParseAll 29 | 30 | cases := []struct { 31 | name string 32 | file string 33 | options Options 34 | expected_output_file string 35 | f RunableFunc 36 | }{ 37 | {"avc", "testdata/avc_with_time.ts", Options{MaxNrPictures: 10, Indent: true, ShowStreamInfo: true, ShowPS: true, ShowStatistics: true}, "testdata/golden_avc.txt", parseAllFunc}, 38 | {"avc_without_ps", "testdata/avc_with_time.ts", Options{MaxNrPictures: 10, ShowStreamInfo: true}, "testdata/golden_avc_without_ps.txt", parseInfoFunc}, 39 | {"avc_with_service", "testdata/80s_with_ad.ts", Options{MaxNrPictures: 0, ShowStreamInfo: true, ShowService: true}, "testdata/golden_avc_with_service.txt", parseInfoFunc}, 40 | {"avc_with_scte35", "testdata/80s_with_ad.ts", Options{MaxNrPictures: 0, ShowStreamInfo: true, ShowSCTE35: true}, "testdata/golden_avc_with_scte35.txt", parseSCTE35Func}, 41 | {"bbb_1s", "testdata/bbb_1s.ts", fullOptionsWith35Pic, "testdata/golden_bbb_1s.txt", parseAllFunc}, 42 | {"bbb_1s_indented", "testdata/bbb_1s.ts", fullOptionsWith2Pic, "testdata/golden_bbb_1s_indented.txt", parseAllFunc}, 43 | {"bbb_1s_no_nalu_no_sei", "testdata/bbb_1s.ts", fullOptionsWith35PicWithoutNALUSEI, "testdata/golden_bbb_1s_no_nalu(no_sei).txt", parseAllFunc}, 44 | {"obs_hevc_aac", "testdata/obs_hevc_aac.ts", fullOptionsWith35Pic, "testdata/golden_obs_hevc_aac.txt", parseAllFunc}, 45 | {"obs_hevc_aac_indented", "testdata/obs_hevc_aac.ts", fullOptionsWith2Pic, "testdata/golden_obs_hevc_aac_indented.txt", parseAllFunc}, 46 | {"obs_hevc_aac_no_nalu_no_sei", "testdata/obs_hevc_aac.ts", fullOptionsWith35PicWithoutNALUSEI, "testdata/golden_obs_hevc_aac_no_nalu(no_sei).txt", parseAllFunc}, 47 | } 48 | 49 | for _, c := range cases { 50 | t.Run(c.name, func(t *testing.T) { 51 | buf := bytes.Buffer{} 52 | ctx := context.TODO() 53 | f, err := os.Open(c.file) 54 | require.NoError(t, err) 55 | err = c.f(ctx, &buf, f, c.options) 56 | require.NoError(t, err) 57 | compareUpdateGolden(t, buf.String(), c.expected_output_file, *update) 58 | }) 59 | } 60 | } 61 | 62 | func getExpectedOutput(t *testing.T, file string) string { 63 | t.Helper() 64 | expected_output, err := os.ReadFile(file) 65 | require.NoError(t, err) 66 | expected_output_str := strings.ReplaceAll(string(expected_output), "\r\n", "\n") 67 | return expected_output_str 68 | } 69 | 70 | func compareUpdateGolden(t *testing.T, actual string, goldenFile string, update bool) { 71 | t.Helper() 72 | if update { 73 | err := os.WriteFile(goldenFile, []byte(actual), 0644) 74 | require.NoError(t, err) 75 | } else { 76 | expected := getExpectedOutput(t, goldenFile) 77 | require.Equal(t, expected, actual, "should produce expected output") 78 | } 79 | } 80 | 81 | // TestMain is to set flags for tests. In particular, the update flag to update golden files. 82 | func TestMain(m *testing.M) { 83 | flag.Parse() 84 | os.Exit(m.Run()) 85 | } 86 | -------------------------------------------------------------------------------- /internal/printer.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type JsonPrinter struct { 10 | W io.Writer 11 | Indent bool 12 | AccError error 13 | } 14 | 15 | func (p *JsonPrinter) Print(data any, show bool) { 16 | if !show { 17 | return 18 | } 19 | var out []byte 20 | var err error 21 | if p.AccError != nil { 22 | return 23 | } 24 | if p.Indent { 25 | out, err = json.MarshalIndent(data, "", " ") 26 | } else { 27 | out, err = json.Marshal(data) 28 | } 29 | if err != nil { 30 | p.AccError = err 31 | return 32 | } 33 | _, p.AccError = fmt.Fprintln(p.W, string(out)) 34 | } 35 | 36 | func (p *JsonPrinter) Error() error { 37 | return p.AccError 38 | } 39 | -------------------------------------------------------------------------------- /internal/scte35.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/Comcast/gots/v2/scte35" 5 | ) 6 | 7 | type SCTE35Info struct { 8 | PID uint16 `json:"pid"` 9 | SpliceCommand SpliceCommand `json:"spliceCommand"` 10 | SegDesc []SegmentationDescriptor `json:"segmentationDes,omitempty"` 11 | } 12 | 13 | type SpliceCommand struct { 14 | Type string `json:"type"` 15 | EventId uint32 `json:"eventId"` 16 | PTS uint64 `json:"pts"` 17 | Duration uint64 `json:"duration,omitempty"` 18 | Out bool `json:"outOfNetwork,omitempty"` 19 | Immediate bool `json:"immediate,omitempty"` 20 | } 21 | 22 | type SegmentationDescriptor struct { 23 | SegmentNumber uint8 `json:"segmentNumber"` 24 | EventId uint32 `json:"eventId"` 25 | Type string `json:"type"` 26 | Duration uint64 `json:"duration,omitempty"` 27 | } 28 | 29 | func toSCTE35(pid uint16, msg scte35.SCTE35) SCTE35Info { 30 | scte35Info := SCTE35Info{PID: pid, SpliceCommand: toSpliceCommand(msg.CommandInfo())} 31 | 32 | if insert, ok := msg.CommandInfo().(scte35.SpliceInsertCommand); ok { 33 | scte35Info.SpliceCommand = toSpliceInsertCommand(insert) 34 | } 35 | for _, desc := range msg.Descriptors() { 36 | segDesc := toSegmentationDescriptor(desc) 37 | scte35Info.SegDesc = append(scte35Info.SegDesc, segDesc) 38 | } 39 | 40 | return scte35Info 41 | } 42 | 43 | func toSpliceCommand(spliceCommand scte35.SpliceCommand) SpliceCommand { 44 | spliceCmd := SpliceCommand{Type: getCommandType(spliceCommand)} 45 | if spliceCommand.HasPTS() { 46 | spliceCmd.PTS = uint64(spliceCommand.PTS()) 47 | } 48 | 49 | return spliceCmd 50 | } 51 | 52 | func toSegmentationDescriptor(segdesc scte35.SegmentationDescriptor) SegmentationDescriptor { 53 | segDesc := SegmentationDescriptor{} 54 | segDesc.EventId = segdesc.EventID() 55 | segDesc.Type = scte35.SegDescTypeNames[segdesc.TypeID()] 56 | segDesc.SegmentNumber = segdesc.SegmentNumber() 57 | if segdesc.HasDuration() { 58 | segDesc.Duration = uint64(segdesc.Duration()) 59 | } 60 | return segDesc 61 | } 62 | 63 | func toSpliceInsertCommand(spliceCommand scte35.SpliceInsertCommand) SpliceCommand { 64 | spliceCmd := SpliceCommand{Type: getCommandType(spliceCommand)} 65 | spliceCmd.EventId = spliceCommand.EventID() 66 | spliceCmd.Immediate = spliceCommand.SpliceImmediate() 67 | spliceCmd.Out = spliceCommand.IsOut() 68 | if spliceCommand.HasPTS() { 69 | spliceCmd.PTS = uint64(spliceCommand.PTS()) 70 | } 71 | if spliceCommand.HasDuration() { 72 | spliceCmd.Duration = uint64(spliceCommand.Duration()) 73 | } 74 | 75 | return spliceCmd 76 | } 77 | 78 | func getCommandType(spliceCommand scte35.SpliceCommand) string { 79 | return scte35.SpliceCommandTypeNames[spliceCommand.CommandType()] 80 | } 81 | -------------------------------------------------------------------------------- /internal/sdt.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "github.com/asticode/go-astits" 4 | 5 | type sdtServiceDescriptor struct { 6 | ServiceName string `json:"serviceName"` 7 | ProviderName string `json:"providerName"` 8 | } 9 | 10 | type sdtService struct { 11 | ServiceID uint16 `json:"serviceId"` 12 | Descriptors []sdtServiceDescriptor `json:"descriptors"` 13 | } 14 | 15 | type sdtInfo struct { 16 | SdtServices []sdtService `json:"SDT"` 17 | } 18 | 19 | func (p *JsonPrinter) PrintSdtInfo(sdt *astits.SDTData, show bool) { 20 | sdtInfo := ToSdtInfo(sdt) 21 | p.Print(sdtInfo, show) 22 | } 23 | 24 | func ToSdtInfo(sdt *astits.SDTData) sdtInfo { 25 | sdtInfo := sdtInfo{ 26 | SdtServices: make([]sdtService, 0, len(sdt.Services)), 27 | } 28 | 29 | for _, s := range sdt.Services { 30 | sdtService := toSdtService(s) 31 | sdtInfo.SdtServices = append(sdtInfo.SdtServices, sdtService) 32 | } 33 | 34 | return sdtInfo 35 | } 36 | 37 | func toSdtService(s *astits.SDTDataService) sdtService { 38 | sdtService := sdtService{ 39 | ServiceID: s.ServiceID, 40 | Descriptors: make([]sdtServiceDescriptor, 0, len(s.Descriptors)), 41 | } 42 | 43 | for _, d := range s.Descriptors { 44 | if d.Tag == astits.DescriptorTagService { 45 | sdtServiceDescriptor := toSdtServiceDescriptor(d.Service) 46 | sdtService.Descriptors = append(sdtService.Descriptors, sdtServiceDescriptor) 47 | } 48 | } 49 | 50 | return sdtService 51 | } 52 | 53 | func toSdtServiceDescriptor(sd *astits.DescriptorService) sdtServiceDescriptor { 54 | return sdtServiceDescriptor{ 55 | ProviderName: string(sd.Provider), 56 | ServiceName: string(sd.Name), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/smpte2038.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | 9 | "github.com/Eyevinn/mp4ff/bits" 10 | "github.com/asticode/go-astits" 11 | ) 12 | 13 | // didMap maps SMPTE-20348 [did] values registered by SMPTE 14 | // [dids]: https://smpte-ra.org/smpte-ancillary-data-smpte-st-291 15 | type SMPTE291Identifier struct { 16 | did, sdid byte 17 | } 18 | 19 | var SMPTE291Map = map[SMPTE291Identifier]string{ 20 | {0x41, 0x05}: "AFD and Bar Data", 21 | {0x41, 0x06}: "Pan and Scan Information", 22 | {0x41, 0x07}: "ANSI/SCTE 104 messages", 23 | {0x41, 0x08}: "DVB/SCTE VBI data", 24 | {0x43, 0x01}: "Inter Station Control Packet Data", 25 | {0x43, 0x02}: "OP-47 SDP - Subtitling Data", 26 | {0x43, 0x03}: "OP-47 Multi Packet ANC Data", 27 | {0x45, 0x01}: "Audio Metadata (Single Program)", 28 | {0x45, 0x02}: "Audio Metadata Ch 01/02", 29 | {0x45, 0x03}: "Audio Metadata Ch 03/04", 30 | {0x45, 0x04}: "Audio Metadata Ch 05/06", 31 | {0x45, 0x05}: "Audio Metadata Ch 07/08", 32 | {0x45, 0x06}: "Audio Metadata Ch 09/10", 33 | {0x45, 0x07}: "Audio Metadata Ch 11/12", 34 | {0x45, 0x08}: "Audio Metadata Ch 13/14", 35 | {0x45, 0x09}: "Audio Metadata Ch 15/16", 36 | {0x50, 0x01}: "WSS - Wide Screen Signaling", 37 | {0x50, 0x30}: "User Defined", 38 | {0x50, 0x49}: "User Defined", 39 | {0x60, 0x60}: "ATC - Ancillary Timecode", 40 | {0x61, 0x01}: "EIA 708B Data mapping into VANC space", 41 | {0x61, 0x02}: "EIA 608 Data mapping into VANC space", 42 | {0x62, 0x01}: "DTV Program Description", 43 | {0x62, 0x02}: "DTV Data Broadcast", 44 | {0x62, 0x03}: "VBI Data", 45 | } 46 | 47 | type smpte2038Data struct { 48 | PID uint16 `json:"pid"` 49 | PTS int64 `json:"pts"` 50 | Entries []smpte2038Entry 51 | } 52 | 53 | type smpte2038Entry struct { 54 | LineNr byte `json:"lineNr"` 55 | HorOffset byte `json:"horOffset"` 56 | DID byte `json:"did"` 57 | SDID byte `json:"sdid"` 58 | DataCount byte `json:"dataCount"` 59 | Type string `json:"type"` 60 | } 61 | 62 | func ParseSMPTE2038(jp *JsonPrinter, d *astits.DemuxerData, o Options) { 63 | pl := d.PES.Data 64 | pdtDtsIndicator := d.PES.Header.OptionalHeader.PTSDTSIndicator 65 | if pdtDtsIndicator != 2 { 66 | fmt.Printf("SMPTE-2038: invalid PDT_DTS_Indicator=%d\n", pdtDtsIndicator) 67 | } 68 | pts := d.PES.Header.OptionalHeader.PTS 69 | rd := bytes.NewBuffer(pl) 70 | r := bits.NewReader(rd) 71 | smpteData := smpte2038Data{PID: d.PID, PTS: pts.Base} 72 | for { 73 | z := r.Read(6) 74 | if r.AccError() == io.EOF { 75 | break 76 | } 77 | if z == 0xffffffffffff { 78 | z2 := r.Read(2) 79 | if z2 != 0x3 { 80 | log.Printf("SMPTE-2038: invalid stuffing\n") 81 | return 82 | } 83 | _ = r.ReadRemainingBytes() 84 | } 85 | if z != 0 { 86 | log.Printf("SMPTE-2038: reserved bits not zero %x\n", z) 87 | return 88 | } 89 | _ = r.Read(1) // cNotYChFlag 90 | lineNr := r.Read(11) 91 | horOffset := r.Read(12) 92 | did := r.Read(10) 93 | did = did & 0xff // 8 bits 94 | sdid := r.Read(10) 95 | sdid = sdid & 0xff // 8 bits 96 | didStr := SMPTE291Map[SMPTE291Identifier{byte(did), byte(sdid)}] 97 | if didStr == "" { 98 | didStr = "unknown SID/DID" 99 | } 100 | dataCount := int(r.Read(10)) & 0xff // 8 bits 101 | for j := 0; j < dataCount; j++ { 102 | _ = r.Read(10) 103 | } 104 | _ = r.Read(10) // checkSumWord 105 | if r.NrBitsReadInCurrentByte() != 8 { 106 | _ = r.Read(8 - r.NrBitsReadInCurrentByte()) 107 | } 108 | if r.AccError() != nil { 109 | fmt.Printf("SMPTE-2038: read error\n") 110 | return 111 | } 112 | smpteData.Entries = append(smpteData.Entries, smpte2038Entry{ 113 | LineNr: byte(lineNr), 114 | HorOffset: byte(horOffset), 115 | DID: byte(did), 116 | SDID: byte(sdid), 117 | DataCount: byte(dataCount), 118 | Type: didStr, 119 | }) 120 | } 121 | if jp != nil { 122 | jp.Print(smpteData, true) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /internal/statistics.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type PidFilterStatistics struct { 4 | PidsToDrop []int `json:"pidsToDrop"` 5 | TotalPackets uint32 `json:"total"` 6 | FilteredPackets uint32 `json:"filtered"` 7 | PacketsBeforePAT uint32 `json:"packetBeforePAT"` 8 | Percentage float32 `json:"percentage"` 9 | } 10 | 11 | type StreamStatistics struct { 12 | Type string `json:"streamType"` 13 | Pid uint16 `json:"pid"` 14 | FrameRate float64 `json:"frameRate"` 15 | TimeStamps []int64 `json:"-"` 16 | MaxStep int64 `json:"maxStep,omitempty"` 17 | MinStep int64 `json:"minStep,omitempty"` 18 | AvgStep int64 `json:"avgStep,omitempty"` 19 | // RAI-markers 20 | RAIPTS []int64 `json:"-"` 21 | IDRPTS []int64 `json:"-"` 22 | RAIGOPDuration int64 `json:"RAIGoPDuration,omitempty"` 23 | IDRGOPDuration int64 `json:"IDRGoPDuration,omitempty"` 24 | // Errors 25 | Errors []string `json:"errors,omitempty"` 26 | } 27 | 28 | func (p *JsonPrinter) PrintFilter(s PidFilterStatistics, show bool) { 29 | if s.TotalPackets == 0 { 30 | s.Percentage = 0 31 | } else { 32 | s.Percentage = (float32(s.PacketsBeforePAT) + float32(s.FilteredPackets)) / float32(s.TotalPackets) 33 | } 34 | 35 | // print statistics 36 | p.Print(s, show) 37 | } 38 | 39 | func (p *JsonPrinter) PrintStatistics(s StreamStatistics, show bool) { 40 | // fmt.Fprintf(p.w, "Print statistics for PID: %d\n", s.Pid) 41 | s.calculateFrameRate(TimeScale) 42 | s.calculateGoPDuration(TimeScale) 43 | // TODO: format statistics 44 | 45 | // print statistics 46 | p.Print(s, show) 47 | } 48 | 49 | func sliceMinMaxAverage(values []int64) (min, max, avg int64) { 50 | if len(values) == 0 { 51 | return 0, 0, 0 52 | } 53 | 54 | min = values[0] 55 | max = values[0] 56 | sum := int64(0) 57 | for _, number := range values { 58 | if number < min { 59 | min = number 60 | } 61 | if number > max { 62 | max = number 63 | } 64 | sum += number 65 | } 66 | avg = sum / int64(len(values)) 67 | return min, max, avg 68 | } 69 | 70 | func CalculateSteps(timestamps []int64) []int64 { 71 | if len(timestamps) < 2 { 72 | return nil 73 | } 74 | 75 | // PTS/DTS are 33-bit values, so it wraps around after 26.5 hours 76 | steps := make([]int64, len(timestamps)-1) 77 | for i := 0; i < len(timestamps)-1; i++ { 78 | steps[i] = SignedPTSDiff(timestamps[i+1], timestamps[i]) 79 | } 80 | return steps 81 | } 82 | 83 | // Calculate frame rate from DTS or PTS steps 84 | func (s *StreamStatistics) calculateFrameRate(timescale int64) { 85 | if len(s.TimeStamps) < 2 { 86 | s.Errors = append(s.Errors, "too few timestamps to calculate frame rate") 87 | return 88 | } 89 | 90 | steps := CalculateSteps(s.TimeStamps) 91 | minStep, maxStep, avgStep := sliceMinMaxAverage(steps) 92 | if maxStep != minStep { 93 | s.Errors = append(s.Errors, "irregular PTS/DTS steps") 94 | s.MinStep, s.MaxStep, s.AvgStep = minStep, maxStep, avgStep 95 | } 96 | 97 | // fmt.Printf("Steps: %v\n", steps) 98 | // fmt.Printf("Average step: %f\n", avgStep) 99 | s.FrameRate = float64(timescale) / float64(avgStep) 100 | } 101 | 102 | func (s *StreamStatistics) calculateGoPDuration(timescale int64) { 103 | if len(s.RAIPTS) < 2 || len(s.IDRPTS) < 2 { 104 | s.Errors = append(s.Errors, "no GoP duration since less than 2 I-frames") 105 | return 106 | } 107 | 108 | // Calculate GOP duration 109 | RAIPTSSteps := CalculateSteps(s.RAIPTS) 110 | IDRPTSSteps := CalculateSteps(s.IDRPTS) 111 | _, _, RAIGOPStep := sliceMinMaxAverage(RAIPTSSteps) 112 | _, _, IDRGOPStep := sliceMinMaxAverage(IDRPTSSteps) 113 | // fmt.Printf("RAIPTSSteps: %v\n", RAIPTSSteps) 114 | // fmt.Printf("RAIGOPStep: %d\n", RAIGOPStep) 115 | s.RAIGOPDuration = RAIGOPStep / timescale 116 | s.IDRGOPDuration = IDRGOPStep / timescale 117 | } 118 | -------------------------------------------------------------------------------- /internal/step_calculation_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCalculateStepsInSlice(t *testing.T) { 11 | testCases := []struct { 12 | name string 13 | values []int64 14 | want []int64 15 | }{ 16 | { 17 | name: "Monotonically increasing values", 18 | values: []int64{1, 2, 3, 4, 5}, 19 | want: []int64{1, 1, 1, 1}, 20 | }, 21 | { 22 | name: "Negative values", 23 | values: []int64{-5, -4, -3, -2, -1}, 24 | want: []int64{1, 1, 1, 1}, 25 | }, 26 | { 27 | name: "Mixed values", 28 | values: []int64{-3, -2, -1, 0, 1, 2, 3}, 29 | want: []int64{1, 1, 1, 1, 1, 1}, 30 | }, 31 | { 32 | name: "Big values", 33 | values: []int64{int64(math.Pow(2, 33)) - 5, int64(math.Pow(2, 33)) - 3, int64(math.Pow(2, 33)) - 1, 2, 4, 6}, 34 | want: []int64{2, 2, 3, 2, 2}, 35 | }, 36 | { 37 | name: "Big values 2", 38 | values: []int64{int64(math.Pow(2, 33)) - 4, int64(math.Pow(2, 33)) - 2, 1, 3, 5, 7}, 39 | want: []int64{2, 3, 2, 2, 2}, 40 | }, 41 | } 42 | 43 | for _, tc := range testCases { 44 | t.Run(tc.name, func(t *testing.T) { 45 | got := CalculateSteps(tc.values) 46 | require.Equal(t, tc.want, got) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/testdata/80s_with_ad.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eyevinn/mp2ts-tools/f8617b340007216631e11eed34746b06c7c3c665/internal/testdata/80s_with_ad.ts -------------------------------------------------------------------------------- /internal/testdata/avc_with_time.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eyevinn/mp2ts-tools/f8617b340007216631e11eed34746b06c7c3c665/internal/testdata/avc_with_time.ts -------------------------------------------------------------------------------- /internal/testdata/bbb_1s.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eyevinn/mp2ts-tools/f8617b340007216631e11eed34746b06c7c3c665/internal/testdata/bbb_1s.ts -------------------------------------------------------------------------------- /internal/testdata/golden_avc.txt: -------------------------------------------------------------------------------- 1 | { 2 | "pid": 512, 3 | "codec": "AVC", 4 | "type": "video" 5 | } 6 | { 7 | "pid": 512, 8 | "parameterSet": "SPS", 9 | "nr": 0, 10 | "hex": "27640020ac2b402802dd80880000030008000003032742001458000510edef7c1da1c32a", 11 | "length": 36 12 | } 13 | { 14 | "pid": 512, 15 | "parameterSet": "PPS", 16 | "nr": 0, 17 | "hex": "28ee3cb0", 18 | "length": 4 19 | } 20 | { 21 | "streamType": "AVC", 22 | "pid": 512, 23 | "frameRate": 50, 24 | "errors": [ 25 | "no GoP duration since less than 2 I-frames" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /internal/testdata/golden_avc_with_scte35.txt: -------------------------------------------------------------------------------- 1 | {"pid":256,"codec":"AVC","type":"video"} 2 | {"pid":257,"codec":"AAC","type":"audio"} 3 | {"pid":1001,"codec":"SCTE35","type":"cue"} 4 | {"pid":1001,"spliceCommand":{"type":"SpliceInsert","eventId":255,"pts":1032000,"duration":1800000,"outOfNetwork":true}} 5 | -------------------------------------------------------------------------------- /internal/testdata/golden_avc_with_service.txt: -------------------------------------------------------------------------------- 1 | {"pid":256,"codec":"AVC","type":"video"} 2 | {"pid":257,"codec":"AAC","type":"audio"} 3 | {"pid":1001,"codec":"SCTE35","type":"cue"} 4 | {"SDT":[{"serviceId":1,"descriptors":[{"serviceName":"Service01","providerName":"FFmpeg"}]}]} 5 | -------------------------------------------------------------------------------- /internal/testdata/golden_avc_without_ps.txt: -------------------------------------------------------------------------------- 1 | {"pid":512,"codec":"AVC","type":"video"} 2 | -------------------------------------------------------------------------------- /internal/testdata/golden_bbb_1s.txt: -------------------------------------------------------------------------------- 1 | {"pid":256,"codec":"AVC","type":"video"} 2 | {"pid":257,"codec":"AAC","type":"audio"} 3 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"6764001facd9405005bb011000000300100000030300f1831960","length":26} 4 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"68ebecb22c","length":5} 5 | {"pid":256,"rai":true,"pts":133500,"dts":126000,"imgType":"[I]","nalus":[{"type":"AUD_9","len":2},{"type":"SEI_6","len":701,"data":[{"msg":"SEIUserDataUnregisteredType (5)","payload":{"UUID":"3EXpvebZSLeWLNgg2SPu7w=="}}]},{"type":"SPS_7","len":26},{"type":"PPS_8","len":5},{"type":"IDR_5","len":209}]} 6 | {"pid":256,"rai":false,"pts":144750,"dts":129750,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":34}]} 7 | {"pid":256,"rai":false,"pts":137250,"dts":133500,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":32}]} 8 | {"pid":256,"rai":false,"pts":141000,"dts":137250,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":32}]} 9 | {"pid":256,"rai":false,"pts":148500,"dts":141000,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":48}]} 10 | {"pid":256,"rai":false,"pts":152250,"dts":144750,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":145}]} 11 | {"pid":256,"rai":false,"pts":156000,"dts":148500,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":204}]} 12 | {"pid":256,"rai":false,"pts":163500,"dts":152250,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":143}]} 13 | {"pid":256,"rai":false,"pts":159750,"dts":156000,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":150}]} 14 | {"pid":256,"rai":false,"pts":167250,"dts":159750,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":315}]} 15 | {"pid":256,"rai":false,"pts":171000,"dts":163500,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":679}]} 16 | {"SDT":[{"serviceId":1,"descriptors":[{"serviceName":"ts-info","providerName":"Eyevinn Technology"}]}]} 17 | {"pid":256,"rai":false,"pts":174750,"dts":167250,"imgType":"[I]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":1718}]} 18 | {"pid":256,"rai":false,"pts":182250,"dts":171000,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":918}]} 19 | {"pid":256,"rai":false,"pts":178500,"dts":174750,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":506}]} 20 | {"pid":256,"rai":false,"pts":189750,"dts":178500,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":1342}]} 21 | {"pid":256,"rai":false,"pts":186000,"dts":182250,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":704}]} 22 | {"pid":256,"rai":false,"pts":201000,"dts":186000,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":6516}]} 23 | {"pid":256,"rai":false,"pts":193500,"dts":189750,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":1073}]} 24 | {"pid":256,"rai":false,"pts":197250,"dts":193500,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":1739}]} 25 | {"pid":256,"rai":false,"pts":204750,"dts":197250,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":12196}]} 26 | {"pid":256,"rai":false,"pts":219750,"dts":201000,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":18657}]} 27 | {"pid":256,"rai":false,"pts":212250,"dts":204750,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":7371}]} 28 | {"pid":256,"rai":false,"pts":208500,"dts":208500,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":3435}]} 29 | {"pid":256,"rai":false,"pts":216000,"dts":212250,"imgType":"[B]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":4061}]} 30 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"6764001facd9405005bb011000000300100000030300f1831960","length":26} 31 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"68ebecb22c","length":5} 32 | {"pid":256,"rai":true,"pts":223500,"dts":216000,"imgType":"[I]","nalus":[{"type":"AUD_9","len":2},{"type":"SPS_7","len":26},{"type":"PPS_8","len":5},{"type":"IDR_5","len":12975}]} 33 | {"pid":256,"rai":false,"pts":234750,"dts":219750,"imgType":"[P]","nalus":[{"type":"AUD_9","len":2},{"type":"NonIDR_1","len":24332}]} 34 | {"streamType":"AVC","pid":256,"frameRate":24,"RAIGoPDuration":1,"IDRGoPDuration":1} 35 | -------------------------------------------------------------------------------- /internal/testdata/golden_bbb_1s_indented.txt: -------------------------------------------------------------------------------- 1 | { 2 | "pid": 256, 3 | "codec": "AVC", 4 | "type": "video" 5 | } 6 | { 7 | "pid": 257, 8 | "codec": "AAC", 9 | "type": "audio" 10 | } 11 | { 12 | "pid": 256, 13 | "parameterSet": "SPS", 14 | "nr": 0, 15 | "hex": "6764001facd9405005bb011000000300100000030300f1831960", 16 | "length": 26 17 | } 18 | { 19 | "pid": 256, 20 | "parameterSet": "PPS", 21 | "nr": 0, 22 | "hex": "68ebecb22c", 23 | "length": 5 24 | } 25 | { 26 | "pid": 256, 27 | "rai": true, 28 | "pts": 133500, 29 | "dts": 126000, 30 | "imgType": "[I]", 31 | "nalus": [ 32 | { 33 | "type": "AUD_9", 34 | "len": 2 35 | }, 36 | { 37 | "type": "SEI_6", 38 | "len": 701, 39 | "data": [ 40 | { 41 | "msg": "SEIUserDataUnregisteredType (5)", 42 | "payload": { 43 | "UUID": "3EXpvebZSLeWLNgg2SPu7w==" 44 | } 45 | } 46 | ] 47 | }, 48 | { 49 | "type": "SPS_7", 50 | "len": 26 51 | }, 52 | { 53 | "type": "PPS_8", 54 | "len": 5 55 | }, 56 | { 57 | "type": "IDR_5", 58 | "len": 209 59 | } 60 | ] 61 | } 62 | { 63 | "pid": 256, 64 | "rai": false, 65 | "pts": 144750, 66 | "dts": 129750, 67 | "imgType": "[P]", 68 | "nalus": [ 69 | { 70 | "type": "AUD_9", 71 | "len": 2 72 | }, 73 | { 74 | "type": "NonIDR_1", 75 | "len": 34 76 | } 77 | ] 78 | } 79 | { 80 | "streamType": "AVC", 81 | "pid": 256, 82 | "frameRate": 24, 83 | "errors": [ 84 | "no GoP duration since less than 2 I-frames" 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /internal/testdata/golden_bbb_1s_no_nalu(no_sei).txt: -------------------------------------------------------------------------------- 1 | {"pid":256,"codec":"AVC","type":"video"} 2 | {"pid":257,"codec":"AAC","type":"audio"} 3 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"6764001facd9405005bb011000000300100000030300f1831960","length":26} 4 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"68ebecb22c","length":5} 5 | {"SDT":[{"serviceId":1,"descriptors":[{"serviceName":"ts-info","providerName":"Eyevinn Technology"}]}]} 6 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"6764001facd9405005bb011000000300100000030300f1831960","length":26} 7 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"68ebecb22c","length":5} 8 | {"streamType":"AVC","pid":256,"frameRate":24,"RAIGoPDuration":1,"IDRGoPDuration":1} 9 | -------------------------------------------------------------------------------- /internal/testdata/golden_obs_hevc_aac.txt: -------------------------------------------------------------------------------- 1 | {"pid":256,"codec":"HEVC","type":"video"} 2 | {"pid":257,"codec":"AAC","type":"audio"} 3 | {"pid":256,"parameterSet":"VPS","nr":0,"hex":"40010c01ffff016000000300b00000030000030078170240","length":24} 4 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"420101016000000300b00000030000030078a005020171f2e205ee45914bff2e7f13fa9a8080808040","length":41} 5 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"4401c072f05324","length":7} 6 | {"pid":256,"rai":true,"pts":1920,"dts":1920,"nalus":[{"type":"AUD_35","len":3},{"type":"VPS_32","len":24},{"type":"SPS_33","len":41},{"type":"PPS_34","len":7},{"type":"SEI_39","len":31,"data":[{"msg":"SEIUserDataUnregisteredType (5)"}]},{"type":"RAP_IDR_20","len":12860}]} 7 | {"pid":256,"rai":false,"pts":4920,"dts":4920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":409}]} 8 | {"pid":256,"rai":false,"pts":7920,"dts":7920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":332}]} 9 | {"pid":256,"rai":false,"pts":10920,"dts":10920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":427}]} 10 | {"pid":256,"rai":false,"pts":13920,"dts":13920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":4952}]} 11 | {"pid":256,"rai":false,"pts":16920,"dts":16920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":791}]} 12 | {"pid":256,"rai":false,"pts":19920,"dts":19920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":616}]} 13 | {"pid":256,"rai":false,"pts":22920,"dts":22920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":2400}]} 14 | {"pid":256,"rai":false,"pts":25920,"dts":25920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":733}]} 15 | {"pid":256,"rai":false,"pts":28920,"dts":28920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":719}]} 16 | {"pid":256,"rai":false,"pts":31920,"dts":31920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":5505}]} 17 | {"pid":256,"rai":false,"pts":34920,"dts":34920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":751}]} 18 | {"pid":256,"rai":false,"pts":37920,"dts":37920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":796}]} 19 | {"pid":256,"rai":false,"pts":40920,"dts":40920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":1662}]} 20 | {"pid":256,"rai":false,"pts":43920,"dts":43920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":349}]} 21 | {"pid":256,"rai":false,"pts":46920,"dts":46920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":254}]} 22 | {"pid":256,"rai":false,"pts":49920,"dts":49920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":792}]} 23 | {"pid":256,"rai":false,"pts":52920,"dts":52920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":257}]} 24 | {"pid":256,"rai":false,"pts":55920,"dts":55920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":172}]} 25 | {"pid":256,"rai":false,"pts":58920,"dts":58920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":692}]} 26 | {"pid":256,"rai":false,"pts":61920,"dts":61920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":342}]} 27 | {"pid":256,"rai":false,"pts":64920,"dts":64920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":458}]} 28 | {"pid":256,"rai":false,"pts":67920,"dts":67920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":905}]} 29 | {"pid":256,"rai":false,"pts":70920,"dts":70920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":423}]} 30 | {"pid":256,"rai":false,"pts":73920,"dts":73920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":229}]} 31 | {"pid":256,"rai":false,"pts":76920,"dts":76920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":602}]} 32 | {"pid":256,"rai":false,"pts":79920,"dts":79920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":291}]} 33 | {"pid":256,"rai":false,"pts":82920,"dts":82920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":153}]} 34 | {"pid":256,"rai":false,"pts":85920,"dts":85920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":471}]} 35 | {"pid":256,"rai":false,"pts":88920,"dts":88920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":352}]} 36 | {"pid":256,"parameterSet":"VPS","nr":0,"hex":"40010c01ffff016000000300b00000030000030078170240","length":24} 37 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"420101016000000300b00000030000030078a005020171f2e205ee45914bff2e7f13fa9a8080808040","length":41} 38 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"4401c072f05324","length":7} 39 | {"pid":256,"rai":true,"pts":91920,"dts":91920,"nalus":[{"type":"AUD_35","len":3},{"type":"VPS_32","len":24},{"type":"SPS_33","len":41},{"type":"PPS_34","len":7},{"type":"RAP_IDR_20","len":24298}]} 40 | {"pid":256,"rai":false,"pts":94920,"dts":94920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":402}]} 41 | {"pid":256,"rai":false,"pts":97920,"dts":97920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":400}]} 42 | {"pid":256,"rai":false,"pts":100920,"dts":100920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":293}]} 43 | {"pid":256,"rai":false,"pts":103920,"dts":103920,"nalus":[{"type":"AUD_35","len":3},{"type":"NonRAP_Trail_1","len":566}]} 44 | {"streamType":"HEVC","pid":256,"frameRate":30,"RAIGoPDuration":1,"IDRGoPDuration":1} 45 | -------------------------------------------------------------------------------- /internal/testdata/golden_obs_hevc_aac_indented.txt: -------------------------------------------------------------------------------- 1 | { 2 | "pid": 256, 3 | "codec": "HEVC", 4 | "type": "video" 5 | } 6 | { 7 | "pid": 257, 8 | "codec": "AAC", 9 | "type": "audio" 10 | } 11 | { 12 | "pid": 256, 13 | "parameterSet": "VPS", 14 | "nr": 0, 15 | "hex": "40010c01ffff016000000300b00000030000030078170240", 16 | "length": 24 17 | } 18 | { 19 | "pid": 256, 20 | "parameterSet": "SPS", 21 | "nr": 0, 22 | "hex": "420101016000000300b00000030000030078a005020171f2e205ee45914bff2e7f13fa9a8080808040", 23 | "length": 41 24 | } 25 | { 26 | "pid": 256, 27 | "parameterSet": "PPS", 28 | "nr": 0, 29 | "hex": "4401c072f05324", 30 | "length": 7 31 | } 32 | { 33 | "pid": 256, 34 | "rai": true, 35 | "pts": 1920, 36 | "dts": 1920, 37 | "nalus": [ 38 | { 39 | "type": "AUD_35", 40 | "len": 3 41 | }, 42 | { 43 | "type": "VPS_32", 44 | "len": 24 45 | }, 46 | { 47 | "type": "SPS_33", 48 | "len": 41 49 | }, 50 | { 51 | "type": "PPS_34", 52 | "len": 7 53 | }, 54 | { 55 | "type": "SEI_39", 56 | "len": 31, 57 | "data": [ 58 | { 59 | "msg": "SEIUserDataUnregisteredType (5)" 60 | } 61 | ] 62 | }, 63 | { 64 | "type": "RAP_IDR_20", 65 | "len": 12860 66 | } 67 | ] 68 | } 69 | { 70 | "pid": 256, 71 | "rai": false, 72 | "pts": 4920, 73 | "dts": 4920, 74 | "nalus": [ 75 | { 76 | "type": "AUD_35", 77 | "len": 3 78 | }, 79 | { 80 | "type": "NonRAP_Trail_1", 81 | "len": 409 82 | } 83 | ] 84 | } 85 | { 86 | "streamType": "HEVC", 87 | "pid": 256, 88 | "frameRate": 30, 89 | "errors": [ 90 | "no GoP duration since less than 2 I-frames" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /internal/testdata/golden_obs_hevc_aac_no_nalu(no_sei).txt: -------------------------------------------------------------------------------- 1 | {"pid":256,"codec":"HEVC","type":"video"} 2 | {"pid":257,"codec":"AAC","type":"audio"} 3 | {"pid":256,"parameterSet":"VPS","nr":0,"hex":"40010c01ffff016000000300b00000030000030078170240","length":24} 4 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"420101016000000300b00000030000030078a005020171f2e205ee45914bff2e7f13fa9a8080808040","length":41} 5 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"4401c072f05324","length":7} 6 | {"pid":256,"parameterSet":"VPS","nr":0,"hex":"40010c01ffff016000000300b00000030000030078170240","length":24} 7 | {"pid":256,"parameterSet":"SPS","nr":0,"hex":"420101016000000300b00000030000030078a005020171f2e205ee45914bff2e7f13fa9a8080808040","length":41} 8 | {"pid":256,"parameterSet":"PPS","nr":0,"hex":"4401c072f05324","length":7} 9 | {"streamType":"HEVC","pid":256,"frameRate":30,"RAIGoPDuration":1,"IDRGoPDuration":1} 10 | -------------------------------------------------------------------------------- /internal/testdata/obs_hevc_aac.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eyevinn/mp2ts-tools/f8617b340007216631e11eed34746b06c7c3c665/internal/testdata/obs_hevc_aac.ts -------------------------------------------------------------------------------- /internal/utils.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "os/signal" 13 | "strconv" 14 | "strings" 15 | "syscall" 16 | 17 | "github.com/Comcast/gots/v2" 18 | "github.com/Comcast/gots/v2/packet" 19 | "github.com/Comcast/gots/v2/psi" 20 | "github.com/asticode/go-astits" 21 | slices "golang.org/x/exp/slices" 22 | ) 23 | 24 | type Options struct { 25 | MaxNrPictures int 26 | Version bool 27 | Indent bool 28 | ShowStreamInfo bool 29 | ShowService bool 30 | ShowPS bool 31 | VerbosePSInfo bool 32 | ShowNALU bool 33 | ShowSEIDetails bool 34 | ShowSMPTE2038 bool 35 | ShowSCTE35 bool 36 | ShowStatistics bool 37 | FilterPids bool 38 | PidsToDrop string 39 | OutPutTo string 40 | } 41 | 42 | func CreateFullOptions(max int) Options { 43 | return Options{MaxNrPictures: max, ShowStreamInfo: true, ShowService: true, ShowPS: true, ShowNALU: true, ShowSEIDetails: true, ShowSMPTE2038: true, ShowStatistics: true} 44 | } 45 | 46 | const ( 47 | ANC_REGISTERED_IDENTIFIER = 0x56414E43 48 | ANC_DESCRIPTOR_TAG = 0xC4 49 | ) 50 | 51 | type OptionParseFunc func() Options 52 | type RunableFunc func(ctx context.Context, w io.Writer, f io.Reader, o Options) error 53 | 54 | func ReadPMTPackets(r io.Reader, pid int) ([]packet.Packet, psi.PMT, error) { 55 | packets := []packet.Packet{} 56 | var pkt = &packet.Packet{} 57 | var err error 58 | var pmt psi.PMT 59 | 60 | pmtAcc := packet.NewAccumulator(psi.PmtAccumulatorDoneFunc) 61 | done := false 62 | 63 | for !done { 64 | if _, err := io.ReadFull(r, pkt[:]); err != nil { 65 | if err == io.EOF || err == io.ErrUnexpectedEOF { 66 | return nil, nil, gots.ErrPMTNotFound 67 | } 68 | return nil, nil, err 69 | } 70 | currPid := pkt.PID() 71 | if currPid != pid { 72 | continue 73 | } 74 | packets = append(packets, *pkt) 75 | 76 | _, err = pmtAcc.WritePacket(pkt) 77 | if err == gots.ErrAccumulatorDone { 78 | pmt, err = psi.NewPMT(pmtAcc.Bytes()) 79 | if err != nil { 80 | return nil, nil, err 81 | } 82 | if len(pmt.Pids()) == 0 { 83 | done = false 84 | pmtAcc = packet.NewAccumulator(psi.PmtAccumulatorDoneFunc) 85 | continue 86 | } 87 | done = true 88 | } else if err != nil { 89 | return nil, nil, err 90 | } 91 | } 92 | 93 | return packets, pmt, nil 94 | } 95 | 96 | func WritePacket(pkt *packet.Packet, w io.Writer) error { 97 | _, err := w.Write(pkt[:]) 98 | return err 99 | } 100 | 101 | func RemoveFileIfExists(file string) error { 102 | // Remove the file if it exists 103 | if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { 104 | // file does not exist 105 | return nil 106 | } 107 | 108 | err := os.Remove(file) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func OpenFileAndAppend(file string) (*os.File, error) { 117 | // Create and append to the new file 118 | fo, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 119 | if err != nil { 120 | return nil, fmt.Errorf("creating output file %w", err) 121 | } 122 | 123 | return fo, nil 124 | } 125 | 126 | func ParseAstitsElementaryStreamInfo(es *astits.PMTElementaryStream) *ElementaryStreamInfo { 127 | var streamInfo *ElementaryStreamInfo 128 | switch es.StreamType { 129 | case astits.StreamTypeH264Video: 130 | streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "AVC", Type: "video"} 131 | case astits.StreamTypeAACAudio: 132 | streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "AAC", Type: "audio"} 133 | case astits.StreamTypeH265Video: 134 | streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "HEVC", Type: "video"} 135 | case astits.StreamTypeSCTE35: 136 | streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "SCTE35", Type: "cue"} 137 | case astits.StreamTypePrivateData: 138 | streamInfo = &ElementaryStreamInfo{PID: es.ElementaryPID, Codec: "PrivateData", Type: "data"} 139 | default: 140 | return nil 141 | } 142 | for _, d := range es.ElementaryStreamDescriptors { 143 | switch d.Tag { 144 | case astits.DescriptorTagISO639LanguageAndAudioType: 145 | l := d.ISO639LanguageAndAudioType 146 | fmt.Printf("Language: %s\n", l.Language) 147 | case astits.DescriptorTagDataStreamAlignment: 148 | a := d.DataStreamAlignment 149 | log.Printf("PID %d: Descriptor Data stream alignment: %d\n", es.ElementaryPID, a.Type) 150 | case astits.DescriptorTagRegistration: 151 | r := d.Registration 152 | switch r.FormatIdentifier { 153 | case ANC_REGISTERED_IDENTIFIER: 154 | streamInfo.Codec = "SMPTE-2038" 155 | streamInfo.Type = "ANC" 156 | } 157 | case ANC_DESCRIPTOR_TAG: 158 | if streamInfo.Type != "ANC" { 159 | log.Printf("PID %d: bad combination of descriptor 0xc4 and no preceding ANC", es.ElementaryPID) 160 | continue 161 | } 162 | u := d.UserDefined 163 | log.Printf("PID %d: Got ancillary descriptor with data: %q\n", es.ElementaryPID, hex.EncodeToString(u)) 164 | default: 165 | // Nothing 166 | } 167 | } 168 | 169 | return streamInfo 170 | } 171 | 172 | func ParseElementaryStreamInfo(es psi.PmtElementaryStream) *ElementaryStreamInfo { 173 | pid := uint16(es.ElementaryPid()) 174 | var streamInfo *ElementaryStreamInfo 175 | switch es.StreamType() { 176 | case psi.PmtStreamTypeMpeg4VideoH264: 177 | streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "AVC", Type: "video"} 178 | case psi.PmtStreamTypeAac: 179 | streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "AAC", Type: "audio"} 180 | case psi.PmtStreamTypeMpeg4VideoH265: 181 | streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "HEVC", Type: "video"} 182 | case psi.PmtStreamTypeScte35: 183 | streamInfo = &ElementaryStreamInfo{PID: pid, Codec: "SCTE35", Type: "cue"} 184 | } 185 | 186 | return streamInfo 187 | } 188 | 189 | func ParsePacketToPAT(pkt *packet.Packet) (pat psi.PAT, e error) { 190 | if packet.IsPat(pkt) { 191 | pay, err := packet.Payload(pkt) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | pat, err = psi.NewPAT(pay) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | return pat, nil 202 | } 203 | 204 | return nil, fmt.Errorf("unable to parse packet to PAT") 205 | } 206 | 207 | // Check if two sets contain same elements 208 | func IsTwoSlicesOverlapping(s1 []int, s2 []int) bool { 209 | intersection := GetIntersectionOfTwoSlices(s1, s2) 210 | return len(intersection) != 0 211 | } 212 | 213 | // Return a set that contains those elements of s1 that are also in s2 214 | func GetIntersectionOfTwoSlices(s1 []int, s2 []int) []int { 215 | intersection := []int{} 216 | for _, e := range s1 { 217 | if slices.Contains(s2, e) { 218 | intersection = append(intersection, e) 219 | } 220 | } 221 | 222 | return intersection 223 | } 224 | 225 | // Return a set that contains those elements of s1 that are NOT in s2 226 | func GetDifferenceOfTwoSlices(s1 []int, s2 []int) []int { 227 | difference := []int{} 228 | for _, e := range s1 { 229 | if !slices.Contains(s2, e) { 230 | difference = append(difference, e) 231 | } 232 | } 233 | 234 | return difference 235 | } 236 | 237 | func ParsePidsFromString(input string) []int { 238 | words := strings.Fields(input) 239 | var pids []int 240 | for _, word := range words { 241 | number, err := strconv.Atoi(word) 242 | if err != nil { 243 | continue 244 | } 245 | pids = append(pids, number) 246 | } 247 | return pids 248 | } 249 | 250 | func ParseParams(function OptionParseFunc) (o Options, inFile string) { 251 | o = function() 252 | if o.Version { 253 | fmt.Printf("ts-info version %s\n", GetVersion()) 254 | os.Exit(0) 255 | } 256 | if len(flag.Args()) < 1 { 257 | flag.Usage() 258 | os.Exit(1) 259 | } 260 | inFile = flag.Args()[0] 261 | return o, inFile 262 | } 263 | 264 | func Execute(w io.Writer, o Options, inFile string, function RunableFunc) error { 265 | // Create a cancellable context in case you want to stop reading packets/data any time you want 266 | ctx, cancel := context.WithCancel(context.Background()) 267 | // Handle SIGTERM signal 268 | ch := make(chan os.Signal, 1) 269 | signal.Notify(ch, syscall.SIGINT) 270 | go func() { 271 | <-ch 272 | cancel() 273 | }() 274 | 275 | var f io.Reader 276 | if inFile == "-" { 277 | f = os.Stdin 278 | } else { 279 | var err error 280 | fh, err := os.Open(inFile) 281 | if err != nil { 282 | log.Fatal(err) 283 | } 284 | f = fh 285 | defer fh.Close() 286 | } 287 | 288 | err := function(ctx, w, f, o) 289 | if err != nil { 290 | return err 291 | } 292 | return nil 293 | } 294 | -------------------------------------------------------------------------------- /internal/version.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | var ( 10 | commitVersion string = "v0.2.1" // May be updated using build flags 11 | commitDate string = "1706176128" // commitDate in Epoch seconds (may be overridden using build flags) 12 | ) 13 | 14 | // GetVersion - get version and also commitHash and commitDate if inserted via Makefile 15 | func GetVersion() string { 16 | seconds, _ := strconv.Atoi(commitDate) 17 | if commitDate != "" { 18 | t := time.Unix(int64(seconds), 0) 19 | return fmt.Sprintf("%s, date: %s", commitVersion, t.Format("2006-01-02")) 20 | } 21 | return commitVersion 22 | } 23 | --------------------------------------------------------------------------------