From 59a77254f5100c9efd0874275510c11cee71d0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B6rn=20Einarson?= Date: Fri, 29 Sep 2023 14:40:02 +0200 Subject: [PATCH] fix: make wvttlister worke with Unified Streaming wvtt ismt file --- CHANGELOG.md | 18 +++- cmd/mp4ff-wvttlister/main.go | 65 +++++++++----- cmd/mp4ff-wvttlister/main_test.go | 81 ++++++++++++++++++ .../testdata/sample_short.ismt | Bin 0 -> 1521 bytes mp4/file.go | 27 ++++-- mp4/fragment.go | 9 +- mp4/mediasegment.go | 1 + mp4/mfra.go | 10 +++ mp4/mfra_test.go | 10 +-- mp4/tfra.go | 54 +++++++----- 10 files changed, 217 insertions(+), 58 deletions(-) create mode 100644 cmd/mp4ff-wvttlister/main_test.go create mode 100644 cmd/mp4ff-wvttlister/testdata/sample_short.ismt diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f0497a..a46642fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet +### Changed + +- TfraEntry Time and MoofOffset types changed to unsigned +- TfraEntr attribute name SampleDelta corrected to SampleNumber + +### Added + +- MediaSegment and Fragment have new StartPos attribute +- mp4.File now has Mfra pointer +- MfraBox has new method FindEntry + +### Fixed + +- mp4ff-wvttlister works with Unified Streaming wvtt ismt file +- Fragment.GetFullSamples() allows tfdt to be absent +- Fragment.GetFullSamples() defaults to offset being moof +- mp4ff-wvttlister works for Unified Streaming wvtt asset ## [0.38.1] - 2023-09-22 diff --git a/cmd/mp4ff-wvttlister/main.go b/cmd/mp4ff-wvttlister/main.go index bd143045..968c7ec6 100644 --- a/cmd/mp4ff-wvttlister/main.go +++ b/cmd/mp4ff-wvttlister/main.go @@ -5,6 +5,7 @@ import ( "bytes" "flag" "fmt" + "io" "log" "os" "strings" @@ -15,7 +16,7 @@ import ( var usg = `Usage of mp4ff-wvttlister: mp4ff-wvttlister lists and displays content of wvtt (WebVTT in ISOBMFF) samples. -Use track with given non-zero track ID or first wvtt track found in an asset. +Uses track with given non-zero track ID or first wvtt track found in an asset. ` var usage = func() { @@ -49,26 +50,33 @@ func main() { log.Fatalln(err) } defer ifd.Close() - parsedMp4, err := mp4.DecodeFile(ifd) + + err = run(ifd, os.Stdout, *trackID, *maxNrSamples) if err != nil { log.Fatal(err) } +} + +func run(ifd io.ReadSeeker, w io.Writer, trackID, maxNrSamples int) error { + parsedMp4, err := mp4.DecodeFile(ifd, mp4.WithDecodeFlags(mp4.DecISMFlag)) + if err != nil { + return err + } if !parsedMp4.IsFragmented() { // Progressive file - err = parseProgressiveMp4(parsedMp4, uint32(*trackID), *maxNrSamples) + err = parseProgressiveMp4(parsedMp4, w, uint32(trackID), maxNrSamples) if err != nil { - fmt.Printf("Error: %s\n", err) - os.Exit(1) + return err } - return + return nil } // Fragmented file - err = parseFragmentedMp4(parsedMp4, uint32(*trackID), *maxNrSamples) + err = parseFragmentedMp4(parsedMp4, w, uint32(trackID), maxNrSamples) if err != nil { - fmt.Printf("Error: %s\n", err) - os.Exit(1) + return err } + return nil } func findTrack(moov *mp4.MoovBox, hdlrType string, trackID uint32) (*mp4.TrakBox, error) { @@ -84,10 +92,10 @@ func findTrack(moov *mp4.MoovBox, hdlrType string, trackID uint32) (*mp4.TrakBox } return inTrak, nil } - return nil, fmt.Errorf("No matching track found") + return nil, fmt.Errorf("no matching track found") } -func parseProgressiveMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { +func parseProgressiveMp4(f *mp4.File, w io.Writer, trackID uint32, maxNrSamples int) error { wvttTrak, err := findTrack(f.Moov, "text", trackID) if err != nil { return err @@ -95,10 +103,10 @@ func parseProgressiveMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { stbl := wvttTrak.Mdia.Minf.Stbl if stbl.Stsd.Wvtt == nil { - return fmt.Errorf("No wvtt track found") + return fmt.Errorf("no wvtt track found") } - fmt.Printf("Track %d, timescale = %d\n", wvttTrak.Tkhd.TrackID, wvttTrak.Mdia.Mdhd.Timescale) + fmt.Fprintf(w, "Track %d, timescale = %d\n", wvttTrak.Tkhd.TrackID, wvttTrak.Mdia.Mdhd.Timescale) err = stbl.Stsd.Wvtt.VttC.Info(os.Stdout, "", " ", " ") if err != nil { return err @@ -129,7 +137,7 @@ func parseProgressiveMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { // Next find sample bytes as slice in mdat offsetInMdatData := uint64(offset) - mdatPayloadStart sample := mdat.Data[offsetInMdatData : offsetInMdatData+uint64(size)] - err = printWvttSample(sample, sampleNr, decTime+uint64(cto), dur) + err = printWvttSample(w, sample, sampleNr, decTime+uint64(cto), dur) if err != nil { return err } @@ -140,7 +148,7 @@ func parseProgressiveMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { return nil } -func parseFragmentedMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { +func parseFragmentedMp4(f *mp4.File, w io.Writer, trackID uint32, maxNrSamples int) error { var wvttTrex *mp4.TrexBox if f.Init != nil { // Print vttC header and timescale if moov-box is present wvttTrak, err := findTrack(f.Init.Moov, "text", trackID) @@ -150,11 +158,11 @@ func parseFragmentedMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { stbl := wvttTrak.Mdia.Minf.Stbl if stbl.Stsd.Wvtt == nil { - return fmt.Errorf("No wvtt track found") + return fmt.Errorf("no wvtt track found") } - fmt.Printf("Track %d, timescale = %d\n", wvttTrak.Tkhd.TrackID, wvttTrak.Mdia.Mdhd.Timescale) - err = stbl.Stsd.Wvtt.VttC.Info(os.Stdout, "", " ", " ") + fmt.Fprintf(w, "Track %d, timescale = %d\n", wvttTrak.Tkhd.TrackID, wvttTrak.Mdia.Mdhd.Timescale) + err = stbl.Stsd.Wvtt.VttC.Info(w, " ", "", " ") if err != nil { return err } @@ -167,16 +175,29 @@ func parseFragmentedMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { iSamples := make([]mp4.FullSample, 0) for _, iSeg := range f.Segments { for _, iFrag := range iSeg.Fragments { + var tfraTime uint64 + if f.Mfra != nil { + moofOffset := iFrag.Moof.StartPos + entry := f.Mfra.FindEntry(moofOffset, iFrag.Moof.Traf.Tfhd.TrackID) + if entry != nil { + tfraTime = entry.Time + } + } fSamples, err := iFrag.GetFullSamples(wvttTrex) if err != nil { return err } + if tfraTime != 0 && fSamples[0].DecodeTime == 0 { + for i := range fSamples { + fSamples[i].DecodeTime += tfraTime + } + } iSamples = append(iSamples, fSamples...) } } var err error for i, sample := range iSamples { - err = printWvttSample(sample.Data, i+1, sample.PresentationTime(), sample.Dur) + err = printWvttSample(w, sample.Data, i+1, sample.PresentationTime(), sample.Dur) if err != nil { return err @@ -188,12 +209,12 @@ func parseFragmentedMp4(f *mp4.File, trackID uint32, maxNrSamples int) error { return nil } -func printWvttSample(sample []byte, nr int, pts uint64, dur uint32) error { - fmt.Printf("Sample %d, pts=%d, dur=%d\n", nr, pts, dur) +func printWvttSample(w io.Writer, sample []byte, nr int, pts uint64, dur uint32) error { + fmt.Fprintf(w, "Sample %d, pts=%d, dur=%d\n", nr, pts, dur) buf := bytes.NewBuffer(sample) box, err := mp4.DecodeBox(0, buf) if err != nil { return err } - return box.Info(os.Stdout, "", " ", " ") + return box.Info(w, " ", "", " ") } diff --git a/cmd/mp4ff-wvttlister/main_test.go b/cmd/mp4ff-wvttlister/main_test.go new file mode 100644 index 00000000..5dce3bd5 --- /dev/null +++ b/cmd/mp4ff-wvttlister/main_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "bytes" + "os" + "strings" + "testing" +) + +var wanted = `Track 1, timescale = 1000 +[vttC] size=14 + - config: "WEBVTT" +Sample 1, pts=0, dur=6640 +[vttc] size=52 + [sttg] size=18 + - settings: align:left + [payl] size=26 + - cueText: "..." +Sample 2, pts=6640, dur=320 +[vtte] size=8 +Sample 3, pts=6960, dur=3040 +[vttc] size=129 + [sttg] size=20 + - settings: align:center + [payl] size=89 + - cueText: "-Tout, tout, tout pourri,\ntout, tout, tout plaplat," + [vsid] size=12 + - sourceID: 4068696550 +Sample 4, pts=10000, dur=880 +[vttc] size=129 + [sttg] size=20 + - settings: align:center + [payl] size=89 + - cueText: "-Tout, tout, tout pourri,\ntout, tout, tout plaplat," + [vsid] size=12 + - sourceID: 4068696550 +Sample 5, pts=10880, dur=320 +[vtte] size=8 +Sample 6, pts=11200, dur=3160 +[vttc] size=127 + [sttg] size=20 + - settings: align:center + [payl] size=99 + - cueText: "Chien Pourri et Chaplapla,\nc'est moi, le chien, toi, le chat." +Sample 7, pts=14360, dur=320 +[vtte] size=8 +Sample 8, pts=14680, dur=5320 +[vttc] size=131 + [sttg] size=20 + - settings: align:center + [payl] size=91 + - cueText: "Un ami, une poubelle,\net pour nous, la vie est belle." + [vsid] size=12 + - sourceID: 1833399447 +` + +func TestWvttLister(t *testing.T) { + testFile := "testdata/sample_short.ismt" + + ifh, err := os.Open(testFile) + if err != nil { + t.Error(err) + } + defer ifh.Close() + var w bytes.Buffer + err = run(ifh, &w, 0, -1) + if err != nil { + t.Error(err) + } + got := w.String() + gotLines := strings.Split(got, "\n") + wantedLines := strings.Split(wanted, "\n") + if len(gotLines) != len(wantedLines) { + t.Errorf("got %d lines, wanted %d", len(gotLines), len(wantedLines)) + } + for i := range gotLines { + if gotLines[i] != wantedLines[i] { + t.Errorf("line %d: got: %q\n wanted %q", i, gotLines[i], wantedLines[i]) + } + } +} diff --git a/cmd/mp4ff-wvttlister/testdata/sample_short.ismt b/cmd/mp4ff-wvttlister/testdata/sample_short.ismt new file mode 100644 index 0000000000000000000000000000000000000000..faebc226822dd4d1a5a858cc882086643ca7859c GIT binary patch literal 1521 zcmc&zO=}ZD7@nkxMS}#1pdvVk&`XTi_9UT%2CD~=LK8pG%Vc-9yL4uE*_~Y*5o*cZ zs|Wo#D*g;l;!*qsKJO%(joXl`FPpqy&pRK_`wnBQ<(Y9Dq|qjW%yipJq#PdX^U;|1 zBUc{yQG9ioFjn8vQIw-p>D+f|jZ)TrS-Zvg8z2GCleSaT8MRZE|1~2wDeutb}COfG&hOOQHWAy znj(z37#X@{Q+sm;2=fGOrT8^7jo*fC>+A1X4|dxmS%^DOYz;rKas4;+HefyIb;d6J zfIh|Vt9Aw5dmC)f-LBxyhe8F1;R7W-%G`*>SaqF_7Kbu4VyDyTbniJk$gHDALZ8lR z!ADW+0BYO#vQm9_5M`##&HSIoQI;e@o7TLzuuX}WqIs+YK4XE{w2`NQ`)4A4!Zu5Y z!u(_IR}= 1 { for i, e := range b.Entries { bd.write(" - %d: time=%d moofOffset=%d trafNr=%d trunNr=%d sampleDelta=%d", - i+1, e.Time, e.MoofOffset, e.TrafNumber, e.TrunNumber, e.SampleDelta) + i+1, e.Time, e.MoofOffset, e.TrafNumber, e.TrunNumber, e.SampleNumber) } } return bd.err } + +// FindEntry - find entry for moofStart. Return nil if not found +func (b *TfraBox) FindEntry(moofStart uint64) *TfraEntry { + for _, e := range b.Entries { + if uint64(e.MoofOffset) == moofStart { + return &e + } + } + return nil +}