Skip to content

Commit

Permalink
fix: patch generator returns 425 or 410 if too early or too late
Browse files Browse the repository at this point in the history
  • Loading branch information
tobbee committed Apr 19, 2024
1 parent 6c3ae96 commit cdc4bac
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 39 deletions.
10 changes: 9 additions & 1 deletion cmd/livesim2/app/handler_patch.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app

import (
"errors"
"log/slog"
"net/http"
"strconv"
Expand Down Expand Up @@ -50,7 +51,14 @@ func (s *Server) patchHandlerFunc(w http.ResponseWriter, r *http.Request) {
s.livesimHandlerFunc(new, r)

doc, err := patch.MPDDiff(old.body, new.body)
if err != nil {
switch {
case errors.Is(err, patch.ErrPatchSamePublishTime):
http.Error(w, err.Error(), http.StatusTooEarly)
return
case errors.Is(err, patch.ErrPatchTooLate):
http.Error(w, err.Error(), http.StatusGone)
return
case err != nil:
slog.Error("MPDDiff", "err", err)
http.Error(w, "MPDDiff", http.StatusInternalServerError)
return
Expand Down
17 changes: 10 additions & 7 deletions cmd/livesim2/app/handler_patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import (
"github.com/stretchr/testify/require"
)

var wantedPatchNoUpdateYet = `<?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-16T07:34:38Z" publishTime="2024-04-16T07:34:38Z"/>
`

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>
Expand Down Expand Up @@ -75,9 +71,16 @@ func TestPatchHandler(t *testing.T) {
{
desc: "segTimeline no update yet",
url: "/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-04-16T07:34:38Z&nowDate=2024-04-16T07:34:39Z",
wantedStatusCode: http.StatusOK,
wantedContentType: "application/dash-patch+xml",
wantedBody: wantedPatchNoUpdateYet,
wantedStatusCode: http.StatusTooEarly,
wantedContentType: "text/plain; charset=utf-8",
wantedBody: "",
},
{
desc: "segTimeline too late",
url: "/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-04-16T07:34:38Z&nowDate=2024-04-16T07:44:39Z",
wantedStatusCode: http.StatusGone,
wantedContentType: "text/plain; charset=utf-8",
wantedBody: "",
},
{
desc: "segTimeline",
Expand Down
52 changes: 51 additions & 1 deletion pkg/patch/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ package patch
import (
"fmt"
"sort"
"strconv"
"time"

"github.com/beevik/etree"
)

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

type patchDoc struct {
doc *etree.Document
}
Expand Down Expand Up @@ -40,7 +45,7 @@ func newPatchDoc(oldRoot, newRoot *etree.Element) (*patchDoc, error) {
}, nil
}

// MPDDiff compares two MPDs and returns a patch document.
// MPDDiff compares two MPDs and returns a patch document or an error.
func MPDDiff(mpdOld, mpdNew []byte) (*etree.Document, error) {
dOld := etree.NewDocument()
err := dOld.ReadFromBytes(mpdOld)
Expand All @@ -54,6 +59,12 @@ func MPDDiff(mpdOld, mpdNew []byte) (*etree.Document, error) {
}
oldRoot := dOld.Root()
newRoot := dNew.Root()

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

pDoc, err := newPatchDoc(oldRoot, newRoot)
if err != nil {
return nil, fmt.Errorf("failed to create patch doc: %w", err)
Expand All @@ -69,6 +80,45 @@ func MPDDiff(mpdOld, mpdNew []byte) (*etree.Document, error) {
return pDoc.doc, nil
}

func checkPatchConditions(oldRoot, newRoot *etree.Element) error {
if oldRoot.Tag != "MPD" || newRoot.Tag != "MPD" {
return 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")
}
if newPublishTime == oldPublishTime {
return ErrPatchSamePublishTime
}
oldPatchLocation := oldRoot.SelectElement("PatchLocation")
if oldPatchLocation == nil {
return 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")
}
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)
}
oldPT, err := time.Parse(time.RFC3339, oldPublishTime)
if err != nil {
return 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)
}
endTime := oldPT.Add(2 * time.Duration(ttl) * time.Second)
if newPT.After(endTime) {
return ErrPatchTooLate
}
return nil
}

// getAttrValue returns value if key exists, of empty string
func getAttrValue(e *etree.Element, key string) string {
a := e.SelectAttr(key)
Expand Down
49 changes: 19 additions & 30 deletions pkg/patch/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@ func TestDiff(t *testing.T) {
oldMPD string
newMPD string
wantedDiff 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: "segmentTimelineTime",
oldMPD: "testdata/testpic_2s_1.mpd",
Expand All @@ -79,6 +87,13 @@ func TestDiff(t *testing.T) {
newMPD: "testdata/testpic_2s_snr_2.mpd",
wantedDiff: wantedPatchSegmentTimelineNumber,
},
{
desc: "no diff",
oldMPD: "testdata/testpic_2s_snr_1.mpd",
newMPD: "testdata/testpic_2s_snr_1.mpd",
wantedDiff: "",
wantedErr: ErrPatchSamePublishTime.Error(),
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
Expand All @@ -87,6 +102,10 @@ func TestDiff(t *testing.T) {
in2, err := os.ReadFile(c.newMPD)
require.NoError(t, err)
patch, err := MPDDiff(in1, in2)
if c.wantedErr != "" {
require.Error(t, err, c.wantedErr)
return
}
require.NoError(t, err)
patch.Indent(2)
out, err := patch.WriteToString()
Expand All @@ -97,36 +116,6 @@ func TestDiff(t *testing.T) {
}
}

func TestMissingIDs(t *testing.T) {
cases := []struct {
desc string
oldMPD string
newMPD string
wantedErr string
}{
{
desc: "missing MPD ID",
oldMPD: `<MPD type="dynamic" publishTime="2024-03-28T15:43:10Z"></MPD>`,
newMPD: `<MPD type="dynamic" publishTime="2024-03-28T15:43:18Z"></MPD>`,
wantedErr: "failed to create patch doc: not the same non-empty id in both MPDs",
},
{
desc: "missing Period ID",
oldMPD: `<MPD type="dynamic" id="a" publishTime="2024-03-28T15:43:10Z"><Period></Period></MPD>`,
newMPD: `<MPD type="dynamic" id="a" publishTime="2024-03-28T15:43:18Z"><Period></Period></MPD>`,
wantedErr: "addElemChanges for /MPD/Period[1]: id attribute missing in Period",
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
_, err := MPDDiff([]byte(c.oldMPD), []byte(c.newMPD))
require.Error(t, err)
require.Equal(t, err.Error(), c.wantedErr)
})

}
}

func TestAttrDiff(t *testing.T) {
oldAttr := []etree.Attr{
{Key: "publishTime", Value: "2021-07-01T00:00:00Z"},
Expand Down
44 changes: 44 additions & 0 deletions pkg/patch/testdata/testpic_2s_2_late_publish.mpd
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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-03-28T15:46:18Z" minimumUpdatePeriod="PT2S" minBufferTime="PT2S" timeShiftBufferDepth="PT1M" maxSegmentDuration="PT2S">
<ProgramInformation>
<Title>640x360@30 video, 48kHz audio, 2s segments</Title>
</ProgramInformation>
<PatchLocation ttl="60">/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-03-28T15%3A4%3A18Z</PatchLocation>
<Period id="P0" start="PT0S">
<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">
<SegmentTimeline>
<S t="82158745728000" d="96256" r="2"></S>
<S d="95232"></S>
<S d="96256" r="2"></S>
<S d="95232"></S>
<S d="96256" r="2"></S>
<S d="95232"></S>
<S d="96256" r="2"></S>
<S d="95232"></S>
<S d="96256" r="2"></S>
<S d="95232"></S>
<S d="96256" r="2"></S>
<S d="95232"></S>
<S d="96256" r="2"></S>
<S d="95232"></S>
<S d="96256" r="2"></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">
<SegmentTimeline>
<S t="154047648240000" d="180000" r="30"></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>

0 comments on commit cdc4bac

Please sign in to comment.