Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basic annex I query string propagation #239

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions cmd/livesim2/app/configurl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -382,6 +392,8 @@ cfgLoop:
if ttl > 0 {
cfg.PatchTTL = ttl
}
case "annexI":
cfg.Query = sc.ParseQuery(key, val)
default:
contentStartIdx = i
break cfgLoop
Expand Down
58 changes: 58 additions & 0 deletions cmd/livesim2/app/handler_livesim.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 != "" {
Expand Down
44 changes: 44 additions & 0 deletions cmd/livesim2/app/handler_livesim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,23 @@ func TestParamToMPD(t *testing.T) {
`<PatchLocation ttl="60">/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{
`<EssentialProperty schemeIdUri="urn:mpeg:dash:urlparam:2014">`,
`<up:UrlQueryInfo xmlns:up="urn:mpeg:dash:schema:urlparam:2014" queryTemplate="$querypart$" useMPDUrlQuery="true"></up:UrlQueryInfo>`,
},
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
33 changes: 32 additions & 1 deletion cmd/livesim2/app/handler_urlgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"log/slog"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
9 changes: 9 additions & 0 deletions cmd/livesim2/app/livempd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions cmd/livesim2/app/strconv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
11 changes: 10 additions & 1 deletion cmd/livesim2/app/templates/urlgen.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h1>Livesim2 URL generator</h1>
URL= {{.URL}}<br />
<div class="grid">
<span onclick="navigator.clipboard.writeText({{.URL}})" role="button">Copy</span>
<a href="{{(printf .PlayURL .URL)}}" target="_blank" role="button">Play</a>
<a href="{{(print .PlayURL)}}" target="_blank" role="button">Play</a>
<span onclick="window.location.href='/urlgen/';" role="button" class="secondary">Reset</span>
</div>
</article>
Expand Down Expand Up @@ -236,6 +236,15 @@ <h1>Livesim2 URL generator</h1>
</fieldset>
</details>

<details>
<summary>Annex I URL Parameters...</summary>

<label for="annexI">
query parameters in MPD to propagate to all video segment requests.Comma-separated key=value pairs.
<input type="text" id="annexI" name="annexI" value="{{.AnnexI}}" />
</label>
</details>

<details>
<summary>Negative test cases...</summary>

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading