Skip to content

Commit

Permalink
feat: File.UpdateSidx to update or add a top-level sidx box
Browse files Browse the repository at this point in the history
  • Loading branch information
tobbee committed Feb 13, 2024
1 parent 011bf5d commit a0c3201
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [Unreleased]

### Added

- InitSegment.TweakSingleTrakLive changes an init segment to fit live streaming
- File.UpdateSidx to update or add a top level sidx box for a fragmented file

### Fixed

Expand Down
53 changes: 53 additions & 0 deletions examples/add-sidx/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// add-sidx adds a top-level sidx box describing the segments of a fragmented files.
//
// Segments are identified by styp boxes if they exist, otherwise by
// the start of moof or emsg boxes.
package main

import (
"flag"
"fmt"
"log"
"os"

"github.com/Eyevinn/mp4ff/mp4"
)

func main() {

usePTO := flag.Bool("nzept", false, "Use non-zero earliestPresentationTime")
flag.Parse()
args := flag.Args()
if len(args) != 2 {
fmt.Println("Usage: add-sidx <input.mp4> <output.mp4>")
return
}

err := run(args[0], args[1], *usePTO)
if err != nil {
log.Fatal(err)
}
}

func run(inPath, outPath string, nonZeroEPT bool) error {
inFile, err := mp4.ReadMP4File(inPath)
if err != nil {
return err
}

err = inFile.UpdateSidx(true /* addIfNotExists */, nonZeroEPT)
if err != nil {
return fmt.Errorf("addSidx failed: %w", err)
}

w, err := os.Create(outPath)
if err != nil {
return fmt.Errorf("cannot create output file: %w", err)
}
defer w.Close()
err = inFile.Encode(w)
if err != nil {
return fmt.Errorf("failed to encode output file: %w", err)
}
return nil
}
23 changes: 23 additions & 0 deletions examples/add-sidx/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"testing"

"github.com/Eyevinn/mp4ff/mp4"
)

func TestAddSidx(t *testing.T) {
sidxOut := "testV300_sidx.mp4"
inPath := "../resegmenter/testdata/testV300.mp4"
err := run(inPath, sidxOut, false)
if err != nil {
t.Error(err)
}
reRead, err := mp4.ReadMP4File(sidxOut)
if err != nil {
t.Error(err)
}
if reRead.Sidx == nil {
t.Error("No sidx box added")
}
}
151 changes: 151 additions & 0 deletions mp4/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,157 @@ func (f *File) CopySampleData(w io.Writer, rs io.ReadSeeker, trak *TrakBox,
return nil
}

func (f *File) UpdateSidx(addIfNotExists, nonZeroEPT bool) error {

if !f.IsFragmented() {
return fmt.Errorf("input file is not fragmented")
}

initSeg := f.Init
if initSeg == nil {
return fmt.Errorf("input file does not have an init segment")
}

segs := f.Segments
if len(segs) == 0 {
return fmt.Errorf("input file does not have any media segments")
}
exists := f.Sidx != nil
if !exists && !addIfNotExists {
return nil
}

refTrak := findReferenceTrak(initSeg)
trex, ok := initSeg.Moov.Mvex.GetTrex(refTrak.Tkhd.TrackID)
if !ok {
return fmt.Errorf("no trex box found for track %d", refTrak.Tkhd.TrackID)
}
segDatas, err := findSegmentData(segs, refTrak, trex)
if err != nil {
return fmt.Errorf("failed to find segment data: %w", err)
}

var sidx *SidxBox
if exists {
sidx = f.Sidx
} else {
sidx = &SidxBox{}
}
fillSidx(sidx, refTrak, segDatas, nonZeroEPT)
if !exists {
err = insertSidx(f, segDatas, sidx)
if err != nil {
return fmt.Errorf("failed to insert sidx box: %w", err)
}
}
return nil
}

func findReferenceTrak(initSeg *InitSegment) *TrakBox {
var trak *TrakBox
for _, trak = range initSeg.Moov.Traks {
if trak.Mdia.Hdlr.HandlerType == "vide" {
return trak
}
}
for _, trak = range initSeg.Moov.Traks {
if trak.Mdia.Hdlr.HandlerType == "soun" {
return trak
}
}
return initSeg.Moov.Traks[0]
}

type segData struct {
startPos uint64
presentationTime uint64
baseDecodeTime uint64
dur uint32
size uint32
}

func findSegmentData(segs []*MediaSegment, refTrak *TrakBox, trex *TrexBox) ([]segData, error) {
segDatas := make([]segData, 0, len(segs))
for _, seg := range segs {
frag := seg.Fragments[0]
for _, traf := range frag.Moof.Trafs {
tfhd := traf.Tfhd
if tfhd.TrackID == refTrak.Tkhd.TrackID {
// Found the track that the sidx should be based on
baseTime := traf.Tfdt.BaseMediaDecodeTime()
dur := uint32(0)
var firstCompositionTimeOffest int64
for i, trun := range traf.Truns {
trun.AddSampleDefaultValues(tfhd, trex)
samples := trun.GetSamples()
for j, sample := range samples {
if i == 0 && j == 0 {
firstCompositionTimeOffest = int64(sample.CompositionTimeOffset)
}
dur += sample.Dur
}
}
sd := segData{
startPos: seg.StartPos,
presentationTime: uint64(int64(baseTime) + firstCompositionTimeOffest),
baseDecodeTime: baseTime,
dur: dur,
size: uint32(seg.Size()),
}
segDatas = append(segDatas, sd)
break
}
}
}
return segDatas, nil
}

func fillSidx(sidx *SidxBox, refTrak *TrakBox, segDatas []segData, nonZeroEPT bool) {
ept := uint64(0)
if nonZeroEPT {
ept = segDatas[0].presentationTime
}
sidx.Version = 1
sidx.Timescale = refTrak.Mdia.Mdhd.Timescale
sidx.ReferenceID = 1
sidx.EarliestPresentationTime = ept
sidx.FirstOffset = 0
sidx.SidxRefs = make([]SidxRef, 0, len(segDatas))

for _, segData := range segDatas {
size := segData.size
sidx.SidxRefs = append(sidx.SidxRefs, SidxRef{
ReferencedSize: size,
SubSegmentDuration: segData.dur,
StartsWithSAP: 1,
SAPType: 1,
})
}
}

func insertSidx(inFile *File, segDatas []segData, sidx *SidxBox) error {
// insert sidx box before first media segment
// TODO. Handle case where startPos is not reliable. Maybe first box of first segment
firstMediaBox, err := inFile.Segments[0].FirstBox()
if err != nil {
return fmt.Errorf("could not find position to insert sidx box: %w", err)
}
var mediaStartIdx = 0
for i, ch := range inFile.Children {
if ch == firstMediaBox {
mediaStartIdx = i
break
}
}
if mediaStartIdx == 0 {
return fmt.Errorf("could not find position to insert sidx box")
}
inFile.Children = append(inFile.Children[:mediaStartIdx], append([]Box{sidx}, inFile.Children[mediaStartIdx:]...)...)
inFile.Sidx = sidx
inFile.Sidxs = []*SidxBox{sidx}
return nil
}

func min(a, b int) int {
if a < b {
return a
Expand Down
26 changes: 26 additions & 0 deletions mp4/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,29 @@ func TestGetSegmentBoundariesFromMfra(t *testing.T) {
t.Errorf("not 3 segments in file but %d", len(parsedFile.Segments))
}
}

func TestUpdateSidx(t *testing.T) {
file, err := os.Open("./testdata/prog_8s_dec_dashinit.mp4")
if err != nil {
t.Error(err)
}

parsedFile, err := DecodeFile(file)
if err != nil {
t.Error(err)
}
err = parsedFile.UpdateSidx(false, false)
if err != nil {
t.Error(err)
}
if parsedFile.Sidx != nil {
t.Error("sidx should not be present")
}
err = parsedFile.UpdateSidx(true, false)
if err != nil {
t.Error(err)
}
if parsedFile.Sidx == nil {
t.Error("sidx should be present")
}
}
16 changes: 16 additions & 0 deletions mp4/mediasegment.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,19 @@ func (s *MediaSegment) CommonSampleDuration(trex *TrexBox) (uint32, error) {
}
return commonDur, nil
}

// FirstBox returns the first box in the segment, or an error if no boxes are found.
func (s *MediaSegment) FirstBox() (Box, error) {
if s.Styp != nil {
return s.Styp, nil
}
if len(s.Sidxs) > 0 {
return s.Sidxs[0], nil
}
if len(s.Fragments) > 0 {
if len(s.Fragments[0].Children) > 0 {
return s.Fragments[0].Children[0], nil
}
}
return nil, fmt.Errorf("no boxes in segment")
}

0 comments on commit a0c3201

Please sign in to comment.