asticode / go-astits

Demux and mux MPEG Transport Streams (.ts) natively in GO

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Incorrect PTS calculation on 32 bit platforms

jidckii opened this issue · comments

Hello.
I use your library for native prober
There was a need for distribution under raspberry pi (arm 32)
And I found that I get incorrect PTS values with 32 bit architecture.

my code:

package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"

	astits "github.com/asticode/go-astits"
)

var (
	filename = flag.String("f", "", "path to ts file")
	verbose  = flag.Bool("verbose", false, "verbose duration")
)

func main() {
	flag.Parse()

	// Create a cancellable context in case you want to stop reading packets/data any time you want
	ctx, cancel := context.WithCancel(context.Background())

	// Handle SIGTERM signal
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGTERM)
	go func() {
		<-ch
		cancel()
	}()

	durationBasedPTS, startTime, err := GetChunkFileDuration(ctx, *filename, *verbose)
	if err != nil {
		fmt.Printf("Failed to open file: %v\n", err)
		return
	}
	fmt.Printf("start_time: [%v]\n", startTime)
	fmt.Printf("duration: [%v]\n", durationBasedPTS)
}

// GetChunkFileDuration find PTS and calculate  duration
func GetChunkFileDuration(ctx context.Context, filename string, verbose bool) (time.Duration, time.Duration, error) {
	var (
		firstPTSDuration time.Duration
		lastPTSDuration  time.Duration
	)

	// Open your file or initialize any kind of io.Reader
	f, err := os.Open(filename)
	if err != nil {
		return 0, 0, err
	}
	defer f.Close()
	var esCount int
	// Create the demuxer
	dmx := astits.New(ctx, f)
	for {
		d, err := dmx.NextData()
		if err != nil {
			if err == astits.ErrNoMorePackets {
				durationPTS := lastPTSDuration - firstPTSDuration
				if durationPTS != 0 {
					duration := CalculateDuration(esCount, durationPTS, verbose)
					return duration, firstPTSDuration, nil
				}
			}
			// Если есть ошибка и это не индикатор окончания данных, то
			// возвращаем саму ошибку
			return 0, 0, err
		}

		// Если тип элементарного стрима video, то считываем
		// значение PTS
		if d.PES != nil {
			typeES := ESType(d.PES.Header.StreamID)
			if typeES == "video" {
				ptsDuration := d.PES.Header.OptionalHeader.PTS.Duration()
				if verbose {
					fmt.Printf("PTS: %v\n", ptsDuration)
				}
				esCount++
				if firstPTSDuration == 0 {
					firstPTSDuration = ptsDuration
				} else {
					lastPTSDuration = ptsDuration
				}
			}
		}
	}
}

// ESType get content type in ES
func ESType(currentType uint8) string {
	// ref:
	// https://github.com/asticode/go-astits/blob/master/data_pes.go#L51
	// https://en.wikipedia.org/wiki/Packetized_elementary_stream
	var (
		StreamIDVideo = []byte{0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF}
		StreamIDAudio = []byte{0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF}
	)
	streamIDMap := map[string][]byte{
		"video": StreamIDVideo,
		"audio": StreamIDAudio,
	}
	for name, bytes := range streamIDMap {
		for _, b := range bytes {
			if b == currentType {
				return name
			}
		}
	}
	return "unknown"
}

// CalculateDuration calculate fps and duration 1 frame.
// And adds up the duration of the PTS and the duration of 1 frame.
func CalculateDuration(esCount int, durationPTS time.Duration, verbose bool) time.Duration {
	durationPTSInt := int(durationPTS)
	fps := (esCount * 1000) / (durationPTSInt / 1000000)
	if verbose {
		fmt.Printf("FPS: [%v]\n", fps)
	}
	var frameDuration = (((1 / float32(fps)) * 1000) * 1000) * 1000
	frameDurationInt := int(frameDuration)
	durationInt := durationPTSInt + frameDurationInt
	return time.Duration(durationInt)
}

test fie : https://yadi.sk/d/Emx9YKJvHNv-1Q
run on amd64:

$ go run main.go -f testdata/ref.ts -verbose
PTS: 11.4s
PTS: 11.44s
PTS: 11.48s
PTS: 11.52s
PTS: 11.56s
PTS: 11.6s
PTS: 11.64s
PTS: 11.68s
PTS: 11.72s
PTS: 11.76s
PTS: 11.8s
PTS: 11.84s
PTS: 11.88s
PTS: 11.92s
PTS: 11.96s
PTS: 12s
PTS: 12.04s
PTS: 12.08s
PTS: 12.12s
PTS: 12.16s
PTS: 12.2s
PTS: 12.24s
PTS: 12.28s
PTS: 12.32s
PTS: 12.36s
PTS: 12.4s
PTS: 12.44s
PTS: 12.48s
PTS: 12.52s
PTS: 12.56s
PTS: 12.6s
PTS: 12.64s
PTS: 12.68s
PTS: 12.72s
PTS: 12.76s
PTS: 12.8s
PTS: 12.84s
PTS: 12.88s
PTS: 12.92s
PTS: 12.96s
PTS: 13s
PTS: 13.04s
PTS: 13.08s
PTS: 13.12s
PTS: 13.16s
PTS: 13.2s
PTS: 13.24s
PTS: 13.28s
PTS: 13.32s
PTS: 13.36s
FPS: [25]
start_time: [11.4s]
duration: [2s]

Build for 386 and run:
(on arm 32, actually)

$ GOOS=linux GOARCH=386 go build -o go-probe main.go

$ ./go-probe -f testdata/ref.ts -verbose
PTS: 11.471µs
PTS: 20.554µs
PTS: -18.085µs
PTS: -9.003µs
PTS: 79ns
PTS: 9.161µs
PTS: 18.243µs
PTS: -20.395µs
PTS: -11.313µs
PTS: -2.231µs
PTS: 6.85µs
PTS: 15.933µs
PTS: -22.706µs
PTS: -13.624µs
PTS: -4.541µs
PTS: 4.54µs
PTS: 13.622µs
PTS: 22.705µs
PTS: -15.934µs
PTS: -6.852µs
PTS: 2.23µs
PTS: 11.312µs
PTS: 20.394µs
PTS: -18.244µs
PTS: -9.162µs
PTS: -80ns
PTS: 9.002µs
PTS: 18.084µs
PTS: -20.555µs
PTS: -11.472µs
PTS: -2.39µs
PTS: 6.691µs
PTS: 15.773µs
PTS: -22.865µs
PTS: -13.783µs
PTS: -4.701µs
PTS: 4.381µs
PTS: 13.463µs
PTS: 22.545µs
PTS: -16.093µs
PTS: -7.011µs
PTS: 2.07µs
PTS: 11.153µs
PTS: 20.235µs
PTS: -18.404µs
PTS: -9.321µs
PTS: -239ns
PTS: 8.842µs
PTS: 17.924µs
PTS: -20.714µs
esCount:  50
durationPTSInt:  -32185
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.CalculateDuration(0x32, 0xffff8247, 0xffffffff, 0xa496601, 0xa4d0f4c, 0x1)
	/home/jidckii/go/src/gitlab.com/yuccastream/go-probe/main.go:123 +0x26c
main.GetChunkFileDuration(0x839c9c0, 0xa49aaa0, 0xfff2bf56, 0xf, 0xa496601, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
	/home/jidckii/go/src/gitlab.com/yuccastream/go-probe/main.go:65 +0x3fd
main.main()
	/home/jidckii/go/src/gitlab.com/yuccastream/go-probe/main.go:34 +0x14c

Very nice find.

I've pushed a fix on master and also created a v1.4.0 tag.

Can you confirm this is working properly?

Now everything is correct.
Thank you so much.