Skip to content

Commit db82d0a

Browse files
cotid-qualabsjuanmanuel-qualabssebastianpiqgemini-code-assist[bot]emilsas
committed
feat: support l3d-dash (SSR)
Squash merge l3d-dash (#12) * Add low delay config to urlgen * Fix low delay config in urlgen and read it on livempd * mpd profile DASHProfileLinear * add SchemeIdUriSSR to essential property on representations * Set Role as main in main Adaptation Set * cleanup for fresh start * update LowDelay flag to take value from url instead of query parameter * add essential property, role and startWithSAP at Adaptation Set level * PartialSegments configuracion added to ResponseConfig and urlgen form * Revert "PartialSegments configuracion added to ResponseConfig and urlgen form" This reverts commit 491b159. * add SubNumber for Low Delay to SegmentTimeline with $Number$ * rename LowDelayFlag * l3d-dash/S@k (#3) * Add chunkDuration parameter to generateTimelineEntries and related functions, calculating s@k and adding it to mpd * Add simple Unit Tests for Calculate K function * Add unit test for generateTimelineEntries with chunkDuration cases * Moved Low Delay Checkbox to Low Latency section * Update code to only send `s@K` if Low Latency flag is on * Add LowDelay support and refactor related configurations * l3d-dash/calc_subsegment_part_unit_tests (#5) * Enhance calcSubSegementPart function to handle errors and add comprehensive unit tests * Refactor segment handling: split writeChunkedSegment into prepareChunks and setHeaders for improved readability and error handling * Add TestWriteSubSegment for validating sub-segment handling and error cases * Refactor TestWriteSubSegment: remove unused mediaTimescale and cancelContext fields for clarity * Update cmd/livesim2/app/livesegment_test.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Refactor TestWriteSubSegment: add availabilityTimeOffsetS to test cases for improved accuracy * Fix wrong chunk index generation * Implement SegmentSequenceProperties for l3d * Undo SegmentSequenceProperties implementation * Implement single chunk generation in the sub segments * Remove unused lowDelay config * Remove unnecessary sleep on writing sub segemnts * Add l3d testdata * Fix writeSubSegment debug messages * Fix wrong asset naming * Update l3d dash to no-cmaf * Fix return segements without a sub segment * Add audio to de l3d-dash sample and fix template generation * Fix unit test * Update Low Delay sample * Add SubNumber to $Time$ with SegmentTimeline * feat: update dash-mpd to v0.13.0 and improve Makefile * fix: use new S type struct from dash-mpd * feat: add L3D to Number wo timeline * fix: failing tests with K * feat: Add SegmentSequenceProperties element * fix: availabilityTimeOffset not defined issue * fix: lint errors * fix: lint errors * remove unnecessary withe spaces * refactor: rename chunk duration parameters for clarity * refactor: update comments related with low-delay for clarity * refactor: rename chunk duration in seconds variable for clarity * refactor: rename DASH profile constant for consistency and update usage in LiveMPD function * fix: remove duration attribute from Period element for clarity * refactor: Add chunkIndex explanation comment in the chunkSegment generation * rafactor: Update ssr schme uri constant naming aligment * rafactor: update writeSubSegment comments for clarity * rafactor: remove harcoded role for low delay assets * refactor: rename low delay mode to SSR and update related configurations * refactor ssr essential property * switching implementation * remove cfg EnableSSR from handler livesim * fix * test fix * fix * add lowdelay chunk duration config * parse low dealy chunk duration * add unit test cases * add chunk duration for low delay * bugfix * bugfix - representation id regex update when the id string is a substring of other representation id * update assets for low delay and regular renditions * update assets for low delay and regular renditions * refactor low delay flag and implement k for low latency * code refactor * code refactor * remove unused parameter * add comment * condition refactor * refactor segment template for low delay and added low latency cadence * fix linter issues * ssr html name update * remove segment sequence properties from low delay low latency * remove cadence from low latency and add ssr * low latency start with sap * low delay audio * rename low delay adaptation set to ssrAS * rename low delay chunk duration to chunk dur ssr * Fixed validation to ensure adaptationSetId is a non-negative integer * Adding ProfileAdvancedLinear only if it is not already included * Fix typo * Error handling for pairs different than 2 * Error handling for extra spaces on configuration * Error handling chunk duration greater than segment duration * Error handling for empty and invalid configurations * Fix chunking fallback * AvailabilityTimeOffset left as default and ignored for SSR * Remove availabilityTimeOffset from write sub segment test * remove space * fix chunkdurssr cant be used on its own * Fix chunking fallback * fix chunkdurssr cant be used on its own * AvailabilityTimeOffset left as default and ignored for SSR * remove space * error handling for ssras not configured when using chunkdurssr * unit test fix * fixes after main merge * revert anchor regex for matching representations --------- Co-authored-by: Juan Manuel Gonzalez <[email protected]> Co-authored-by: Sebastian Piquerez <[email protected]> Co-authored-by: Juan Manuel Gonzalez <[email protected]> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Sebastian Piquerez <[email protected]> Co-authored-by: Emil Santurio <[email protected]> Co-authored-by: Torbjörn Einarsson <[email protected]>
1 parent 592e920 commit db82d0a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1522
-59
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Then run
164164
```sh
165165
> go mod tidy
166166
```
167+
167168
to fetch and install all dependencies.
168169
169170
To build `dashfetcher` and `livesim2` you can use the `Makefile` like

cmd/dashfetcher/app/fetcher.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ func AutoDir(rawMPDurl, outDir string) (string, error) {
325325
if outRange > len(uBaseParts) {
326326
break
327327
}
328-
for i := 0; i < outRange; i++ {
328+
for i := range outRange {
329329
if outParts[outStart+i] != uBaseParts[i] {
330330
match = false
331331
break

cmd/livesim2/app/asset.go

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,31 @@ func (l lastSegInfo) availabilityTime(ato float64) float64 {
453453
return math.Round(float64(l.startTime+l.dur)/float64(l.timescale)) - ato
454454
}
455455

456+
func calculateK(segmentDuration uint64, mediaTimescale int, chunkDurS *float64) (*uint64, error) {
457+
if chunkDurS == nil || *chunkDurS <= 0 {
458+
return nil, nil
459+
}
460+
chunkDurInTimescale := *chunkDurS * float64(mediaTimescale)
461+
if chunkDurInTimescale <= 0 {
462+
return nil, nil
463+
}
464+
465+
// Validate that chunk duration is not greater than segment duration
466+
segmentDurS := float64(segmentDuration) / float64(mediaTimescale)
467+
if *chunkDurS > segmentDurS {
468+
return nil, fmt.Errorf("chunk duration %.2fs must be less than or equal to segment duration %.2fs", *chunkDurS, segmentDurS)
469+
}
470+
471+
kVal := uint64(math.Round(float64(segmentDuration) / chunkDurInTimescale))
472+
if kVal > 1 {
473+
return &kVal, nil
474+
}
475+
return nil, nil
476+
}
477+
456478
// generateTimelineEntries generates timeline entries for the given representation.
457479
// If no segments are available, startNr and lsi.nr are set to -1.
458-
func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int) segEntries {
480+
func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int, explicitChunkDurS *float64) (segEntries, error) {
459481
rep := a.Reps[repID]
460482
segs := rep.Segments
461483
nrSegs := len(segs)
@@ -497,14 +519,20 @@ func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int) s
497519
if wt.nowWraps < 0 { // no segment finished yet. Return an empty list and set startNr and lsi.nr = -1
498520
se.startNr = -1
499521
se.lsi.nr = -1
500-
return se
522+
return se, nil
501523
}
502524

503525
se.startNr = wt.startWraps*nrSegs + relStartIdx
504526
nowNr := wt.nowWraps*nrSegs + relNowIdx
505527
t := uint64(rep.duration()*wt.startWraps) + segs[relStartIdx].StartTime
506528
d := segs[relStartIdx].dur()
507-
s := &m.S{T: Ptr(t), D: d}
529+
530+
k, err := calculateK(d, rep.MediaTimescale, explicitChunkDurS)
531+
if err != nil {
532+
return se, err
533+
}
534+
535+
s := &m.S{T: Ptr(t), D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
508536
lsi := lastSegInfo{
509537
timescale: uint64(rep.MediaTimescale),
510538
startTime: t,
@@ -522,18 +550,18 @@ func (a *asset) generateTimelineEntries(repID string, wt wrapTimes, atoMS int) s
522550
continue
523551
}
524552
d = seg.dur()
525-
s = &m.S{D: d}
553+
s = &m.S{D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
526554
se.entries = append(se.entries, s)
527555
lsi.dur = d
528556
lsi.nr = nr
529557
}
530558
se.lsi = lsi
531-
return se
559+
return se, nil
532560
}
533561

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

549577
if refSE.startNr < 0 {
550-
return se
578+
return se, nil
551579
}
552580

553581
sampleDur := uint64(rep.sampleDur())
@@ -562,7 +590,7 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
562590
editListOffset := uint64(rep.EditListOffset)
563591
expectedTime := t // Track what the time should be without explicit T
564592
var s *m.S
565-
593+
var k *uint64
566594
for _, rs := range refSE.entries {
567595
refD := rs.D
568596
for j := 0; j <= rs.R; j++ {
@@ -571,6 +599,12 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
571599
d := nextT - t
572600

573601
if s == nil {
602+
var err error
603+
k, err = calculateK(d, rep.MediaTimescale, explicitChunkDurS)
604+
if err != nil {
605+
return se, err
606+
}
607+
574608
// First segment: apply editListOffset adjustment
575609
adjustedT := t
576610
adjustedD := d
@@ -590,12 +624,18 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
590624
}
591625
}
592626

593-
s = &m.S{T: m.Ptr(adjustedT), D: adjustedD}
627+
s = &m.S{T: m.Ptr(adjustedT), D: adjustedD, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
594628
se.entries = append(se.entries, s)
595629
expectedTime = adjustedT + adjustedD // Update expected time after first segment
596630
} else {
597631
// Subsequent segments
598632
if s.D != d {
633+
var err error
634+
k, err = calculateK(d, rep.MediaTimescale, explicitChunkDurS)
635+
if err != nil {
636+
return se, err
637+
}
638+
599639
// New segment with different duration
600640
adjustedT := t
601641
if editListOffset > 0 && t >= editListOffset {
@@ -605,10 +645,10 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
605645
// Only add explicit T if the time is not continuous
606646
if adjustedT == expectedTime {
607647
// Time is continuous, no need for explicit T
608-
s = &m.S{D: d}
648+
s = &m.S{D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
609649
} else {
610650
// Time is discontinuous, need explicit T
611-
s = &m.S{T: m.Ptr(adjustedT), D: d}
651+
s = &m.S{T: m.Ptr(adjustedT), D: d, CommonSegmentSequenceAttributes: m.CommonSegmentSequenceAttributes{K: k}}
612652
}
613653
se.entries = append(se.entries, s)
614654
expectedTime = adjustedT + d
@@ -620,7 +660,7 @@ func (a *asset) generateTimelineEntriesFromRef(refSE segEntries, repID string) s
620660
t = nextT
621661
}
622662
}
623-
return se
663+
return se, nil
624664
}
625665

626666
func (a *asset) setReferenceRep() error {
@@ -822,6 +862,7 @@ type RepData struct {
822862
ConstantSampleDuration *uint32 `json:"constantSampleDuration,omitempty"` // Non-zero if all samples have the same duration
823863
EditListOffset int64 `json:"editListOffset,omitempty"`
824864
PreEncrypted bool `json:"preEncrypted"`
865+
ChunkDurSSRS *float64 `json:"chunkDurSSRS,omitempty"` // Low delay chunk duration in seconds
825866
mediaRegexp *regexp.Regexp `json:"-"`
826867
initSeg *mp4.InitSegment `json:"-"`
827868
initBytes []byte `json:"-"`

cmd/livesim2/app/asset_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,103 @@ func TestAssetLookupForNameOverlap(t *testing.T) {
171171
require.Equal(t, "assets/testpic_2s_1", a.AssetPath)
172172
}
173173

174+
func TestCalculateK(t *testing.T) {
175+
testCases := []struct {
176+
description string
177+
segmentDuration uint64
178+
mediaTimescale int
179+
chunkDuration *float64
180+
expectedK *uint64
181+
expectedError string
182+
}{
183+
{
184+
description: "nil chunk duration",
185+
segmentDuration: 192000,
186+
mediaTimescale: 96000,
187+
chunkDuration: nil,
188+
expectedK: nil,
189+
},
190+
{
191+
description: "zero chunk duration",
192+
segmentDuration: 192000,
193+
mediaTimescale: 96000,
194+
chunkDuration: Ptr(0.0),
195+
expectedK: nil,
196+
},
197+
{
198+
description: "negative chunk duration",
199+
segmentDuration: 192000,
200+
mediaTimescale: 96000,
201+
chunkDuration: Ptr(-1.0),
202+
expectedK: nil,
203+
},
204+
{
205+
description: "zero media timescale",
206+
segmentDuration: 192000,
207+
mediaTimescale: 0,
208+
chunkDuration: Ptr(1.0),
209+
expectedK: nil,
210+
},
211+
{
212+
description: "k=4",
213+
segmentDuration: 192000,
214+
mediaTimescale: 96000,
215+
chunkDuration: Ptr(0.5),
216+
expectedK: Ptr(uint64(4)),
217+
},
218+
{
219+
description: "k=1, returns nil",
220+
segmentDuration: 192000,
221+
mediaTimescale: 96000,
222+
chunkDuration: Ptr(2.0),
223+
expectedK: nil,
224+
},
225+
{
226+
description: "rounding up",
227+
segmentDuration: 192000,
228+
mediaTimescale: 96000,
229+
chunkDuration: Ptr(0.57), // 3.5087... -> 4
230+
expectedK: Ptr(uint64(4)),
231+
},
232+
{
233+
description: "rounding down",
234+
segmentDuration: 192000,
235+
mediaTimescale: 96000,
236+
chunkDuration: Ptr(0.58), // 3.448... -> 3
237+
expectedK: Ptr(uint64(3)),
238+
},
239+
{
240+
description: "chunk duration greater than segment duration",
241+
segmentDuration: 192000,
242+
mediaTimescale: 96000,
243+
chunkDuration: Ptr(2.5), // 2.5s > 2.0s segment duration
244+
expectedK: nil,
245+
expectedError: "chunk duration 2.50s must be less than or equal to segment duration 2.00s",
246+
},
247+
}
248+
249+
for _, tc := range testCases {
250+
t.Run(tc.description, func(t *testing.T) {
251+
gotK, err := calculateK(tc.segmentDuration, tc.mediaTimescale, tc.chunkDuration)
252+
253+
if tc.expectedError != "" {
254+
require.Error(t, err)
255+
require.Equal(t, tc.expectedError, err.Error())
256+
require.Nil(t, gotK)
257+
return
258+
}
259+
260+
require.NoError(t, err)
261+
if tc.expectedK == nil {
262+
require.Nil(t, gotK)
263+
} else {
264+
require.NotNil(t, gotK)
265+
require.Equal(t, *tc.expectedK, *gotK)
266+
}
267+
})
268+
}
269+
}
270+
174271
func copyDir(srcDir, dstDir string) error {
175272
if err := os.MkdirAll(dstDir, 0755); err != nil {
176273
return err

cmd/livesim2/app/cmaf-ingester.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,16 +522,26 @@ func (c *cmafIngester) sendMediaSegments(ctx context.Context, nextSegNr, nowMS i
522522
atoMS := int(c.cfg.getAvailabilityTimeOffsetS() * 1000)
523523
for idx, rd := range c.repsData {
524524
var se segEntries
525+
var err error
525526
// The first representation is used as reference for generating timeline entries
526527
if idx == 0 {
527-
refSegEntries = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS)
528+
refSegEntries, err = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS, nil)
529+
if err != nil {
530+
return err
531+
}
528532
se = refSegEntries
529533
} else {
530534
switch rd.contentType {
531535
case "video", "text", "image":
532-
se = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS)
536+
se, err = c.asset.generateTimelineEntries(rd.repID, wTimes, atoMS, nil)
537+
if err != nil {
538+
return err
539+
}
533540
case "audio":
534-
se = c.asset.generateTimelineEntriesFromRef(refSegEntries, rd.repID)
541+
se, err = c.asset.generateTimelineEntriesFromRef(refSegEntries, rd.repID, nil)
542+
if err != nil {
543+
return err
544+
}
535545
default:
536546
return fmt.Errorf("unknown content type %s", rd.contentType)
537547
}

cmd/livesim2/app/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
defaultReqIntervalS = 24 * 3600
2828
defaultAvailabilityStartTimeS = 0
2929
defaultAvailabilityTimeComplete = true
30+
defaultSSRFlag = false
3031
defaultTimeShiftBufferDepthS = 60
3132
defaultStartNr = 0
3233
timeShiftBufferDepthMarginS = 10

cmd/livesim2/app/configurl.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ const (
6565
)
6666

6767
const (
68-
UrlParamSchemeIdUri = "urn:mpeg:dash:urlparam:2014"
68+
UrlParamSchemeIdUri = "urn:mpeg:dash:urlparam:2014"
69+
SsrSchemeIdUri = "urn:mpeg:dash:ssr:2023"
70+
AdaptationSetSwitchingSchemeIdUri = "urn:mpeg:dash:adaptation-set-switching:2016"
6971
)
7072

7173
type ResponseConfig struct {
@@ -110,6 +112,9 @@ type ResponseConfig struct {
110112
SegStatusCodes []SegStatusCodes `json:"SegStatus,omitempty"`
111113
Traffic []LossItvls `json:"Traffic,omitempty"`
112114
Query *Query `json:"Query,omitempty"`
115+
SSRFlag bool `json:"SSRFlag,omitempty"`
116+
SSRAS string `json:"SSRAS,omitempty"`
117+
ChunkDurSSR string `json:"ChunkDurSSR,omitempty"`
113118
}
114119

115120
// SegStatusCodes configures regular extraordinary segment response codes
@@ -241,6 +246,7 @@ func NewResponseConfig() *ResponseConfig {
241246
c := ResponseConfig{
242247
StartTimeS: defaultAvailabilityStartTimeS,
243248
AvailabilityTimeCompleteFlag: defaultAvailabilityTimeComplete,
249+
SSRFlag: defaultSSRFlag,
244250
TimeShiftBufferDepthS: Ptr(defaultTimeShiftBufferDepthS),
245251
StartNr: Ptr(defaultStartNr),
246252
TimeSubsDurMS: defaultTimeSubsDurMS,
@@ -394,6 +400,11 @@ cfgLoop:
394400
}
395401
case "annexI":
396402
cfg.Query = sc.ParseQuery(key, val)
403+
case "ssras":
404+
cfg.SSRAS = val
405+
cfg.SSRFlag = true
406+
case "chunkdurssr":
407+
cfg.ChunkDurSSR = val
397408
default:
398409
contentStartIdx = i
399410
break cfgLoop
@@ -447,6 +458,10 @@ func verifyAndFillConfig(cfg *ResponseConfig, nowMS int) error {
447458
}
448459
// We do not check here that the drm is one that has been configured,
449460
// since pre-encrypted content will influence what is valid.
461+
462+
if cfg.ChunkDurSSR != "" && cfg.SSRAS == "" {
463+
return fmt.Errorf("chunkDurSSR requires ssrAS to be configured")
464+
}
450465
return nil
451466
}
452467

0 commit comments

Comments
 (0)