Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the output ftyp. Encrypted content, unsupported edit lists, zero
timescales, truncated byte ranges, and payloads larger than the input
file are rejected
- `mp4.Defragment` and `mp4.DefragmentTracks` resolve overlapping fragments:
when a fragment's tfdt re-declares an earlier decode time (a
retransmission), the fragment appearing later in the file wins and the
superseded samples are dropped at sample granularity, whether or not the
re-sent bytes are identical. Every abandoned time range must be declared
again by surviving fragments, so no declared content is ever silently
dropped. Ambiguous overlaps fail closed: cuts inside a sample, abandoned
time ranges that no surviving later fragment declares again, and
overlapping files whose fragments use absolute base data offsets are
rejected
- `mp4.Defragment` and `mp4.DefragmentTracks` accept hybrid files that carry
progressive samples in the moov before the first fragment: the progressive
samples come first with their chunk structure preserved (also when stco
offsets are not monotone), and the fragment samples are appended.
Fragments may supersede progressive samples under the same
later-declarer-wins and coverage rules as fragment overlaps, and hostile
sample tables are validated before any count-proportional allocation
- `SttsBox.SampleDurations`, `CttsBox.CompositionTimeOffsets`, and
`StssBox.SampleIsSync` expand a whole sample table into one value per
sample, validating the entries against the declared sample count
- `mp4ff-defragment` command line tool exposing the defragmentation with
optional track selection

Expand Down
29 changes: 29 additions & 0 deletions mp4/ctts.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ func (b *CttsBox) AddSampleCountsAndOffset(counts []uint32, offsets []int32) err
return nil
}

// CompositionTimeOffsets returns the offset of each of nrSamples samples,
// validating that the entries cover exactly that many samples. A box
// without entries covers no samples.
func (b *CttsBox) CompositionTimeOffsets(nrSamples uint32) ([]int32, error) {
ctss := make([]int32, nrSamples)
if len(b.EndSampleNr) < 2 {
if nrSamples == 0 {
return ctss, nil
}
return nil, fmt.Errorf("ctts covers 0 samples, not the %d declared", nrSamples)
}
if b.EndSampleNr[len(b.EndSampleNr)-1] != nrSamples {
return nil, fmt.Errorf("ctts covers %d samples, not the %d declared",
b.EndSampleNr[len(b.EndSampleNr)-1], nrSamples)
}
for i := 0; i < b.NrSampleCount(); i++ {
// The decoded EndSampleNr accumulation can wrap uint32, so the final
// entry alone proves nothing about the intermediate ones.
if b.EndSampleNr[i] > b.EndSampleNr[i+1] || b.EndSampleNr[i+1] > nrSamples {
return nil, fmt.Errorf("ctts entry %d covers samples (%d, %d], outside the %d declared samples",
i+1, b.EndSampleNr[i], b.EndSampleNr[i+1], nrSamples)
}
for nr := b.EndSampleNr[i]; nr < b.EndSampleNr[i+1]; nr++ {
ctss[nr] = b.SampleOffset[i]
}
}
return ctss, nil
}

// GetCompositionTimeOffset - composition time offset for (one-based) sampleNr in track timescale.
// Returns 0 for a sampleNr beyond the samples covered by this box.
func (b *CttsBox) GetCompositionTimeOffset(sampleNr uint32) int32 {
Expand Down
27 changes: 27 additions & 0 deletions mp4/ctts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,30 @@ func TestGetCompositionTimeOffsetOutsideBox(t *testing.T) {
t.Errorf("decoded empty ctts: got cto %d instead of 0", cto)
}
}

func TestCttsCompositionTimeOffsets(t *testing.T) {
ctts := &mp4.CttsBox{}
if err := ctts.AddSampleCountsAndOffset([]uint32{2, 1}, []int32{100, -200}); err != nil {
t.Fatal(err)
}
offsets, err := ctts.CompositionTimeOffsets(3)
if err != nil {
t.Fatal(err)
}
want := []int32{100, 100, -200}
for i := range want {
if offsets[i] != want[i] {
t.Errorf("offset %d is %d, want %d", i, offsets[i], want[i])
}
}
if _, err := ctts.CompositionTimeOffsets(2); err == nil {
t.Error("entries covering more samples than declared must be an error")
}
empty := &mp4.CttsBox{}
if offsets, err := empty.CompositionTimeOffsets(0); err != nil || len(offsets) != 0 {
t.Errorf("empty box with 0 samples gave %v, %v", offsets, err)
}
if _, err := empty.CompositionTimeOffsets(3); err == nil {
t.Error("an empty box covering declared samples must be an error")
}
}
Loading
Loading