Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Then run
```sh
> go mod tidy
```

to fetch and install all dependencies.

To build `dashfetcher` and `livesim2` you can use the `Makefile` like
Expand Down
2 changes: 1 addition & 1 deletion cmd/dashfetcher/app/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ func AutoDir(rawMPDurl, outDir string) (string, error) {
if outRange > len(uBaseParts) {
break
}
for i := 0; i < outRange; i++ {
for i := range outRange {
if outParts[outStart+i] != uBaseParts[i] {
match = false
break
Expand Down
65 changes: 53 additions & 12 deletions cmd/livesim2/app/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,31 @@ func (l lastSegInfo) availabilityTime(ato float64) float64 {
return math.Round(float64(l.startTime+l.dur)/float64(l.timescale)) - ato
}

func calculateK(segmentDuration uint64, mediaTimescale int, chunkDurS *float64) (*uint64, error) {
if chunkDurS == nil || *chunkDurS <= 0 {
return nil, nil
}
chunkDurInTimescale := *chunkDurS * float64(mediaTimescale)
if chunkDurInTimescale <= 0 {
return nil, nil
}

// Validate that chunk duration is not greater than segment duration
segmentDurS := float64(segmentDuration) / float64(mediaTimescale)
if *chunkDurS > segmentDurS {
return nil, fmt.Errorf("chunk duration %.2fs must be less than or equal to segment duration %.2fs", *chunkDurS, segmentDurS)
}

kVal := uint64(math.Round(float64(segmentDuration) / chunkDurInTimescale))
if kVal > 1 {
return &kVal, nil
}
return nil, nil
}

// generateTimelineEntries generates timeline entries for the given representation.
// If no segments are available, startNr and lsi.nr are set to -1.
func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int) segEntries {
func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int, explicitChunkDurS *float64) (segEntries, error) {
rep := a.Reps[repID]
segs := rep.Segments
nrSegs := len(segs)
Expand Down Expand Up @@ -497,14 +519,20 @@ func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int) s
if wt.nowWraps < 0 { // no segment finished yet. Return an empty list and set startNr and lsi.nr = -1
se.startNr = -1
se.lsi.nr = -1
return se
return se, nil
}

se.startNr = wt.startWraps*nrSegs + relStartIdx
nowNr := wt.nowWraps*nrSegs + relNowIdx
t := uint64(rep.duration()*wt.startWraps) + segs[relStartIdx].StartTime
d := segs[relStartIdx].dur()
s := &m.S{T: Ptr(t), D: d}

k, err := calculateK(d, rep.MediaTimescale, explicitChunkDurS)
if err != nil {
return se, err
}

s := &m.S{T: Ptr(t), D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
lsi := lastSegInfo{
timescale: uint64(rep.MediaTimescale),
startTime: t,
Expand All @@ -522,18 +550,18 @@ func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int) s
continue
}
d = seg.dur()
s = &m.S{D: d}
s = &m.S{D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
se.entries = append(se.entries, s)
lsi.dur = d
lsi.nr = nr
}
se.lsi = lsi
return se
return se, nil
}

// generateTimelineEntriesFromRef generates timeline entries for the given representation given reference.
// This is based on sample duration and the type of media.
func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) segEntries {
func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string, explicitChunkDurS *float64) (segEntries, error) {
rep := a.Reps[repID]
nrSegs := 0
for _, rs := range refSE.entries {
Expand All @@ -547,7 +575,7 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
}

if refSE.startNr < 0 {
return se
return se, nil
}

sampleDur := uint64(rep.sampleDur())
Expand All @@ -562,7 +590,7 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
editListOffset := uint64(rep.EditListOffset)
expectedTime := t // Track what the time should be without explicit T
var s *m.S

var k *uint64
for _, rs := range refSE.entries {
refD := rs.D
for j := 0; j <= rs.R; j++ {
Expand All @@ -571,6 +599,12 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
d := nextT - t

if s == nil {
var err error
k, err = calculateK(d, rep.MediaTimescale, explicitChunkDurS)
if err != nil {
return se, err
}

// First segment: apply editListOffset adjustment
adjustedT := t
adjustedD := d
Expand All @@ -590,12 +624,18 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
}
}

s = &m.S{T: m.Ptr(adjustedT), D: adjustedD}
s = &m.S{T: m.Ptr(adjustedT), D: adjustedD, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
se.entries = append(se.entries, s)
expectedTime = adjustedT + adjustedD // Update expected time after first segment
} else {
// Subsequent segments
if s.D != d {
var err error
k, err = calculateK(d, rep.MediaTimescale, explicitChunkDurS)
if err != nil {
return se, err
}

// New segment with different duration
adjustedT := t
if editListOffset > 0 && t >= editListOffset {
Expand All @@ -605,10 +645,10 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
// Only add explicit T if the time is not continuous
if adjustedT == expectedTime {
// Time is continuous, no need for explicit T
s = &m.S{D: d}
s = &m.S{D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
} else {
// Time is discontinuous, need explicit T
s = &m.S{T: m.Ptr(adjustedT), D: d}
s = &m.S{T: m.Ptr(adjustedT), D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
}
se.entries = append(se.entries, s)
expectedTime = adjustedT + d
Expand All @@ -620,7 +660,7 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
t = nextT
}
}
return se
return se, nil
}

func (a *asset) setReferenceRep() error {
Expand Down Expand Up @@ -822,6 +862,7 @@ type RepData struct {
ConstantSampleDuration *uint32 `json:"constantSampleDuration,omitempty"` // Non-zero if all samples have the same duration
EditListOffset int64 `json:"editListOffset,omitempty"`
PreEncrypted bool `json:"preEncrypted"`
ChunkDurSSRS *float64 `json:"chunkDurSSRS,omitempty"` // Low delay chunk duration in seconds
mediaRegexp *regexp.Regexp `json:"-"`
initSeg *mp4.InitSegment `json:"-"`
initBytes []byte `json:"-"`
Expand Down
97 changes: 97 additions & 0 deletions cmd/livesim2/app/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,103 @@ func TestAssetLookupForNameOverlap(t *testing.T) {
require.Equal(t, "assets/testpic_2s_1", a.AssetPath)
}

func TestCalculateK(t *testing.T) {
testCases := []struct {
description string
segmentDuration uint64
mediaTimescale int
chunkDuration *float64
expectedK *uint64
expectedError string
}{
{
description: "nil chunk duration",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: nil,
expectedK: nil,
},
{
description: "zero chunk duration",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: Ptr(0.0),
expectedK: nil,
},
{
description: "negative chunk duration",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: Ptr(-1.0),
expectedK: nil,
},
{
description: "zero media timescale",
segmentDuration: 192000,
mediaTimescale: 0,
chunkDuration: Ptr(1.0),
expectedK: nil,
},
{
description: "k=4",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: Ptr(0.5),
expectedK: Ptr(uint64(4)),
},
{
description: "k=1, returns nil",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: Ptr(2.0),
expectedK: nil,
},
{
description: "rounding up",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: Ptr(0.57), // 3.5087... -> 4
expectedK: Ptr(uint64(4)),
},
{
description: "rounding down",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: Ptr(0.58), // 3.448... -> 3
expectedK: Ptr(uint64(3)),
},
{
description: "chunk duration greater than segment duration",
segmentDuration: 192000,
mediaTimescale: 96000,
chunkDuration: Ptr(2.5), // 2.5s > 2.0s segment duration
expectedK: nil,
expectedError: "chunk duration 2.50s must be less than or equal to segment duration 2.00s",
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
gotK, err := calculateK(tc.segmentDuration, tc.mediaTimescale, tc.chunkDuration)

if tc.expectedError != "" {
require.Error(t, err)
require.Equal(t, tc.expectedError, err.Error())
require.Nil(t, gotK)
return
}

require.NoError(t, err)
if tc.expectedK == nil {
require.Nil(t, gotK)
} else {
require.NotNil(t, gotK)
require.Equal(t, *tc.expectedK, *gotK)
}
})
}
}

func copyDir(srcDir, dstDir string) error {
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
Expand Down
16 changes: 13 additions & 3 deletions cmd/livesim2/app/cmaf-ingester.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,16 +522,26 @@ func (c *cmafIngester) sendMediaSegments(ctx context.Context, nextSegNr, nowMS i
atoMS := int(c.cfg.getAvailabilityTimeOffsetS() * 1000)
for idx, rd := range c.repsData {
var se segEntries
var err error
// The first representation is used as reference for generating timeline entries
if idx == 0 {
refSegEntries = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS)
refSegEntries, err = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS, nil)
if err != nil {
return err
}
se = refSegEntries
} else {
switch rd.contentType {
case "video", "text", "image":
se = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS)
se, err = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS, nil)
if err != nil {
return err
}
case "audio":
se = c.asset.generateTimelineEntriesFromRef(refSegEntries, rd.repID)
se, err = c.asset.generateTimelineEntriesFromRef(refSegEntries, rd.repID, nil)
if err != nil {
return err
}
default:
return fmt.Errorf("unknown content type %s", rd.contentType)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/livesim2/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
defaultReqIntervalS = 24 * 3600
defaultAvailabilityStartTimeS = 0
defaultAvailabilityTimeComplete = true
defaultSSRFlag = false
defaultTimeShiftBufferDepthS = 60
defaultStartNr = 0
timeShiftBufferDepthMarginS = 10
Expand Down
17 changes: 16 additions & 1 deletion cmd/livesim2/app/configurl.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ const (
)

const (
UrlParamSchemeIdUri = "urn:mpeg:dash:urlparam:2014"
UrlParamSchemeIdUri = "urn:mpeg:dash:urlparam:2014"
SsrSchemeIdUri = "urn:mpeg:dash:ssr:2023"
AdaptationSetSwitchingSchemeIdUri = "urn:mpeg:dash:adaptation-set-switching:2016"
)

type ResponseConfig struct {
Expand Down Expand Up @@ -110,6 +112,9 @@ type ResponseConfig struct {
SegStatusCodes []SegStatusCodes `json:"SegStatus,omitempty"`
Traffic []LossItvls `json:"Traffic,omitempty"`
Query *Query `json:"Query,omitempty"`
SSRFlag bool `json:"SSRFlag,omitempty"`
SSRAS string `json:"SSRAS,omitempty"`
ChunkDurSSR string `json:"ChunkDurSSR,omitempty"`
}

// SegStatusCodes configures regular extraordinary segment response codes
Expand Down Expand Up @@ -241,6 +246,7 @@ func NewResponseConfig() *ResponseConfig {
c := ResponseConfig{
StartTimeS: defaultAvailabilityStartTimeS,
AvailabilityTimeCompleteFlag: defaultAvailabilityTimeComplete,
SSRFlag: defaultSSRFlag,
TimeShiftBufferDepthS: Ptr(defaultTimeShiftBufferDepthS),
StartNr: Ptr(defaultStartNr),
TimeSubsDurMS: defaultTimeSubsDurMS,
Expand Down Expand Up @@ -394,6 +400,11 @@ cfgLoop:
}
case "annexI":
cfg.Query = sc.ParseQuery(key, val)
case "ssras":
cfg.SSRAS = val
cfg.SSRFlag = true
case "chunkdurssr":
cfg.ChunkDurSSR = val
default:
contentStartIdx = i
break cfgLoop
Expand Down Expand Up @@ -447,6 +458,10 @@ func verifyAndFillConfig(cfg *ResponseConfig, nowMS int) error {
}
// We do not check here that the drm is one that has been configured,
// since pre-encrypted content will influence what is valid.

if cfg.ChunkDurSSR != "" && cfg.SSRAS == "" {
return fmt.Errorf("chunkDurSSR requires ssrAS to be configured")
}
return nil
}

Expand Down
Loading
Loading