├── .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 | [![Build Status](https://github.com/unki2aut/go-mpd/actions/workflows/ci.yml/badge.svg)](https://github.com/unki2aut/go-mpd/actions/workflows/ci.yml) [![Go Coverage](https://github.com/unki2aut/go-mpd/wiki/coverage.svg)](https://raw.githack.com/wiki/unki2aut/go-mpd/coverage.html) [![Go Report Card](https://goreportcard.com/badge/github.com/unki2aut/go-mpd)](https://goreportcard.com/report/github.com/unki2aut/go-mpd) [![GoDoc](https://godoc.org/github.com/unki2aut/go-mpd?status.svg)](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(`>`) 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 | --------------------------------------------------------------------------------