Skip to content

Commit

Permalink
Merge pull request #178 from Dash-Industry-Forum/fix-multi-period-patch
Browse files Browse the repository at this point in the history
fix: handle multi-period MPDs properly
  • Loading branch information
tobbee authored Apr 23, 2024
2 parents 683348d + ab2d78c commit 531f57e
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 44 deletions.
57 changes: 25 additions & 32 deletions pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,6 @@ func addAttrChanges(patchRoot, oldElem, newElem *etree.Element, elemPath string)
return nil
}

type elementCounter struct {
latestTag string
counter int
}

func (ec *elementCounter) update(tag string) {
if ec.latestTag == tag {
ec.counter++
return
}
ec.latestTag = tag
ec.counter = 0
}

// index returns a 0-based index for an element
func (ec *elementCounter) index(tag string) int {
if ec.latestTag == tag {
return ec.counter + 1
}
return 0
}

// addElemChanges adds changes to patchRoot for elements in new relative to old
// elemPath is the path to the element in the MPD needed for the patch
func addElemChanges(patchRoot, old, new *etree.Element, elemPath string) error {
Expand Down Expand Up @@ -203,17 +181,21 @@ func addElemChanges(patchRoot, old, new *etree.Element, elemPath string) error {
newChildren := new.ChildElements()
diffOps := MyersDiff(oldChildren, newChildren, sameElements)
oldIdx := 0
ec := elementCounter{}
newIdx := 0
lastNewPath := ""
lastNewIdx := make(map[string]int) // Last index for each tag
for _, d := range diffOps {
for d.OldPos > oldIdx {
// Elements to keep at start. Look for changes at lower level
newElemPath := fmt.Sprintf("%s/%s[%d]", elemPath, oldChildren[oldIdx].Tag, oldIdx)
err := addElemChanges(patchRoot, oldChildren[oldIdx], newChildren[oldIdx], newElemPath)
oldElem := oldChildren[oldIdx]
tag := oldElem.Tag
addr := calcAddr(oldElem, lastNewIdx[tag])
newElemPath := fmt.Sprintf("%s/%s", elemPath, addr)
err := addElemChanges(patchRoot, oldChildren[oldIdx], newChildren[newIdx], newElemPath)
if err != nil {
return fmt.Errorf("addElemChanges for %s: %w", newElemPath, err)
}
ec.update(oldChildren[oldIdx].Tag)
lastNewIdx[tag]++
lastNewPath = newElemPath
oldIdx++
newIdx++
}
Expand All @@ -222,15 +204,26 @@ func addElemChanges(patchRoot, old, new *etree.Element, elemPath string) error {
case OpDelete:
e := patchRoot.CreateElement("remove")
oldElem := oldChildren[d.OldPos]
addr := calcAddr(oldElem, ec.index(oldElem.Tag))
addr := calcAddr(oldElem, oldIdx)
e.CreateAttr("sel", fmt.Sprintf("%s/%s", elemPath, addr))
oldIdx++
case OpInsert:
e := patchRoot.CreateElement("add")
newElem := newChildren[d.NewPos]
addr := calcAddr(newElem, ec.index(newElem.Tag))
e.CreateAttr("sel", fmt.Sprintf("%s/%s[%d]", elemPath, addr, oldIdx))
addr := calcAddr(newElem, lastNewIdx[newElem.Tag])
newPath := fmt.Sprintf("%s/%s", elemPath, addr)
if lastNewPath == "" {
// Always use prepend on insertion at start (since the list may be empty)
e.CreateAttr("sel", elemPath)
e.CreateAttr("pos", "prepend")
} else {
// Put after previous element
e.CreateAttr("sel", lastNewPath)
e.CreateAttr("pos", "after")
}
e.AddChild(newElem.Copy())
lastNewPath = newPath
lastNewIdx[newElem.Tag]++
newIdx++
}
}
Expand All @@ -239,13 +232,13 @@ func addElemChanges(patchRoot, old, new *etree.Element, elemPath string) error {
for oldIdx < len(oldChildren) {
oldElem := oldChildren[oldIdx]
newElem := newChildren[newIdx]
addr := calcAddr(oldElem, ec.index(oldElem.Tag))
addr := calcAddr(oldElem, lastNewIdx[oldElem.Tag])
newElemPath := fmt.Sprintf("%s/%s", elemPath, addr)
err := addElemChanges(patchRoot, oldElem, newElem, newElemPath)
if err != nil {
return fmt.Errorf("addElemChanges for %s: %w", newElemPath, err)
}
ec.update(oldElem.Tag)
lastNewIdx[oldElem.Tag]++
oldIdx++
newIdx++
}
Expand Down
38 changes: 26 additions & 12 deletions pkg/patch/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package patch

import (
"os"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -63,18 +64,24 @@ const wantedPatchSegmentTimelineNumber = (`<?xml version="1.0" encoding="UTF-8"?

func TestDiff(t *testing.T) {
cases := []struct {
desc string
oldMPD string
newMPD string
wantedDiff string
wantedErr string
desc string
oldMPD string
newMPD string
wantedDiff string
wantedDiffFile string
wantedErr string
}{
{
desc: "too big publishTime diff vs ttl",
oldMPD: "testdata/testpic_2s_1.mpd",
newMPD: "testdata/testpic_2s_2_late_publish.mpd",
wantedDiff: "",
wantedErr: ErrPatchTooLate.Error(),
desc: "multiPeriodPatch",
oldMPD: "testdata/multiperiod_1.mpd",
newMPD: "testdata/multiperiod_2.mpd",
wantedDiffFile: "testdata/multiperiod_patch.mpp",
},
{
desc: "too big publishTime diff vs ttl",
oldMPD: "testdata/testpic_2s_1.mpd",
newMPD: "testdata/testpic_2s_2_late_publish.mpd",
wantedErr: ErrPatchTooLate.Error(),
},
{
desc: "segmentTimelineTime",
Expand Down Expand Up @@ -109,9 +116,16 @@ func TestDiff(t *testing.T) {
require.NoError(t, err)
patch.Indent(2)
out, err := patch.WriteToString()
//os.WriteFile(fmt.Sprintf("%s.mpp", c.desc), []byte(out), 0o644)
wantedDiff := c.wantedDiff
if c.wantedDiffFile != "" {
//os.WriteFile(c.wantedDiffFile, []byte(out), 0o644)
d, err := os.ReadFile(c.wantedDiffFile)
require.NoError(t, err)
wantedDiff = string(d)
wantedDiff = strings.Replace(wantedDiff, "\r\n", "\n", -1) // Windows line endings
}
require.NoError(t, err)
require.Equal(t, c.wantedDiff, out)
require.Equal(t, wantedDiff, out)
})
}
}
Expand Down
67 changes: 67 additions & 0 deletions pkg/patch/testdata/multiperiod_1.mpd
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" id="base" profiles="" type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z" publishTime="2024-04-21T06:10:58Z" minimumUpdatePeriod="PT2S" minBufferTime="PT2S" timeShiftBufferDepth="PT1M" maxSegmentDuration="PT2S">
<ProgramInformation>
<Title>640x360@30 video, 48kHz audio, 2s segments</Title>
</ProgramInformation>
<PatchLocation ttl="60">/patch/livesim2/segtimeline_1/patch_60/periods_60/testpic_2s/Manifest.mpp?publishTime=2024-04-21T06%3A10%3A58Z</PatchLocation>
<Period id="P28561329" start="PT476022H9M">
<AdaptationSet id="1" lang="en" contentType="audio" segmentAlignment="true" mimeType="audio/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="48000" presentationTimeOffset="82256627520000">
<SegmentTimeline>
<S t="82256630208512" d="96256"></S>
<S t="82256630304768" d="95232"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="A48" bandwidth="48000" audioSamplingRate="48000" codecs="mp4a.40.2">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"></AudioChannelConfiguration>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="video" par="16:9" minWidth="640" maxWidth="640" minHeight="360" maxHeight="360" maxFrameRate="60/2" segmentAlignment="true" mimeType="video/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="90000" presentationTimeOffset="154231176600000">
<SegmentTimeline>
<S t="154231181640000" d="180000" r="1"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="V300" bandwidth="300000" width="640" height="360" sar="1:1" frameRate="60/2" codecs="avc1.64001e"></Representation>
</AdaptationSet>
</Period>
<Period id="P28561330" start="PT476022H10M">
<AdaptationSet id="1" lang="en" contentType="audio" segmentAlignment="true" mimeType="audio/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="48000" presentationTimeOffset="82256630400000">
<SegmentTimeline>
<S t="82256630400000" d="96256" r="2"></S>
<S t="82256630688768" d="95232"></S>
<S t="82256630784000" d="96256" r="2"></S>
<S t="82256631072768" d="95232"></S>
<S t="82256631168000" d="96256" r="2"></S>
<S t="82256631456768" d="95232"></S>
<S t="82256631552000" d="96256" r="2"></S>
<S t="82256631840768" d="95232"></S>
<S t="82256631936000" d="96256" r="2"></S>
<S t="82256632224768" d="95232"></S>
<S t="82256632320000" d="96256" r="2"></S>
<S t="82256632608768" d="95232"></S>
<S t="82256632704000" d="96256" r="2"></S>
<S t="82256632992768" d="95232"></S>
<S t="82256633088000" d="96256"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="A48" bandwidth="48000" audioSamplingRate="48000" codecs="mp4a.40.2">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"></AudioChannelConfiguration>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="video" par="16:9" minWidth="640" maxWidth="640" minHeight="360" maxHeight="360" maxFrameRate="60/2" segmentAlignment="true" mimeType="video/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="90000" presentationTimeOffset="154231182000000">
<SegmentTimeline>
<S t="154231182000000" d="180000" r="28"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="V300" bandwidth="300000" width="640" height="360" sar="1:1" frameRate="60/2" codecs="avc1.64001e"></Representation>
</AdaptationSet>
</Period>
<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014" value="https://time.akamai.com/?iso&amp;ms"></UTCTiming>
</MPD>
67 changes: 67 additions & 0 deletions pkg/patch/testdata/multiperiod_2.mpd
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" id="base" profiles="" type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z" publishTime="2024-04-21T06:11:04Z" minimumUpdatePeriod="PT2S" minBufferTime="PT2S" timeShiftBufferDepth="PT1M" maxSegmentDuration="PT2S">
<ProgramInformation>
<Title>640x360@30 video, 48kHz audio, 2s segments</Title>
</ProgramInformation>
<PatchLocation ttl="60">/patch/livesim2/segtimeline_1/patch_60/periods_60/testpic_2s/Manifest.mpp?publishTime=2024-04-21T06%3A11%3A04Z</PatchLocation>
<Period id="P28561330" start="PT476022H10M">
<AdaptationSet id="1" lang="en" contentType="audio" segmentAlignment="true" mimeType="audio/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="48000" presentationTimeOffset="82256630400000">
<SegmentTimeline>
<S t="82256630496256" d="96256" r="1"></S>
<S t="82256630688768" d="95232"></S>
<S t="82256630784000" d="96256" r="2"></S>
<S t="82256631072768" d="95232"></S>
<S t="82256631168000" d="96256" r="2"></S>
<S t="82256631456768" d="95232"></S>
<S t="82256631552000" d="96256" r="2"></S>
<S t="82256631840768" d="95232"></S>
<S t="82256631936000" d="96256" r="2"></S>
<S t="82256632224768" d="95232"></S>
<S t="82256632320000" d="96256" r="2"></S>
<S t="82256632608768" d="95232"></S>
<S t="82256632704000" d="96256" r="2"></S>
<S t="82256632992768" d="95232"></S>
<S t="82256633088000" d="96256" r="1"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="A48" bandwidth="48000" audioSamplingRate="48000" codecs="mp4a.40.2">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"></AudioChannelConfiguration>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="video" par="16:9" minWidth="640" maxWidth="640" minHeight="360" maxHeight="360" maxFrameRate="60/2" segmentAlignment="true" mimeType="video/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="90000" presentationTimeOffset="154231182000000">
<SegmentTimeline>
<S t="154231182180000" d="180000" r="28"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="V300" bandwidth="300000" width="640" height="360" sar="1:1" frameRate="60/2" codecs="avc1.64001e"></Representation>
</AdaptationSet>
</Period>
<Period id="P28561331" start="PT476022H11M">
<AdaptationSet id="1" lang="en" contentType="audio" segmentAlignment="true" mimeType="audio/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="48000" presentationTimeOffset="82256633280000">
<SegmentTimeline>
<S t="82256633280512" d="96256"></S>
<S t="82256633376768" d="95232"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="A48" bandwidth="48000" audioSamplingRate="48000" codecs="mp4a.40.2">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"></AudioChannelConfiguration>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="video" par="16:9" minWidth="640" maxWidth="640" minHeight="360" maxHeight="360" maxFrameRate="60/2" segmentAlignment="true" mimeType="video/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"></Role>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="90000" presentationTimeOffset="154231187400000">
<SegmentTimeline>
<S t="154231187400000" d="180000" r="1"></S>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="V300" bandwidth="300000" width="640" height="360" sar="1:1" frameRate="60/2" codecs="avc1.64001e"></Representation>
</AdaptationSet>
</Period>
<UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014" value="https://time.akamai.com/?iso&amp;ms"></UTCTiming>
</MPD>
45 changes: 45 additions & 0 deletions pkg/patch/testdata/multiperiod_patch.mpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<Patch xmlns="urn:mpeg:dash:schema:mpd-patch:2020" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd-patch:2020 DASH-MPD-PATCH.xsd" mpdId="base" originalPublishTime="2024-04-21T06:10:58Z" publishTime="2024-04-21T06:11:04Z">
<replace sel="/MPD/@publishTime">2024-04-21T06:11:04Z</replace>
<replace sel="/MPD/PatchLocation[1]">
<PatchLocation ttl="60">/patch/livesim2/segtimeline_1/patch_60/periods_60/testpic_2s/Manifest.mpp?publishTime=2024-04-21T06%3A11%3A04Z</PatchLocation>
</replace>
<remove sel="/MPD/Period[@id=&apos;P28561329&apos;]"/>
<remove sel="/MPD/Period[@id=&apos;P28561330&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline/S[1]"/>
<add sel="/MPD/Period[@id=&apos;P28561330&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline" pos="prepend">
<S t="82256630496256" d="96256" r="1"/>
</add>
<remove sel="/MPD/Period[@id=&apos;P28561330&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline/S[15]"/>
<add sel="/MPD/Period[@id=&apos;P28561330&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline/S[14]" pos="after">
<S t="82256633088000" d="96256" r="1"/>
</add>
<remove sel="/MPD/Period[@id=&apos;P28561330&apos;]/AdaptationSet[@id=&apos;2&apos;]/SegmentTemplate/SegmentTimeline/S[1]"/>
<add sel="/MPD/Period[@id=&apos;P28561330&apos;]/AdaptationSet[@id=&apos;2&apos;]/SegmentTemplate/SegmentTimeline" pos="prepend">
<S t="154231182180000" d="180000" r="28"/>
</add>
<add sel="/MPD/Period[@id=&apos;P28561330&apos;]" pos="after">
<Period id="P28561331" start="PT476022H11M">
<AdaptationSet id="1" lang="en" contentType="audio" segmentAlignment="true" mimeType="audio/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="48000" presentationTimeOffset="82256633280000">
<SegmentTimeline>
<S t="82256633280512" d="96256"/>
<S t="82256633376768" d="95232"/>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="A48" bandwidth="48000" audioSamplingRate="48000" codecs="mp4a.40.2">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="video" par="16:9" minWidth="640" maxWidth="640" minHeight="360" maxHeight="360" maxFrameRate="60/2" segmentAlignment="true" mimeType="video/mp4" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>
<SegmentTemplate media="$RepresentationID$/$Time$.m4s" initialization="$RepresentationID$/init.mp4" timescale="90000" presentationTimeOffset="154231187400000">
<SegmentTimeline>
<S t="154231187400000" d="180000" r="1"/>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="V300" bandwidth="300000" width="640" height="360" sar="1:1" frameRate="60/2" codecs="avc1.64001e"/>
</AdaptationSet>
</Period>
</add>
</Patch>

0 comments on commit 531f57e

Please sign in to comment.