From c99844b9afd1e98e606c8dcd1ca008031e3ecce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B6rn=20Einarson?= Date: Mon, 12 Feb 2024 23:11:10 +0100 Subject: [PATCH 1/2] New add-sidx example and supporting function. fix: GetTrex return values feat: File.UpdateSidx to update or add a top-level sidx box feat: Can remove unused enc boxes feat: New decoder option to segment on moof start feat: New moov.IsEncrypted() method fix: make add-sidx work with unused senc and piff boxes fix: GetTrex now returns proper OK values --- CHANGELOG.md | 16 +- Makefile | 4 + examples/add-sidx/main.go | 127 +++++++++++++ examples/add-sidx/main_test.go | 93 ++++++++++ .../testdata/clear_with_enc_boxes.mp4 | Bin 0 -> 15330 bytes mp4/boxsr.go | 10 +- mp4/file.go | 172 +++++++++++++++++- mp4/file_test.go | 26 +++ mp4/mediasegment.go | 16 ++ mp4/moov.go | 18 ++ mp4/mvex.go | 4 +- mp4/senc.go | 9 + mp4/traf.go | 4 +- mp4/uuid.go | 2 + 14 files changed, 487 insertions(+), 14 deletions(-) create mode 100644 examples/add-sidx/main.go create mode 100644 examples/add-sidx/main_test.go create mode 100644 examples/add-sidx/testdata/clear_with_enc_boxes.mp4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 01abf0bb..aaa6a6f0 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 a599aa37..af1ca8f8 100644 --- a/Makefile +++ b/Makefile @@ -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 00000000..a50204ed --- /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 00000000..c2e96241 --- /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 0000000000000000000000000000000000000000..51d47a1d9fd6604feb7ff4ac8131e4aae25c5285 GIT binary patch literal 15330 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 LQ~aE73qt<^4Q>-* literal 0 HcmV?d00001 diff --git a/mp4/boxsr.go b/mp4/boxsr.go index f6e1afea..c74a89de 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 75173940..6883515a 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 8e55e24e..01b964cb 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 05f12f37..e85a6f63 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 7df45a3d..7b5f13d0 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 31dc44be..5cad0c30 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 0a66527c..bae211a5 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 240fe5ea..84a301e8 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 a4951098..56b56121 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) } From 7a6a5fa5de4d6dc5ebc8dce979ea5873acd83c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B6rn=20Einarson?= Date: Fri, 19 Apr 2024 12:46:18 +0200 Subject: [PATCH 2/2] fix: replace wvttlister with subslister in Makefile --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index af1ca8f8..4eb25cab 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