From a29ef46585e1c847801ddfb6e5214689c560432e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbjo=CC=88rn=20Einarsson?= Date: Sun, 27 Oct 2024 22:46:21 +0100 Subject: [PATCH 1/2] feat: much better logging for asset scanning. Also report when total duration differ between pre-encrypted representations --- cmd/livesim2/app/asset.go | 97 ++++++++++++++++------------ cmd/livesim2/app/asset_test.go | 6 +- cmd/livesim2/app/livempd_test.go | 34 ++++++---- cmd/livesim2/app/livesegment_test.go | 27 +++++--- cmd/livesim2/app/start.go | 6 +- 5 files changed, 105 insertions(+), 65 deletions(-) diff --git a/cmd/livesim2/app/asset.go b/cmd/livesim2/app/asset.go index 085704a..e7046ff 100644 --- a/cmd/livesim2/app/asset.go +++ b/cmd/livesim2/app/asset.go @@ -68,12 +68,12 @@ func (am *assetMgr) addAsset(assetPath string) *asset { } // discoverAssets walks the file tree and finds all directories containing MPD files. -func (am *assetMgr) discoverAssets() error { +func (am *assetMgr) discoverAssets(logger *slog.Logger) error { err := fs.WalkDir(am.vodFS, ".", func(p string, d fs.DirEntry, err error) error { if path.Ext(p) == ".mpd" { - err := am.loadAsset(p) + err := am.loadAsset(logger, p) if err != nil { - slog.Warn("Asset loading problem. Skipping", "asset", p, "err", err.Error()) + logger.Warn("Asset loading problem. Skipping", "asset", p, "err", err.Error()) } } return nil @@ -86,22 +86,24 @@ func (am *assetMgr) discoverAssets() error { } for aID, a := range am.assets { - err := a.consolidateAsset() + logger := logger.With("assetPath", a.AssetPath) + err := a.consolidateAsset(logger) if err != nil { - slog.Warn("Asset consolidation problem. Skipping", "error", err.Error()) + logger.Warn("Asset consolidation problem. Skipping", "error", err.Error()) delete(am.assets, aID) // This deletion should be safe continue } - slog.Info("Asset consolidated", "asset", a.AssetPath, "loopDurMS", a.LoopDurMS) + logger.Info("Asset consolidated", "loopDurMS", a.LoopDurMS) } return nil } -func (am *assetMgr) loadAsset(mpdPath string) error { +func (am *assetMgr) loadAsset(logger *slog.Logger, mpdPath string) error { assetPath, mpdName := path.Split(mpdPath) if assetPath != "" { assetPath = assetPath[:len(assetPath)-1] } + logger = logger.With("assetPath", assetPath, "mpdName", mpdName) asset := am.addAsset(assetPath) md := internal.ReadMPDData(am.vodFS, mpdPath) @@ -144,10 +146,10 @@ func (am *assetMgr) loadAsset(mpdPath string) error { return fmt.Errorf("segmentTemplate on Representation level. Only supported on AdaptationSet level") } if _, ok := asset.Reps[rep.Id]; ok { - slog.Debug("Representation already loaded", "rep", rep.Id, "asset", mpdPath) + logger.Debug("Representation already loaded", "rep", rep.Id) continue } - r, err := am.loadRep(assetPath, as, rep) + r, err := am.loadRep(logger, assetPath, as, rep) if err != nil { return fmt.Errorf("getRep: %w", err) } @@ -166,24 +168,24 @@ func (am *assetMgr) loadAsset(mpdPath string) error { } } } - slog.Info("asset MPD loaded", "asset", assetPath, "mpdName", mpdPath) + logger.Info("Asset MPD loaded") return nil } -func (am *assetMgr) loadRep(assetPath string, as *m.AdaptationSetType, rep *m.RepresentationType) (*RepData, error) { +func (am *assetMgr) loadRep(logger *slog.Logger, assetPath string, as *m.AdaptationSetType, rep *m.RepresentationType) (*RepData, error) { + logger = logger.With("rep", rep.Id, "assetPath", assetPath) rp := RepData{ID: rep.Id, ContentType: string(as.ContentType), Codecs: as.Codecs, MpdTimescale: 1, } if !am.writeRepData { - ok, err := rp.readFromJSON(am.vodFS, am.repDataDir, assetPath) + ok, err := rp.readFromJSON(logger, am.vodFS, am.repDataDir, assetPath) if ok { - slog.Info("Representation loaded from JSON", "rep", rp.ID, "asset", assetPath) return &rp, err } } - slog.Debug("Loading full representation", "rep", rp.ID, "asset", assetPath) + logger.Debug("Loading full representation", "rep", rp.ID, "asset", assetPath) st := as.SegmentTemplate if rep.SegmentTemplate != nil { st = rep.SegmentTemplate @@ -199,7 +201,7 @@ func (am *assetMgr) loadRep(assetPath string, as *m.AdaptationSetType, rep *m.Re if st.Timescale != nil { rp.MpdTimescale = int(*st.Timescale) } - err := rp.addRegExpAndInit(am.vodFS, assetPath) + err := rp.addRegExpAndInit(logger, am.vodFS, assetPath) if err != nil { return nil, fmt.Errorf("addRegExpAndInit: %w", err) } @@ -292,15 +294,13 @@ segLoop: if !am.writeRepData { return &rp, nil } - err = rp.writeToJSON(am.repDataDir, assetPath) - if err == nil { - slog.Info("Representation data written to JSON file", "rep", rp.ID, "asset", assetPath) - } + err = rp.writeToJSON(logger, am.repDataDir, assetPath) return &rp, err } // readFromJSON reads the representation data from a gzipped or plain JSON file. -func (rp *RepData) readFromJSON(vodFS fs.FS, repDataDir, assetPath string) (bool, error) { +func (rp *RepData) readFromJSON(logger *slog.Logger, vodFS fs.FS, repDataDir, assetPath string) (bool, error) { + logger = logger.With("rep", rp.ID, "assetPath", assetPath) if repDataDir == "" { return false, nil } @@ -323,7 +323,7 @@ func (rp *RepData) readFromJSON(vodFS fs.FS, repDataDir, assetPath string) (bool if err != nil { return true, err } - slog.Debug("Read repdata", "path", gzipPath) + logger.Info("Read gzipped repdata", "path", gzipPath) } if len(data) == 0 { _, err := os.Stat(repDataPath) @@ -332,7 +332,7 @@ func (rp *RepData) readFromJSON(vodFS fs.FS, repDataDir, assetPath string) (bool if err != nil { return true, err } - slog.Debug("Read repdata", "path", repDataPath) + logger.Info("Read repdata", "path", repDataPath) } } if len(data) == 0 { @@ -341,14 +341,14 @@ func (rp *RepData) readFromJSON(vodFS fs.FS, repDataDir, assetPath string) (bool if err := json.Unmarshal(data, &rp); err != nil { return true, err } - err = rp.addRegExpAndInit(vodFS, assetPath) + err = rp.addRegExpAndInit(logger, vodFS, assetPath) if err != nil { return true, fmt.Errorf("addRegExpAndInit: %w", err) } return true, nil } -func (rp *RepData) addRegExpAndInit(vodFS fs.FS, assetPath string) error { +func (rp *RepData) addRegExpAndInit(logger *slog.Logger, vodFS fs.FS, assetPath string) error { switch { case strings.Contains(rp.MediaURI, "$Number$"): rexStr := strings.ReplaceAll(rp.MediaURI, "$Number$", `(\d+)`) @@ -361,7 +361,7 @@ func (rp *RepData) addRegExpAndInit(vodFS fs.FS, assetPath string) error { } if rp.ContentType != "image" { - err := rp.readInit(vodFS, assetPath) + err := rp.readInit(logger, vodFS, assetPath) if err != nil { return err } @@ -370,7 +370,8 @@ func (rp *RepData) addRegExpAndInit(vodFS fs.FS, assetPath string) error { } // writeToJSON writes the representation data to a gzipped JSON file. -func (rp *RepData) writeToJSON(repDataDir, assetPath string) error { +func (rp *RepData) writeToJSON(logger *slog.Logger, repDataDir, assetPath string) error { + logger = logger.With("rep", rp.ID, "assetPath", assetPath) if repDataDir == "" { return nil } @@ -397,7 +398,7 @@ func (rp *RepData) writeToJSON(repDataDir, assetPath string) error { if err != nil { return err } - slog.Debug("Wrote repData", "path", gzipPath) + logger.Debug("Wrote repData", "path", gzipPath) return nil } @@ -592,7 +593,8 @@ func (a *asset) setReferenceRep() error { } // consolidateAsset sets up reference track and loop duration if possible -func (a *asset) consolidateAsset() error { +func (a *asset) consolidateAsset(logger *slog.Logger) error { + logger = logger.With("assetPath", a.AssetPath) err := a.setReferenceRep() if err != nil { return fmt.Errorf("setReferenceRep: %w", err) @@ -603,15 +605,20 @@ func (a *asset) consolidateAsset() error { // This is not an integral number of milliseconds, so we should drop this asset return fmt.Errorf("cannot match loop duration %d for asset %s rep %s", a.LoopDurMS, a.AssetPath, refRep.ID) } + badPreEncrypted := false for _, rep := range a.Reps { - if rep.ContentType != refRep.ContentType { + if rep.ContentType != refRep.ContentType && !rep.PreEncrypted { continue } repDurMS := 1000 * rep.duration() / rep.MediaTimescale if repDurMS != a.LoopDurMS { - slog.Info("Rep duration differs from loop duration", "rep", rep.ID, "asset", a.AssetPath) + logger.Warn("Duration differs", "representation", rep.ID, "referenceRepresentation", refRep.ID, "refDurMS", a.LoopDurMS, "repDurMS", repDurMS) + badPreEncrypted = true } } + if badPreEncrypted { + return fmt.Errorf("pre-encrypted representations do not all have same duration") + } return nil } @@ -741,7 +748,7 @@ func prepareForEncryption(codec string) bool { return strings.HasPrefix(codec, "avc") || strings.HasPrefix(codec, "mp4a.40") } -func (r *RepData) readInit(vodFS fs.FS, assetPath string) error { +func (r *RepData) readInit(logger *slog.Logger, vodFS fs.FS, assetPath string) error { data, err := fs.ReadFile(vodFS, path.Join(assetPath, r.InitURI)) if err != nil { return fmt.Errorf("read initURI %q: %w", r.InitURI, err) @@ -757,7 +764,7 @@ func (r *RepData) readInit(vodFS fs.FS, assetPath string) error { if prepareForEncryption(r.Codecs) { assetName := path.Base(assetPath) - err = r.addEncryption(assetName, data) + err = r.addEncryption(logger, assetName, data) if err != nil { return fmt.Errorf("addEncryption: %w", err) } @@ -774,25 +781,33 @@ func (r *RepData) readInit(vodFS fs.FS, assetPath string) error { return nil } -func (r *RepData) addEncryption(assetName string, data []byte) error { +func (r *RepData) addEncryption(logger *slog.Logger, assetName string, data []byte) error { // Set up the encryption data for this representation given asset ed := repEncData{} ed.keyID = kidFromString(assetName) ed.key = kidToKey(ed.keyID) ed.iv = defaultIV - // Generate cbcs data or exit if already encrypted + // Generate cbcs data, but exit if already encrypted initSeg, err := getInitSeg(data) if err != nil { return fmt.Errorf("decode init: %w", err) } stsd := initSeg.Moov.Trak.Mdia.Minf.Stbl.Stsd for _, c := range stsd.Children { - switch c.Type() { - case "encv", "enca": - slog.Info("asset", assetName, "repID", r.ID, "Init segment already encrypted") - r.PreEncrypted = true - return nil + switch box := c.(type) { + case *mp4.VisualSampleEntryBox: + if box.Type() == "encv" && box.Sinf != nil && box.Sinf.Schm != nil { + logger.Info("Video pre-encrypted", "repID", r.ID, "scheme", box.Sinf.Schm.SchemeType, "init", r.InitURI) + r.PreEncrypted = true + return nil + } + case *mp4.AudioSampleEntryBox: + if box.Type() == "enca" && box.Sinf != nil && box.Sinf.Schm != nil { + logger.Info("Video pre-encrypted", "repID", r.ID, "scheme", box.Sinf.Schm.SchemeType, "init", r.InitURI) + r.PreEncrypted = true + return nil + } } } kid, err := mp4.NewUUIDFromHex(hex.EncodeToString(ed.keyID[:])) @@ -803,7 +818,7 @@ func (r *RepData) addEncryption(assetName string, data []byte) error { if err != nil { return fmt.Errorf("init protect cbcs: %w", err) } - slog.Info("Encrypted init segment with cbcs", "asset", assetName, "repID", r.ID) + logger.Info("Generate init segment for encryption", "scheme", "cbcs", "repID", r.ID) ed.cbcsPD = ipd ed.cbcsInitSeg = initSeg ed.cbcsInitBytes, err = getInitBytes(initSeg) @@ -820,7 +835,7 @@ func (r *RepData) addEncryption(assetName string, data []byte) error { if err != nil { return fmt.Errorf("init protect cenc: %w", err) } - slog.Info("Encrypted init segment with cenc", "asset", assetName, "repID", r.ID) + logger.Info("Generate init segment for encryption", "scheme", "cenc", "repID", r.ID) ed.cencPD = ipd ed.cencInitSeg = initSeg ed.cencInitBytes, err = getInitBytes(initSeg) diff --git a/cmd/livesim2/app/asset_test.go b/cmd/livesim2/app/asset_test.go index b5ae996..b1ba99c 100644 --- a/cmd/livesim2/app/asset_test.go +++ b/cmd/livesim2/app/asset_test.go @@ -5,6 +5,7 @@ package app import ( + "log/slog" "os" "testing" @@ -16,7 +17,8 @@ func TestLoadAsset(t *testing.T) { vodFS := os.DirFS("testdata") tmpDir := t.TempDir() am := newAssetMgr(vodFS, tmpDir, true) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) // This was first time asset, ok := am.findAsset("assets/testpic_2s") @@ -31,7 +33,7 @@ func TestLoadAsset(t *testing.T) { assert.Equal(t, 8000, asset.LoopDurMS) // Second time we load using gzipped repData files am = newAssetMgr(vodFS, tmpDir, true) - err = am.discoverAssets() + err = am.discoverAssets(logger) require.NoError(t, err) asset, ok = am.findAsset("assets/testpic_2s") require.True(t, ok) diff --git a/cmd/livesim2/app/livempd_test.go b/cmd/livesim2/app/livempd_test.go index 9ff1fb1..d61c7cf 100644 --- a/cmd/livesim2/app/livempd_test.go +++ b/cmd/livesim2/app/livempd_test.go @@ -5,6 +5,7 @@ package app import ( + "log/slog" "math" "os" "path" @@ -23,7 +24,8 @@ func TestLiveMPDStart(t *testing.T) { vodFS := os.DirFS("testdata/assets") tmpDir := t.TempDir() am := newAssetMgr(vodFS, tmpDir, false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -123,7 +125,8 @@ func TestLiveMPDStart(t *testing.T) { func TestLiveMPDWithTimeSubs(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -180,7 +183,8 @@ var liveSubEn = "" + func TestSegmentTimes(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -248,7 +252,8 @@ func TestLastAvailableSegment(t *testing.T) { vodFS := os.DirFS("testdata/assets") tmpDir := t.TempDir() am := newAssetMgr(vodFS, tmpDir, true) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { desc string @@ -365,7 +370,8 @@ func TestPublishTime(t *testing.T) { vodFS := os.DirFS("testdata/assets") tmpDir := t.TempDir() am := newAssetMgr(vodFS, tmpDir, false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -544,7 +550,8 @@ func TestPublishTime(t *testing.T) { func TestNormalAvailabilityTimeOffset(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -623,7 +630,8 @@ func TestNormalAvailabilityTimeOffset(t *testing.T) { func TestUTCTiming(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -703,7 +711,8 @@ func segTimingsFromS(ss []*m.S) []segTiming { func TestAudioSegmentTimeFollowsVideo(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -780,7 +789,8 @@ func TestAudioSegmentTimeFollowsVideo(t *testing.T) { func TestMultiPeriod(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -892,7 +902,8 @@ func TestMultiPeriod(t *testing.T) { func TestRelStartStopTimeIntoLocation(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -928,7 +939,8 @@ func TestFractionalFramerateMPDs(t *testing.T) { vodFS := os.DirFS("testdata/assets") tmpDir := t.TempDir() am := newAssetMgr(vodFS, tmpDir, false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { diff --git a/cmd/livesim2/app/livesegment_test.go b/cmd/livesim2/app/livesegment_test.go index b0733f4..cd18ec2 100644 --- a/cmd/livesim2/app/livesegment_test.go +++ b/cmd/livesim2/app/livesegment_test.go @@ -26,7 +26,8 @@ import ( func TestLiveSegment(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -112,7 +113,8 @@ func TestLiveSegment(t *testing.T) { func TestAc3Timing(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) asset, ok := am.findAsset("bbb_hevc_ac3_8s") @@ -134,7 +136,8 @@ func TestAc3Timing(t *testing.T) { func TestCheckAudioSegmentTimeAddressing(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -189,7 +192,8 @@ func TestCheckAudioSegmentTimeAddressing(t *testing.T) { func TestLiveThumbSegment(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -241,7 +245,8 @@ func TestLiveThumbSegment(t *testing.T) { func TestWriteChunkedSegment(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cfg := NewResponseConfig() cfg.AvailabilityTimeCompleteFlag = false @@ -384,7 +389,8 @@ func TestTTMLTimeShifts(t *testing.T) { func TestStartNumber(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) err = logging.InitSlog("debug", "discard") require.NoError(t, err) @@ -451,7 +457,8 @@ func TestStartNumber(t *testing.T) { func TestLLSegmentAvailability(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) err = logging.InitSlog("error", "discard") require.NoError(t, err) @@ -587,7 +594,8 @@ func TestLLSegmentAvailability(t *testing.T) { func TestSegmentStatusCodeResponse(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) cases := []struct { @@ -699,7 +707,8 @@ func TestSegmentStatusCodeResponse(t *testing.T) { func TestMehdBoxRemovedFromInitSegment(t *testing.T) { vodFS := os.DirFS("testdata/assets") am := newAssetMgr(vodFS, "", false) - err := am.discoverAssets() + logger := slog.Default() + err := am.discoverAssets(logger) require.NoError(t, err) asset, ok := am.findAsset("testpic_8s") require.True(t, ok) diff --git a/cmd/livesim2/app/start.go b/cmd/livesim2/app/start.go index 18080d9..b1260ae 100644 --- a/cmd/livesim2/app/start.go +++ b/cmd/livesim2/app/start.go @@ -86,13 +86,15 @@ func SetupServer(ctx context.Context, cfg *ServerConfig) (*Server, error) { } start := time.Now() - err = server.assetMgr.discoverAssets() + logger.Debug("Loading VOD assets", "vodRoot", cfg.VodRoot) + err = server.assetMgr.discoverAssets(logger) if err != nil { return nil, fmt.Errorf("findAssets: %w", err) } elapsedSeconds := fmt.Sprintf("%.3fs", time.Since(start).Seconds()) - logger.Info("Vod asset found", + logger.Info("Vod assets loaded", + "vodRoot", cfg.VodRoot, "count", len(server.assetMgr.assets), "elapsed seconds", elapsedSeconds) for name := range server.assetMgr.assets { From 9b0d88e5423158502611773758ba46e171321bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbjo=CC=88rn=20Einarsson?= Date: Sun, 27 Oct 2024 16:33:43 +0100 Subject: [PATCH 2/2] feat: support pre-encrypted content by not refragmenting it --- CHANGELOG.md | 9 +++++- cmd/livesim2/app/livesegment.go | 49 +++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b05af..286eabf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet +### Added + +- Better logging when loading asset representation data +- Check that pre-encrypted content has the same duration for all representations + +### Fixed + +- Pre-encrypted content is not re-fragmented, but left as it ## [1.5.0] - 2024-10-02 diff --git a/cmd/livesim2/app/livesegment.go b/cmd/livesim2/app/livesegment.go index 5afeb4b..55b177d 100644 --- a/cmd/livesim2/app/livesegment.go +++ b/cmd/livesim2/app/livesegment.go @@ -47,6 +47,16 @@ func genLiveSegment(vodFS fs.FS, a *asset, cfg *ResponseConfig, segmentPart stri return so, fmt.Errorf("not 1 but %d segments", len(segFile.Segments)) } seg := segFile.Segments[0] + if seg.Sidx != nil { + if len(seg.Sidxs) > 1 { + slog.Error("more than one sidx not supported", "asset", a.AssetPath, "segment", segmentPart) + return so, fmt.Errorf("more than one sidx not supported") + } + if seg.Sidx.Timescale != meta.timescale { + seg.Sidx.Timescale = meta.timescale + } + seg.Sidx.EarliestPresentationTime = meta.newTime + } timeShift := meta.newTime - seg.Fragments[0].Moof.Traf.Tfdt.BaseMediaDecodeTime() if strings.HasPrefix(meta.rep.Codecs, "stpp") { // Shift segment and TTML timestamps inside segment @@ -56,9 +66,24 @@ func genLiveSegment(vodFS fs.FS, a *asset, cfg *ResponseConfig, segmentPart stri } } else { for _, frag := range seg.Fragments { + traf := frag.Moof.Traf + tfdt := traf.Tfdt + oldTfdtSize := tfdt.Size() frag.Moof.Mfhd.SequenceNumber = meta.newNr - oldTime := frag.Moof.Traf.Tfdt.BaseMediaDecodeTime() - frag.Moof.Traf.Tfdt.SetBaseMediaDecodeTime(oldTime + timeShift) + oldTime := tfdt.BaseMediaDecodeTime() + tfdt.SetBaseMediaDecodeTime(oldTime + timeShift) + newTfdtSize := tfdt.Size() + tfdtSizeDiff := int32(newTfdtSize) - int32(oldTfdtSize) + if tfdtSizeDiff != 0 { + traf.Trun.DataOffset += tfdtSizeDiff + } + if traf.Saio != nil { + if saioAfterTfdt(traf) { + for i := range traf.Saio.Offset { + traf.Saio.Offset[i] += int64(tfdtSizeDiff) + } + } + } } } @@ -84,6 +109,24 @@ func genLiveSegment(vodFS fs.FS, a *asset, cfg *ResponseConfig, segmentPart stri return outSeg, nil } +// saioAfterTfdt saio box comes after tfdt in traf +func saioAfterTfdt(traf *mp4.TrafBox) bool { + tfdtIndex := -1 + saioIndex := -1 + for i, c := range traf.Children { + switch c.Type() { + case "saio": + saioIndex = i + case "tfdt": + tfdtIndex = i + } + } + if tfdtIndex == -1 || saioIndex == -1 { + return false + } + return saioIndex > tfdtIndex +} + func isImage(segPath string) bool { return path.Ext(segPath) == ".jpg" } @@ -421,7 +464,7 @@ func createOutSeg(vodFS fs.FS, a *asset, cfg *ResponseConfig, segmentPart string return so, fmt.Errorf("findRepAndSegmentID: %w", err) } - if rep.ContentType == "audio" { + if rep.ContentType == "audio" && !rep.PreEncrypted { so, err = createAudioSegment(vodFS, a, cfg, segmentPart, nowMS, rep, segID) if err != nil { return so, fmt.Errorf("createAudioSegment: %w", err)