Repository: Eyevinn/mp4ff
Pull request #423 (2025-05-07 14:37:23)

feat: export TFHD flag constants for external use
Hey,

I wanted to use these constants when I noticed they aren't actually exposed. This PR aims to export them in a similar manner like how it was done in trun's case:

https://github.com/Eyevinn/mp4ff/blob/5e414b1f5b54ef5147fe4c039df0b81975b98de2/mp4/trun.go#L22-L27

I wonder if there is a reason for these to be local in the first place.

PS: I hope the CI will let it pass without adding more test coverage as there isn't much to add here for extra testing. :relaxed: 

Diniboy1123: Thanks for the explanation. I think it makes sense for it to be public since the raw `Flags` value is also public. And also to have matching API to `trun` boxes.

My use-case is again twisty. I am the guy who makes that on the fly mss to mpd repackager. Subtitles in the source stream are stpp segments. In mss the subtitle timestamps are apparently relative to the segment start time so I have to convert that to absolute. Of course for that I need to unpack the TTML data from `mdat` and do my thing on it. If I replace `mdat` that's not enough, because mp4ff won't recalculate the trun and tfhd sample sizes automatically. I experimented a lot and figured that if I delete all samples from `trun`, add an empty one there and set `DefaultSampleSize` and `~Duration` inside `tfhd` suddenly all players will display the sub. Of course that isn't ideal when I have multiple samples inside `mdat`, but I haven't encountered such a case yet and VLC doesn't seem to like if I do this from truns. I can't use VLC much since most of the content is DRM protected, but I prefer to cover more players...

And yeah, I need to alter flags manually since I set the DefaultSampleSize.
tobbee: @Diniboy1123  Thanks for the context. It is a nuisance that the stpp/TTML timing is relative in MSS segments and absolute in DASH.  

I have some code for changing the content of an STPP segment in [livesim2](https://github.com/Dash-Industry-Forum/livesim2/blob/9a2ee0a04b4687fc61e2bc40e8cae8def1119251/cmd/livesim2/app/livesegment.go#L816)

As you can see there, it is enough to change the mdat payload and then change the default Tfhd size if there is one, or change the sample size in trun if the size is there. With that approach, there is no need to change the flags....

Patch:
diff --git a/mp4/tfhd.go b/mp4/tfhd.go
index a30b508..1dba165 100644
--- a/mp4/tfhd.go
+++ b/mp4/tfhd.go
@@ -6,13 +6,13 @@ import (
 	"github.com/Eyevinn/mp4ff/bits"
 )
 
-const baseDataOffsetPresent uint32 = 0x000001
-const sampleDescriptionIndexPresent uint32 = 0x000002
-const defaultSampleDurationPresent uint32 = 0x000008
-const defaultSampleSizePresent uint32 = 0x000010
-const defaultSampleFlagsPresent uint32 = 0x000020
-const durationIsEmpty uint32 = 0x010000
-const defaultBaseIsMoof uint32 = 0x020000
+const TfhdBaseDataOffsetPresentFlag uint32 = 0x000001
+const TfhdSampleDescriptionIndexPresentFlag uint32 = 0x000002
+const TfhdDefaultSampleDurationPresentFlag uint32 = 0x000008
+const TfhdDefaultSampleSizePresentFlag uint32 = 0x000010
+const TfhdDefaultSampleFlagsPresentFlag uint32 = 0x000020
+const TfhdDurationIsEmptyFlag uint32 = 0x010000
+const TfhdDefaultBaseIsMoofFlag uint32 = 0x020000
 
 // TfhdBox - Track Fragment Header Box (tfhd)
 //
@@ -75,7 +75,7 @@ func CreateTfhd(trackID uint32) *TfhdBox {
 	// The only flag set is defaultBaseIsMoof
 	tfhd := &TfhdBox{
 		Version:                0,
-		Flags:                  defaultBaseIsMoof,
+		Flags:                  TfhdDefaultBaseIsMoofFlag,
 		TrackID:                trackID,
 		BaseDataOffset:         0,
 		SampleDescriptionIndex: 1,
@@ -88,37 +88,37 @@ func CreateTfhd(trackID uint32) *TfhdBox {
 
 // HasBaseDataOffset - interpreted flags value
 func (t *TfhdBox) HasBaseDataOffset() bool {
-	return t.Flags&baseDataOffsetPresent != 0
+	return t.Flags&TfhdBaseDataOffsetPresentFlag != 0
 }
 
 // HasSampleDescriptionIndex - interpreted flags value
 func (t *TfhdBox) HasSampleDescriptionIndex() bool {
-	return t.Flags&sampleDescriptionIndexPresent != 0
+	return t.Flags&TfhdSampleDescriptionIndexPresentFlag != 0
 }
 
 // HasDefaultSampleDuration - interpreted flags value
 func (t *TfhdBox) HasDefaultSampleDuration() bool {
-	return t.Flags&defaultSampleDurationPresent != 0
+	return t.Flags&TfhdDefaultSampleDurationPresentFlag != 0
 }
 
 // HasDefaultSampleSize - interpreted flags value
 func (t *TfhdBox) HasDefaultSampleSize() bool {
-	return t.Flags&defaultSampleSizePresent != 0
+	return t.Flags&TfhdDefaultSampleSizePresentFlag != 0
 }
 
 // HasDefaultSampleFlags - interpreted flags value
 func (t *TfhdBox) HasDefaultSampleFlags() bool {
-	return t.Flags&defaultSampleFlagsPresent != 0
+	return t.Flags&TfhdDefaultSampleFlagsPresentFlag != 0
 }
 
 // DurationIsEmpty - interpreted flags value
 func (t *TfhdBox) DurationIsEmpty() bool {
-	return t.Flags&durationIsEmpty != 0
+	return t.Flags&TfhdDurationIsEmptyFlag != 0
 }
 
 // DefaultBaseIfMoof - interpreted flags value
 func (t *TfhdBox) DefaultBaseIfMoof() bool {
-	return t.Flags&defaultBaseIsMoof != 0
+	return t.Flags&TfhdDefaultBaseIsMoofFlag != 0
 }
 
 // Type - returns box type
@@ -190,7 +190,7 @@ func (t *TfhdBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string
 	bd := newInfoDumper(w, indent, t, int(t.Version), t.Flags)
 	bd.write(" - trackID: %d", t.TrackID)
 
-	if t.Flags&defaultBaseIsMoof != 0 {
+	if t.Flags&TfhdDefaultBaseIsMoofFlag != 0 {
 		bd.write(" - defaultBaseIsMoof: true")
 	}
 
diff --git a/mp4/traf.go b/mp4/traf.go
index 7f90b2c..921f06a 100644
--- a/mp4/traf.go
+++ b/mp4/traf.go
@@ -202,7 +202,7 @@ func (t *TrafBox) OptimizeTfhdTrun() error {
 		}
 		if hasCommonDur {
 			// Set defaultSampleDuration in tfhd and remove from trun
-			tfhd.Flags = tfhd.Flags | defaultSampleDurationPresent
+			tfhd.Flags = tfhd.Flags | TfhdDefaultSampleDurationPresentFlag
 			tfhd.DefaultSampleDuration = commonDur
 			trun.Flags = trun.Flags & ^TrunSampleDurationPresentFlag
 		}
@@ -219,7 +219,7 @@ func (t *TrafBox) OptimizeTfhdTrun() error {
 		}
 		if hasCommonSize {
 			// Set defaultSampleSize in tfhd and remove from trun
-			tfhd.Flags = tfhd.Flags | defaultSampleSizePresent
+			tfhd.Flags = tfhd.Flags | TfhdDefaultSampleSizePresentFlag
 			tfhd.DefaultSampleSize = commonSize
 			trun.Flags = trun.Flags & ^TrunSampleSizePresentFlag
 		}
@@ -241,7 +241,7 @@ func (t *TrafBox) OptimizeTfhdTrun() error {
 			if firstSampleFlags != commonSampleFlags {
 				trun.SetFirstSampleFlags(firstSampleFlags)
 			}
-			tfhd.Flags = tfhd.Flags | defaultSampleFlagsPresent
+			tfhd.Flags = tfhd.Flags | TfhdDefaultSampleFlagsPresentFlag
 			tfhd.DefaultSampleFlags = commonSampleFlags
 			trun.Flags = trun.Flags & ^TrunSampleFlagsPresentFlag
 		}
@@ -282,6 +282,16 @@ func (t *TrafBox) RemoveEncryptionBoxes() uint64 {
 				nrBytesRemoved += ch.Size()
 				t.UUIDSenc = nil
 			}
+		case *SbgpBox:
+			if box.GroupingType == "seig" {
+				nrBytesRemoved += ch.Size()
+				t.Sbgp = nil
+			}
+		case *SgpdBox:
+			if box.GroupingType == "seig" {
+				nrBytesRemoved += ch.Size()
+				t.Sgpd = nil
+			}
 		default:
 			remainingChildren = append(remainingChildren, ch)
 		}


Test patch:
diff --git a/mp4/traf_test.go b/mp4/traf_test.go
index e10bed6..a1947f9 100644
--- a/mp4/traf_test.go
+++ b/mp4/traf_test.go
@@ -75,6 +75,82 @@ func TestTrafTrunWithoutOptimization(t *testing.T) {
 		runEncodeDecode(t, test, optimization)
 	}
 }
+func TestRemoveEncryptionBoxes(t *testing.T) {
+	// Create a traf box with various child boxes
+	traf := &mp4.TrafBox{}
+
+	// Add boxes that should be removed
+	saizBox := &mp4.SaizBox{Version: 0}
+	saioBox := &mp4.SaioBox{Version: 0}
+	sencBox := &mp4.SencBox{Version: 0}
+	uuidSencBox := &mp4.UUIDBox{}
+	_ = uuidSencBox.SetUUID(mp4.UUIDPiffSenc)
+	uuidSencBox.Senc = &mp4.SencBox{Version: 0}
+	uuidSencBox.Senc = &mp4.SencBox{Version: 0}
+	sbgpSeigBox := &mp4.SbgpBox{GroupingType: "seig"}
+	sgpdSeigBox := &mp4.SgpdBox{GroupingType: "seig"}
+	boxesToBeRemoved := []mp4.Box{saizBox, saioBox, sencBox, uuidSencBox, sbgpSeigBox, sgpdSeigBox}
+
+	// Add boxes that should NOT be removed
+	tfhdBox := &mp4.TfhdBox{}
+	trunBox := mp4.CreateTrun(0)
+
+	// Add all boxes to traf
+	_ = traf.AddChild(saizBox)
+	_ = traf.AddChild(saioBox)
+	_ = traf.AddChild(sencBox)
+	_ = traf.AddChild(uuidSencBox)
+	_ = traf.AddChild(sbgpSeigBox)
+	_ = traf.AddChild(sgpdSeigBox)
+	_ = traf.AddChild(tfhdBox)
+	_ = traf.AddChild(trunBox)
+
+	// Original children count
+	originalChildrenCount := len(traf.Children)
+
+	//
+	expectedBytesRemoved := saizBox.Size() + saioBox.Size() + sencBox.Size() +
+		uuidSencBox.Size() + sbgpSeigBox.Size() + sgpdSeigBox.Size()
+
+	// Call RemoveEncryptionBoxes
+	bytesRemoved := traf.RemoveEncryptionBoxes()
+
+	// Verify expected number of bytes removed (6 boxes * mockBoxSize)
+	if bytesRemoved != expectedBytesRemoved {
+		t.Errorf("Expected %d bytes removed, got %d", expectedBytesRemoved, bytesRemoved)
+	}
+
+	// Verify correct number of children remain
+	expectedRemainingChildren := originalChildrenCount - len(boxesToBeRemoved)
+	if len(traf.Children) != expectedRemainingChildren {
+		t.Errorf("Expected %d children remaining, got %d", expectedRemainingChildren, len(traf.Children))
+	}
+
+	// Verify encryption box pointers are nil
+	if traf.Saiz != nil {
+		t.Error("Saiz box was not removed")
+	}
+	if traf.Saio != nil {
+		t.Error("Saio box was not removed")
+	}
+	if traf.Senc != nil {
+		t.Error("Senc box was not removed")
+	}
+	if traf.Sbgp != nil && traf.Sbgp.GroupingType == "seig" {
+		t.Error("Sbgp box with GroupingType 'seig' was not removed")
+	}
+	if traf.Sgpd != nil && traf.Sgpd.GroupingType == "seig" {
+		t.Error("Sgpd box with GroupingType 'seig' was not removed")
+	}
+
+	// Verify Tfhd and Trun are still present
+	if traf.Tfhd == nil {
+		t.Error("Tfhd was incorrectly removed")
+	}
+	if traf.Trun == nil {
+		t.Error("Trun was incorrectly removed")
+	}
+}
 
 func runEncodeDecode(t *testing.T, test testSamples, withOptimization bool) {
 	traf := createTestTrafBox()