Repository: Eyevinn/mp4ff
Pull request #377 (2024-09-27 15:53:29)

Feat: improved prft
Improved PrftBox implementation as well as NTP timestamp interpretation

### Changed

- CreatePrftBox now takes flags parameter
- PrftBox Info output

### Added

- NTP64 struct with methods to convert to time.Time
- Constants for PrftBox flags



Patch:
diff --git a/.gitignore b/.gitignore
index 52b215f..67ae1c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,6 @@
 vendor/
 
 /mp4/test_data/out_init.cmfv
+
+# VS Code settings
+.vscode/
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index ee1c233..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "markdownlint.config": {
-        "MD002": false,
-        "MD024": {
-            "siblings_only": true
-        },
-        "MD041": false,
-        "MD046": false
-    }
-}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a5120a..826f1bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Changed
+
+- CreatePrftBox now takes flags parameter
+- PrftBox Info output
+
+### Added
+
+- NTP64 struct with methods to convert to time.Time
+- Constants for PrftBox flags
+
 ### Fixed
 
 - Allow missing optional DecoderSpecificInfo
diff --git a/mp4/ntp64.go b/mp4/ntp64.go
new file mode 100644
index 0000000..61e1c05
--- /dev/null
+++ b/mp4/ntp64.go
@@ -0,0 +1,47 @@
+package mp4
+
+import "time"
+
+const (
+	NTPEpochOffset = 2208988800 // NTP epoch is 1900, Unix epoch is 1970
+)
+
+// NTP64 is NTP timestamp in RFC 5905 64-bit format: uint32 seconds since 1900-01-01, and uint32 fraction.
+type NTP64 uint64
+
+// Seconds returns integral seconds part of NTP64
+func (n NTP64) Seconds() uint32 {
+	return uint32(n >> 32)
+}
+
+// UTCSeconds returns seconds of NTP64 shifted to UNIX epoch.
+func (n NTP64) UTCSeconds() uint64 {
+	return uint64(n.Seconds()) - NTPEpochOffset
+}
+
+// Fraction returns 32-bit fractional part of NTP64.
+func (n NTP64) Fraction() uint32 {
+	return uint32(n & 0xffffffff)
+}
+
+// UTC returns NTP64 as UTC time in seconds.
+func (n NTP64) UTC() float64 {
+	return float64(n.UTCSeconds()) + float64(n.Fraction())/float64(1<<32)
+}
+
+// Time returns NTP64 as time.Time in UTC.
+func (n NTP64) Time() time.Time {
+	return time.Unix(int64(n.UTCSeconds()), int64(float64(n.Fraction())*1.e9/float64((1<<32)))).UTC()
+}
+
+// NewNTP64 creates NTP64 from UTC time in seconds.
+func NewNTP64(utcTime float64) NTP64 {
+	seconds := uint64(utcTime)
+	fraction := uint64((utcTime - float64(seconds)) * float64(1<<32))
+	return NTP64((seconds+NTPEpochOffset)<<32 | fraction)
+}
+
+// String returns NTP64 as UTC time in string format.
+func (n NTP64) String() string {
+	return n.Time().String()
+}
diff --git a/mp4/prft.go b/mp4/prft.go
index c4058ab..d6ce393 100644
--- a/mp4/prft.go
+++ b/mp4/prft.go
@@ -6,6 +6,24 @@ import (
 	"github.com/Eyevinn/mp4ff/bits"
 )
 
+const (
+	PrftTimeEncoderInput       = 0
+	PrftTimeEncoderOutput      = 1
+	PrftTimeMoofFinalized      = 2
+	PrftTimeMoofWritten        = 4
+	PrftTimeArbitraryConsitent = 8
+	PrftTimeCaptured           = 24
+)
+
+var PrftFlagsInterpretation = map[uint32]string{
+	PrftTimeEncoderInput:       "time_encoder_input",
+	PrftTimeEncoderOutput:      "time_encoder_output",
+	PrftTimeMoofFinalized:      "time_moof_finalized",
+	PrftTimeMoofWritten:        "time_moof_written",
+	PrftTimeArbitraryConsitent: "time_arbitrary_consistent",
+	PrftTimeCaptured:           "time_captured",
+}
+
 // PrftBox - Producer Reference Box (prft)
 //
 // Contained in File before moof box
@@ -13,15 +31,15 @@ type PrftBox struct {
 	Version          byte
 	Flags            uint32
 	ReferenceTrackID uint32
-	NTPTimestamp     uint64
+	NTPTimestamp     NTP64
 	MediaTime        uint64
 }
 
 // CreatePrftBox creates a new PrftBox.
-func CreatePrftBox(version byte, refTrackID uint32, ntp, mediatime uint64) *PrftBox {
+func CreatePrftBox(version byte, flags, refTrackID uint32, ntp NTP64, mediatime uint64) *PrftBox {
 	return &PrftBox{
 		Version:          version,
-		Flags:            0,
+		Flags:            flags,
 		ReferenceTrackID: refTrackID,
 		NTPTimestamp:     ntp,
 		MediaTime:        mediatime,
@@ -44,7 +62,7 @@ func DecodePrftSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, err
 	version := byte(versionAndFlags >> 24)
 	flags := versionAndFlags & flagsMask
 	refTrackID := sr.ReadUint32()
-	ntp := sr.ReadUint64()
+	ntp := NTP64(sr.ReadUint64())
 	var mediatime uint64
 	if version == 0 {
 		mediatime = uint64(sr.ReadUint32())
@@ -92,7 +110,7 @@ func (b *PrftBox) EncodeSW(sw bits.SliceWriter) error {
 	versionAndFlags := (uint32(b.Version) << 24) + b.Flags
 	sw.WriteUint32(versionAndFlags)
 	sw.WriteUint32(b.ReferenceTrackID)
-	sw.WriteUint64(b.NTPTimestamp)
+	sw.WriteUint64(uint64(b.NTPTimestamp))
 	if b.Version == 0 {
 		sw.WriteUint32(uint32(b.MediaTime))
 	} else {
@@ -105,7 +123,16 @@ func (b *PrftBox) EncodeSW(sw bits.SliceWriter) error {
 func (b *PrftBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) error {
 	bd := newInfoDumper(w, indent, b, int(b.Version), b.Flags)
 	bd.write(" - referenceTrackID: %d", b.ReferenceTrackID)
-	bd.write(" - ntpTimestamp: %d", b.NTPTimestamp)
+	bd.write(" - type: %s", b.InterpretFlags())
+	bd.write(" - ntpTimestamp: %s (%d)", b.NTPTimestamp, b.NTPTimestamp)
 	bd.write(" - mediaTime: %d", b.MediaTime)
 	return bd.err
 }
+
+// InterpretFlags - return string representation of flags.
+func (b *PrftBox) InterpretFlags() string {
+	if interpretation, ok := PrftFlagsInterpretation[b.Flags]; ok {
+		return interpretation
+	}
+	return "unknown"
+}


Test patch:
diff --git a/mp4/ntp64_test.go b/mp4/ntp64_test.go
new file mode 100644
index 0000000..702b556
--- /dev/null
+++ b/mp4/ntp64_test.go
@@ -0,0 +1,42 @@
+package mp4_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/Eyevinn/mp4ff/mp4"
+)
+
+func TestNTP64(t *testing.T) {
+	cases := []struct {
+		desc         string
+		ntp          mp4.NTP64
+		expectedUTC  float64
+		expectedTime time.Time
+	}{
+		{"1970-01-01", mp4.NTP64(mp4.NTPEpochOffset << 32), 0, time.Unix(0, 0).UTC()},
+		{"+1s", mp4.NTP64((mp4.NTPEpochOffset + 1) << 32), 1.0, time.Unix(1, 0).UTC()},
+		{"+0.5s", mp4.NTP64((mp4.NTPEpochOffset << 32) + (1 << 31)), 0.5, time.Unix(0, 500_000_000).UTC()},
+		{"1.25s", mp4.NewNTP64(1.25), 1.25, time.Unix(1, 250_000_000).UTC()},
+		{"2024-date", mp4.NewNTP64(1725828561.125), 1725828561.125, time.Unix(1725828561, 125_000_000).UTC()},
+	}
+
+	for _, c := range cases {
+		t.Run(c.desc, func(t *testing.T) {
+			if c.ntp.UTC() != c.expectedUTC {
+				t.Errorf("Expected %f, got %f", c.expectedUTC, c.ntp.UTC())
+			}
+			if c.ntp.Time() != c.expectedTime {
+				t.Errorf("Expected %s, got %s", c.expectedTime, c.ntp.Time())
+			}
+		})
+	}
+}
+
+func TestNTP64String(t *testing.T) {
+	ntp := mp4.NewNTP64(1725612561.375)
+	expected := "2024-09-06 08:49:21.375 +0000 UTC"
+	if ntp.String() != expected {
+		t.Errorf("Expected %s, got %s", expected, ntp.String())
+	}
+}
diff --git a/mp4/prft_test.go b/mp4/prft_test.go
index b96e883..5556c53 100644
--- a/mp4/prft_test.go
+++ b/mp4/prft_test.go
@@ -9,8 +9,8 @@ import (
 
 func TestPrft(t *testing.T) {
 	prfts := []*PrftBox{
-		CreatePrftBox(0, 1, 8998, 98),
-		CreatePrftBox(1, 2, 8998, 98),
+		CreatePrftBox(0, PrftTimeCaptured, 1, 8998, 98),
+		CreatePrftBox(1, PrftTimeEncoderOutput, 2, 8998, 98),
 	}
 	for _, prft := range prfts {
 		boxDiffAfterEncodeAndDecode(t, prft)