Skip to content
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
78 changes: 78 additions & 0 deletions mpd/content_steering.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package mpd

// ServiceLocation is the value of BaseURL@serviceLocation and
// ContentSteering@defaultServiceLocation (ETSI TS 103 998).
type ServiceLocation string

// SteeredBaseURL is one attributed BaseURL for content steering: a URI and its
// serviceLocation label.
type SteeredBaseURL struct {
Location ServiceLocation
Value string
}

// BaseURLValue is one DASH BaseURL element. ServiceLocation is used for
// DASH content steering (ETSI TS 103 998 / DASH-IF); omit for plain BaseURLs.
type BaseURLValue struct {
ServiceLocation string `xml:"serviceLocation,attr,omitempty"`
Value string `xml:",chardata"`
}

// ContentSteering is the MPD-level ContentSteering element (steering server URI
// as character data; defaultServiceLocation names the preferred CDN at startup).
type ContentSteering struct {
DefaultServiceLocation *string `xml:"defaultServiceLocation,attr,omitempty"`
QueryBeforeStart *bool `xml:"queryBeforeStart,attr,omitempty"`
ClientRequirement *bool `xml:"clientRequirement,attr,omitempty"`
URI string `xml:",chardata"`
}

// StringsToBaseURLs converts plain URL strings to BaseURLValue entries.
func StringsToBaseURLs(ss []string) []BaseURLValue {
out := make([]BaseURLValue, len(ss))
for i, s := range ss {
out[i] = BaseURLValue{Value: s}
}
return out
}

// BaseURLsToStrings returns each BaseURL element's text (ignores serviceLocation).
func BaseURLsToStrings(bs []BaseURLValue) []string {
out := make([]string, len(bs))
for i, b := range bs {
out[i] = b.Value
}
return out
}

// ApplyContentSteeringOptions merges o into m so Write/Encode output includes policy-driven
// steering (ETSI TS 103 998): the ContentSteering element and optional BaseURL@serviceLocation
// rows. Non-empty SteeringURI replaces m.ContentSteering; SteeringBaseURLs are appended
// to m.BaseURL (skipping entries with an empty Value).
//
// ReadFromStringWithOptions calls this automatically after a successful decode when
// opts.ContentSteering is non-nil.
func ApplyContentSteeringOptions(m *MPD, o *ContentSteeringOptions) {
if m == nil || o == nil {
return
}
if o.SteeringURI != "" {
cs := &ContentSteering{URI: o.SteeringURI}
if o.DefaultServiceLocation != "" {
s := string(o.DefaultServiceLocation)
cs.DefaultServiceLocation = &s
}
cs.QueryBeforeStart = o.QueryBeforeStart
cs.ClientRequirement = o.ClientRequirement
m.ContentSteering = cs
}
for _, sb := range o.SteeringBaseURLs {
if sb.Value == "" {
continue
}
m.BaseURL = append(m.BaseURL, BaseURLValue{
ServiceLocation: string(sb.Location),
Value: sb.Value,
})
}
}
135 changes: 135 additions & 0 deletions mpd/content_steering_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package mpd

import (
"strings"
"testing"

. "github.com/cbsinteractive/go-dash/v3/helpers/ptrs"
"github.com/cbsinteractive/go-dash/v3/helpers/require"
)

func TestReadFromStringWithOptionsNilMatchesReadFromString(t *testing.T) {
xml := `<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="static" mediaPresentationDuration="PT10S" minBufferTime="PT2S">
<Period>
<BaseURL>https://origin.example/</BaseURL>
</Period>
</MPD>`
m1, err1 := ReadFromString(xml)
require.NoError(t, err1)
m2, err2 := ReadFromStringWithOptions(xml, nil)
require.NoError(t, err2)
require.EqualStringSlice(t, BaseURLsToStrings(m1.Periods[0].BaseURL), BaseURLsToStrings(m2.Periods[0].BaseURL))
}

func TestReadFromStringWithOptionsNonNilOptsParsesContentSteering(t *testing.T) {
xml := `<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="static" mediaPresentationDuration="PT10S" minBufferTime="PT2S">
<BaseURL serviceLocation="fastly">https://fastly.example/out/v1/x/</BaseURL>
<BaseURL serviceLocation="akamai">https://akamai.example/out/v1/x/</BaseURL>
<ContentSteering defaultServiceLocation="fastly">https://steer.example/api/v1/steer</ContentSteering>
<Period id="0">
<AdaptationSet mimeType="video/mp4">
<Representation id="v1" bandwidth="1000000" width="1280" height="720" codecs="avc1.64001F">
<BaseURL>seg/</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>`
opts := &Options{ContentSteering: &ContentSteeringOptions{}}
m, err := ReadFromStringWithOptions(xml, opts)
require.NoError(t, err)
if m.ContentSteering == nil {
t.Fatal("expected ContentSteering")
}
if strings.TrimSpace(m.ContentSteering.URI) != "https://steer.example/api/v1/steer" {
t.Fatalf("steering URI %q", m.ContentSteering.URI)
}
if m.ContentSteering.DefaultServiceLocation == nil || *m.ContentSteering.DefaultServiceLocation != "fastly" {
t.Fatalf("defaultServiceLocation %+v", m.ContentSteering.DefaultServiceLocation)
}
if len(m.BaseURL) != 2 {
t.Fatalf("base URLs: %d", len(m.BaseURL))
}
if m.BaseURL[0].ServiceLocation != "fastly" || m.BaseURL[0].Value != "https://fastly.example/out/v1/x/" {
t.Fatalf("first BaseURL %+v", m.BaseURL[0])
}
if m.BaseURL[1].ServiceLocation != "akamai" {
t.Fatalf("second BaseURL %+v", m.BaseURL[1])
}
}

func TestContentSteeringRoundTripWriteRead(t *testing.T) {
m := NewMPD(DASH_PROFILE_ONDEMAND, "PT6M16S", "PT1.97S")
m.BaseURL = []BaseURLValue{
{ServiceLocation: "fastly", Value: "https://f.example/path/"},
{ServiceLocation: "akamai", Value: "https://a.example/path/"},
}
m.ContentSteering = &ContentSteering{
DefaultServiceLocation: Strptr("fastly"),
URI: "https://steer.example/steer",
}
out, err := m.WriteToString()
require.NoError(t, err)
m2, err := ReadFromStringWithOptions(out, &Options{ContentSteering: &ContentSteeringOptions{}})
require.NoError(t, err)
if m2.ContentSteering == nil || strings.TrimSpace(m2.ContentSteering.URI) != m.ContentSteering.URI {
t.Fatalf("ContentSteering after round trip: %+v", m2.ContentSteering)
}
if len(m2.BaseURL) != 2 {
t.Fatal(len(m2.BaseURL))
}
}

func TestContentSteeringOptionalAttributesRoundTrip(t *testing.T) {
m := NewMPD(DASH_PROFILE_ONDEMAND, "PT1S", "PT1S")
m.ContentSteering = &ContentSteering{
DefaultServiceLocation: Strptr("c"),
QueryBeforeStart: Boolptr(true),
ClientRequirement: Boolptr(false),
URI: "https://steer/x",
}
out, err := m.WriteToString()
require.NoError(t, err)
m2, err := ReadFromString(out)
require.NoError(t, err)
require.NotNil(t, m2.ContentSteering)
if m2.ContentSteering.QueryBeforeStart == nil || !*m2.ContentSteering.QueryBeforeStart {
t.Fatal("queryBeforeStart")
}
if m2.ContentSteering.ClientRequirement == nil || *m2.ContentSteering.ClientRequirement {
t.Fatal("clientRequirement")
}
}

func TestStringsToBaseURLsAndBack(t *testing.T) {
ss := []string{"a", "b"}
b := StringsToBaseURLs(ss)
require.EqualStringSlice(t, ss, BaseURLsToStrings(b))
}

func TestReadFromStringWithOptionsAppliesSteeringOptionsForWrite(t *testing.T) {
xml := `<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="static" mediaPresentationDuration="PT10S" minBufferTime="PT2S">
<Period id="0"><AdaptationSet mimeType="video/mp4">
<Representation id="v1" bandwidth="1000000" codecs="avc1.64001F"><BaseURL>v/</BaseURL></Representation>
</AdaptationSet></Period>
</MPD>`
m, err := ReadFromStringWithOptions(xml, &Options{ContentSteering: &ContentSteeringOptions{
SteeringURI: "https://steer.example/steer",
DefaultServiceLocation: "cdn-a",
SteeringBaseURLs: []SteeredBaseURL{
{Location: "cdn-a", Value: "https://cdn-a.example/out/"},
{Location: "cdn-b", Value: "https://cdn-b.example/out/"},
},
}})
require.NoError(t, err)
out, err := m.WriteToString()
require.NoError(t, err)
if !strings.Contains(out, "<ContentSteering") || !strings.Contains(out, "https://steer.example/steer") {
t.Fatalf("missing ContentSteering in output: %s", out)
}
if !strings.Contains(out, `serviceLocation="cdn-a"`) || !strings.Contains(out, "https://cdn-a.example/out/") {
t.Fatalf("missing steered BaseURL in output: %s", out)
}
}
15 changes: 8 additions & 7 deletions mpd/mpd.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ type MPD struct {
PublishTime *string `xml:"publishTime,attr"`
TimeShiftBufferDepth *string `xml:"timeShiftBufferDepth,attr"`
SuggestedPresentationDelay *Duration `xml:"suggestedPresentationDelay,attr,omitempty"`
BaseURL []string `xml:"BaseURL,omitempty"`
Location string `xml:"Location,omitempty"`
BaseURL []BaseURLValue `xml:"BaseURL,omitempty"`
ContentSteering *ContentSteering `xml:"ContentSteering,omitempty"`
Location string `xml:"Location,omitempty"`
period *Period
Periods []*Period `xml:"Period,omitempty"`
UTCTiming *DescriptorType `xml:"UTCTiming,omitempty"`
Expand Down Expand Up @@ -153,7 +154,7 @@ type Period struct {
ID string `xml:"id,attr,omitempty"`
Duration Duration `xml:"duration,attr,omitempty"`
Start *Duration `xml:"start,attr,omitempty"`
BaseURL []string `xml:"BaseURL,omitempty"`
BaseURL []BaseURLValue `xml:"BaseURL,omitempty"`
SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"`
SegmentList *SegmentList `xml:"SegmentList,omitempty"`
SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"`
Expand Down Expand Up @@ -256,7 +257,7 @@ type AdaptationSet struct {
Labels []string `xml:"Label,omitempty"`
Representations []*Representation `xml:"Representation,omitempty"`
AccessibilityElems []*Accessibility `xml:"Accessibility,omitempty"`
BaseURL []string `xml:"BaseURL,omitempty"`
BaseURL []BaseURLValue `xml:"BaseURL,omitempty"`
}

func (as *AdaptationSet) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
Expand Down Expand Up @@ -470,7 +471,7 @@ type Representation struct {
Height *int64 `xml:"height,attr"` // Video
ID *string `xml:"id,attr"` // Audio + Video
Width *int64 `xml:"width,attr"` // Video
BaseURL []string `xml:"BaseURL,omitempty"` // On-Demand Profile
BaseURL []BaseURLValue `xml:"BaseURL,omitempty"` // On-Demand Profile
SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"` // On-Demand Profile
SegmentList *SegmentList `xml:"SegmentList,omitempty"`
SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"`
Expand Down Expand Up @@ -1265,7 +1266,7 @@ func (r *Representation) SetNewBaseURL(baseURL string) error {
return ErrBaseURLEmpty
}
// overwrite for backwards compatability
r.BaseURL = []string{baseURL}
r.BaseURL = []BaseURLValue{{Value: baseURL}}
return nil
}

Expand All @@ -1275,7 +1276,7 @@ func (r *Representation) AddNewBaseURL(baseURL string) error {
if baseURL == "" {
return ErrBaseURLEmpty
}
r.BaseURL = append(r.BaseURL, baseURL)
r.BaseURL = append(r.BaseURL, BaseURLValue{Value: baseURL})
return nil
}

Expand Down
16 changes: 15 additions & 1 deletion mpd/mpd_read_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,22 @@ func ReadFromFile(path string) (*MPD, error) {
// Reads a string into a MPD object.
// xmlStr - MPD manifest data as a string.
func ReadFromString(xmlStr string) (*MPD, error) {
return ReadFromStringWithOptions(xmlStr, nil)
}

// ReadFromStringWithOptions parses xmlStr into an MPD. If opts is nil, behavior
// matches ReadFromString. When opts.ContentSteering is non-nil, ApplyContentSteeringOptions
// runs after decode so the in-memory MPD reflects policy before WriteToString/Write.
func ReadFromStringWithOptions(xmlStr string, opts *Options) (*MPD, error) {
b := bytes.NewBufferString(xmlStr)
return Read(b)
m, err := Read(b)
if err != nil {
return nil, err
}
if opts != nil && opts.ContentSteering != nil {
ApplyContentSteeringOptions(m, opts.ContentSteering)
}
return m, nil
}

// Reads from an io.Reader interface into an MPD object.
Expand Down
2 changes: 1 addition & 1 deletion mpd/mpd_read_write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func TestFullLiveProfileMultiBaseURLWriteToString(t *testing.T) {
m := LiveProfile()
require.NotNil(t, m)

m.BaseURL = []string{"./", "../a/", "../b/"}
m.BaseURL = StringsToBaseURLs([]string{"./", "../a/", "../b/"})

xmlStr, err := m.WriteToString()
require.NoError(t, err)
Expand Down
10 changes: 5 additions & 5 deletions mpd/mpd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func TestWidevineContentProtection_ImplementsInterface(t *testing.T) {

func TestNewMPDLiveWithBaseURLInMPD(t *testing.T) {
m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME)
m.BaseURL = []string{VALID_BASE_URL_VIDEO}
m.BaseURL = StringsToBaseURLs([]string{VALID_BASE_URL_VIDEO})
require.NotNil(t, m)
expectedMPD := &MPD{
XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"),
Expand All @@ -166,7 +166,7 @@ func TestNewMPDLiveWithBaseURLInMPD(t *testing.T) {
MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME),
period: &Period{},
Periods: []*Period{{}},
BaseURL: []string{VALID_BASE_URL_VIDEO},
BaseURL: StringsToBaseURLs([]string{VALID_BASE_URL_VIDEO}),
}

expectedString, err := expectedMPD.WriteToString()
Expand All @@ -179,10 +179,10 @@ func TestNewMPDLiveWithBaseURLInMPD(t *testing.T) {

func TestNewMPDLiveWithBaseURLInPeriod(t *testing.T) {
m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME)
m.period.BaseURL = []string{VALID_BASE_URL_VIDEO}
m.period.BaseURL = StringsToBaseURLs([]string{VALID_BASE_URL_VIDEO})
require.NotNil(t, m)
period := &Period{
BaseURL: []string{VALID_BASE_URL_VIDEO},
BaseURL: StringsToBaseURLs([]string{VALID_BASE_URL_VIDEO}),
}
expectedMPD := &MPD{
XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"),
Expand Down Expand Up @@ -451,7 +451,7 @@ func TestAddNewBaseURLVideo(t *testing.T) {
err = r.AddNewBaseURL("../b/")
require.NoError(t, err)

require.EqualStringSlice(t, []string{"./", "../a/", "../b/"}, r.BaseURL)
require.EqualStringSlice(t, []string{"./", "../a/", "../b/"}, BaseURLsToStrings(r.BaseURL))
}

func TestSetNewBaseURLSubtitle(t *testing.T) {
Expand Down
29 changes: 29 additions & 0 deletions mpd/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package mpd

// Options configures MPD read/write behavior for optional extensions.
type Options struct {
// ContentSteering, when non-nil, is merged onto the MPD after ReadFromStringWithOptions
// decodes XML (see ApplyContentSteeringOptions), so Write encodes steering elements.
ContentSteering *ContentSteeringOptions
}

// ContentSteeringOptions holds header- or policy-derived values used when rendering
// attributed BaseURL and ContentSteering elements (ETSI TS 103 998).
type ContentSteeringOptions struct {
// SteeringURI is the steering server or manifest URI (character data of the
// ContentSteering element).
SteeringURI string

// DefaultServiceLocation is ContentSteering@defaultServiceLocation: the preferred
// service location until steering resolves.
DefaultServiceLocation ServiceLocation

// SteeringBaseURLs lists BaseURL elements with serviceLocation (one CDN root per location).
SteeringBaseURLs []SteeredBaseURL

// QueryBeforeStart is ContentSteering@queryBeforeStart when non-nil.
QueryBeforeStart *bool

// ClientRequirement is ContentSteering@clientRequirement when non-nil.
ClientRequirement *bool
}
4 changes: 2 additions & 2 deletions mpd/segment_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestSegmentListDeserialization(t *testing.T) {
expected := getSegmentListMPD()

require.EqualString(t, m.Comment, "Generated with https://github.com/shaka-project/shaka-packager version 288eddc863-release")
require.EqualStringSlice(t, expected.Periods[0].BaseURL, m.Periods[0].BaseURL)
require.EqualStringSlice(t, BaseURLsToStrings(expected.Periods[0].BaseURL), BaseURLsToStrings(m.Periods[0].BaseURL))

expectedAudioSegList := expected.Periods[0].AdaptationSets[0].Representations[0].SegmentList
audioSegList := m.Periods[0].AdaptationSets[0].Representations[0].SegmentList
Expand Down Expand Up @@ -60,7 +60,7 @@ func TestSegmentListDeserialization(t *testing.T) {

func getSegmentListMPD() *MPD {
m := NewMPD(DASH_PROFILE_LIVE, "PT30.016S", "PT2.000S")
m.period.BaseURL = []string{"http://localhost:8002/dash/"}
m.period.BaseURL = StringsToBaseURLs([]string{"http://localhost:8002/dash/"})
m.Comment = "Generated with https://github.com/shaka-project/shaka-packager version 288eddc863-release"

aas, _ := m.AddNewAdaptationSetAudioWithID("1", "audio/mp4", true, 1, "English")
Expand Down
Loading
Loading