Skip to content

Commit

Permalink
Merge pull request #174 from Dash-Industry-Forum/patch
Browse files Browse the repository at this point in the history
MPD Patch support
  • Loading branch information
tobbee authored Apr 21, 2024
2 parents a90c1ae + 0d5d7c8 commit 683348d
Show file tree
Hide file tree
Showing 24 changed files with 1,412 additions and 76 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
"gots"
],
"cSpell.words": [
"apos",
"Atof",
"autodetected",
"autoplay",
"beevik",
"caddyserver",
"cbcs",
"cenc",
Expand All @@ -39,6 +41,7 @@
"emsg",
"enca",
"encv",
"etree",
"Eyevinn",
"genurl",
"golangci",
Expand Down
1 change: 1 addition & 0 deletions .wwhrd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ denylist:
allowlist:
- Apache-2.0
- MIT
- BSD-2-Clause
- BSD-3-Clause
- CC0-1.0

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- MPD Patch functionality with new `/patch_ttl` URL configuration
- nowDate query parameter as an alternative to nowMS for MPD and patch

### Fixed

- Timed stpp subtitles EBU-TT-D linePadding
Expand Down
6 changes: 6 additions & 0 deletions cmd/livesim2/app/configurl.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ type ResponseConfig struct {
TimeSubsDurMS int `json:"TimeSubsDurMS,omitempty"`
TimeSubsRegion int `json:"TimeSubsRegion,omitempty"`
Host string `json:"Host,omitempty"`
PatchTTL int `json:"Patch,omitempty"`
// DashIFECCP is DASH-IF Enhanced Clear Key Content Protection
DashIFECCP string `json:"ECCP,omitempty"`
SegStatusCodes []SegStatusCodes `json:"SegStatus,omitempty"`
Expand Down Expand Up @@ -375,6 +376,11 @@ cfgLoop:
cfg.Traffic = sc.ParseLossItvls(key, val)
case "eccp":
cfg.DashIFECCP = val
case "patch":
ttl := sc.Atoi(key, val)
if ttl > 0 {
cfg.PatchTTL = ttl
}
default:
contentStartIdx = i
break cfgLoop
Expand Down
45 changes: 38 additions & 7 deletions cmd/livesim2/app/handler_livesim.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,29 @@ func (s *Server) livesimHandlerFunc(w http.ResponseWriter, r *http.Request) {
return
}

var nowMS int // Set from query string or from wall-clock
q := r.URL.Query()
nowMSValue := q.Get("nowMS")
if nowMSValue != "" {
nowMS, err = strconv.Atoi(nowMSValue)
nowMS, err := getNowMS(q.Get("nowMS"))
if err != nil {
http.Error(w, "bad nowMS query", http.StatusBadRequest)
return
}

nowDate := q.Get("nowDate")
if nowDate != "" {
nowMS, err = getMSFromDate(nowDate)
if err != nil {
http.Error(w, "bad date query", http.StatusBadRequest)
return
}
}

publishTime := q.Get("publishTime")
if publishTime != "" {
nowMS, err = getMSFromDate(publishTime)
if err != nil {
http.Error(w, "bad nowMS query", http.StatusBadRequest)
http.Error(w, "bad publishTime query", http.StatusBadRequest)
return
}
} else {
nowMS = int(time.Now().UnixMilli())
}

cfg, err := processURLCfg(u.String(), nowMS)
Expand Down Expand Up @@ -144,6 +156,25 @@ func (s *Server) livesimHandlerFunc(w http.ResponseWriter, r *http.Request) {
}
}

// getNowMS returns value from query or local clock.
func getNowMS(nowMSValue string) (nowMS int, err error) {
if nowMSValue != "" {
return strconv.Atoi(nowMSValue)
}
return int(time.Now().UnixMilli()), nil
}

// getMSFromDate returns a nowMS value based on date (+1ms).
// The extra millisecond is there to ensure that the corresponding manifest
// can be generated
func getMSFromDate(publishTimeValue string) (nowMS int, err error) {
t, err := time.Parse(time.RFC3339, publishTimeValue)
if err != nil {
return -1, err
}
return int(t.UnixMilli()) + 1, nil
}

// extractPattern extracts the pattern number and return a modified segmentPart.
func extractPattern(segmentPart string) (int, string) {
parts := strings.Split(segmentPart, "/")
Expand Down
12 changes: 12 additions & 0 deletions cmd/livesim2/app/handler_livesim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ func TestParamToMPD(t *testing.T) {
`<dashif:Laurl xmlns:dashif="https://dashif.org/CPS" licenseType="EME-1.0">`,
},
},
{
desc: "MPD with Patch",
mpd: "testpic_6s/Manifest.mpd",
params: "patch_60/",
wantedStatusCode: http.StatusOK,
wantedInMPD: []string{
`id="auto-patch-id"`, // id in MPD
`id="1"`, // id in AdaptationSet
`id="2"`, // id in AdaptationSet
`<PatchLocation ttl="60">/patch/livesim2/patch_60/testpic_6s/Manifest.mpp?publishTime=`, // PatchLocation
},
},
}

for _, tc := range testCases {
Expand Down
97 changes: 97 additions & 0 deletions cmd/livesim2/app/handler_patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package app

import (
"errors"
"log/slog"
"net/http"
"strconv"
"strings"

"github.com/Dash-Industry-Forum/livesim2/pkg/patch"
)

type rec struct {
body []byte
status int
}

func (r *rec) Write(b []byte) (int, error) {
r.body = append(r.body, b...)
return len(b), nil
}

func (r *rec) WriteHeader(status int) {
r.status = status
}

func (r *rec) Header() http.Header {
return http.Header{}
}

// patchHandlerFunc returns an MPD patch
func (s *Server) patchHandlerFunc(w http.ResponseWriter, r *http.Request) {
origQuery := r.URL.RawQuery
q := r.URL.Query()
publishTime := q.Get("publishTime")
if publishTime == "" {
slog.Warn("publishTime query is required, but not provided in patch request")
http.Error(w, "publishTime query is required", http.StatusBadRequest)
}
old := &rec{}
oldQuery := removeQuery(origQuery, "nowMS")
oldQuery = removeQuery(oldQuery, "nowDate")
mpdPath := mpdPathFromPatchPath(r.URL.Path)
r.URL.Path = mpdPath
r.URL.RawQuery = oldQuery
s.livesimHandlerFunc(old, r)

new := &rec{}
newQuery := removeQuery(origQuery, "publishTime")
r.URL.RawQuery = newQuery
s.livesimHandlerFunc(new, r)

doc, err := patch.MPDDiff(old.body, new.body)
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
}
doc.Indent(2)
b, err := doc.WriteToBytes()
if err != nil {
slog.Error("WriteToBytes", "err", err)
http.Error(w, "WriteToBytes", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/dash-patch+xml")
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
_, err = w.Write(b)
if err != nil {
slog.Error("Write", "err", err)
}
w.WriteHeader(http.StatusOK)
}

func mpdPathFromPatchPath(patchPath string) string {
mpdPath := strings.Replace(patchPath, ".mpp", ".mpd", 1)
//TODO. Handle a possible set prefix before patch
return strings.TrimPrefix(mpdPath, "/patch")
}

func removeQuery(query, key string) string {
q := strings.Split(query, "&")
for i, kv := range q {
if strings.HasPrefix(kv, key+"=") {
q = append(q[:i], q[i+1:]...)
break
}
}
return strings.Join(q, "&")
}
113 changes: 113 additions & 0 deletions cmd/livesim2/app/handler_patch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package app

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/Dash-Industry-Forum/livesim2/pkg/logging"
"github.com/stretchr/testify/require"
)

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>
<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>
</replace>
<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"/>
</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"/>
</add>
</Patch>
`

var wantedPatchSegTimelineNumberWithAddAtEnd = `<?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:56Z">
<replace sel="/MPD/@publishTime">2024-04-16T07:34:56Z</replace>
<replace sel="/MPD/PatchLocation[1]">
<PatchLocation ttl="60">/patch/livesim2/patch_60/segtimelinenr_1/testpic_2s/Manifest.mpp?publishTime=2024-04-16T07%3A34%3A56Z</PatchLocation>
</replace>
<replace sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/@startNumber">856626417</replace>
<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="82236136032256" d="96256" r="1"/>
</add>
<add sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;1&apos;]/SegmentTemplate/SegmentTimeline/S[15]" pos="after">
<S d="95232"/>
</add>
<replace sel="/MPD/Period[@id=&apos;P0&apos;]/AdaptationSet[@id=&apos;2&apos;]/SegmentTemplate/@startNumber">856626417</replace>
<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="154192755060000" d="180000" r="30"/>
</add>
</Patch>
`

func TestPatchHandler(t *testing.T) {
cfg := ServerConfig{
VodRoot: "testdata/assets",
TimeoutS: 0,
LogFormat: logging.LogDiscard,
}
err := logging.InitSlog(cfg.LogLevel, cfg.LogFormat)
require.NoError(t, err)
server, err := SetupServer(context.Background(), &cfg)
require.NoError(t, err)
ts := httptest.NewServer(server.Router)
defer ts.Close()
testCases := []struct {
desc string
url string
wantedStatusCode int
wantedContentType string
wantedBody string
}{
{
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.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",
url: "/patch/livesim2/patch_60/segtimeline_1/testpic_2s/Manifest.mpp?publishTime=2024-04-02T15:50:56Z&nowMS=1712073160000",
wantedStatusCode: http.StatusOK,
wantedContentType: "application/dash-patch+xml",
wantedBody: wantedPatchSegTimelineTime,
},
{
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,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
resp, body := testFullRequest(t, ts, "GET", tc.url, nil)
require.Equal(t, tc.wantedStatusCode, resp.StatusCode)
require.Equal(t, tc.wantedContentType, resp.Header.Get("Content-Type"))
if tc.wantedStatusCode != http.StatusOK {
return
}
bodyStr := string(body)
require.Equal(t, tc.wantedBody, bodyStr)
})
}
}
11 changes: 11 additions & 0 deletions cmd/livesim2/app/handler_urlgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type urlGenData struct {
StartRel string // sets timeline start (and availabilityStartTime) relative to now (in seconds). Normally negative value.
StopRel string // sets stop-time for time-limited event relative to now (in seconds)
Scte35Var string // SCTE-35 insertion variant
PatchTTL string // MPD Patch TTL inv value in seconds (> 0 to be valid))
StatusCodes string // comma-separated list of response code patterns to return
Traffic string // comma-separated list of up/down/slow/hang intervals for one or more BaseURLs in MPD
Errors []string // error messages to display due to bad configuration
Expand Down Expand Up @@ -273,6 +274,16 @@ func createURL(r *http.Request, aInfo assetsInfo) urlGenData {
sb.WriteString(fmt.Sprintf("ltgt_%d/", lt))
}
}
if ptl := q.Get("patch-ttl"); ptl != "" {
patchTTL, err := strconv.Atoi(ptl)
if err != nil {
panic("bad patch-ttl")
}
if patchTTL > 0 {
data.PatchTTL = ptl
sb.WriteString(fmt.Sprintf("patch_%s/", ptl))
}
}
start := q.Get("start")
if start != "" {
data.Start = start
Expand Down
Loading

0 comments on commit 683348d

Please sign in to comment.