diff --git a/CHANGELOG.md b/CHANGELOG.md index 01abf0b..aaa6a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,18 +3,28 @@ 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 -- New TryDecodeMfro function -- New mp4ff-subslister tool replacing mp4ff-wvttlister, but also supporting stpp +- New `TryDecodeMfro` function +- New `mp4ff-subslister` tool replacing `mp4ff-wvttlister`. It supports `wvtt` and `stpp` +- `File.UpdateSidx()` to update or add a top level sidx box for a fragmented file +- `mp4.DecStartSegmentOnMoof` flag to make the Decoder interpret every moof as + a new segment start, unless styp, sidx, or mfra boxes give that information. +- New example `add-sidx` shows how on can add a top-level `sidx` box to a fragmented file. + It further has the option to remove unused encryption boxes, and to interpret each + moof box as starting a new segment. +- New method `MoovBox.IsEncrypted()` checks if an encrypted codec is signaled ### Fixed - More robust check for mfro at the end of file +- GetTrex() return value +- Can now write PIFF `uuid` box that has previously been read +- Does now avoid the second parsing of `senc` box if the file is ot encrypted as seen in moov box. ### Removed diff --git a/Makefile b/Makefile index a599aa3..4eb25ca 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,13 @@ all: test check coverage build .PHONY: build -build: mp4ff-crop mp4ff-decrypt mp4ff-encrypt mp4ff-info mp4ff-nallister mp4ff-pslister mp4ff-wvttlister examples +build: mp4ff-crop mp4ff-decrypt mp4ff-encrypt mp4ff-info mp4ff-nallister mp4ff-pslister mp4ff-subslister examples .PHONY: prepare prepare: go mod vendor -mp4ff-crop mp4ff-decrypt mp4ff-encrypt mp4ff-info mp4ff-nallister mp4ff-pslister mp4ff-wvttlister: +mp4ff-crop mp4ff-decrypt mp4ff-encrypt mp4ff-info mp4ff-nallister mp4ff-pslister mp4ff-subslister: go build -ldflags "-X github.com/Eyevinn/mp4ff/mp4.commitVersion=$$(git describe --tags HEAD) -X github.com/Eyevinn/mp4ff/mp4.commitDate=$$(git log -1 --format=%ct)" -o out/$@ ./cmd/$@/main.go .PHONY: examples @@ -21,6 +21,10 @@ initcreator multitrack resegmenter segmenter: test: prepare go test ./... +.PHONY: testsum +testsum: prepare + gotestsum + .PHONY: coverage coverage: # Ignore (allow) packages without any tests diff --git a/examples/add-sidx/main.go b/examples/add-sidx/main.go new file mode 100644 index 0000000..a50204e --- /dev/null +++ b/examples/add-sidx/main.go @@ -0,0 +1,127 @@ +// 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" + "io" + "log" + "os" + "strings" + + "github.com/Eyevinn/mp4ff/mp4" +) + +var usg = `Usage of add-sidx: + +add-sidx adds a top-level sidx box to a fragmented file provided it does not exist. +If styp boxes are present, they signal new segments. It is possible to interpret +every moof box as the start of a new segment, by specifying the "-startSegOnMoof" option. +One can further remove unused encryption boxes with the "-removeEnc" option. + + +` + +var usage = func() { + parts := strings.Split(os.Args[0], "/") + name := parts[len(parts)-1] + fmt.Fprintln(os.Stderr, usg) + fmt.Fprintf(os.Stderr, "%s [options] \n", name) + flag.PrintDefaults() +} + +func main() { + removeEncBoxes := flag.Bool("removeEnc", false, "Remove unused encryption boxes") + usePTO := flag.Bool("nzEPT", false, "Use non-zero earliestPresentationTime") + segOnMoof := flag.Bool("startSegOnMoof", false, "Start a new segment on every moof") + version := flag.Bool("version", false, "Get mp4ff version") + + flag.Parse() + + if *version { + fmt.Printf("add-sidx %s\n", mp4.GetVersion()) + os.Exit(0) + } + flag.Parse() + + if *version { + fmt.Printf("add-sidx %s\n", mp4.GetVersion()) + os.Exit(0) + } + + args := flag.Args() + if len(args) != 2 { + fmt.Fprintf(os.Stderr, "must specify infile and outfile\n") + usage() + os.Exit(1) + } + + inFilePath := flag.Arg(0) + outFilePath := flag.Arg(1) + + ifd, err := os.Open(inFilePath) + if err != nil { + fmt.Fprintln(os.Stderr, err) + usage() + os.Exit(1) + } + defer ifd.Close() + ofd, err := os.Create(outFilePath) + if err != nil { + fmt.Fprintln(os.Stderr, err) + usage() + os.Exit(1) + } + defer ofd.Close() + err = run(ifd, ofd, *usePTO, *removeEncBoxes, *segOnMoof) + if err != nil { + log.Fatal(err) + } +} + +func run(in io.Reader, out io.Writer, nonZeroEPT, removeEncBoxes, segOnMoof bool) error { + var flags mp4.DecFileFlags + if segOnMoof { + flags |= mp4.DecStartOnMoof + } + mp4Root, err := mp4.DecodeFile(in, mp4.WithDecodeFlags(flags)) + if err != nil { + return err + } + fmt.Printf("creating sidx with %d segment(s)\n", len(mp4Root.Segments)) + + if removeEncBoxes { + removeEncryptionBoxes(mp4Root) + } + + addIfNotExists := true + err = mp4Root.UpdateSidx(addIfNotExists, nonZeroEPT) + if err != nil { + return fmt.Errorf("addSidx failed: %w", err) + } + + err = mp4Root.Encode(out) + if err != nil { + return fmt.Errorf("failed to encode output file: %w", err) + } + return nil +} + +func removeEncryptionBoxes(inFile *mp4.File) { + for _, seg := range inFile.Segments { + for _, frag := range seg.Fragments { + bytesRemoved := uint64(0) + for _, traf := range frag.Moof.Trafs { + bytesRemoved += traf.RemoveEncryptionBoxes() + } + for _, traf := range frag.Moof.Trafs { + for _, trun := range traf.Truns { + trun.DataOffset -= int32(bytesRemoved) + } + } + } + } +} diff --git a/examples/add-sidx/main_test.go b/examples/add-sidx/main_test.go new file mode 100644 index 0000000..c2e9624 --- /dev/null +++ b/examples/add-sidx/main_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "bytes" + "os" + "testing" + + "github.com/Eyevinn/mp4ff/mp4" +) + +func TestAddSidx(t *testing.T) { + inPath := "testdata/clear_with_enc_boxes.mp4" + testCases := []struct { + desc string + inPath string + removeEnc bool + segOnMoof bool + wantedNrSegs uint32 + wantedSize uint32 + wantedFirstDur uint32 + }{ + { + desc: "sidx, enc boxes, 1 segment", + inPath: inPath, + removeEnc: false, + segOnMoof: false, + wantedNrSegs: 1, + wantedFirstDur: 2 * 144144, + }, + { + desc: "sidx, enc boxes, many segments", + inPath: inPath, + removeEnc: false, + segOnMoof: true, + wantedNrSegs: 2, + wantedFirstDur: 144144, + }, + { + desc: "sidx, no enc boxes, many segments", + inPath: inPath, + removeEnc: true, + segOnMoof: true, + wantedNrSegs: 2, + wantedFirstDur: 144144, + }, + { + desc: "normal file with styp", + inPath: "../resegmenter/testdata/testV300.mp4", + removeEnc: false, + segOnMoof: false, + wantedNrSegs: 4, + wantedFirstDur: 180000, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + in, err := os.Open(tc.inPath) + if err != nil { + t.Error(err) + } + out := bytes.Buffer{} + err = run(in, &out, false, tc.removeEnc, tc.segOnMoof) + if err != nil { + return + } + decOut, err := mp4.DecodeFile(&out) + if err != nil { + t.Error() + } + if decOut.Sidx == nil { + t.Error("no sidx box") + } + sidxEntries := decOut.Sidx.SidxRefs + gotNrEntries := len(sidxEntries) + if gotNrEntries != int(tc.wantedNrSegs) { + t.Errorf("got %d sidx entries instead of %d", gotNrEntries, tc.wantedNrSegs) + } + if sidxEntries[0].SubSegmentDuration != tc.wantedFirstDur { + t.Errorf("got first duration %d instead of %d", sidxEntries[0].SubSegmentDuration, tc.wantedFirstDur) + } + if tc.removeEnc { + for _, seg := range decOut.Segments { + for _, frag := range seg.Fragments { + if frag.Moof.Traf.Senc != nil { + t.Error("senc is still present in fragment") + } + } + } + } + }) + } +} diff --git a/examples/add-sidx/testdata/clear_with_enc_boxes.mp4 b/examples/add-sidx/testdata/clear_with_enc_boxes.mp4 new file mode 100644 index 0000000..51d47a1 Binary files /dev/null and b/examples/add-sidx/testdata/clear_with_enc_boxes.mp4 differ diff --git a/mp4/boxsr.go b/mp4/boxsr.go index f6e1afe..c74a89d 100644 --- a/mp4/boxsr.go +++ b/mp4/boxsr.go @@ -225,17 +225,21 @@ LoopBoxes: moof := box.(*MoofBox) for _, traf := range moof.Trafs { if ok, parsed := traf.ContainsSencBox(); ok && !parsed { + isEncrypted := true defaultIVSize := byte(0) // Should get this from tenc in sinf if f.Moov != nil { trackID := traf.Tfhd.TrackID + isEncrypted = f.Moov.IsEncrypted(trackID) sinf := f.Moov.GetSinf(trackID) if sinf != nil && sinf.Schi != nil && sinf.Schi.Tenc != nil { defaultIVSize = sinf.Schi.Tenc.DefaultPerSampleIVSize } } - err = traf.ParseReadSenc(defaultIVSize, moof.StartPos) - if err != nil { - return nil, err + if isEncrypted { + err = traf.ParseReadSenc(defaultIVSize, moof.StartPos) + if err != nil { + return nil, err + } } } } diff --git a/mp4/file.go b/mp4/file.go index 7517394..6883515 100644 --- a/mp4/file.go +++ b/mp4/file.go @@ -69,7 +69,11 @@ type DecFileFlags uint32 const ( DecNoFlags DecFileFlags = 0 // DecISMFlag tries to read mfra box at end to find segment boundaries (for ISM files) - DecISMFlag DecFileFlags = 1 + DecISMFlag DecFileFlags = (1 << 0) + // DecStartOnMoof starts a segment at each moof boundary + // This is provided no styp, or sidx/mfra box gives other information + DecStartOnMoof = (1 << 1) + // if no styp box, or sidx/mfra strudture ) // EncOptimize - encoder optimization mode @@ -197,17 +201,21 @@ LoopBoxes: moof := box.(*MoofBox) for _, traf := range moof.Trafs { if ok, parsed := traf.ContainsSencBox(); ok && !parsed { + isEncrypted := true defaultIVSize := byte(0) // Should get this from tenc in sinf if f.Moov != nil { trackID := traf.Tfhd.TrackID + isEncrypted = f.Moov.IsEncrypted(trackID) sinf := f.Moov.GetSinf(trackID) if sinf != nil && sinf.Schi != nil && sinf.Schi.Tenc != nil { defaultIVSize = sinf.Schi.Tenc.DefaultPerSampleIVSize } } - err = traf.ParseReadSenc(defaultIVSize, moof.StartPos) - if err != nil { - return nil, err + if isEncrypted { // Don't do if encryption boxes still remain, but are not + err = traf.ParseReadSenc(defaultIVSize, moof.StartPos) + if err != nil { + return nil, err + } } } } @@ -318,6 +326,8 @@ func (f *File) startSegmentIfNeeded(b Box, boxStartPos uint64) { if boxStartPos == uint64(f.tfra.Entries[idx].MoofOffset) { segStart = true } + case (f.fileDecFlags & DecStartOnMoof) != 0: + segStart = true default: segStart = (idx == 0) } @@ -701,6 +711,160 @@ 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 +} + +// findSegmentData returns a slice of segment media data using a reference track. +func findSegmentData(segs []*MediaSegment, refTrak *TrakBox, trex *TrexBox) ([]segData, error) { + segDatas := make([]segData, 0, len(segs)) + for _, seg := range segs { + var firstCompositionTimeOffest int64 + dur := uint32(0) + var baseTime uint64 + for fIdx, frag := range seg.Fragments { + for _, traf := range frag.Moof.Trafs { + tfhd := traf.Tfhd + if tfhd.TrackID == refTrak.Tkhd.TrackID { // Find track that gives sidx time values + if fIdx == 0 { + baseTime = traf.Tfdt.BaseMediaDecodeTime() + } + for i, trun := range traf.Truns { + trun.AddSampleDefaultValues(tfhd, trex) + samples := trun.GetSamples() + for j, sample := range samples { + if fIdx == 0 && 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) + } + 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 diff --git a/mp4/file_test.go b/mp4/file_test.go index 8e55e24..01b964c 100644 --- a/mp4/file_test.go +++ b/mp4/file_test.go @@ -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") + } +} diff --git a/mp4/mediasegment.go b/mp4/mediasegment.go index 05f12f3..e85a6f6 100644 --- a/mp4/mediasegment.go +++ b/mp4/mediasegment.go @@ -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") +} diff --git a/mp4/moov.go b/mp4/moov.go index 7df45a3..7b5f13d 100644 --- a/mp4/moov.go +++ b/mp4/moov.go @@ -157,3 +157,21 @@ func (m *MoovBox) GetSinf(trackID uint32) *SinfBox { } return nil } + +// IsEncrypted returns true if SampleEntryBox is "encv" or "enca" +func (m *MoovBox) IsEncrypted(trackID uint32) bool { + for _, trak := range m.Traks { + if trak.Tkhd.TrackID == trackID { + stsd := trak.Mdia.Minf.Stbl.Stsd + sd := stsd.Children[0] // Get first (and only) + switch box := sd.(type) { + case *VisualSampleEntryBox: + return box.Type() == "encv" + case *AudioSampleEntryBox: + return box.Type() == "enca" + } + } + } + return false + +} diff --git a/mp4/mvex.go b/mp4/mvex.go index 31dc44b..5cad0c3 100644 --- a/mp4/mvex.go +++ b/mp4/mvex.go @@ -97,8 +97,8 @@ func (m *MvexBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string func (m *MvexBox) GetTrex(trackID uint32) (trex *TrexBox, ok bool) { for _, trex := range m.Trexs { if trex.TrackID == trackID { - return trex, false + return trex, true } } - return nil, true + return nil, false } diff --git a/mp4/senc.go b/mp4/senc.go index 0a66527..bae211a 100644 --- a/mp4/senc.go +++ b/mp4/senc.go @@ -284,10 +284,19 @@ func (s *SencBox) EncodeSW(sw bits.SliceWriter) error { if err != nil { return err } + err = s.EncodeSWNoHdr(sw) + return err +} +// EncodeSWNoHdr encodes without header (useful for PIFF box) +func (s *SencBox) EncodeSWNoHdr(sw bits.SliceWriter) error { versionAndFlags := (uint32(s.Version) << 24) + s.Flags sw.WriteUint32(versionAndFlags) sw.WriteUint32(s.SampleCount) + if s.readButNotParsed { + sw.WriteBytes(s.rawData) + return sw.AccError() + } perSampleIVSize := s.GetPerSampleIVSize() for i := 0; i < int(s.SampleCount); i++ { if perSampleIVSize > 0 { diff --git a/mp4/traf.go b/mp4/traf.go index 240fe5e..84a301e 100644 --- a/mp4/traf.go +++ b/mp4/traf.go @@ -89,8 +89,8 @@ func (t *TrafBox) ParseReadSenc(defaultIVSize byte, moofStartPos uint64) error { posFromSaio := t.Saio.Offset[0] + int64(moofStartPos) if uint64(posFromSaio) != senc.StartPos+16 { //TODO. Reenable - //return fmt.Errorf("offset from saio (%d) and moof differs from senc data start %d", posFromSaio, senc.StartPos+16) - fmt.Printf("offset from saio (%d) and moof differs from senc data start %d", posFromSaio, senc.StartPos+16) + return fmt.Errorf("offset from saio (%d) relative moof start differs from senc data start %d", posFromSaio, senc.StartPos+16) + //fmt.Printf("offset from saio (%d) and moof differs from senc data start %d\n", posFromSaio, senc.StartPos+16) } } diff --git a/mp4/uuid.go b/mp4/uuid.go index a495109..56b5612 100644 --- a/mp4/uuid.go +++ b/mp4/uuid.go @@ -197,6 +197,8 @@ func (b *UUIDBox) EncodeSW(sw bits.SliceWriter) error { err = b.Tfxd.encode(sw) case u.Equal(uuidTfrf): err = b.Tfrf.encode(sw) + case u.Equal(uuidPiffSenc): + err = b.Senc.EncodeSWNoHdr(sw) default: sw.WriteBytes(b.UnknownPayload) }