├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── conditional_unit.go
├── fixtures
├── akamai_bbb_30fps.mpd
├── elemental_delta_1.6.1_live.mpd
├── elemental_delta_live.mpd
└── elemental_delta_vod.mpd
├── go.mod
├── go.sum
├── mpd.go
└── mpd_test.go
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [ master ]
4 |
5 | jobs:
6 | deploy:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: actions/setup-go@v4
12 | with:
13 | go-version: '^1.20'
14 |
15 | - name: Run tests
16 | run: go test -v ./... -check.v
17 |
18 | - name: Update coverage report
19 | uses: ncruces/go-coverage-report@v0
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.ignore
2 | .idea
3 | go-mpd.iml
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 mc² software
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/unki2aut/go-mpd/actions/workflows/ci.yml) [](https://raw.githack.com/wiki/unki2aut/go-mpd/coverage.html) [](https://goreportcard.com/report/github.com/unki2aut/go-mpd) [](https://godoc.org/github.com/unki2aut/go-mpd)
2 | # go-mpd
3 |
4 | Go library for parsing and generating MPEG-DASH Media Presentation Description (MPD) files.
5 |
6 | This project is based on https://github.com/mc2soft/mpd.
7 |
8 | ## Usage
9 |
10 | ```go
11 | package main
12 |
13 | import (
14 | "fmt"
15 | "github.com/unki2aut/go-mpd"
16 | )
17 |
18 | func main() {
19 | mpd := new(mpd.MPD)
20 | mpd.Decode([]byte(`
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | `))
38 |
39 | fmt.Println(mpd.MediaPresentationDuration)
40 | }
41 | ```
42 |
43 | ## Related links
44 | * https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP
45 | * [ISO_IEC_23009-1_2014 Standard](http://standards.iso.org/ittf/PubliclyAvailableStandards/c065274_ISO_IEC_23009-1_2014.zip)
46 |
47 | ## MPD parsing/generation in other languages
48 | * Javascript - https://github.com/videojs/mpd-parser
49 | * Python - https://github.com/sangwonl/python-mpegdash
50 | * Cpp - https://github.com/bitmovin/libdash
51 | * Java - https://github.com/carlanton/mpd-tools
52 |
--------------------------------------------------------------------------------
/conditional_unit.go:
--------------------------------------------------------------------------------
1 | package mpd
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "strconv"
7 | )
8 |
9 | // ConditionalUint (ConditionalUintType) defined in XSD as a union of unsignedInt and boolean.
10 | type ConditionalUint struct {
11 | u *uint64
12 | b *bool
13 | }
14 |
15 | // MarshalXMLAttr encodes ConditionalUint.
16 | func (c ConditionalUint) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
17 | if c.u != nil {
18 | return xml.Attr{Name: name, Value: strconv.FormatUint(*c.u, 10)}, nil
19 | }
20 |
21 | if c.b != nil {
22 | return xml.Attr{Name: name, Value: strconv.FormatBool(*c.b)}, nil
23 | }
24 |
25 | // both are nil - no attribute, client will threat it like "false"
26 | return xml.Attr{}, nil
27 | }
28 |
29 | // UnmarshalXMLAttr decodes ConditionalUint.
30 | func (c *ConditionalUint) UnmarshalXMLAttr(attr xml.Attr) error {
31 | u, err := strconv.ParseUint(attr.Value, 10, 64)
32 | if err == nil {
33 | c.u = &u
34 | return nil
35 | }
36 |
37 | b, err := strconv.ParseBool(attr.Value)
38 | if err == nil {
39 | c.b = &b
40 | return nil
41 | }
42 |
43 | return fmt.Errorf("ConditionalUint: can't UnmarshalXMLAttr %#v", attr)
44 | }
45 |
46 | // check interfaces
47 | var (
48 | _ xml.MarshalerAttr = ConditionalUint{}
49 | _ xml.UnmarshalerAttr = &ConditionalUint{}
50 | )
51 |
--------------------------------------------------------------------------------
/fixtures/akamai_bbb_30fps.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | ./
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/fixtures/elemental_delta_1.6.1_live.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/fixtures/elemental_delta_live.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/fixtures/elemental_delta_vod.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/unki2aut/go-mpd
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/unki2aut/go-xsd-types v0.0.0-20200220223938-30e5405398f8
7 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
8 | )
9 |
10 | require (
11 | github.com/kr/pretty v0.2.1 // indirect
12 | github.com/kr/text v0.1.0 // indirect
13 | github.com/stretchr/testify v1.8.4 // indirect
14 | )
15 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
3 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
4 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
5 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
6 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
9 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
10 | github.com/unki2aut/go-xsd-types v0.0.0-20200220223938-30e5405398f8 h1:u0Bi6Mf8BKPQnxGJ7QubdMyhb0SJjnQU7kX0BA9eASk=
11 | github.com/unki2aut/go-xsd-types v0.0.0-20200220223938-30e5405398f8/go.mod h1:uIeMfpmWIZ8SGp+fTfwDBWiiRn3aJm4b7rFSro9s++Q=
12 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
13 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
14 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
15 |
--------------------------------------------------------------------------------
/mpd.go:
--------------------------------------------------------------------------------
1 | // Package mpd implements parsing and generating of MPEG-DASH Media Presentation Description (MPD) files.
2 | package mpd
3 |
4 | import (
5 | "bytes"
6 | "encoding/xml"
7 | "github.com/unki2aut/go-xsd-types"
8 | "io"
9 | "regexp"
10 | )
11 |
12 | // http://mpeg.chiariglione.org/standards/mpeg-dash
13 | // https://www.brendanlong.com/the-structure-of-an-mpeg-dash-mpd.html
14 | // http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd
15 |
16 | var emptyElementRE = regexp.MustCompile(`>[A-Za-z]+>`)
17 |
18 | // MPD represents root XML element.
19 | type MPD struct {
20 | XMLNS *string `xml:"xmlns,attr"`
21 | Type *string `xml:"type,attr"`
22 | MinimumUpdatePeriod *xsd.Duration `xml:"minimumUpdatePeriod,attr"`
23 | AvailabilityStartTime *xsd.DateTime `xml:"availabilityStartTime,attr"`
24 | AvailabilityEndTime *xsd.DateTime `xml:"availabilityEndTime,attr"`
25 | MediaPresentationDuration *xsd.Duration `xml:"mediaPresentationDuration,attr"`
26 | MinBufferTime *xsd.Duration `xml:"minBufferTime,attr"`
27 | SuggestedPresentationDelay *xsd.Duration `xml:"suggestedPresentationDelay,attr"`
28 | TimeShiftBufferDepth *xsd.Duration `xml:"timeShiftBufferDepth,attr"`
29 | PublishTime *xsd.DateTime `xml:"publishTime,attr"`
30 | Profiles string `xml:"profiles,attr"`
31 | BaseURL []*BaseURL `xml:"BaseURL,omitempty"`
32 | Period []*Period `xml:"Period,omitempty"`
33 | }
34 |
35 | // Do not try to use encoding.TextMarshaler and encoding.TextUnmarshaler:
36 | // https://github.com/golang/go/issues/6859#issuecomment-118890463
37 |
38 | // Encode generates MPD XML.
39 | func (m *MPD) Encode() ([]byte, error) {
40 | x := new(bytes.Buffer)
41 | e := xml.NewEncoder(x)
42 | e.Indent("", " ")
43 | err := e.Encode(m)
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | // hacks for self-closing tags
49 | res := new(bytes.Buffer)
50 | res.WriteString(``)
51 | res.WriteByte('\n')
52 | for {
53 | s, err := x.ReadString('\n')
54 | if s != "" {
55 | s = emptyElementRE.ReplaceAllString(s, `/>`)
56 | res.WriteString(s)
57 | }
58 | if err == io.EOF {
59 | break
60 | }
61 | if err != nil {
62 | return nil, err
63 | }
64 | }
65 | res.WriteByte('\n')
66 | return res.Bytes(), err
67 | }
68 |
69 | // Decode parses MPD XML.
70 | func (m *MPD) Decode(b []byte) error {
71 | return xml.Unmarshal(b, m)
72 | }
73 |
74 | // Period represents XSD's PeriodType.
75 | type Period struct {
76 | Start *xsd.Duration `xml:"start,attr"`
77 | ID *string `xml:"id,attr"`
78 | Duration *xsd.Duration `xml:"duration,attr"`
79 | AdaptationSets []*AdaptationSet `xml:"AdaptationSet,omitempty"`
80 | BaseURL []*BaseURL `xml:"BaseURL,omitempty"`
81 | }
82 |
83 | // BaseURL represents XSD's BaseURLType.
84 | type BaseURL struct {
85 | Value string `xml:",chardata"`
86 | ServiceLocation *string `xml:"serviceLocation,attr"`
87 | ByteRange *string `xml:"byteRange,attr"`
88 | AvailabilityTimeOffset *uint64 `xml:"availabilityTimeOffset,attr"`
89 | AvailabilityTimeComplete *bool `xml:"availabilityTimeComplete,attr"`
90 | }
91 |
92 | // AdaptationSet represents XSD's AdaptationSetType.
93 | type AdaptationSet struct {
94 | MimeType string `xml:"mimeType,attr"`
95 | ContentType *string `xml:"contentType,attr"`
96 | SegmentAlignment ConditionalUint `xml:"segmentAlignment,attr"`
97 | SubsegmentAlignment ConditionalUint `xml:"subsegmentAlignment,attr"`
98 | StartWithSAP ConditionalUint `xml:"startWithSAP,attr"`
99 | SubsegmentStartsWithSAP ConditionalUint `xml:"subsegmentStartsWithSAP,attr"`
100 | BitstreamSwitching *bool `xml:"bitstreamSwitching,attr"`
101 | Lang *string `xml:"lang,attr"`
102 | Par *string `xml:"par,attr"`
103 | Codecs *string `xml:"codecs,attr"`
104 | Role []*Descriptor `xml:"Role,omitempty"`
105 | BaseURL []*BaseURL `xml:"BaseURL,omitempty"`
106 | SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"`
107 | ContentProtections []Descriptor `xml:"ContentProtection,omitempty"`
108 | Representations []Representation `xml:"Representation,omitempty"`
109 | }
110 |
111 | // Representation represents XSD's RepresentationType.
112 | type Representation struct {
113 | ID *string `xml:"id,attr"`
114 | Width *uint64 `xml:"width,attr"`
115 | Height *uint64 `xml:"height,attr"`
116 | FrameRate *string `xml:"frameRate,attr"`
117 | Bandwidth *uint64 `xml:"bandwidth,attr"`
118 | AudioSamplingRate *string `xml:"audioSamplingRate,attr"`
119 | Codecs *string `xml:"codecs,attr"`
120 | SAR *string `xml:"sar,attr"`
121 | ScanType *string `xml:"scanType,attr"`
122 | ContentProtections []Descriptor `xml:"ContentProtection,omitempty"`
123 | SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"`
124 | BaseURL []*BaseURL `xml:"BaseURL,omitempty"`
125 | }
126 |
127 | // Descriptor represents XSD's DescriptorType.
128 | type Descriptor struct {
129 | SchemeIDURI *string `xml:"schemeIdUri,attr"`
130 | Value *string `xml:"value,attr"`
131 | CencDefaultKeyId *string `xml:"default_KID,attr,omitempty"`
132 | MSPRPro *string `xml:"pro,omitempty"`
133 | CencPSSH *string `xml:"pssh,omitempty"`
134 | }
135 |
136 | // SegmentTemplate represents XSD's SegmentTemplateType.
137 | type SegmentTemplate struct {
138 | Duration *uint64 `xml:"duration,attr"`
139 | Timescale *uint64 `xml:"timescale,attr"`
140 | Media *string `xml:"media,attr"`
141 | Initialization *string `xml:"initialization,attr"`
142 | StartNumber *uint64 `xml:"startNumber,attr"`
143 | PresentationTimeOffset *uint64 `xml:"presentationTimeOffset,attr"`
144 | SegmentTimeline *SegmentTimeline `xml:"SegmentTimeline,omitempty"`
145 | }
146 |
147 | // SegmentTimeline represents XSD's SegmentTimelineType.
148 | type SegmentTimeline struct {
149 | S []*SegmentTimelineS `xml:"S"`
150 | }
151 |
152 | // SegmentTimelineS represents XSD's SegmentTimelineType's inner S elements.
153 | type SegmentTimelineS struct {
154 | T *uint64 `xml:"t,attr"`
155 | D uint64 `xml:"d,attr"`
156 | R *int64 `xml:"r,attr"`
157 | }
158 |
--------------------------------------------------------------------------------
/mpd_test.go:
--------------------------------------------------------------------------------
1 | package mpd
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "strings"
7 | "testing"
8 |
9 | . "gopkg.in/check.v1"
10 | )
11 |
12 | func Test(t *testing.T) { TestingT(t) }
13 |
14 | type MPDSuite struct{}
15 |
16 | var _ = Suite(&MPDSuite{})
17 |
18 | func readFile(c *C, name string) (*MPD, string, string) {
19 | expected, err := ioutil.ReadFile(name)
20 | c.Assert(err, IsNil)
21 |
22 | mpd := new(MPD)
23 | err = mpd.Decode(expected)
24 | c.Assert(err, IsNil)
25 |
26 | obtained, err := mpd.Encode()
27 | c.Assert(err, IsNil)
28 | obtainedName := name + ".ignore"
29 | err = ioutil.WriteFile(obtainedName, obtained, 0666)
30 | c.Assert(err, IsNil)
31 |
32 | os.Remove(obtainedName)
33 |
34 | return mpd, string(expected), string(obtained)
35 | }
36 |
37 | func checkLineByLine(c *C, obtained string, expected string) {
38 | obtainedSlice := strings.Split(strings.TrimSpace(obtained), "\n")
39 | expectedSlice := strings.Split(strings.TrimSpace(expected), "\n")
40 | c.Assert(obtainedSlice, HasLen, len(expectedSlice))
41 |
42 | for i := range obtainedSlice {
43 | c.Check(obtainedSlice[i], Equals, expectedSlice[i], Commentf("line %d", i+1))
44 | }
45 | }
46 |
47 | func testUnmarshalMarshalElemental(c *C, name string) {
48 | _, expected, obtained := readFile(c, name)
49 |
50 | // strip stupid XML rubbish
51 | expected = strings.Replace(expected, `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" `, ``, 1)
52 | expected = strings.Replace(expected, `xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd" `, ``, 1)
53 |
54 | checkLineByLine(c, obtained, expected)
55 | }
56 |
57 | func testUnmarshalMarshalAkamai(c *C, name string) {
58 | _, expected, obtained := readFile(c, name)
59 |
60 | // strip stupid XML rubbish
61 | expected = strings.Replace(expected, `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" `, ``, 1)
62 | expected = strings.Replace(expected, ` xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"`, ``, 1)
63 |
64 | checkLineByLine(c, obtained, expected)
65 | }
66 |
67 | func (s *MPDSuite) TestUnmarshalMarshalVod(c *C) {
68 | testUnmarshalMarshalElemental(c, "fixtures/elemental_delta_vod.mpd")
69 | }
70 |
71 | func (s *MPDSuite) TestUnmarshalMarshalLive(c *C) {
72 | testUnmarshalMarshalElemental(c, "fixtures/elemental_delta_live.mpd")
73 | }
74 |
75 | func (s *MPDSuite) TestUnmarshalMarshalLiveDelta161(c *C) {
76 | testUnmarshalMarshalElemental(c, "fixtures/elemental_delta_1.6.1_live.mpd")
77 | }
78 |
79 | func (s *MPDSuite) TestUnmarshalMarshalSegmentTemplate(c *C) {
80 | testUnmarshalMarshalAkamai(c, "fixtures/akamai_bbb_30fps.mpd")
81 | }
82 |
--------------------------------------------------------------------------------