Skip to content

Commit 36896d8

Browse files
committed
fix: enable Pattern for SegmentTimeline with Number
1 parent 5a5476d commit 36896d8

File tree

3 files changed

+291
-84
lines changed

3 files changed

+291
-84
lines changed

cmd/livesim2/app/configurl.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -366,12 +366,12 @@ cfgLoop:
366366
} else {
367367
cfg.SegTimelineMode = SegTimelineModeTime
368368
}
369-
case "segtimeline_pattern":
370-
cfg.SegTimelineMode = SegTimelineModePattern
371-
case "segtimelinenr_pattern":
372-
cfg.SegTimelineMode = SegTimelineModeNrPattern
373369
case "segtimelinenr":
374-
cfg.SegTimelineNrFlag = true
370+
if val == "pattern" {
371+
cfg.SegTimelineMode = SegTimelineModeNrPattern
372+
} else {
373+
cfg.SegTimelineNrFlag = true
374+
}
375375
case "peroff": // Set the period offset
376376
cfg.PeriodOffset = sc.AtoiPtr(key, val)
377377
case "scte35": // Signal this many SCTE-35 ad periods inband (emsg messages) every minute

cmd/livesim2/app/livempd_test.go

Lines changed: 263 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,7 +1262,7 @@ func TestPatternGeneration(t *testing.T) {
12621262
}
12631263
}
12641264

1265-
// TestPatternConsistency tests that the same pattern is generated regardless of sliding window position
1265+
// TestPatternConsistency tests that the same pattern is generated for both $Time$ and $Number$ addressing modes
12661266
func TestPatternConsistency(t *testing.T) {
12671267
vodFS := os.DirFS("testdata/assets")
12681268
tmpDir := t.TempDir()
@@ -1275,19 +1275,246 @@ func TestPatternConsistency(t *testing.T) {
12751275
require.True(t, ok)
12761276

12771277
// Test with different nowMS values to get different sliding windows
1278-
// For a mulitple of 8 seconds, the pE should be 0, and then increase for each 2s step.
1279-
// For nowMS = 800000, the first segemnt start with tsbd=30s is 768s, which is a multiple of 8s.
1278+
// For a multiple of 8 seconds, the pE should be 0, and then increase for each 2s step.
1279+
// For nowMS = 8000000, the first segment start with tsbd=30s is 768s, which is a multiple of 8s.
12801280
testTimes := []int{8000000, 802000, 804000, 806000}
1281-
var previousPattern *m.PatternType
12821281

1283-
for _, nowMS := range testTimes {
1284-
t.Run(fmt.Sprintf("nowMS_%d", nowMS), func(t *testing.T) {
1285-
cfg := NewResponseConfig()
1286-
cfg.SegTimelineMode = SegTimelineModePattern
1287-
cfg.TimeShiftBufferDepthS = Ptr(30)
1282+
// Test both $Time$ and $Number$ modes
1283+
modes := []struct {
1284+
name string
1285+
mode SegTimelineMode
1286+
expectedMedia string
1287+
shouldHaveStartNr bool
1288+
}{
1289+
{
1290+
name: "$Time$",
1291+
mode: SegTimelineModePattern,
1292+
expectedMedia: "$RepresentationID$/$Time$.m4s",
1293+
shouldHaveStartNr: false,
1294+
},
1295+
{
1296+
name: "$Number$",
1297+
mode: SegTimelineModeNrPattern,
1298+
expectedMedia: "$RepresentationID$/$Number$.m4s",
1299+
shouldHaveStartNr: true,
1300+
},
1301+
}
1302+
1303+
// Store patterns from $Time$ mode to compare with $Number$ mode
1304+
var timePatterns []*m.PatternType
1305+
var timePEs []uint32
1306+
var timeSegmentTimelines []*m.SegmentTimelineType
1307+
1308+
for modeIdx, modeTest := range modes {
1309+
t.Run(modeTest.name, func(t *testing.T) {
1310+
for timeIdx, nowMS := range testTimes {
1311+
t.Run(fmt.Sprintf("nowMS_%d", nowMS), func(t *testing.T) {
1312+
cfg := NewResponseConfig()
1313+
cfg.SegTimelineMode = modeTest.mode
1314+
cfg.TimeShiftBufferDepthS = Ptr(30)
1315+
1316+
liveMPD, err := LiveMPD(asset, "Manifest.mpd", cfg, nil, nowMS)
1317+
require.NoError(t, err)
1318+
1319+
// Find audio adaptation set
1320+
var audioAS *m.AdaptationSetType
1321+
for _, as := range liveMPD.Periods[0].AdaptationSets {
1322+
if as.ContentType == "audio" {
1323+
audioAS = as
1324+
break
1325+
}
1326+
}
1327+
require.NotNil(t, audioAS, "Should have audio adaptation set")
1328+
1329+
// Verify media template
1330+
assert.Equal(t, modeTest.expectedMedia, audioAS.SegmentTemplate.Media,
1331+
"Media template should match expected for %s mode", modeTest.name)
1332+
1333+
// Verify startNumber
1334+
if modeTest.shouldHaveStartNr {
1335+
require.NotNil(t, audioAS.SegmentTemplate.StartNumber,
1336+
"StartNumber should be set for $Number$ mode")
1337+
}
1338+
1339+
stl := audioAS.SegmentTemplate.SegmentTimeline
1340+
require.NotNil(t, stl, "Should have SegmentTimeline")
1341+
require.NotNil(t, stl.Pattern, "Should have Pattern")
1342+
require.Len(t, stl.Pattern, 1, "Should have exactly one Pattern")
1343+
1344+
pattern := stl.Pattern[0]
12881345

1289-
liveMPD, err := LiveMPD(asset, "Manifest.mpd", cfg, nil, nowMS)
1346+
// Verify the pattern starts with the longest duration
1347+
if len(pattern.P) > 0 {
1348+
maxDur := pattern.P[0].D
1349+
for _, p := range pattern.P {
1350+
assert.LessOrEqual(t, p.D, maxDur, "First duration should be the maximum")
1351+
}
1352+
}
1353+
1354+
// Check that S element has proper PE value
1355+
require.NotNil(t, stl.S, "Should have S elements")
1356+
require.Greater(t, len(stl.S), 0, "Should have at least one S element")
1357+
s := stl.S[0]
1358+
require.NotNil(t, s.PE, "Should have PE value")
1359+
require.NotNil(t, s.T, "S element should have T attribute (same for both modes)")
1360+
1361+
// Calculate expected PE value based on nowMS
1362+
var expectedPE int
1363+
switch nowMS {
1364+
case 8000000:
1365+
expectedPE = 0
1366+
case 802000:
1367+
expectedPE = 1
1368+
case 804000:
1369+
expectedPE = 2
1370+
case 806000:
1371+
expectedPE = 3
1372+
}
1373+
1374+
assert.Equal(t, expectedPE, int(*s.PE),
1375+
fmt.Sprintf("PE value should match expected based on first segment position (nowMS=%d, actual PE=%d)",
1376+
nowMS, int(*s.PE)))
1377+
1378+
// PE should be between 0 and pattern length - 1
1379+
patternLen := 0
1380+
for _, p := range pattern.P {
1381+
patternLen += int(p.R) + 1
1382+
}
1383+
assert.GreaterOrEqual(t, int(*s.PE), 0, "PE should be >= 0")
1384+
assert.Less(t, int(*s.PE), patternLen, "PE should be < pattern length")
1385+
1386+
// Verify EssentialProperty is present
1387+
hasPatternProperty := false
1388+
for _, prop := range audioAS.EssentialProperties {
1389+
if prop.SchemeIdUri == "urn:mpeg:dash:pattern:2024" {
1390+
hasPatternProperty = true
1391+
break
1392+
}
1393+
}
1394+
assert.True(t, hasPatternProperty, "Should have EssentialProperty for pattern support")
1395+
1396+
// Store patterns from $Time$ mode for comparison
1397+
if modeIdx == 0 {
1398+
timePatterns = append(timePatterns, pattern)
1399+
timePEs = append(timePEs, *s.PE)
1400+
timeSegmentTimelines = append(timeSegmentTimelines, stl)
1401+
} else {
1402+
// Compare $Number$ mode with $Time$ mode
1403+
timePattern := timePatterns[timeIdx]
1404+
assert.Equal(t, len(timePattern.P), len(pattern.P),
1405+
"Pattern length should be identical for both modes")
1406+
for i := range pattern.P {
1407+
assert.Equal(t, timePattern.P[i].D, pattern.P[i].D,
1408+
"Pattern durations should be identical for both modes")
1409+
assert.Equal(t, timePattern.P[i].R, pattern.P[i].R,
1410+
"Pattern repetitions should be identical for both modes")
1411+
}
1412+
1413+
// PE values should be identical
1414+
assert.Equal(t, timePEs[timeIdx], *s.PE,
1415+
"PE values should be identical for both modes")
1416+
1417+
// SegmentTimeline S elements should be identical (same T, D, R, p, pE)
1418+
timeSTL := timeSegmentTimelines[timeIdx]
1419+
require.Equal(t, len(timeSTL.S), len(stl.S),
1420+
"Number of S elements should be identical")
1421+
for i := range stl.S {
1422+
assert.Equal(t, timeSTL.S[i].T, stl.S[i].T,
1423+
"S element T should be identical for both modes")
1424+
assert.Equal(t, timeSTL.S[i].D, stl.S[i].D,
1425+
"S element D should be identical for both modes")
1426+
assert.Equal(t, timeSTL.S[i].R, stl.S[i].R,
1427+
"S element R should be identical for both modes")
1428+
assert.Equal(t, timeSTL.S[i].P, stl.S[i].P,
1429+
"S element p should be identical for both modes")
1430+
assert.Equal(t, timeSTL.S[i].PE, stl.S[i].PE,
1431+
"S element pE should be identical for both modes")
1432+
}
1433+
}
1434+
})
1435+
}
1436+
})
1437+
}
1438+
}
1439+
1440+
// TestURLParsingForNrPattern tests that segtimelinenr_pattern/ URL parameter is correctly parsed
1441+
func TestURLParsingForNrPattern(t *testing.T) {
1442+
testCases := []struct {
1443+
url string
1444+
expectedMode SegTimelineMode
1445+
}{
1446+
{
1447+
url: "/livesim2/segtimelinenr_pattern/testpic_2s/Manifest.mpd",
1448+
expectedMode: SegTimelineModeNrPattern,
1449+
},
1450+
{
1451+
url: "/livesim2/segtimeline_pattern/testpic_2s/Manifest.mpd",
1452+
expectedMode: SegTimelineModePattern,
1453+
},
1454+
{
1455+
url: "/livesim2/segtimeline_time/testpic_2s/Manifest.mpd",
1456+
expectedMode: SegTimelineModeTime,
1457+
},
1458+
}
1459+
1460+
for _, tc := range testCases {
1461+
t.Run(tc.url, func(t *testing.T) {
1462+
cfg, err := processURLCfg(tc.url, 1000000)
12901463
require.NoError(t, err)
1464+
assert.Equal(t, tc.expectedMode, cfg.SegTimelineMode, "SegTimelineMode should match")
1465+
})
1466+
}
1467+
}
1468+
1469+
// TestURLToMPDWithPattern tests end-to-end URL to MPD generation with Pattern
1470+
func TestURLToMPDWithPattern(t *testing.T) {
1471+
vodFS := os.DirFS("testdata/assets")
1472+
tmpDir := t.TempDir()
1473+
am := newAssetMgr(vodFS, tmpDir, false)
1474+
logger := slog.Default()
1475+
err := am.discoverAssets(logger)
1476+
require.NoError(t, err)
1477+
1478+
testCases := []struct {
1479+
url string
1480+
nowMS int
1481+
expectedMedia string
1482+
shouldHavePattern bool
1483+
description string
1484+
}{
1485+
{
1486+
url: "/livesim2/segtimelinenr_pattern/testpic_2s/Manifest.mpd",
1487+
nowMS: 8000000,
1488+
expectedMedia: "$RepresentationID$/$Number$.m4s",
1489+
shouldHavePattern: true,
1490+
description: "segtimelinenr_pattern should use $Number$ and Pattern",
1491+
},
1492+
{
1493+
url: "/livesim2/segtimeline_pattern/testpic_2s/Manifest.mpd",
1494+
nowMS: 8000000,
1495+
expectedMedia: "$RepresentationID$/$Time$.m4s",
1496+
shouldHavePattern: true,
1497+
description: "segtimeline_pattern should use $Time$ and Pattern",
1498+
},
1499+
}
1500+
1501+
for _, tc := range testCases {
1502+
t.Run(tc.description, func(t *testing.T) {
1503+
// Parse URL configuration
1504+
cfg, err := processURLCfg(tc.url, tc.nowMS)
1505+
require.NoError(t, err)
1506+
1507+
// Find asset
1508+
contentPart := cfg.URLContentPart()
1509+
asset, ok := am.findAsset(contentPart)
1510+
require.True(t, ok, "Should find asset")
1511+
1512+
// Extract MPD name
1513+
_, mpdName := path.Split(contentPart)
1514+
1515+
// Generate live MPD
1516+
liveMPD, err := LiveMPD(asset, mpdName, cfg, nil, tc.nowMS)
1517+
require.NoError(t, err, "LiveMPD should succeed")
12911518

12921519
// Find audio adaptation set
12931520
var audioAS *m.AdaptationSetType
@@ -1299,64 +1526,36 @@ func TestPatternConsistency(t *testing.T) {
12991526
}
13001527
require.NotNil(t, audioAS, "Should have audio adaptation set")
13011528

1302-
stl := audioAS.SegmentTemplate.SegmentTimeline
1303-
require.NotNil(t, stl, "Should have SegmentTimeline")
1304-
require.NotNil(t, stl.Pattern, "Should have Pattern")
1305-
require.Len(t, stl.Pattern, 1, "Should have exactly one Pattern")
1306-
1307-
pattern := stl.Pattern[0]
1308-
1309-
// The pattern should always be the same canonical pattern
1310-
if previousPattern != nil {
1311-
assert.Equal(t, len(previousPattern.P), len(pattern.P), "Pattern length should be consistent")
1312-
for i := range pattern.P {
1313-
assert.Equal(t, previousPattern.P[i].D, pattern.P[i].D, "Pattern durations should match")
1314-
assert.Equal(t, previousPattern.P[i].R, pattern.P[i].R, "Pattern repetitions should match")
1315-
}
1316-
}
1317-
1318-
// Verify the pattern starts with the longest duration
1319-
if len(pattern.P) > 0 {
1320-
maxDur := pattern.P[0].D
1321-
for _, p := range pattern.P {
1322-
assert.LessOrEqual(t, p.D, maxDur, "First duration should be the maximum")
1529+
// Verify media template
1530+
assert.Equal(t, tc.expectedMedia, audioAS.SegmentTemplate.Media,
1531+
"Media template should match expected")
1532+
1533+
// Verify Pattern
1534+
if tc.shouldHavePattern {
1535+
stl := audioAS.SegmentTemplate.SegmentTimeline
1536+
require.NotNil(t, stl, "Should have SegmentTimeline")
1537+
require.NotNil(t, stl.Pattern, "Should have Pattern")
1538+
require.Len(t, stl.Pattern, 1, "Should have exactly one Pattern")
1539+
require.NotNil(t, stl.S, "Should have S elements")
1540+
require.Greater(t, len(stl.S), 0, "Should have at least one S element")
1541+
require.NotNil(t, stl.S[0].PE, "Should have PE value")
1542+
1543+
// Verify EssentialProperty for pattern support
1544+
hasPatternProperty := false
1545+
for _, prop := range audioAS.EssentialProperties {
1546+
if prop.SchemeIdUri == "urn:mpeg:dash:pattern:2024" {
1547+
hasPatternProperty = true
1548+
break
1549+
}
13231550
}
1551+
assert.True(t, hasPatternProperty, "Should have EssentialProperty for pattern support")
13241552
}
13251553

1326-
previousPattern = pattern
1327-
1328-
// Check that S element has proper PE value
1329-
require.NotNil(t, stl.S, "Should have S elements")
1330-
require.Greater(t, len(stl.S), 0, "Should have at least one S element")
1331-
s := stl.S[0]
1332-
require.NotNil(t, s.PE, "Should have PE value")
1333-
1334-
// Calculate expected PE value based on nowMS
1335-
// For testpic_2s: tsbd=30s, audio segments are ~2s long following video segments
1336-
// PE should be based on segment number modulo 4 (pattern length)
1337-
var expectedPE int
1338-
switch nowMS {
1339-
case 8000000: // 800000 -> first segment at 768s -> segment 384 -> 384%4 = 0 -> PE = 0
1340-
expectedPE = 0
1341-
case 802000: // 802000 -> first segment at 770s -> segment 385 -> 385%4 = 1 -> PE = 1
1342-
expectedPE = 1
1343-
case 804000: // 804000 -> first segment at 772s -> segment 386 -> 386%4 = 2 -> PE = 2
1344-
expectedPE = 2
1345-
case 806000: // 806000 -> first segment at 774s -> segment 387 -> 387%4 = 3 -> PE = 3
1346-
expectedPE = 3
1347-
}
1348-
1349-
assert.Equal(t, expectedPE, int(*s.PE),
1350-
fmt.Sprintf("PE value should match expected based on first segment position (nowMS=%d, actual PE=%d)",
1351-
nowMS, int(*s.PE)))
1352-
1353-
// PE should be between 0 and pattern length - 1
1354-
patternLen := 0
1355-
for _, p := range pattern.P {
1356-
patternLen += int(p.R) + 1
1554+
// For $Number$ mode, verify startNumber is set
1555+
if strings.Contains(tc.expectedMedia, "$Number$") {
1556+
require.NotNil(t, audioAS.SegmentTemplate.StartNumber,
1557+
"StartNumber should be set for $Number$ mode")
13571558
}
1358-
assert.GreaterOrEqual(t, int(*s.PE), 0, "PE should be >= 0")
1359-
assert.Less(t, int(*s.PE), patternLen, "PE should be < pattern length")
13601559
})
13611560
}
13621561
}

0 commit comments

Comments
 (0)