Skip to content

Commit

Permalink
Merge pull request #180 from Dash-Industry-Forum/add-expires-to-patch
Browse files Browse the repository at this point in the history
fix: add Expires header to Patch response
  • Loading branch information
tobbee authored Apr 23, 2024
2 parents 531f57e + 5c5eaf5 commit 5f5059d
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 48 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- MPD Patch functionality with new `/patch_ttl` URL configuration
- nowDate query parameter as an alternative to nowMS for MPD and patch
- MPD Patch has Expires header equal to publishTime + ttl + 10s

### Fixed

Expand Down
3 changes: 2 additions & 1 deletion cmd/livesim2/app/handler_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (s *Server) patchHandlerFunc(w http.ResponseWriter, r *http.Request) {
r.URL.RawQuery = newQuery
s.livesimHandlerFunc(new, r)

doc, err := patch.MPDDiff(old.body, new.body)
doc, expiration, err := patch.MPDDiff(old.body, new.body)
switch {
case errors.Is(err, patch.ErrPatchSamePublishTime):
http.Error(w, err.Error(), http.StatusTooEarly)
Expand All @@ -71,6 +71,7 @@ func (s *Server) patchHandlerFunc(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/dash-patch+xml")
w.Header().Set("Expires", expiration.Format(http.TimeFormat))
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
_, err = w.Write(b)
if err != nil {
Expand Down
22 changes: 16 additions & 6 deletions cmd/livesim2/app/handler_patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ import (
)

var wantedPatchSegTimelineTime = `<?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-02T15:50:56Z" publishTime="2024-04-02T15:52:40Z">
<replace sel="/MPD/@publishTime">2024-04-02T15:52:40Z</replace>
<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-02T15:50:56Z" publishTime="2024-04-02T15:51:40Z">
<replace sel="/MPD/@publishTime">2024-04-02T15:51:40Z</replace>
<replace sel="/MPD/PatchLocation[1]">
<PatchLocation ttl="60">/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-04-02T15%3A52%3A40Z</PatchLocation>
<PatchLocation ttl="60">/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-04-02T15%3A51%3A40Z</PatchLocation>
</replace>
<remove sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline/S[1]"/>
<remove sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline/S[1]"/>
<add sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline" pos="prepend">
<S t="82179508704256" d="96256" r="1"/>
<S t="82179505824768" d="95232"/>
</add>
<add sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline/S[15]" pos="after">
<S d="96256" r="1"/>
</add>
<remove sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;2&apos;]/SegmentTemplate/SegmentTimeline/S[1]"/>
<add sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;2&apos;]/SegmentTemplate/SegmentTimeline" pos="prepend">
<S t="154086578820000" d="180000" r="30"/>
<S t="154086573420000" d="180000" r="30"/>
</add>
</Patch>
`
Expand Down Expand Up @@ -67,6 +71,7 @@ func TestPatchHandler(t *testing.T) {
wantedStatusCode int
wantedContentType string
wantedBody string
wantedExpires string
}{
{
desc: "segTimeline no update yet",
Expand All @@ -84,17 +89,19 @@ func TestPatchHandler(t *testing.T) {
},
{
desc: "segTimeline",
url: "/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-04-02T15:50:56Z&nowMS=1712073160000",
url: "/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-04-02T15:50:56Z&nowDate=2024-04-02T15:51:40Z",
wantedStatusCode: http.StatusOK,
wantedContentType: "application/dash-patch+xml",
wantedBody: wantedPatchSegTimelineTime,
wantedExpires: "Tue, 02 Apr 2024 15:52:06 GMT",
},
{
desc: "segTimeline with Number",
url: "/patch/livesim2/patch_60/segtimelinenr_1/testpic_2s/Manifest.mpp?publishTime=2024-04-16T07:34:38Z&nowDate=2024-04-16T07:34:57Z",
wantedStatusCode: http.StatusOK,
wantedContentType: "application/dash-patch+xml",
wantedBody: wantedPatchSegTimelineNumberWithAddAtEnd,
wantedExpires: "Tue, 16 Apr 2024 07:35:48 GMT",
},
}

Expand All @@ -106,6 +113,9 @@ func TestPatchHandler(t *testing.T) {
if tc.wantedStatusCode != http.StatusOK {
return
}
if tc.wantedExpires != "" {
require.Equal(t, tc.wantedExpires, resp.Header.Get("Expires"))
}
bodyStr := string(body)
require.Equal(t, tc.wantedBody, bodyStr)
})
Expand Down
47 changes: 25 additions & 22 deletions pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"github.com/beevik/etree"
)

// PatchExpirationMargin is added to the HTTP expiration beyond ttl.
const PatchExpirationMargin = 10 * time.Second

var ErrPatchSamePublishTime = fmt.Errorf("same publishTime in both MPDs")
var ErrPatchTooLate = fmt.Errorf("patch TTL exceeded")

Expand Down Expand Up @@ -46,77 +49,77 @@ func newPatchDoc(oldRoot, newRoot *etree.Element) (*patchDoc, error) {
}

// MPDDiff compares two MPDs and returns a patch document or an error.
func MPDDiff(mpdOld, mpdNew []byte) (*etree.Document, error) {
func MPDDiff(mpdOld, mpdNew []byte) (doc *etree.Document, expiration time.Time, err error) {
dOld := etree.NewDocument()
err := dOld.ReadFromBytes(mpdOld)
err = dOld.ReadFromBytes(mpdOld)
if err != nil {
return nil, fmt.Errorf("failed to read old MPD: %w", err)
return nil, expiration, fmt.Errorf("failed to read old MPD: %w", err)
}
dNew := etree.NewDocument()
err = dNew.ReadFromBytes(mpdNew)
if err != nil {
return nil, fmt.Errorf("failed to read new MPD: %w", err)
return nil, expiration, fmt.Errorf("failed to read new MPD: %w", err)
}
oldRoot := dOld.Root()
newRoot := dNew.Root()

err = checkPatchConditions(oldRoot, newRoot)
expiration, err = checkPatchConditions(oldRoot, newRoot)
if err != nil {
return nil, err
return nil, expiration, err
}

pDoc, err := newPatchDoc(oldRoot, newRoot)
if err != nil {
return nil, fmt.Errorf("failed to create patch doc: %w", err)
return nil, expiration, fmt.Errorf("failed to create patch doc: %w", err)
}

elemPath := "/MPD"
root := pDoc.doc.Root()
err = addElemChanges(root, oldRoot, newRoot, elemPath)
if err != nil {
return nil, err
return nil, expiration, err
}

return pDoc.doc, nil
return pDoc.doc, expiration, nil
}

func checkPatchConditions(oldRoot, newRoot *etree.Element) error {
func checkPatchConditions(oldRoot, newRoot *etree.Element) (expiration time.Time, err error) {
if oldRoot.Tag != "MPD" || newRoot.Tag != "MPD" {
return fmt.Errorf("not MPD root element in both MPDs")
return expiration, fmt.Errorf("not MPD root element in both MPDs")
}
newPublishTime := getAttrValue(newRoot, "publishTime")
oldPublishTime := getAttrValue(oldRoot, "publishTime")
if newPublishTime == "" || oldPublishTime == "" {
return fmt.Errorf("lacking publishTime attribute in MPD")
return expiration, fmt.Errorf("lacking publishTime attribute in MPD")
}
if newPublishTime == oldPublishTime {
return ErrPatchSamePublishTime
return expiration, ErrPatchSamePublishTime
}
oldPatchLocation := oldRoot.SelectElement("PatchLocation")
if oldPatchLocation == nil {
return fmt.Errorf("no PatchLocation element in old MPD")
return expiration, fmt.Errorf("no PatchLocation element in old MPD")
}
oldTTL := oldPatchLocation.SelectAttr("ttl")
if oldTTL == nil {
return fmt.Errorf("no ttl attribute in PatchLocation element in old MPD")
return expiration, fmt.Errorf("no ttl attribute in PatchLocation element in old MPD")
}
ttl, err := strconv.Atoi(oldTTL.Value)
if err != nil {
return fmt.Errorf("failed to convert ttl attribute in PatchLocation element in old MPD: %w", err)
return expiration, fmt.Errorf("failed to convert ttl attribute in PatchLocation element in old MPD: %w", err)
}
oldPT, err := time.Parse(time.RFC3339, oldPublishTime)
if err != nil {
return fmt.Errorf("failed to parse old publishTime: %w", err)
return expiration, fmt.Errorf("failed to parse old publishTime: %w", err)
}
newPT, err := time.Parse(time.RFC3339, newPublishTime)
if err != nil {
return fmt.Errorf("failed to parse new publishTime: %w", err)
return expiration, fmt.Errorf("failed to parse new publishTime: %w", err)
}
endTime := oldPT.Add(2 * time.Duration(ttl) * time.Second)
if newPT.After(endTime) {
return ErrPatchTooLate
expiration = oldPT.Add(time.Duration(ttl)*time.Second + PatchExpirationMargin)
if newPT.After(expiration) {
return expiration, ErrPatchTooLate
}
return nil
return expiration, nil
}

// getAttrValue returns value if key exists, of empty string
Expand Down
45 changes: 26 additions & 19 deletions pkg/patch/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"

Expand Down Expand Up @@ -64,18 +65,20 @@ const wantedPatchSegmentTimelineNumber = (`<?xml version="1.0" encoding="UTF-8"?

func TestDiff(t *testing.T) {
cases := []struct {
desc string
oldMPD string
newMPD string
wantedDiff string
wantedDiffFile string
wantedErr string
desc string
oldMPD string
newMPD string
wantedDiff string
wantedDiffFile string
wantedExpiration time.Time
wantedErr string
}{
{
desc: "multiPeriodPatch",
oldMPD: "testdata/multiperiod_1.mpd",
newMPD: "testdata/multiperiod_2.mpd",
wantedDiffFile: "testdata/multiperiod_patch.mpp",
desc: "multiPeriodPatch",
oldMPD: "testdata/multiperiod_1.mpd",
newMPD: "testdata/multiperiod_2.mpd",
wantedDiffFile: "testdata/multiperiod_patch.mpp",
wantedExpiration: time.Date(2024, 4, 21, 6, 12, 8, 0, time.UTC),
},
{
desc: "too big publishTime diff vs ttl",
Expand All @@ -84,15 +87,17 @@ func TestDiff(t *testing.T) {
wantedErr: ErrPatchTooLate.Error(),
},
{
desc: "segmentTimelineTime",
oldMPD: "testdata/testpic_2s_1.mpd",
newMPD: "testdata/testpic_2s_2.mpd",
wantedDiff: wantedPatchSegmentTimelineTime,
desc: "segmentTimelineTime",
oldMPD: "testdata/testpic_2s_1.mpd",
newMPD: "testdata/testpic_2s_2.mpd",
wantedDiff: wantedPatchSegmentTimelineTime,
wantedExpiration: time.Date(2024, 3, 28, 15, 44, 20, 0, time.UTC),
}, {
desc: "segmentTimelineNumber",
oldMPD: "testdata/testpic_2s_snr_1.mpd",
newMPD: "testdata/testpic_2s_snr_2.mpd",
wantedDiff: wantedPatchSegmentTimelineNumber,
desc: "segmentTimelineNumber",
oldMPD: "testdata/testpic_2s_snr_1.mpd",
newMPD: "testdata/testpic_2s_snr_2.mpd",
wantedDiff: wantedPatchSegmentTimelineNumber,
wantedExpiration: time.Date(2024, 3, 28, 15, 44, 20, 0, time.UTC),
},
{
desc: "no diff",
Expand All @@ -108,12 +113,14 @@ func TestDiff(t *testing.T) {
require.NoError(t, err)
in2, err := os.ReadFile(c.newMPD)
require.NoError(t, err)
patch, err := MPDDiff(in1, in2)
patch, expiration, err := MPDDiff(in1, in2)
if c.wantedErr != "" {
require.Error(t, err, c.wantedErr)
return
}
require.NoError(t, err)
expirationDiff := c.wantedExpiration.Sub(expiration)
require.Equal(t, time.Duration(0), expirationDiff)
patch.Indent(2)
out, err := patch.WriteToString()
wantedDiff := c.wantedDiff
Expand Down

0 comments on commit 5f5059d

Please sign in to comment.