From a57c656d81785919362bd41cfff9b4612721ffcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbjo=CC=88rn=20Einarsson?= Date: Fri, 20 Dec 2024 15:17:18 +0100 Subject: [PATCH 1/2] feat: basic annex I query string propagation and check. --- CHANGELOG.md | 5 ++ cmd/livesim2/app/configurl.go | 12 +++++ cmd/livesim2/app/handler_livesim.go | 58 ++++++++++++++++++++++++ cmd/livesim2/app/handler_livesim_test.go | 44 ++++++++++++++++++ cmd/livesim2/app/livempd.go | 9 ++++ cmd/livesim2/app/strconv.go | 25 ++++++++++ go.mod | 2 +- go.sum | 4 +- 8 files changed, 156 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a0de5..e398d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CMAF ingest of full segments now send Content-Length header +### Added + +- Basic Annex I support for announcing that MPD query parameters should be used in all video segment requests +- Verification that the MPD and the video segments carry the URL-specified query parameters + ### Fixed - endNumber in live MPD (Issue #235) diff --git a/cmd/livesim2/app/configurl.go b/cmd/livesim2/app/configurl.go index 65695b5..907b552 100644 --- a/cmd/livesim2/app/configurl.go +++ b/cmd/livesim2/app/configurl.go @@ -64,6 +64,10 @@ const ( UtcTimingHeadAsset = "/static/time.txt" ) +const ( + UrlParamSchemeIdUri = "urn:mpeg:dash:urlparam:2014" +) + type ResponseConfig struct { URLParts []string `json:"-"` URLContentIdx int `json:"-"` @@ -105,6 +109,7 @@ type ResponseConfig struct { DRM string `json:"DRM,omitempty"` // Includes ECCP as eccp-cbcs or eccp-cenc SegStatusCodes []SegStatusCodes `json:"SegStatus,omitempty"` Traffic []LossItvls `json:"Traffic,omitempty"` + Query *Query `json:"Query,omitempty"` } // SegStatusCodes configures regular extraordinary segment response codes @@ -222,6 +227,11 @@ type LossItvl struct { state lossState } +type Query struct { + raw string + parts url.Values +} + func baseURL(nr int) string { return fmt.Sprintf("bu%d/", nr) } @@ -382,6 +392,8 @@ cfgLoop: if ttl > 0 { cfg.PatchTTL = ttl } + case "annexI": + cfg.Query = sc.ParseQuery(key, val) default: contentStartIdx = i break cfgLoop diff --git a/cmd/livesim2/app/handler_livesim.go b/cmd/livesim2/app/handler_livesim.go index 08fc883..798fb63 100644 --- a/cmd/livesim2/app/handler_livesim.go +++ b/cmd/livesim2/app/handler_livesim.go @@ -110,6 +110,13 @@ func (s *Server) livesimHandlerFunc(w http.ResponseWriter, r *http.Request) { cfg.SetHost(s.Cfg.Host, r) switch filepath.Ext(r.URL.Path) { case ".mpd": + if !checkQuery(cfg.Query, r.URL) { + if !checkQuery(cfg.Query, r.URL) { + log.Error("query check mismatch", "cfg", cfg.Query.raw, "url", r.URL.RawQuery) + http.Error(w, "query check mismatch ", http.StatusBadRequest) + return + } + } _, mpdName := path.Split(contentPart) err := writeLiveMPD(log, w, cfg, s.Cfg.DrmCfg, a, mpdName, nowMS) if err != nil { @@ -143,6 +150,13 @@ func (s *Server) livesimHandlerFunc(w http.ResponseWriter, r *http.Request) { } } } + if cfg.Query != nil && contentTypeFromURL(cfg, a, segmentPart[1:]) == "video" { + if !checkQuery(cfg.Query, r.URL) { + log.Error("query check mismatch", "cfg", cfg.Query.raw, "url", r.URL.RawQuery) + http.Error(w, "query check mismatch ", http.StatusBadRequest) + return + } + } code, err := writeSegment(r.Context(), w, log, cfg, s.Cfg.DrmCfg, s.assetMgr.vodFS, a, segmentPart[1:], nowMS, s.textTemplates, false /*isLast */) if err != nil { @@ -172,6 +186,50 @@ func (s *Server) livesimHandlerFunc(w http.ResponseWriter, r *http.Request) { } } +func checkQuery(cfgQuery *Query, u *url.URL) bool { + if cfgQuery == nil { + return true + } + uq := u.Query() + for key, val := range cfgQuery.parts { + uVal, ok := uq[key] + if !ok { + return false + } + if len(val) != len(uVal) { + return false + } + for i := range val { + if val[i] != uVal[i] { + return false + } + } + } + return true +} + +// contentTypeFromURL returns the content type of the segment. +// Should work for both init and media segments. +func contentTypeFromURL(cfg *ResponseConfig, a *asset, segmentPart string) string { + // First check imit for time subs + _, _, ok, err := matchTimeSubsInitLang(cfg, segmentPart) + if ok && err == nil { + return "subtitle" + } + // Next match against init segments + for _, rep := range a.Reps { + if segmentPart == rep.InitURI { + return rep.ContentType + } + } + // Finally match against media segments + rep, _, err := findRepAndSegmentID(a, segmentPart) + if err != nil { + return "unknown" + } + return rep.ContentType +} + // getNowMS returns value from query or local clock. func getNowMS(nowMSValue string) (nowMS int, err error) { if nowMSValue != "" { diff --git a/cmd/livesim2/app/handler_livesim_test.go b/cmd/livesim2/app/handler_livesim_test.go index 7a82c0c..2a4775a 100644 --- a/cmd/livesim2/app/handler_livesim_test.go +++ b/cmd/livesim2/app/handler_livesim_test.go @@ -86,6 +86,23 @@ func TestParamToMPD(t *testing.T) { `/patch/livesim2/patch_60/testpic_6s/Manifest.mpp?publishTime=`, // PatchLocation }, }, + { + desc: "annexI without url query", + mpd: "testpic_2s/Manifest.mpd", + params: "annexI_a=1,b=3,a=3/", + wantedStatusCode: http.StatusBadRequest, + wantedInMPD: nil, + }, + { + desc: "annexI with url query", + mpd: "testpic_2s/Manifest.mpd?a=1&b=3&a=3", + params: "annexI_a=1,b=3,a=3/", + wantedStatusCode: http.StatusOK, + wantedInMPD: []string{ + ``, + ``, + }, + }, } for _, tc := range testCases { @@ -124,6 +141,13 @@ func TestFetches(t *testing.T) { wantedStatusCode int wantedContentType string }{ + { + desc: "mpd", + url: "testpic_2s/Manifest_thumbs.mpd", + params: "", + wantedStatusCode: http.StatusOK, + wantedContentType: `application/dash+xml`, + }, { desc: "mpd", url: "testpic_2s/Manifest_thumbs.mpd", @@ -187,6 +211,26 @@ func TestFetches(t *testing.T) { wantedStatusCode: 425, wantedContentType: `video/mp4`, }, + { + desc: "video init segment Annex I, without query", + url: "testpic_2s/V300/init.mp4", + params: "annexI_a=1/", + wantedStatusCode: http.StatusBadRequest, + }, + { + desc: "video init segment Annex I, with query", + url: "testpic_2s/V300/init.mp4?a=1", + params: "annexI_a=1/", + wantedStatusCode: http.StatusOK, + wantedContentType: `video/mp4`, + }, + { + desc: "audio init segment Annex I, without query", + url: "testpic_2s/A48/init.mp4", + params: "annexI_a=1/", + wantedStatusCode: http.StatusOK, + wantedContentType: `audio/mp4`, + }, } for _, tc := range testCases { diff --git a/cmd/livesim2/app/livempd.go b/cmd/livesim2/app/livempd.go index 9af7a3a..9e9e940 100644 --- a/cmd/livesim2/app/livempd.go +++ b/cmd/livesim2/app/livempd.go @@ -216,6 +216,15 @@ func LiveMPD(a *asset, mpdName string, cfg *ResponseConfig, drmCfg *drm.DrmConfi } } } + if as.ContentType == "video" && cfg.Query != nil { + ep := m.NewDescriptor(UrlParamSchemeIdUri, "", "") + ep.UrlQueryInfo = &m.UrlQueryInfoType{ + QueryTemplate: "$querypart$", + UseMPDUrlQuery: true, + } + as.EssentialProperties = append(as.EssentialProperties, ep) + } + if as.ContentType == "video" && cfg.SCTE35PerMinute != nil { // Add SCTE35 signaling as.InbandEventStreams = append(as.InbandEventStreams, diff --git a/cmd/livesim2/app/strconv.go b/cmd/livesim2/app/strconv.go index cc431dd..042d9d1 100644 --- a/cmd/livesim2/app/strconv.go +++ b/cmd/livesim2/app/strconv.go @@ -182,3 +182,28 @@ func (s *strConvAccErr) ParseLossItvls(key, val string) []LossItvls { } return itvls } + +func (s *strConvAccErr) ParseQuery(key, val string) *Query { + if s.err != nil { + return nil + } + q := &Query{ + parts: make(map[string][]string), + } + pairs := strings.Split(val, ",") + parts := make([]string, 0, len(pairs)) + for _, pair := range pairs { + kv := strings.Split(pair, "=") + switch len(kv) { + case 2: + q.parts[kv[0]] = append(q.parts[kv[0]], kv[1]) + parts = append(parts, fmt.Sprintf("%s=%s", kv[0], kv[1])) + case 1: + parts = append(parts, fmt.Sprintf("%s=%s", kv[0], kv[1])) + default: + s.err = fmt.Errorf("key=%s, err=%s", key, "invalid query pair") + } + } + q.raw = strings.Join(parts, "&") + return q +} diff --git a/go.mod b/go.mod index ddd1b30..19b8468 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/Comcast/gots/v2 v2.2.1 - github.com/Eyevinn/dash-mpd v0.11.1 + github.com/Eyevinn/dash-mpd v0.12.0 github.com/Eyevinn/mp4ff v0.47.0 github.com/beevik/etree v1.4.1 github.com/caddyserver/certmagic v0.21.4 diff --git a/go.sum b/go.sum index 7f24811..e16b179 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Comcast/gots/v2 v2.2.1 h1:LU/SRg7p2KQqVkNqInV7I4MOQKAqvWQP/PSSLtygP2s= github.com/Comcast/gots/v2 v2.2.1/go.mod h1:firJ11on3eUiGHAhbY5cZNqG0OqhQ1+nSZHfsEEzVVU= -github.com/Eyevinn/dash-mpd v0.11.1 h1:p9r31p7+YNp9E548m+sLIdohvQhQt+TmLoOxU8eGqns= -github.com/Eyevinn/dash-mpd v0.11.1/go.mod h1:loc8wzf1XW4NIWI4M7U6TAPk+bx2H8wGpjFTvxepmcI= +github.com/Eyevinn/dash-mpd v0.12.0 h1:fFNE9KPLqe4OG79fYyT/KalmFbQT2vG4Z01ppmEC4Aw= +github.com/Eyevinn/dash-mpd v0.12.0/go.mod h1:yym2itvB74evfJFDZB99p700LQddQFsN1YCbk9t6mAA= github.com/Eyevinn/mp4ff v0.47.0 h1:XSSHYt5+I0fyOnHWoNwM72DtivlmHFR0V9azgIi+ZVU= github.com/Eyevinn/mp4ff v0.47.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= From ccdb17fcc16ddd918936022126ec23ca8e52f9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbjo=CC=88rn=20Einarsson?= Date: Wed, 15 Jan 2025 15:50:19 +0100 Subject: [PATCH 2/2] feat: add urlgen support for Annex I --- cmd/livesim2/app/handler_urlgen.go | 33 +++++++++++++++++++++++++- cmd/livesim2/app/templates/urlgen.html | 11 ++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/cmd/livesim2/app/handler_urlgen.go b/cmd/livesim2/app/handler_urlgen.go index adab208..eab80a3 100644 --- a/cmd/livesim2/app/handler_urlgen.go +++ b/cmd/livesim2/app/handler_urlgen.go @@ -8,6 +8,7 @@ import ( "fmt" "log/slog" "net/http" + "net/url" "sort" "strconv" "strings" @@ -156,6 +157,7 @@ type urlGenData struct { 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 + AnnexI string // comma-separated list of Annex I parameters as key=value pairs 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 } @@ -373,6 +375,11 @@ func createURL(r *http.Request, aInfo assetsInfo, drmCfg *drm.DrmConfig) urlGenD data.Scte35Var = scte35 sb.WriteString(fmt.Sprintf("scte35_%s/", scte35)) } + annexI := q.Get("annexI") + if annexI != "" { + data.AnnexI = annexI + sb.WriteString(fmt.Sprintf("annexI_%s/", annexI)) + } statusCodes := q.Get("statuscode") if statusCodes != "" { sc := newStringConverter() @@ -393,13 +400,37 @@ func createURL(r *http.Request, aInfo assetsInfo, drmCfg *drm.DrmConfig) urlGenD sb.WriteString(fmt.Sprintf("traffic_%s/", traffic)) } sb.WriteString(fmt.Sprintf("%s/%s", asset, mpd)) + if annexI != "" { + query, err := queryFromAnnexI(annexI) + if err != nil { + data.Errors = append(data.Errors, fmt.Sprintf("bad annexI: %s", err.Error())) + } + sb.WriteString(query) + } if len(data.Errors) > 0 { data.URL = "" data.PlayURL = "" } else { data.URL = sb.String() - data.PlayURL = aInfo.PlayURL + data.PlayURL = fmt.Sprintf(aInfo.PlayURL, url.QueryEscape(data.URL)) } data.Host = aInfo.Host return data } + +func queryFromAnnexI(annexI string) (string, error) { + out := "" + pairs := strings.Split(annexI, ",") + for i, p := range pairs { + parts := strings.Split(p, "=") + if len(parts) != 2 { + return "", fmt.Errorf("bad key-value pair: %s", p) + } + if i == 0 { + out += fmt.Sprintf("?%s=%s", parts[0], parts[1]) + } else { + out += fmt.Sprintf("&%s=%s", parts[0], parts[1]) + } + } + return out, nil +} diff --git a/cmd/livesim2/app/templates/urlgen.html b/cmd/livesim2/app/templates/urlgen.html index 6744165..a240216 100644 --- a/cmd/livesim2/app/templates/urlgen.html +++ b/cmd/livesim2/app/templates/urlgen.html @@ -20,7 +20,7 @@

Livesim2 URL generator

URL= {{.URL}}
Copy - Play + Play Reset
@@ -236,6 +236,15 @@

Livesim2 URL generator

+
+ Annex I URL Parameters... + + +
+
Negative test cases...