├── .circleci └── config.yml ├── .gitignore ├── .whitesource ├── LICENSE ├── Makefile ├── README.md ├── THIRD_PARTY_NOTICES ├── examples ├── live.go ├── live.mpd ├── ondemand.go ├── ondemand.mpd └── ondemand_with_thumbnails.mpd ├── go.mod ├── go.sum ├── helpers ├── ptrs │ └── ptrs.go ├── require │ └── require.go └── testfixtures │ └── testfixtures.go └── mpd ├── duration.go ├── duration_test.go ├── events.go ├── events_test.go ├── fixtures ├── adaptationset_switching.mpd ├── audio_channel_configuration.mpd ├── events.mpd ├── hbbtv_profile.mpd ├── inband_event_stream.mpd ├── invalid.mpd ├── live_profile.mpd ├── live_profile_dynamic.mpd ├── live_profile_multi_base_url.mpd ├── location.mpd ├── multiple_supplementals.mpd ├── newperiod.mpd ├── ondemand_profile.mpd ├── scte35.mpd ├── segment_list.mpd ├── segment_timeline.mpd ├── segment_timeline_multi_period.mpd ├── truncate.mpd └── truncate_short.mpd ├── mpd.go ├── mpd_attr.go ├── mpd_read_write.go ├── mpd_read_write_test.go ├── mpd_test.go ├── pssh.go ├── pssh_test.go ├── scte35.go ├── scte35_test.go ├── segment.go ├── segment_list_test.go ├── segment_timeline_test.go └── validate.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | jobs: 4 | build: 5 | docker: 6 | - image: cimg/go:1.23.4 7 | steps: 8 | - checkout 9 | - run: make test 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | coverage/ 3 | vendor/ 4 | .idea/ 5 | \#* 6 | *~ 7 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "zencoder/whitesource-config@main" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Brightcove, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifdef VERBOSE 2 | V = -v 3 | X = -x 4 | else 5 | .SILENT: 6 | endif 7 | 8 | .DEFAULT_GOAL := all 9 | 10 | .PHONY: all 11 | all: test 12 | 13 | .PHONY: test 14 | test: 15 | go test $(V) ./... -race 16 | 17 | .PHONY: generate 18 | generate: export GENERATE_FIXTURES=true 19 | generate: test 20 | 21 | .PHONY: fmt 22 | fmt: 23 | go fmt $(X) ./... 24 | 25 | .PHONY: clean 26 | clean: 27 | go clean -i $(X) -cache -testcache 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-dash [![godoc](https://godoc.org/github.com/zencoder/go-dash/mpd?status.svg)](http://godoc.org/github.com/zencoder/go-dash/mpd) 2 | 3 | A [Go](https://golang.org) library for generating [MPEG-DASH](https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP) manifests. 4 | 5 | ## Install 6 | 7 | ``` 8 | go get -u github.com/zencoder/go-dash 9 | ``` 10 | 11 | ## Supported Features 12 | 13 | * Profiles 14 | * Live 15 | * On Demand 16 | * Adaption Sets / Representations / Roles 17 | * Audio 18 | * Video 19 | * Subtitles 20 | * Multiple periods (multi-part playlist) 21 | * DRM (ContentProtection) 22 | * PlayReady 23 | * Widevine 24 | 25 | ## Known Limitations (for now) (PRs welcome) 26 | 27 | * No PSSH/PRO generation 28 | * Limited Profile Support 29 | 30 | ## Example Usage 31 | 32 | See the [examples/](https://github.com/zencoder/go-dash/tree/master/examples) directory. 33 | 34 | ## Development 35 | 36 | ``` 37 | make test 38 | ``` 39 | 40 | ### CI 41 | 42 | [This project builds in Circle CI](https://circleci.com/gh/zencoder/go-dash/) 43 | 44 | ## License 45 | 46 | [Apache License Version 2.0](LICENSE) 47 | -------------------------------------------------------------------------------- /THIRD_PARTY_NOTICES: -------------------------------------------------------------------------------- 1 | ### Third-Party Libraries 2 | 3 | #### Library: scte35-go 4 | - License: Apache License 2.0 5 | - URL: https://github.com/Comcast/scte35-go/tree/main 6 | - Copyright: None explicitly stated. 7 | - Notes: This library does not include a copyright notice, but it is licensed under the Apache License 2.0. 8 | 9 | This library is used in compliance with the Apache License, Version 2.0. 10 | -------------------------------------------------------------------------------- /examples/live.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zencoder/go-dash/v3/mpd" 7 | ) 8 | 9 | func main() { 10 | m := mpd.NewMPD(mpd.DASH_PROFILE_LIVE, "PT6M16S", "PT1.97S") 11 | 12 | audioAS, _ := m.AddNewAdaptationSetAudio(mpd.DASH_MIME_TYPE_AUDIO_MP4, true, 1, "und") 13 | _, _ = audioAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 14 | _, _ = audioAS.AddNewContentProtectionSchemeWidevineWithPSSH([]byte{}) 15 | _, _ = audioAS.AddNewContentProtectionSchemePlayready("mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A") 16 | _, _ = audioAS.SetNewSegmentTemplate(1968, "$RepresentationID$/audio/en/init.mp4", "$RepresentationID$/audio/en/seg-$Number$.m4f", 0, 1000) 17 | _, _ = audioAS.AddNewRepresentationAudio(44100, 67095, "mp4a.40.2", "800") 18 | 19 | videoAS, _ := m.AddNewAdaptationSetVideo(mpd.DASH_MIME_TYPE_VIDEO_MP4, "progressive", true, 1) 20 | _, _ = videoAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 21 | _, _ = videoAS.AddNewContentProtectionSchemeWidevineWithPSSH([]byte{}) 22 | _, _ = videoAS.AddNewContentProtectionSchemePlayready("mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A") 23 | _, _ = videoAS.SetNewSegmentTemplate(1968, "$RepresentationID$/video/1/init.mp4", "$RepresentationID$/video/1/seg-$Number$.m4f", 0, 1000) 24 | _, _ = videoAS.AddNewRepresentationVideo(1518664, "avc1.4d401f", "800", "30000/1001", 960, 540) 25 | _, _ = videoAS.AddNewRepresentationVideo(1911775, "avc1.4d401f", "1000", "30000/1001", 1024, 576) 26 | _, _ = videoAS.AddNewRepresentationVideo(2295158, "avc1.4d401f", "1200", "30000/1001", 1024, 576) 27 | _, _ = videoAS.AddNewRepresentationVideo(2780732, "avc1.4d401f", "1500", "30000/1001", 1280, 720) 28 | 29 | subtitleAS, _ := m.AddNewAdaptationSetSubtitle(mpd.DASH_MIME_TYPE_SUBTITLE_VTT, "en", "Subtitle (En)") 30 | subtitleRep, _ := subtitleAS.AddNewRepresentationSubtitle(256, "subtitle_en") 31 | _ = subtitleRep.SetNewBaseURL("http://example.com/content/sintel/subtitles/subtitles_en.vtt") 32 | schemeIDURI := "urn:mpeg:dash:utc:direct:2014" 33 | value := "2019-10-23T15:56:29Z" 34 | m.UTCTiming = &mpd.DescriptorType{ 35 | SchemeIDURI: &schemeIDURI, 36 | Value: &value, 37 | } 38 | 39 | mpdStr, _ := m.WriteToString() 40 | fmt.Println(mpdStr) 41 | } 42 | -------------------------------------------------------------------------------- /examples/live.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/ondemand.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zencoder/go-dash/v3/mpd" 7 | ) 8 | 9 | func exampleOndemand() { 10 | m := mpd.NewMPD(mpd.DASH_PROFILE_ONDEMAND, "PT30S", "PT1.97S") 11 | 12 | audioAS, _ := m.AddNewAdaptationSetAudio(mpd.DASH_MIME_TYPE_AUDIO_MP4, true, 1, "und") 13 | _, _ = audioAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 14 | _, _ = audioAS.AddNewContentProtectionSchemeWidevineWithPSSH([]byte{}) 15 | _, _ = audioAS.AddNewContentProtectionSchemePlayready("mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A") 16 | 17 | audioRep, _ := audioAS.AddNewRepresentationAudio(44100, 128558, "mp4a.40.5", "800k/audio-und") 18 | _ = audioRep.SetNewBaseURL("800k/output-audio-und.mp4") 19 | _, _ = audioRep.AddNewSegmentBase("629-756", "0-628") 20 | 21 | videoAS, _ := m.AddNewAdaptationSetVideo(mpd.DASH_MIME_TYPE_VIDEO_MP4, "progressive", true, 1) 22 | _, _ = videoAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 23 | _, _ = videoAS.AddNewContentProtectionSchemeWidevineWithPSSH([]byte{}) 24 | _, _ = videoAS.AddNewContentProtectionSchemePlayready("mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A") 25 | 26 | videoRep1, _ := videoAS.AddNewRepresentationVideo(1100690, "avc1.4d401e", "800k/video-1", "30000/1001", 640, 360) 27 | _ = videoRep1.SetNewBaseURL("800k/output-video-1.mp4") 28 | _, _ = videoRep1.AddNewSegmentBase("686-813", "0-685") 29 | 30 | videoRep2, _ := videoAS.AddNewRepresentationVideo(1633516, "avc1.4d401f", "1200k/video-1", "30000/1001", 960, 540) 31 | _ = videoRep2.SetNewBaseURL("1200k/output-video-1.mp4") 32 | _, _ = videoRep2.AddNewSegmentBase("686-813", "0-685") 33 | 34 | subtitleAS, _ := m.AddNewAdaptationSetSubtitle(mpd.DASH_MIME_TYPE_SUBTITLE_VTT, "en", "Subtitle (En)") 35 | subtitleRep, _ := subtitleAS.AddNewRepresentationSubtitle(256, "captions_en") 36 | _ = subtitleRep.SetNewBaseURL("http://example.com/content/sintel/subtitles/subtitles_en.vtt") 37 | 38 | thumbnailsAS, _ := m.AddNewAdaptationSetThumbnails(mpd.DASH_MIME_TYPE_IMAGE_JPEG) 39 | _, _ = thumbnailsAS.SetNewSegmentTemplateThumbnails(1801800, "$RepresentationID$/$Number$.jpg", 0, 30000) 40 | _, _ = thumbnailsAS.AddNewRepresentationThumbnails("thumbnails", "5x4", "http://dashif.org/guidelines/thumbnail_tile", 50000, 1600, 720) 41 | 42 | mpdStr, _ := m.WriteToString() 43 | fmt.Println(mpdStr) 44 | } 45 | -------------------------------------------------------------------------------- /examples/ondemand.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A 9 | 10 | 11 | 800k/output-audio-und.mp4 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A 22 | 23 | 24 | 800k/output-video-1.mp4 25 | 26 | 27 | 28 | 29 | 30 | 1200k/output-video-1.mp4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/ondemand_with_thumbnails.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A 9 | 10 | 11 | 800k/output-audio-und.mp4 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | mgIAAAEAAQCQAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQQBtAGYAagBDAFQATwBQAGIARQBPAGwAMwBXAEQALwA1AG0AYwBlAGMAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCAEcAdwAxAGEAWQBaADEAWQBYAE0APQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A 22 | 23 | 24 | 800k/output-video-1.mp4 25 | 26 | 27 | 28 | 29 | 30 | 1200k/output-video-1.mp4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zencoder/go-dash/v3 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.2 6 | 7 | require github.com/Comcast/scte35-go v1.4.6 8 | 9 | require ( 10 | github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 // indirect 11 | golang.org/x/text v0.16.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Comcast/scte35-go v1.4.6 h1:jhYrK8xsKjaEf/JmSVGaz0xYrlsUgyjsYYCRY7TGBy4= 2 | github.com/Comcast/scte35-go v1.4.6/go.mod h1:556vqw++rwYrL+GcVH6iX/gaPSDCv9LlNdKAlqpN/ZY= 3 | github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 h1:XNtOMwxmV2PI/vuTHDZnFzGIFNUh8MK73q7+Kna7AXs= 4 | github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883/go.mod h1:9IjZnSQGh45J46HHS45pxuMJ6WFTtSXbaX0FoHDvxh8= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 10 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 11 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 12 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /helpers/ptrs/ptrs.go: -------------------------------------------------------------------------------- 1 | package ptrs 2 | 3 | func Strptr(v string) *string { 4 | p := new(string) 5 | *p = v 6 | return p 7 | } 8 | 9 | func Intptr(v int) *int { 10 | p := new(int) 11 | *p = v 12 | return p 13 | } 14 | 15 | func Int64ptr(v int64) *int64 { 16 | p := new(int64) 17 | *p = v 18 | return p 19 | } 20 | 21 | func Uintptr(v uint) *uint { 22 | p := new(uint) 23 | *p = v 24 | return p 25 | } 26 | 27 | func Uint32ptr(v uint32) *uint32 { 28 | p := new(uint32) 29 | *p = v 30 | return p 31 | } 32 | 33 | func Uint64ptr(v uint64) *uint64 { 34 | p := new(uint64) 35 | *p = v 36 | return p 37 | } 38 | 39 | func Boolptr(v bool) *bool { 40 | p := new(bool) 41 | *p = v 42 | return p 43 | } 44 | 45 | func Float64ptr(v float64) *float64 { 46 | p := new(float64) 47 | *p = v 48 | return p 49 | } 50 | -------------------------------------------------------------------------------- /helpers/require/require.go: -------------------------------------------------------------------------------- 1 | package require 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func NoError(t *testing.T, err error, msgs ...string) { 9 | if err != nil { 10 | t.Errorf("Expected no error, but got: %s", err) 11 | for _, msg := range msgs { 12 | t.Errorf("\n" + msg) 13 | } 14 | t.FailNow() 15 | } 16 | } 17 | 18 | func NotNil(t *testing.T, object interface{}, msgs ...string) { 19 | if isNil(object) { 20 | t.Errorf("Expected nil, but got an object") 21 | for _, msg := range msgs { 22 | t.Errorf("\n" + msg) 23 | } 24 | t.FailNow() 25 | } 26 | } 27 | 28 | func Nil(t *testing.T, object interface{}, msgs ...string) { 29 | if !isNil(object) { 30 | t.Errorf("Expected an object, but got nil") 31 | for _, msg := range msgs { 32 | t.Errorf("\n" + msg) 33 | } 34 | t.FailNow() 35 | } 36 | } 37 | 38 | func isNil(object interface{}) bool { 39 | if object == nil { 40 | return true 41 | } 42 | 43 | value := reflect.ValueOf(object) 44 | kind := value.Kind() 45 | if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { 46 | return true 47 | } 48 | 49 | return false 50 | } 51 | 52 | func EqualError(t *testing.T, theError error, errString string, msgs ...string) { 53 | if theError == nil { 54 | t.Errorf("Expected an error but got nil") 55 | for _, msg := range msgs { 56 | t.Errorf("\n" + msg) 57 | } 58 | t.FailNow() 59 | return 60 | } 61 | 62 | if theError.Error() != errString { 63 | t.Errorf("Expected an error with message:\n%qbut got \n%q", errString, theError) 64 | for _, msg := range msgs { 65 | t.Errorf("\n" + msg) 66 | } 67 | t.FailNow() 68 | } 69 | } 70 | 71 | func EqualFloat64(t *testing.T, expected, actual float64, msgs ...string) { 72 | if expected != actual { 73 | t.Errorf("Expected %f but got %f", expected, actual) 74 | for _, msg := range msgs { 75 | t.Errorf("\n" + msg) 76 | } 77 | t.FailNow() 78 | } 79 | } 80 | 81 | func EqualStringPtr(t *testing.T, expected, actual *string, msgs ...string) { 82 | if expected == nil && actual == nil { 83 | return 84 | } 85 | if expected != nil && actual != nil { 86 | EqualString(t, *expected, *actual, msgs...) 87 | return 88 | } 89 | 90 | t.Errorf("Expected %v but got %v", expected, actual) 91 | for _, msg := range msgs { 92 | t.Errorf("\n" + msg) 93 | } 94 | t.FailNow() 95 | } 96 | 97 | func EqualString(t *testing.T, expected, actual string, msgs ...string) { 98 | if expected != actual { 99 | t.Errorf("Expected %s but got %s", expected, actual) 100 | for _, msg := range msgs { 101 | t.Errorf("\n" + msg) 102 | } 103 | t.FailNow() 104 | } 105 | } 106 | 107 | func EqualStringSlice(t *testing.T, expected, actual []string, msgs ...string) { 108 | if len(expected) != len(actual) { 109 | t.Errorf("Expected %v but got %v", expected, actual) 110 | for _, msg := range msgs { 111 | t.Errorf("\n" + msg) 112 | } 113 | t.FailNow() 114 | } 115 | for i, e := range expected { 116 | a := actual[i] 117 | EqualString(t, e, a, msgs...) 118 | } 119 | } 120 | 121 | func EqualUInt32(t *testing.T, expected, actual uint32, msgs ...string) { 122 | if expected != actual { 123 | t.Errorf("Expected %d but got %d", expected, actual) 124 | for _, msg := range msgs { 125 | t.Errorf("\n" + msg) 126 | } 127 | t.FailNow() 128 | } 129 | } 130 | 131 | func EqualUInt64Ptr(t *testing.T, expected, actual *uint64, msgs ...string) { 132 | if expected == nil && actual == nil { 133 | return 134 | } 135 | if expected != nil && actual != nil { 136 | EqualUInt64(t, *expected, *actual, msgs...) 137 | return 138 | } 139 | 140 | t.Errorf("Expected %v but got %v", expected, actual) 141 | for _, msg := range msgs { 142 | t.Errorf("\n" + msg) 143 | } 144 | t.FailNow() 145 | } 146 | 147 | func EqualUInt64(t *testing.T, expected, actual uint64, msgs ...string) { 148 | if expected != actual { 149 | t.Errorf("Expected %d but got %d", expected, actual) 150 | for _, msg := range msgs { 151 | t.Errorf("\n" + msg) 152 | } 153 | t.FailNow() 154 | } 155 | } 156 | 157 | func EqualIntPtr(t *testing.T, expected, actual *int, msgs ...string) { 158 | if expected == nil && actual == nil { 159 | return 160 | } 161 | if expected != nil && actual != nil { 162 | EqualInt(t, *expected, *actual, msgs...) 163 | return 164 | } 165 | 166 | t.Errorf("Expected %v but got %v", expected, actual) 167 | for _, msg := range msgs { 168 | t.Errorf("\n" + msg) 169 | } 170 | t.FailNow() 171 | } 172 | 173 | func EqualInt(t *testing.T, expected, actual int, msgs ...string) { 174 | if expected != actual { 175 | t.Errorf("Expected %d but got %d", expected, actual) 176 | for _, msg := range msgs { 177 | t.Errorf("\n" + msg) 178 | } 179 | t.FailNow() 180 | } 181 | } 182 | 183 | func EqualErr(t *testing.T, expected, actual error, msgs ...string) { 184 | if expected != actual { 185 | t.Errorf("Expected %s but got %s", expected, actual) 186 | for _, msg := range msgs { 187 | t.Errorf("\n" + msg) 188 | } 189 | t.FailNow() 190 | } 191 | } 192 | 193 | func Implements(t *testing.T, interfaceObject interface{}, object interface{}, msgs ...string) { 194 | interfaceType := reflect.TypeOf(interfaceObject).Elem() 195 | 196 | if !reflect.TypeOf(object).Implements(interfaceType) { 197 | t.Errorf("Expected %T but got %v", object, interfaceType) 198 | for _, msg := range msgs { 199 | t.Errorf("\n" + msg) 200 | } 201 | t.FailNow() 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /helpers/testfixtures/testfixtures.go: -------------------------------------------------------------------------------- 1 | package testfixtures 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/zencoder/go-dash/v3/helpers/require" 10 | ) 11 | 12 | // Load test fixture from path relative to fixtures directory 13 | func LoadFixture(path string) (js string) { 14 | f, err := ioutil.ReadFile(path) 15 | if err != nil { 16 | panic(fmt.Sprintf("LoadFixture Error. ioutil.ReadFile. path = %s, Err = %s", path, err.Error())) 17 | } 18 | return string(f) 19 | } 20 | 21 | func CompareFixture(t *testing.T, fixturePath string, actualContent string) { 22 | expectedContent := LoadFixture(fixturePath) 23 | if os.Getenv("GENERATE_FIXTURES") != "" { 24 | _ = ioutil.WriteFile(fixturePath, []byte(actualContent), os.ModePerm) 25 | fmt.Println("Wrote fixture: " + fixturePath) 26 | return 27 | } 28 | require.EqualString(t, expectedContent, actualContent) 29 | } 30 | -------------------------------------------------------------------------------- /mpd/duration.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Duration is an extension of the original time.Duration. This type is used to 14 | // re-format the String() output to support the ISO 8601 duration standard. And 15 | // add the MarshalXMLAttr and UnmarshalXMLAttr functions. 16 | type Duration time.Duration 17 | 18 | var ( 19 | rStart = "^P" // Must start with a 'P' 20 | rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years 21 | rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T' 22 | rHours = "(\\d+H)?" // Hours 23 | rMinutes = "(\\d+M)?" // Minutes 24 | rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal) 25 | rEnd = ")?$" // end of regex must close "T" capture group 26 | ) 27 | 28 | var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd) 29 | 30 | func (d *Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { 31 | return xml.Attr{Name: name, Value: d.String()}, nil 32 | } 33 | 34 | func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error { 35 | dur, err := ParseDuration(attr.Value) 36 | if err != nil { 37 | return err 38 | } 39 | *d = Duration(dur) 40 | return nil 41 | } 42 | 43 | // String returns a string representing the duration in the form "PT72H3M0.5S". 44 | // Leading zero units are omitted. The zero duration formats as PT0S. 45 | // Based on src/time/time.go's time.Duration.String function. 46 | func (d *Duration) String() string { 47 | // This is inlinable to take advantage of "function outlining". 48 | // Thus, the caller can decide whether a string must be heap allocated. 49 | var arr [32]byte 50 | 51 | if d == nil { 52 | return "PT0S" 53 | } 54 | 55 | n := d.format(&arr) 56 | return "PT" + string(arr[n:]) 57 | } 58 | 59 | // format formats the representation of d into the end of buf and returns the 60 | // offset of the first character. This function is modified to use the iso 1801 61 | // duration standard. This standard only uses the "H", "M", "S" characters. 62 | // // Based on src/time/time.go's time.Duration.Format function. 63 | func (d *Duration) format(buf *[32]byte) int { 64 | // Largest time is 2540400h10m10.000000000s 65 | w := len(buf) 66 | 67 | u := uint64(*d) 68 | neg := *d < 0 69 | if neg { 70 | u = -u 71 | } 72 | 73 | w-- 74 | buf[w] = 'S' 75 | 76 | w, u = fmtFrac(buf[:w], u, 9) 77 | 78 | // u is now integer seconds 79 | w = fmtInt(buf[:w], u%60) 80 | u /= 60 81 | 82 | // u is now integer minutes 83 | if u > 0 { 84 | w-- 85 | buf[w] = 'M' 86 | w = fmtInt(buf[:w], u%60) 87 | u /= 60 88 | 89 | // u is now integer hours 90 | // Stop at hours because days can be different lengths. 91 | if u > 0 { 92 | w-- 93 | buf[w] = 'H' 94 | w = fmtInt(buf[:w], u) 95 | } 96 | } 97 | 98 | if neg { 99 | w-- 100 | buf[w] = '-' 101 | } 102 | 103 | return w 104 | } 105 | 106 | // fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the 107 | // tail of buf, omitting trailing zeros. it omits the decimal 108 | // point too when the fraction is 0. It returns the index where the 109 | // output bytes begin and the value v/10**prec. 110 | // Copied from src/time/time.go. 111 | func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) { 112 | // Omit trailing zeros up to and including decimal point. 113 | w := len(buf) 114 | print := false 115 | for i := 0; i < prec; i++ { 116 | digit := v % 10 117 | print = print || digit != 0 118 | if print { 119 | w-- 120 | buf[w] = byte(digit) + '0' 121 | } 122 | v /= 10 123 | } 124 | if print { 125 | w-- 126 | buf[w] = '.' 127 | } 128 | return w, v 129 | } 130 | 131 | // fmtInt formats v into the tail of buf. 132 | // It returns the index where the output begins. 133 | // Copied from src/time/time.go. 134 | func fmtInt(buf []byte, v uint64) int { 135 | w := len(buf) 136 | if v == 0 { 137 | w-- 138 | buf[w] = '0' 139 | } else { 140 | for v > 0 { 141 | w-- 142 | buf[w] = byte(v%10) + '0' 143 | v /= 10 144 | } 145 | } 146 | return w 147 | } 148 | 149 | func ParseDuration(str string) (time.Duration, error) { 150 | if len(str) < 3 { 151 | return 0, errors.New("at least one number and designator are required") 152 | } 153 | 154 | if strings.Contains(str, "-") { 155 | return 0, errors.New("duration cannot be negative") 156 | } 157 | 158 | // Check that only the parts we expect exist and that everything's in the correct order 159 | if !xmlDurationRegex.Match([]byte(str)) { 160 | return 0, errors.New("duration must be in the format: P[nD][T[nH][nM][nS]]") 161 | } 162 | 163 | var parts = xmlDurationRegex.FindStringSubmatch(str) 164 | var total time.Duration 165 | 166 | if parts[1] != "" { 167 | days, err := strconv.Atoi(strings.TrimRight(parts[1], "D")) 168 | if err != nil { 169 | return 0, fmt.Errorf("error parsing Days: %s", err) 170 | } 171 | total += time.Duration(days) * time.Hour * 24 172 | } 173 | 174 | if parts[2] != "" { 175 | hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H")) 176 | if err != nil { 177 | return 0, fmt.Errorf("error parsing Hours: %s", err) 178 | } 179 | total += time.Duration(hours) * time.Hour 180 | } 181 | 182 | if parts[3] != "" { 183 | mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M")) 184 | if err != nil { 185 | return 0, fmt.Errorf("error parsing Minutes: %s", err) 186 | } 187 | total += time.Duration(mins) * time.Minute 188 | } 189 | 190 | if parts[4] != "" { 191 | secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64) 192 | if err != nil { 193 | return 0, fmt.Errorf("error parsing Seconds: %s", err) 194 | } 195 | total += time.Duration(secs * float64(time.Second)) 196 | } 197 | 198 | return total, nil 199 | } 200 | -------------------------------------------------------------------------------- /mpd/duration_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/zencoder/go-dash/v3/helpers/require" 9 | ) 10 | 11 | func TestDuration(t *testing.T) { 12 | in := map[string]string{ 13 | "0.5ms": "PT0.0005S", 14 | "7ms": "PT0.007S", 15 | "0s": "PT0S", 16 | "6m16s": "PT6M16S", 17 | "1.97s": "PT1.97S", 18 | "4988000000ns": "PT4.988S", 19 | "2h4m30s7ms": "PT2H4M30.007S", 20 | } 21 | for ins, ex := range in { 22 | t.Run(ins, func(t *testing.T) { 23 | timeDur, err := time.ParseDuration(ins) 24 | require.NoError(t, err) 25 | dur := Duration(timeDur) 26 | require.EqualString(t, ex, dur.String()) 27 | }) 28 | } 29 | } 30 | 31 | func TestParseDuration(t *testing.T) { 32 | in := map[string]float64{ 33 | "PT0S": 0, 34 | "PT1M": 60, 35 | "PT2H": 7200, 36 | "PT6M16S": 376, 37 | "PT1.97S": 1.97, 38 | "PT1H2M3.456S": 3723.456, 39 | "P1DT2H": (26 * time.Hour).Seconds(), 40 | "PT20M": (20 * time.Minute).Seconds(), 41 | "PT1M30.5S": (time.Minute + 30*time.Second + 500*time.Millisecond).Seconds(), 42 | "PT1004199059S": (1004199059 * time.Second).Seconds(), 43 | } 44 | for ins, ex := range in { 45 | t.Run(ins, func(t *testing.T) { 46 | act, err := ParseDuration(ins) 47 | require.NoError(t, err, ins) 48 | require.EqualFloat64(t, ex, act.Seconds(), ins) 49 | }) 50 | } 51 | } 52 | 53 | func TestParseBadDurations(t *testing.T) { 54 | in := map[string]string{ 55 | "P20M": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Months (doesn't make sense when converting to duration) 56 | "P20Y": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Years (doesn't make sense when converting to duration) 57 | "P15.5D": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal 58 | "P2H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours 59 | "2DT1H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // "P" must always be present 60 | "PT2M1H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // Hours must appear before Minutes 61 | "P": `at least one number and designator are required`, // At least one number and designator are required 62 | "-P20H": `duration cannot be negative`, // Negative duration doesn't make sense 63 | } 64 | for ins, msg := range in { 65 | t.Run(ins, func(t *testing.T) { 66 | _, err := ParseDuration(ins) 67 | require.EqualError(t, err, msg, fmt.Sprintf("Expected an error for: %s", ins)) 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /mpd/events.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import "encoding/xml" 4 | 5 | type EventStream struct { 6 | XMLName xml.Name `xml:"EventStream"` 7 | SchemeIDURI *string `xml:"schemeIdUri,attr"` 8 | Value *string `xml:"value,attr,omitempty"` 9 | Timescale *uint `xml:"timescale,attr"` 10 | Events []Event `xml:"Event,omitempty"` 11 | } 12 | 13 | type Event struct { 14 | XMLName xml.Name `xml:"Event"` 15 | ID *string `xml:"id,attr,omitempty"` 16 | PresentationTime *uint64 `xml:"presentationTime,attr,omitempty"` 17 | Duration *uint64 `xml:"duration,attr,omitempty"` 18 | Signals []Signal `xml:"Signal,omitempty"` 19 | } 20 | 21 | type ByPresentationTime []Event 22 | 23 | func (p ByPresentationTime) Len() int { 24 | return len(p) 25 | } 26 | 27 | func (p ByPresentationTime) Less(i, j int) bool { 28 | return *p[i].PresentationTime < *p[j].PresentationTime 29 | } 30 | 31 | func (p ByPresentationTime) Swap(i, j int) { 32 | p[i], p[j] = p[j], p[i] 33 | } 34 | -------------------------------------------------------------------------------- /mpd/events_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zencoder/go-dash/v3/helpers/ptrs" 7 | "github.com/zencoder/go-dash/v3/helpers/require" 8 | "github.com/zencoder/go-dash/v3/helpers/testfixtures" 9 | ) 10 | 11 | const ( 12 | VALID_EVENT_STREAM_SCHEME_ID_URI = "urn:example:eventstream" 13 | VALID_EVENT_STREAM_VALUE = "eventstream" 14 | VALID_EVENT_STREAM_TIMESCALE uint = 10 15 | ) 16 | 17 | func newEventStreamMPD() *MPD { 18 | m := NewDynamicMPD( 19 | DASH_PROFILE_LIVE, 20 | VALID_AVAILABILITY_START_TIME, 21 | VALID_MIN_BUFFER_TIME, 22 | ) 23 | p := m.GetCurrentPeriod() 24 | 25 | es := EventStream{ 26 | SchemeIDURI: ptrs.Strptr(VALID_EVENT_STREAM_SCHEME_ID_URI), 27 | Value: ptrs.Strptr(VALID_EVENT_STREAM_VALUE), 28 | Timescale: ptrs.Uintptr(VALID_EVENT_STREAM_TIMESCALE), 29 | } 30 | 31 | e0 := Event{ 32 | ID: ptrs.Strptr("event-0"), 33 | PresentationTime: ptrs.Uint64ptr(100), 34 | Duration: ptrs.Uint64ptr(50), 35 | } 36 | 37 | e1 := Event{ 38 | ID: ptrs.Strptr("event-1"), 39 | PresentationTime: ptrs.Uint64ptr(200), 40 | Duration: ptrs.Uint64ptr(50), 41 | } 42 | 43 | es.Events = append(es.Events, e0, e1) 44 | p.EventStreams = append(p.EventStreams, es) 45 | 46 | return m 47 | } 48 | 49 | func TestEventStreamsWriteToString(t *testing.T) { 50 | m := newEventStreamMPD() 51 | 52 | got, err := m.WriteToString() 53 | require.NoError(t, err) 54 | 55 | testfixtures.CompareFixture(t, "fixtures/events.mpd", got) 56 | } 57 | 58 | func TestReadEventStreams(t *testing.T) { 59 | m, err := ReadFromFile("fixtures/events.mpd") 60 | require.NoError(t, err) 61 | 62 | got, err := m.WriteToString() 63 | require.NoError(t, err) 64 | 65 | testfixtures.CompareFixture(t, "fixtures/events.mpd", got) 66 | } 67 | -------------------------------------------------------------------------------- /mpd/fixtures/adaptationset_switching.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://localhost:8002/dash/ 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 18 | 19 | 20 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 21 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 35 | 36 | 37 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 38 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /mpd/fixtures/audio_channel_configuration.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://localhost:8002/dash/ 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /mpd/fixtures/events.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mpd/fixtures/hbbtv_profile.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 8 | 9 | 10 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 11 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 24 | 25 | 26 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 27 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /mpd/fixtures/inband_event_stream.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /mpd/fixtures/invalid.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /mpd/fixtures/live_profile.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 8 | 9 | 10 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 11 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 22 | 23 | 24 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 25 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /mpd/fixtures/live_profile_dynamic.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 8 | 9 | 10 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 11 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 22 | 23 | 24 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 25 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /mpd/fixtures/live_profile_multi_base_url.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | ./ 4 | ../a/ 5 | ../b/ 6 | 7 | 8 | 9 | 10 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 11 | 12 | 13 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 14 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 25 | 26 | 27 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 28 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /mpd/fixtures/location.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://example.com/location.mpd 4 | 5 | -------------------------------------------------------------------------------- /mpd/fixtures/multiple_supplementals.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://localhost:8002/dash/ 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 | -------------------------------------------------------------------------------- /mpd/fixtures/newperiod.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /mpd/fixtures/ondemand_profile.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 8 | 9 | 10 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 11 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 12 | 13 | 14 | 800k/output-audio-und.mp4 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA== 25 | 26 | 27 | BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= 28 | AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA== 29 | 30 | 31 | 800k/output-video-1.mp4 32 | 33 | 34 | 35 | 36 | 37 | 1200k/output-video-1.mp4 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | http://example.com/content/sintel/subtitles/subtitles_en.vtt 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /mpd/fixtures/scte35.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | /AASAAAAAAAAAAAAAQQAAABifGO7 10 | 11 | 12 | 13 | 14 | /DAgAAAAAAAAAP/wBQb+AABb0AAAABAAAAEAAQCA== 15 | 16 | 17 | 18 | 19 | /AAcAAAAAAAAAAAACwUAAAAAfw8AAAAAAAAA0q3UPw== 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /mpd/fixtures/segment_list.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://localhost:8002/dash/ 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 | -------------------------------------------------------------------------------- /mpd/fixtures/segment_timeline.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://localhost:8002/public/ 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 | -------------------------------------------------------------------------------- /mpd/fixtures/segment_timeline_multi_period.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 | -------------------------------------------------------------------------------- /mpd/fixtures/truncate.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 | -------------------------------------------------------------------------------- /mpd/fixtures/truncate_short.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 | -------------------------------------------------------------------------------- /mpd/mpd_attr.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | type AttrMPD interface { 4 | GetStrptr() *string 5 | } 6 | 7 | type attrAvailabilityStartTime struct { 8 | strptr *string 9 | } 10 | 11 | func (attr *attrAvailabilityStartTime) GetStrptr() *string { 12 | return attr.strptr 13 | } 14 | 15 | // AttrAvailabilityStartTime returns AttrMPD object for NewMPD 16 | func AttrAvailabilityStartTime(value string) AttrMPD { 17 | return &attrAvailabilityStartTime{strptr: &value} 18 | } 19 | 20 | type attrMinimumUpdatePeriod struct { 21 | strptr *string 22 | } 23 | 24 | func (attr *attrMinimumUpdatePeriod) GetStrptr() *string { 25 | return attr.strptr 26 | } 27 | 28 | // AttrMinimumUpdatePeriod returns AttrMPD object for NewMPD 29 | func AttrMinimumUpdatePeriod(value string) AttrMPD { 30 | return &attrMinimumUpdatePeriod{strptr: &value} 31 | } 32 | 33 | type attrMediaPresentationDuration struct { 34 | strptr *string 35 | } 36 | 37 | func (attr *attrMediaPresentationDuration) GetStrptr() *string { 38 | return attr.strptr 39 | } 40 | 41 | // AttrMediaPresentationDuration returns AttrMPD object for NewMPD 42 | func AttrMediaPresentationDuration(value string) AttrMPD { 43 | return &attrMediaPresentationDuration{strptr: &value} 44 | } 45 | 46 | type attrPublishTime struct { 47 | strptr *string 48 | } 49 | 50 | func (attr *attrPublishTime) GetStrptr() *string { 51 | return attr.strptr 52 | } 53 | 54 | // AttrPublishTime returns AttrMPD object for NewMPD 55 | func AttrPublishTime(value string) AttrMPD { 56 | return &attrPublishTime{strptr: &value} 57 | } 58 | -------------------------------------------------------------------------------- /mpd/mpd_read_write.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/xml" 7 | "io" 8 | "os" 9 | ) 10 | 11 | // Reads an MPD XML file from disk into a MPD object. 12 | // path - File path to an MPD on disk 13 | func ReadFromFile(path string) (*MPD, error) { 14 | f, err := os.OpenFile(path, os.O_RDONLY, 0666) 15 | if err != nil { 16 | return nil, err 17 | } 18 | defer f.Close() 19 | 20 | return Read(f) 21 | } 22 | 23 | // Reads a string into a MPD object. 24 | // xmlStr - MPD manifest data as a string. 25 | func ReadFromString(xmlStr string) (*MPD, error) { 26 | b := bytes.NewBufferString(xmlStr) 27 | return Read(b) 28 | } 29 | 30 | // Reads from an io.Reader interface into an MPD object. 31 | // r - Must implement the io.Reader interface. 32 | func Read(r io.Reader) (*MPD, error) { 33 | var mpd MPD 34 | d := xml.NewDecoder(r) 35 | err := d.Decode(&mpd) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &mpd, nil 40 | } 41 | 42 | // Writes an MPD object to a file on disk. 43 | // path - Output path to write the manifest to. 44 | func (m *MPD) WriteToFile(path string) error { 45 | // Open the file to write the XML to 46 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 47 | if err != nil { 48 | return err 49 | } 50 | defer f.Close() 51 | if err = m.Write(f); err != nil { 52 | return err 53 | } 54 | if err = f.Sync(); err != nil { 55 | return err 56 | } 57 | return err 58 | } 59 | 60 | // Writes an MPD object to a string. 61 | func (m *MPD) WriteToString() (string, error) { 62 | var b bytes.Buffer 63 | w := bufio.NewWriter(&b) 64 | err := m.Write(w) 65 | if err != nil { 66 | return "", err 67 | } 68 | err = w.Flush() 69 | if err != nil { 70 | return "", err 71 | } 72 | return b.String(), err 73 | } 74 | 75 | // Writes an MPD object to an io.Writer interface 76 | // w - Must implement the io.Writer interface. 77 | func (m *MPD) Write(w io.Writer) error { 78 | b, err := xml.MarshalIndent(m, "", " ") 79 | if err != nil { 80 | return err 81 | } 82 | 83 | _, _ = w.Write([]byte(xml.Header)) 84 | _, _ = w.Write(b) 85 | _, _ = w.Write([]byte("\n")) 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /mpd/mpd_read_write_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/zencoder/go-dash/v3/helpers/ptrs" 9 | "github.com/zencoder/go-dash/v3/helpers/require" 10 | "github.com/zencoder/go-dash/v3/helpers/testfixtures" 11 | ) 12 | 13 | func TestReadingManifests(t *testing.T) { 14 | var testCases = []struct { 15 | err, filepath string 16 | }{ 17 | {filepath: "fixtures/live_profile.mpd", err: ""}, 18 | {filepath: "fixtures/ondemand_profile.mpd", err: ""}, 19 | {filepath: "fixtures/invalid.mpd", err: "XML syntax error on line 3: unexpected EOF"}, 20 | {filepath: "doesntexist.mpd", err: "open doesntexist.mpd: no such file or directory"}, 21 | } 22 | 23 | for _, tc := range testCases { 24 | // Test reading from manifest files 25 | if m, err := ReadFromFile(tc.filepath); tc.err == "" { 26 | require.NoError(t, err, "Error while reading "+tc.filepath) 27 | require.NotNil(t, m, "Empty result from reading "+tc.filepath) 28 | } else { 29 | require.EqualError(t, err, tc.err) 30 | } 31 | 32 | // Test reading valid files from strings 33 | if tc.err == "" { 34 | xmlStr := testfixtures.LoadFixture(tc.filepath) 35 | _, err := ReadFromString(xmlStr) 36 | require.NotNil(t, xmlStr) 37 | require.NoError(t, err) 38 | } 39 | } 40 | } 41 | 42 | func TestNewMPDLiveWriteToString(t *testing.T) { 43 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME, 44 | AttrAvailabilityStartTime(VALID_AVAILABILITY_START_TIME)) 45 | 46 | xmlStr, err := m.WriteToString() 47 | require.NoError(t, err) 48 | expectedXML := ` 49 | 50 | 51 | 52 | ` 53 | require.EqualString(t, expectedXML, xmlStr) 54 | } 55 | 56 | func TestNewDynamicMPDLiveWriteToString(t *testing.T) { 57 | m := NewDynamicMPD(DASH_PROFILE_LIVE, VALID_AVAILABILITY_START_TIME, VALID_MIN_BUFFER_TIME, 58 | AttrMediaPresentationDuration(VALID_MEDIA_PRESENTATION_DURATION), 59 | AttrMinimumUpdatePeriod(VALID_MINIMUM_UPDATE_PERIOD)) 60 | 61 | xmlStr, err := m.WriteToString() 62 | require.NoError(t, err) 63 | expectedXML := ` 64 | 65 | 66 | 67 | 68 | ` 69 | require.EqualString(t, expectedXML, xmlStr) 70 | } 71 | 72 | func TestNewDynamicMPDLiveWithPeriodStartWriteToString(t *testing.T) { 73 | m := NewDynamicMPD(DASH_PROFILE_LIVE, VALID_AVAILABILITY_START_TIME, VALID_MIN_BUFFER_TIME, 74 | AttrMediaPresentationDuration(VALID_MEDIA_PRESENTATION_DURATION), 75 | AttrMinimumUpdatePeriod(VALID_MINIMUM_UPDATE_PERIOD)) 76 | 77 | // Set first period start time to PT0S 78 | p := m.GetCurrentPeriod() 79 | start := Duration(time.Duration(0)) 80 | p.Start = &start 81 | 82 | xmlStr, err := m.WriteToString() 83 | require.NoError(t, err) 84 | expectedXML := ` 85 | 86 | 87 | 88 | 89 | ` 90 | require.EqualString(t, expectedXML, xmlStr) 91 | } 92 | 93 | func TestNewDynamicMPDLiveWithSuggestedPresentationDelayToString(t *testing.T) { 94 | m := NewDynamicMPD(DASH_PROFILE_LIVE, VALID_AVAILABILITY_START_TIME, VALID_MIN_BUFFER_TIME, 95 | AttrMediaPresentationDuration(VALID_MEDIA_PRESENTATION_DURATION), 96 | AttrMinimumUpdatePeriod(VALID_MINIMUM_UPDATE_PERIOD)) 97 | 98 | // Set first period start time to PT0S 99 | spd := Duration(time.Duration(18) * time.Second) 100 | m.SuggestedPresentationDelay = &spd 101 | 102 | xmlStr, err := m.WriteToString() 103 | require.NoError(t, err) 104 | expectedXML := ` 105 | 106 | 107 | 108 | 109 | ` 110 | require.EqualString(t, expectedXML, xmlStr) 111 | } 112 | 113 | func TestNewMPDOnDemandWriteToString(t *testing.T) { 114 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 115 | 116 | xmlStr, err := m.WriteToString() 117 | require.NoError(t, err) 118 | expectedXML := ` 119 | 120 | 121 | 122 | ` 123 | require.EqualString(t, expectedXML, xmlStr) 124 | } 125 | 126 | func TestAddNewAdaptationSetAudioWriteToString(t *testing.T) { 127 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 128 | 129 | _, _ = m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 130 | 131 | xmlStr, err := m.WriteToString() 132 | require.NoError(t, err) 133 | expectedXML := ` 134 | 135 | 136 | 137 | 138 | 139 | ` 140 | require.EqualString(t, expectedXML, xmlStr) 141 | } 142 | 143 | func TestAddNewAdaptationSetVideoWriteToString(t *testing.T) { 144 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 145 | 146 | as, err := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 147 | require.NoError(t, err) 148 | 149 | as.MinWidth = ptrs.Strptr("720") 150 | as.MaxWidth = ptrs.Strptr("720") 151 | as.MinHeight = ptrs.Strptr("480") 152 | as.MaxHeight = ptrs.Strptr("480") 153 | 154 | xmlStr, err := m.WriteToString() 155 | require.NoError(t, err) 156 | expectedXML := ` 157 | 158 | 159 | 160 | 161 | 162 | ` 163 | require.EqualString(t, expectedXML, xmlStr) 164 | } 165 | 166 | func TestAddNewAdaptationSetSubtitleWriteToString(t *testing.T) { 167 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 168 | 169 | _, _ = m.AddNewAdaptationSetSubtitleWithID("7357", DASH_MIME_TYPE_SUBTITLE_VTT, VALID_LANG, VALID_SUBTITLE_LABEL) 170 | 171 | xmlStr, err := m.WriteToString() 172 | require.NoError(t, err) 173 | expectedXML := ` 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ` 182 | require.EqualString(t, expectedXML, xmlStr) 183 | } 184 | 185 | func TestExampleAddNewPeriod(t *testing.T) { 186 | // a new MPD is created with a single Period 187 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 188 | 189 | // you can add content to the Period 190 | p := m.GetCurrentPeriod() 191 | as, _ := p.AddNewAdaptationSetVideoWithID("1", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 192 | _, _ = as.SetNewSegmentTemplate(1968, "$RepresentationID$/video-1.mp4", "$RepresentationID$/video-1/seg-$Number$.m4f", 0, 1000) 193 | 194 | // or directly to the MPD, which will use the current Period. 195 | as, _ = m.AddNewAdaptationSetAudioWithID("1", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 196 | _, _ = as.SetNewSegmentTemplate(1968, "$RepresentationID$/audio-1.mp4", "$RepresentationID$/audio-1/seg-$Number$.m4f", 0, 1000) 197 | 198 | // add a second period 199 | p = m.AddNewPeriod() 200 | p.SetDuration(3 * time.Minute) 201 | as, _ = p.AddNewAdaptationSetVideoWithID("2", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 202 | _, _ = as.SetNewSegmentTemplate(1968, "$RepresentationID$/video-2.mp4", "$RepresentationID$/video-2/seg-$Number$.m4f", 0, 1000) 203 | 204 | as, _ = m.AddNewAdaptationSetAudioWithID("2", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 205 | _, _ = as.SetNewSegmentTemplate(1968, "$RepresentationID$/audio-2.mp4", "$RepresentationID$/audio-2/seg-$Number$.m4f", 0, 1000) 206 | 207 | xmlStr, err := m.WriteToString() 208 | require.NoError(t, err) 209 | testfixtures.CompareFixture(t, "fixtures/newperiod.mpd", xmlStr) 210 | } 211 | 212 | func TestAddNewAccessibilityElementWriteToString(t *testing.T) { 213 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 214 | audioAS, err := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, 215 | VALID_START_WITH_SAP, VALID_LANG) 216 | if err != nil { 217 | t.Errorf("AddNewAccessibilityElement() error adding audio adaptation set: %v", err) 218 | return 219 | } 220 | 221 | _, err = audioAS.AddNewAccessibilityElement(ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO, "1") 222 | if err != nil { 223 | t.Errorf("AddNewAccessibilityElement() error adding accessibility element: %v", err) 224 | return 225 | } 226 | 227 | xmlStr, err := m.WriteToString() 228 | require.NoError(t, err) 229 | expectedXML := ` 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | ` 238 | require.EqualString(t, expectedXML, xmlStr) 239 | } 240 | 241 | func LiveProfile() *MPD { 242 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME, 243 | AttrAvailabilityStartTime(VALID_AVAILABILITY_START_TIME)) 244 | 245 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 246 | 247 | _, _ = audioAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 248 | _, _ = audioAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 249 | _, _ = audioAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 250 | 251 | _, _ = audioAS.AddNewRole("urn:mpeg:dash:role:2011", VALID_ROLE) 252 | 253 | _, _ = audioAS.SetNewSegmentTemplate(1968, "$RepresentationID$/audio/en/init.mp4", "$RepresentationID$/audio/en/seg-$Number$.m4f", 0, 1000) 254 | _, _ = audioAS.AddNewRepresentationAudio(44100, 67095, "mp4a.40.2", "800") 255 | _, _ = audioAS.AddNewAccessibilityElement(ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO, "1") 256 | 257 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 258 | 259 | _, _ = videoAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 260 | _, _ = videoAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 261 | _, _ = videoAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 262 | 263 | _, _ = videoAS.AddNewRole("urn:mpeg:dash:role:2011", VALID_ROLE) 264 | 265 | _, _ = videoAS.SetNewSegmentTemplate(1968, "$RepresentationID$/video/1/init.mp4", "$RepresentationID$/video/1/seg-$Number$.m4f", 0, 1000) 266 | _, _ = videoAS.AddNewRepresentationVideo(1518664, "avc1.4d401f", "800", "30000/1001", 960, 540) 267 | _, _ = videoAS.AddNewRepresentationVideo(1911775, "avc1.4d401f", "1000", "30000/1001", 1024, 576) 268 | _, _ = videoAS.AddNewRepresentationVideo(2295158, "avc1.4d401f", "1200", "30000/1001", 1024, 576) 269 | _, _ = videoAS.AddNewRepresentationVideo(2780732, "avc1.4d401f", "1500", "30000/1001", 1280, 720) 270 | 271 | subtitleAS, _ := m.AddNewAdaptationSetSubtitleWithID("7357", DASH_MIME_TYPE_SUBTITLE_VTT, VALID_LANG, VALID_SUBTITLE_LABEL) 272 | subtitleRep, _ := subtitleAS.AddNewRepresentationSubtitle(VALID_SUBTITLE_BANDWIDTH, VALID_SUBTITLE_ID) 273 | _ = subtitleRep.SetNewBaseURL(VALID_SUBTITLE_URL) 274 | 275 | return m 276 | } 277 | 278 | func TestFullLiveProfileWriteToString(t *testing.T) { 279 | m := LiveProfile() 280 | require.NotNil(t, m) 281 | xmlStr, err := m.WriteToString() 282 | require.NoError(t, err) 283 | testfixtures.CompareFixture(t, "fixtures/live_profile.mpd", xmlStr) 284 | } 285 | 286 | func TestFullLiveProfileMultiBaseURLWriteToString(t *testing.T) { 287 | m := LiveProfile() 288 | require.NotNil(t, m) 289 | 290 | m.BaseURL = []string{"./", "../a/", "../b/"} 291 | 292 | xmlStr, err := m.WriteToString() 293 | require.NoError(t, err) 294 | testfixtures.CompareFixture(t, "fixtures/live_profile_multi_base_url.mpd", xmlStr) 295 | } 296 | 297 | func TestFullLiveProfileWriteToFile(t *testing.T) { 298 | m := LiveProfile() 299 | require.NotNil(t, m) 300 | err := m.WriteToFile("test_live.mpd") 301 | xmlStr := testfixtures.LoadFixture("test_live.mpd") 302 | expectedXML := testfixtures.LoadFixture("fixtures/live_profile.mpd") 303 | require.EqualString(t, expectedXML, xmlStr) 304 | defer os.Remove("test_live.mpd") 305 | require.NoError(t, err) 306 | } 307 | 308 | func LiveProfileDynamic() *MPD { 309 | m := NewDynamicMPD(DASH_PROFILE_LIVE, VALID_AVAILABILITY_START_TIME, VALID_MIN_BUFFER_TIME, 310 | AttrMediaPresentationDuration(VALID_MEDIA_PRESENTATION_DURATION), 311 | AttrMinimumUpdatePeriod(VALID_MINIMUM_UPDATE_PERIOD)) 312 | 313 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 314 | 315 | _, _ = audioAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 316 | _, _ = audioAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 317 | _, _ = audioAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 318 | 319 | _, _ = audioAS.AddNewRole("urn:mpeg:dash:role:2011", VALID_ROLE) 320 | 321 | _, _ = audioAS.SetNewSegmentTemplate(1968, "$RepresentationID$/audio/en/init.mp4", "$RepresentationID$/audio/en/seg-$Number$.m4f", 0, 1000) 322 | _, _ = audioAS.AddNewRepresentationAudio(44100, 67095, "mp4a.40.2", "800") 323 | _, _ = audioAS.AddNewAccessibilityElement(ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO, "1") 324 | 325 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 326 | 327 | _, _ = videoAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 328 | _, _ = videoAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 329 | _, _ = videoAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 330 | 331 | _, _ = videoAS.AddNewRole("urn:mpeg:dash:role:2011", VALID_ROLE) 332 | 333 | _, _ = videoAS.SetNewSegmentTemplate(1968, "$RepresentationID$/video/1/init.mp4", "$RepresentationID$/video/1/seg-$Number$.m4f", 0, 1000) 334 | _, _ = videoAS.AddNewRepresentationVideo(1518664, "avc1.4d401f", "800", "30000/1001", 960, 540) 335 | _, _ = videoAS.AddNewRepresentationVideo(1911775, "avc1.4d401f", "1000", "30000/1001", 1024, 576) 336 | _, _ = videoAS.AddNewRepresentationVideo(2295158, "avc1.4d401f", "1200", "30000/1001", 1024, 576) 337 | _, _ = videoAS.AddNewRepresentationVideo(2780732, "avc1.4d401f", "1500", "30000/1001", 1280, 720) 338 | 339 | subtitleAS, _ := m.AddNewAdaptationSetSubtitleWithID("7357", DASH_MIME_TYPE_SUBTITLE_VTT, VALID_LANG, VALID_SUBTITLE_LABEL) 340 | subtitleRep, _ := subtitleAS.AddNewRepresentationSubtitle(VALID_SUBTITLE_BANDWIDTH, VALID_SUBTITLE_ID) 341 | _ = subtitleRep.SetNewBaseURL(VALID_SUBTITLE_URL) 342 | 343 | return m 344 | } 345 | 346 | func TestFullLiveProfileDynamicWriteToString(t *testing.T) { 347 | m := LiveProfileDynamic() 348 | require.NotNil(t, m) 349 | xmlStr, err := m.WriteToString() 350 | require.NoError(t, err) 351 | testfixtures.CompareFixture(t, "fixtures/live_profile_dynamic.mpd", xmlStr) 352 | } 353 | 354 | func TestFullLiveProfileDynamicWriteToFile(t *testing.T) { 355 | m := LiveProfileDynamic() 356 | require.NotNil(t, m) 357 | err := m.WriteToFile("test_live_dynamic.mpd") 358 | xmlStr := testfixtures.LoadFixture("test_live_dynamic.mpd") 359 | expectedXML := testfixtures.LoadFixture("fixtures/live_profile_dynamic.mpd") 360 | require.EqualString(t, expectedXML, xmlStr) 361 | defer os.Remove("test_live_dynamic.mpd") 362 | require.NoError(t, err) 363 | } 364 | 365 | func HbbTVProfile() *MPD { 366 | m := NewMPD(DASH_PROFILE_HBBTV_1_5_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 367 | 368 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 369 | 370 | _, _ = audioAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 371 | _, _ = audioAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 372 | _, _ = audioAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 373 | 374 | _, _ = audioAS.AddNewRole("urn:mpeg:dash:role:2011", VALID_ROLE) 375 | 376 | _, _ = audioAS.SetNewSegmentTemplate(1968, "$RepresentationID$/audio/en/init.mp4", "$RepresentationID$/audio/en/seg-$Number$.m4f", 0, 1000) 377 | r, _ := audioAS.AddNewRepresentationAudio(44100, 67095, "mp4a.40.2", "800") 378 | _, _ = r.AddNewAudioChannelConfiguration(AUDIO_CHANNEL_CONFIGURATION_MPEG_DASH, "2") 379 | _, _ = audioAS.AddNewAccessibilityElement(ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO, "1") 380 | 381 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 382 | 383 | _, _ = videoAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 384 | _, _ = videoAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 385 | _, _ = videoAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 386 | 387 | _, _ = videoAS.AddNewRole("urn:mpeg:dash:role:2011", VALID_ROLE) 388 | 389 | _, _ = videoAS.SetNewSegmentTemplate(1968, "$RepresentationID$/video/1/init.mp4", "$RepresentationID$/video/1/seg-$Number$.m4f", 0, 1000) 390 | _, _ = videoAS.AddNewRepresentationVideo(1518664, "avc1.4d401f", "800", "30000/1001", 960, 540) 391 | _, _ = videoAS.AddNewRepresentationVideo(1911775, "avc1.4d401f", "1000", "30000/1001", 1024, 576) 392 | _, _ = videoAS.AddNewRepresentationVideo(2295158, "avc1.4d401f", "1200", "30000/1001", 1024, 576) 393 | _, _ = videoAS.AddNewRepresentationVideo(2780732, "avc1.4d401f", "1500", "30000/1001", 1280, 720) 394 | 395 | subtitleAS, _ := m.AddNewAdaptationSetSubtitleWithID("7357", DASH_MIME_TYPE_SUBTITLE_VTT, VALID_LANG, VALID_SUBTITLE_LABEL) 396 | subtitleRep, _ := subtitleAS.AddNewRepresentationSubtitle(VALID_SUBTITLE_BANDWIDTH, VALID_SUBTITLE_ID) 397 | _ = subtitleRep.SetNewBaseURL(VALID_SUBTITLE_URL) 398 | 399 | return m 400 | } 401 | 402 | func TestFullHbbTVProfileWriteToString(t *testing.T) { 403 | m := HbbTVProfile() 404 | require.NotNil(t, m) 405 | xmlStr, err := m.WriteToString() 406 | require.NoError(t, err) 407 | testfixtures.CompareFixture(t, "fixtures/hbbtv_profile.mpd", xmlStr) 408 | } 409 | 410 | func TestFullHbbTVProfileWriteToFile(t *testing.T) { 411 | m := HbbTVProfile() 412 | require.NotNil(t, m) 413 | err := m.WriteToFile("test_hbbtv.mpd") 414 | xmlStr := testfixtures.LoadFixture("test_hbbtv.mpd") 415 | testfixtures.CompareFixture(t, "fixtures/hbbtv_profile.mpd", xmlStr) 416 | defer os.Remove("test_hbbtv.mpd") 417 | require.NoError(t, err) 418 | } 419 | 420 | func OnDemandProfile() *MPD { 421 | m := NewMPD(DASH_PROFILE_ONDEMAND, "PT30S", VALID_MIN_BUFFER_TIME) 422 | 423 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, "und") 424 | 425 | _, _ = audioAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 426 | _, _ = audioAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 427 | _, _ = audioAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 428 | _, _ = audioAS.AddNewAccessibilityElement(ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO, "1") 429 | 430 | audioRep, _ := audioAS.AddNewRepresentationAudio(44100, 128558, "mp4a.40.5", "800k/audio-und") 431 | _ = audioRep.SetNewBaseURL("800k/output-audio-und.mp4") 432 | _, _ = audioRep.AddNewSegmentBase("629-756", "0-628") 433 | 434 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 435 | 436 | _, _ = videoAS.AddNewContentProtectionRoot("08e367028f33436ca5dd60ffe5571e60") 437 | _, _ = videoAS.AddNewContentProtectionSchemeWidevineWithPSSH(getValidWVHeaderBytes()) 438 | _, _ = videoAS.AddNewContentProtectionSchemePlayreadyWithPSSH(VALID_PLAYREADY_PRO) 439 | 440 | videoRep1, _ := videoAS.AddNewRepresentationVideo(1100690, "avc1.4d401e", "800k/video-1", "30000/1001", 640, 360) 441 | _ = videoRep1.SetNewBaseURL("800k/output-video-1.mp4") 442 | _, _ = videoRep1.AddNewSegmentBase("686-813", "0-685") 443 | 444 | videoRep2, _ := videoAS.AddNewRepresentationVideo(1633516, "avc1.4d401f", "1200k/video-1", "30000/1001", 960, 540) 445 | _ = videoRep2.SetNewBaseURL("1200k/output-video-1.mp4") 446 | _, _ = videoRep2.AddNewSegmentBase("686-813", "0-685") 447 | 448 | subtitleAS, _ := m.AddNewAdaptationSetSubtitleWithID("7357", DASH_MIME_TYPE_SUBTITLE_VTT, VALID_LANG, VALID_SUBTITLE_LABEL) 449 | subtitleRep, _ := subtitleAS.AddNewRepresentationSubtitle(VALID_SUBTITLE_BANDWIDTH, VALID_SUBTITLE_ID) 450 | _ = subtitleRep.SetNewBaseURL(VALID_SUBTITLE_URL) 451 | 452 | return m 453 | } 454 | 455 | func TestFullOnDemandProfileWriteToString(t *testing.T) { 456 | m := OnDemandProfile() 457 | require.NotNil(t, m) 458 | xmlStr, err := m.WriteToString() 459 | require.NoError(t, err) 460 | testfixtures.CompareFixture(t, "fixtures/ondemand_profile.mpd", xmlStr) 461 | } 462 | 463 | func TestFullOnDemandProfileWriteToFile(t *testing.T) { 464 | m := OnDemandProfile() 465 | require.NotNil(t, m) 466 | err := m.WriteToFile("test-ondemand.mpd") 467 | xmlStr := testfixtures.LoadFixture("test-ondemand.mpd") 468 | testfixtures.CompareFixture(t, "fixtures/ondemand_profile.mpd", xmlStr) 469 | defer os.Remove("test-ondemand.mpd") 470 | require.NoError(t, err) 471 | } 472 | 473 | func TestWriteToFileInvalidFilePath(t *testing.T) { 474 | m := LiveProfile() 475 | require.NotNil(t, m) 476 | err := m.WriteToFile("") 477 | require.NotNil(t, err) 478 | } 479 | 480 | func TestWriteToFileTruncate(t *testing.T) { 481 | out := "test-truncate.mpd" 482 | 483 | m, err := ReadFromFile("fixtures/truncate.mpd") 484 | require.NoError(t, err) 485 | 486 | err = m.WriteToFile(out) 487 | require.NoError(t, err) 488 | 489 | defer os.Remove(out) 490 | 491 | xmlStr := testfixtures.LoadFixture(out) 492 | testfixtures.CompareFixture(t, "fixtures/truncate.mpd", xmlStr) 493 | 494 | m, err = ReadFromFile("fixtures/truncate_short.mpd") 495 | require.NoError(t, err) 496 | 497 | err = m.WriteToFile(out) 498 | require.NoError(t, err) 499 | 500 | xmlStr = testfixtures.LoadFixture(out) 501 | testfixtures.CompareFixture(t, "fixtures/truncate_short.mpd", xmlStr) 502 | } 503 | -------------------------------------------------------------------------------- /mpd/mpd_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "encoding/base64" 5 | "path/filepath" 6 | "strconv" 7 | "testing" 8 | 9 | . "github.com/zencoder/go-dash/v3/helpers/ptrs" 10 | "github.com/zencoder/go-dash/v3/helpers/require" 11 | "github.com/zencoder/go-dash/v3/helpers/testfixtures" 12 | ) 13 | 14 | const ( 15 | VALID_MEDIA_PRESENTATION_DURATION string = "PT6M16S" 16 | VALID_MIN_BUFFER_TIME string = "PT1.97S" 17 | VALID_AVAILABILITY_START_TIME string = "1970-01-01T00:00:00Z" 18 | VALID_PUBLISH_TIME string = "2020-03-12T10:39:45Z" 19 | VALID_MINIMUM_UPDATE_PERIOD string = "PT5S" 20 | VALID_SCAN_TYPE string = "progressive" 21 | VALID_SEGMENT_ALIGNMENT bool = true 22 | VALID_START_WITH_SAP int64 = 1 23 | VALID_LANG string = "en" 24 | VALID_DURATION int64 = 1968 25 | VALID_INIT_PATH_AUDIO string = "$RepresentationID$/audio/en/init.mp4" 26 | VALID_MEDIA_PATH_AUDIO string = "$RepresentationID$/audio/en/seg-$Number$.m4f" 27 | VALID_START_NUMBER int64 = 0 28 | VALID_TIMESCALE int64 = 1000 29 | VALID_AUDIO_SAMPLE_RATE int64 = 44100 30 | VALID_AUDIO_BITRATE int64 = 67095 31 | VALID_AUDIO_CODEC string = "mp4a.40.2" 32 | VALID_AUDIO_ID string = "800" 33 | VALID_VIDEO_BITRATE int64 = 1518664 34 | VALID_VIDEO_CODEC string = "avc1.4d401f" 35 | VALID_VIDEO_ID string = "800" 36 | VALID_VIDEO_FRAMERATE string = "30000/1001" 37 | VALID_VIDEO_WIDTH int64 = 960 38 | VALID_VIDEO_HEIGHT int64 = 540 39 | VALID_BASE_URL_VIDEO string = "800k/output-video-1.mp4" 40 | VALID_INDEX_RANGE string = "629-756" 41 | VALID_INIT_RANGE string = "0-628" 42 | VALID_PLAYREADY_PRO string = "BgIAAAEAAQD8ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATAA5AFcAOQBXAGsAcABWAEsAawArADQAMABHAEgAMwBZAFUASgBSAFYAUQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBJAEsAegBZADIASABaAEwAQQBsAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA=" 43 | VALID_WV_HEADER string = "CAESEFq91S9VSk8quNBh92FCUVUaCGNhc3RsYWJzIhhXcjNWTDFWS1R5cTQwR0gzWVVKUlZRPT0yB2RlZmF1bHQ=" 44 | VALID_SUBTITLE_BANDWIDTH int64 = 256 45 | VALID_SUBTITLE_ID string = "subtitle_en" 46 | VALID_SUBTITLE_LABEL string = "Subtitle (En)" 47 | VALID_SUBTITLE_URL string = "http://example.com/content/sintel/subtitles/subtitles_en.vtt" 48 | VALID_ROLE string = "main" 49 | VALID_LOCATION string = "https://example.com/location.mpd" 50 | VALID_SCHEME_ID_URI string = "https://aomedia.org/emsg/ID3" 51 | ) 52 | 53 | func TestNewMPDLive(t *testing.T) { 54 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME, 55 | AttrAvailabilityStartTime(VALID_AVAILABILITY_START_TIME)) 56 | require.NotNil(t, m) 57 | expectedMPD := &MPD{ 58 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 59 | Profiles: Strptr((string)(DASH_PROFILE_LIVE)), 60 | Type: Strptr("static"), 61 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 62 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 63 | AvailabilityStartTime: Strptr(VALID_AVAILABILITY_START_TIME), 64 | period: &Period{}, 65 | Periods: []*Period{{}}, 66 | } 67 | 68 | expectedString, err := expectedMPD.WriteToString() 69 | require.NoError(t, err) 70 | actualString, err := m.WriteToString() 71 | require.NoError(t, err) 72 | 73 | require.EqualString(t, expectedString, actualString) 74 | } 75 | 76 | func TestNewDynamicMPDLive(t *testing.T) { 77 | m := NewDynamicMPD(DASH_PROFILE_LIVE, VALID_AVAILABILITY_START_TIME, VALID_MIN_BUFFER_TIME, 78 | AttrMediaPresentationDuration(VALID_MEDIA_PRESENTATION_DURATION), 79 | AttrMinimumUpdatePeriod(VALID_MINIMUM_UPDATE_PERIOD), 80 | AttrPublishTime(VALID_PUBLISH_TIME)) 81 | require.NotNil(t, m) 82 | expectedMPD := &MPD{ 83 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 84 | Profiles: Strptr((string)(DASH_PROFILE_LIVE)), 85 | Type: Strptr("dynamic"), 86 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 87 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 88 | AvailabilityStartTime: Strptr(VALID_AVAILABILITY_START_TIME), 89 | MinimumUpdatePeriod: Strptr(VALID_MINIMUM_UPDATE_PERIOD), 90 | period: &Period{}, 91 | Periods: []*Period{{}}, 92 | UTCTiming: &DescriptorType{}, 93 | PublishTime: Strptr(VALID_PUBLISH_TIME), 94 | } 95 | 96 | expectedString, err := expectedMPD.WriteToString() 97 | require.NoError(t, err) 98 | actualString, err := m.WriteToString() 99 | require.NoError(t, err) 100 | 101 | require.EqualString(t, expectedString, actualString) 102 | } 103 | 104 | func TestNewMPDMultiPeriod(t *testing.T) { 105 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME, 106 | AttrAvailabilityStartTime(VALID_AVAILABILITY_START_TIME)) 107 | require.NotNil(t, m) 108 | for i := 0; i < 2; i++ { 109 | period := m.AddNewPeriod() 110 | period.ID = strconv.Itoa(i) 111 | } 112 | 113 | expectedMPD := &MPD{ 114 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 115 | Profiles: Strptr((string)(DASH_PROFILE_LIVE)), 116 | Type: Strptr("static"), 117 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 118 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 119 | AvailabilityStartTime: Strptr(VALID_AVAILABILITY_START_TIME), 120 | period: nil, 121 | Periods: []*Period{{ID: "0"}, {ID: "1"}}, 122 | } 123 | 124 | expectedString, err := expectedMPD.WriteToString() 125 | require.NoError(t, err) 126 | actualString, err := m.WriteToString() 127 | require.NoError(t, err) 128 | 129 | require.EqualString(t, expectedString, actualString) 130 | } 131 | 132 | func TestContentProtection_ImplementsInterface(t *testing.T) { 133 | cp := (*ContentProtectioner)(nil) 134 | require.Implements(t, cp, &ContentProtection{}) 135 | require.Implements(t, cp, ContentProtection{}) 136 | } 137 | 138 | func TestCENCContentProtection_ImplementsInterface(t *testing.T) { 139 | cp := (*ContentProtectioner)(nil) 140 | require.Implements(t, cp, &CENCContentProtection{}) 141 | require.Implements(t, cp, CENCContentProtection{}) 142 | } 143 | 144 | func TestPlayreadyContentProtection_ImplementsInterface(t *testing.T) { 145 | cp := (*ContentProtectioner)(nil) 146 | require.Implements(t, cp, &PlayreadyContentProtection{}) 147 | require.Implements(t, cp, PlayreadyContentProtection{}) 148 | } 149 | 150 | func TestWidevineContentProtection_ImplementsInterface(t *testing.T) { 151 | cp := (*ContentProtectioner)(nil) 152 | require.Implements(t, cp, &WidevineContentProtection{}) 153 | require.Implements(t, cp, WidevineContentProtection{}) 154 | } 155 | 156 | func TestNewMPDLiveWithBaseURLInMPD(t *testing.T) { 157 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 158 | m.BaseURL = []string{VALID_BASE_URL_VIDEO} 159 | require.NotNil(t, m) 160 | expectedMPD := &MPD{ 161 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 162 | Profiles: Strptr((string)(DASH_PROFILE_LIVE)), 163 | Type: Strptr("static"), 164 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 165 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 166 | period: &Period{}, 167 | Periods: []*Period{{}}, 168 | BaseURL: []string{VALID_BASE_URL_VIDEO}, 169 | } 170 | 171 | expectedString, err := expectedMPD.WriteToString() 172 | require.NoError(t, err) 173 | actualString, err := m.WriteToString() 174 | require.NoError(t, err) 175 | 176 | require.EqualString(t, expectedString, actualString) 177 | } 178 | 179 | func TestNewMPDLiveWithBaseURLInPeriod(t *testing.T) { 180 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 181 | m.period.BaseURL = []string{VALID_BASE_URL_VIDEO} 182 | require.NotNil(t, m) 183 | period := &Period{ 184 | BaseURL: []string{VALID_BASE_URL_VIDEO}, 185 | } 186 | expectedMPD := &MPD{ 187 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 188 | Profiles: Strptr((string)(DASH_PROFILE_LIVE)), 189 | Type: Strptr("static"), 190 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 191 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 192 | period: period, 193 | Periods: []*Period{period}, 194 | } 195 | 196 | expectedString, err := expectedMPD.WriteToString() 197 | require.NoError(t, err) 198 | actualString, err := m.WriteToString() 199 | require.NoError(t, err) 200 | 201 | require.EqualString(t, expectedString, actualString) 202 | } 203 | 204 | func TestNewMPDHbbTV(t *testing.T) { 205 | m := NewMPD(DASH_PROFILE_HBBTV_1_5_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 206 | require.NotNil(t, m) 207 | expectedMPD := &MPD{ 208 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 209 | Profiles: Strptr((string)(DASH_PROFILE_HBBTV_1_5_LIVE)), 210 | Type: Strptr("static"), 211 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 212 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 213 | period: &Period{}, 214 | Periods: []*Period{{}}, 215 | } 216 | 217 | expectedString, err := expectedMPD.WriteToString() 218 | require.NoError(t, err) 219 | actualString, err := m.WriteToString() 220 | require.NoError(t, err) 221 | 222 | require.EqualString(t, expectedString, actualString) 223 | } 224 | 225 | func TestNewMPDOnDemand(t *testing.T) { 226 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 227 | require.NotNil(t, m) 228 | expectedMPD := &MPD{ 229 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 230 | Profiles: Strptr((string)(DASH_PROFILE_ONDEMAND)), 231 | Type: Strptr("static"), 232 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 233 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 234 | period: &Period{}, 235 | Periods: []*Period{{}}, 236 | } 237 | 238 | expectedString, err := expectedMPD.WriteToString() 239 | require.NoError(t, err) 240 | actualString, err := m.WriteToString() 241 | require.NoError(t, err) 242 | 243 | require.EqualString(t, expectedString, actualString) 244 | } 245 | 246 | func TestAddAdaptationSetErrorNil(t *testing.T) { 247 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 248 | 249 | err := m.period.addAdaptationSet(nil) 250 | require.NotNil(t, err) 251 | require.EqualErr(t, ErrAdaptationSetNil, err) 252 | } 253 | 254 | type TestProprietaryContentProtection struct { 255 | ContentProtectionMarshal 256 | TestAttrA string `xml:"a,attr,omitempty"` 257 | TestAttrB string `xml:"b,attr,omitempty"` 258 | } 259 | 260 | func (s *TestProprietaryContentProtection) ContentProtected() {} 261 | 262 | func TestAddNewContentProtectionRootErrorInvalidLengthDefaultKID(t *testing.T) { 263 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 264 | s, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 265 | 266 | cp, err := s.AddNewContentProtectionRoot("invalidkid") 267 | require.NotNil(t, err) 268 | require.EqualErr(t, ErrInvalidDefaultKID, err) 269 | require.Nil(t, cp) 270 | } 271 | 272 | func TestAddNewContentProtectionRootErrorEmptyDefaultKID(t *testing.T) { 273 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 274 | s, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 275 | 276 | cp, err := s.AddNewContentProtectionRoot("") 277 | require.NotNil(t, err) 278 | require.EqualErr(t, ErrInvalidDefaultKID, err) 279 | require.Nil(t, cp) 280 | } 281 | 282 | func TestAddNewContentProtectionSchemePlayreadyErrorEmptyPRO(t *testing.T) { 283 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 284 | s, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 285 | 286 | cp, err := s.AddNewContentProtectionSchemePlayready("") 287 | require.NotNil(t, err) 288 | require.EqualErr(t, ErrPROEmpty, err) 289 | require.Nil(t, cp) 290 | } 291 | 292 | func TestAddNewContentProtectionSchemePlayreadyV10ErrorEmptyPRO(t *testing.T) { 293 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 294 | s, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 295 | 296 | cp, err := s.AddNewContentProtectionSchemePlayreadyV10("") 297 | require.NotNil(t, err) 298 | require.EqualErr(t, ErrPROEmpty, err) 299 | require.Nil(t, cp) 300 | } 301 | 302 | func TestSetNewSegmentTemplate(t *testing.T) { 303 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 304 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 305 | st, err := audioAS.SetNewSegmentTemplate(VALID_DURATION, VALID_INIT_PATH_AUDIO, VALID_MEDIA_PATH_AUDIO, VALID_START_NUMBER, VALID_TIMESCALE) 306 | require.NotNil(t, st) 307 | require.NoError(t, err) 308 | } 309 | 310 | func TestSetNewSegmentTemplateErrorNoDASHProfile(t *testing.T) { 311 | m := &MPD{ 312 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 313 | Profiles: nil, 314 | Type: Strptr("static"), 315 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 316 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 317 | period: &Period{}, 318 | } 319 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 320 | _, _ = audioAS.SetNewSegmentTemplate(VALID_DURATION, VALID_INIT_PATH_AUDIO, VALID_MEDIA_PATH_AUDIO, VALID_START_NUMBER, VALID_TIMESCALE) 321 | err := m.Validate() 322 | require.EqualErr(t, ErrNoDASHProfileSet, err) 323 | } 324 | 325 | func TestAddRepresentationAudio(t *testing.T) { 326 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 327 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 328 | 329 | r, err := audioAS.AddNewRepresentationAudio(VALID_AUDIO_SAMPLE_RATE, VALID_AUDIO_BITRATE, VALID_AUDIO_CODEC, VALID_AUDIO_ID) 330 | 331 | require.NotNil(t, r) 332 | require.NoError(t, err) 333 | } 334 | 335 | func TestAddAudioChannelConfiguration(t *testing.T) { 336 | m := NewMPD(DASH_PROFILE_HBBTV_1_5_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 337 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 338 | 339 | r, _ := audioAS.AddNewRepresentationAudio(VALID_AUDIO_SAMPLE_RATE, VALID_AUDIO_BITRATE, VALID_AUDIO_CODEC, VALID_AUDIO_ID) 340 | 341 | acc, err := r.AddNewAudioChannelConfiguration(AUDIO_CHANNEL_CONFIGURATION_MPEG_DASH, "2") 342 | 343 | require.NotNil(t, acc) 344 | require.NoError(t, err) 345 | } 346 | 347 | func TestAddRepresentationVideo(t *testing.T) { 348 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 349 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 350 | 351 | r, err := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 352 | 353 | require.NotNil(t, r) 354 | require.NoError(t, err) 355 | } 356 | 357 | func TestAddRepresentationSubtitle(t *testing.T) { 358 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 359 | 360 | subtitleAS, _ := m.AddNewAdaptationSetSubtitleWithID("7357", DASH_MIME_TYPE_SUBTITLE_VTT, VALID_LANG, VALID_SUBTITLE_LABEL) 361 | 362 | r, err := subtitleAS.AddNewRepresentationSubtitle(VALID_SUBTITLE_BANDWIDTH, VALID_SUBTITLE_ID) 363 | 364 | require.NotNil(t, r) 365 | require.NoError(t, err) 366 | } 367 | 368 | func TestAddRepresentationErrorNil(t *testing.T) { 369 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 370 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 371 | 372 | err := videoAS.addRepresentation(nil) 373 | require.NotNil(t, err) 374 | require.EqualErr(t, ErrRepresentationNil, err) 375 | } 376 | 377 | func TestAddRole(t *testing.T) { 378 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 379 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 380 | 381 | r, err := audioAS.AddNewRole("urn:mpeg:dash:role:2011", VALID_ROLE) 382 | 383 | require.NotNil(t, r) 384 | require.NoError(t, err) 385 | } 386 | 387 | func TestSetSegmentTemplateErrorNil(t *testing.T) { 388 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 389 | audioAS, _ := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP, VALID_LANG) 390 | err := audioAS.setSegmentTemplate(nil) 391 | require.NotNil(t, err) 392 | require.EqualErr(t, ErrSegmentTemplateNil, err) 393 | } 394 | 395 | func TestSetNewBaseURLVideo(t *testing.T) { 396 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 397 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 398 | 399 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 400 | 401 | err := r.SetNewBaseURL(VALID_BASE_URL_VIDEO) 402 | 403 | require.NoError(t, err) 404 | } 405 | 406 | func TestAddNewBaseURLVideo(t *testing.T) { 407 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 408 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 409 | 410 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 411 | 412 | err := r.AddNewBaseURL("./") 413 | require.NoError(t, err) 414 | 415 | err = r.AddNewBaseURL("../a/") 416 | require.NoError(t, err) 417 | 418 | err = r.AddNewBaseURL("../b/") 419 | require.NoError(t, err) 420 | 421 | require.EqualStringSlice(t, []string{"./", "../a/", "../b/"}, r.BaseURL) 422 | } 423 | 424 | func TestSetNewBaseURLSubtitle(t *testing.T) { 425 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 426 | subtitleAS, _ := m.AddNewAdaptationSetSubtitleWithID("7357", DASH_MIME_TYPE_SUBTITLE_VTT, VALID_LANG, VALID_SUBTITLE_LABEL) 427 | 428 | r, _ := subtitleAS.AddNewRepresentationSubtitle(VALID_SUBTITLE_BANDWIDTH, VALID_SUBTITLE_ID) 429 | 430 | err := r.SetNewBaseURL(VALID_SUBTITLE_URL) 431 | 432 | require.NoError(t, err) 433 | } 434 | 435 | func TestSetNewBaseURLErrorNoDASHProfile(t *testing.T) { 436 | m := &MPD{ 437 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 438 | Profiles: nil, 439 | Type: Strptr("static"), 440 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 441 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 442 | period: &Period{}, 443 | } 444 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 445 | 446 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 447 | 448 | _ = r.SetNewBaseURL(VALID_BASE_URL_VIDEO) 449 | err := m.Validate() 450 | 451 | require.NotNil(t, err) 452 | require.EqualErr(t, ErrNoDASHProfileSet, err) 453 | } 454 | 455 | func TestSetNewBaseURLErrorEmpty(t *testing.T) { 456 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 457 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 458 | 459 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 460 | 461 | err := r.SetNewBaseURL("") 462 | 463 | require.NotNil(t, err) 464 | require.EqualErr(t, ErrBaseURLEmpty, err) 465 | } 466 | 467 | func TestSetNewSegmentBase(t *testing.T) { 468 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 469 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 470 | 471 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 472 | 473 | sb, err := r.AddNewSegmentBase(VALID_INDEX_RANGE, VALID_INIT_RANGE) 474 | require.NotNil(t, sb) 475 | require.NoError(t, err) 476 | } 477 | 478 | func TestSetNewSegmentBaseErrorNoDASHProfile(t *testing.T) { 479 | m := &MPD{ 480 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 481 | Profiles: nil, 482 | Type: Strptr("static"), 483 | MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION), 484 | MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME), 485 | period: &Period{}, 486 | } 487 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 488 | 489 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 490 | 491 | _, _ = r.AddNewSegmentBase(VALID_INDEX_RANGE, VALID_INIT_RANGE) 492 | 493 | err := m.Validate() 494 | require.EqualErr(t, ErrNoDASHProfileSet, err) 495 | } 496 | 497 | func TestSetSegmentBaseErrorNil(t *testing.T) { 498 | m := NewMPD(DASH_PROFILE_ONDEMAND, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 499 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 500 | 501 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 502 | 503 | err := r.setSegmentBase(nil) 504 | require.NotNil(t, err) 505 | require.EqualErr(t, ErrSegmentBaseNil, err) 506 | } 507 | 508 | func getValidWVHeaderBytes() []byte { 509 | wvHeader, err := base64.StdEncoding.DecodeString(VALID_WV_HEADER) 510 | if err != nil { 511 | panic(err.Error()) 512 | } 513 | return wvHeader 514 | } 515 | 516 | func TestAddNewAccessibilityElement(t *testing.T) { 517 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 518 | audioAS, err := m.AddNewAdaptationSetAudioWithID("7357", DASH_MIME_TYPE_AUDIO_MP4, VALID_SEGMENT_ALIGNMENT, 519 | VALID_START_WITH_SAP, VALID_LANG) 520 | if err != nil { 521 | t.Errorf("AddNewAccessibilityElement() error adding audio adaptation set: %v", err) 522 | return 523 | } 524 | 525 | _, err = audioAS.AddNewAccessibilityElement(ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO, "1") 526 | if err != nil { 527 | t.Errorf("AddNewAccessibilityElement() error adding accessibility element: %v", err) 528 | return 529 | } 530 | 531 | if g, e := len(audioAS.AccessibilityElems), 1; g != e { 532 | t.Errorf("AddNewAccessibilityElement() wrong number of accessibility elements, got: %d, expected: %d", 533 | g, e) 534 | return 535 | } 536 | 537 | elem := audioAS.AccessibilityElems[0] 538 | 539 | require.EqualStringPtr(t, Strptr((string)(ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO)), elem.SchemeIdUri) 540 | require.EqualStringPtr(t, Strptr("1"), elem.Value) 541 | } 542 | 543 | func TestLocationWriteToString(t *testing.T) { 544 | m := &MPD{ 545 | XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), 546 | Profiles: Strptr((string)(DASH_PROFILE_LIVE)), 547 | Type: Strptr("dynamic"), 548 | AvailabilityStartTime: Strptr(VALID_AVAILABILITY_START_TIME), 549 | MinimumUpdatePeriod: Strptr(VALID_MINIMUM_UPDATE_PERIOD), 550 | PublishTime: Strptr(VALID_AVAILABILITY_START_TIME), 551 | Location: VALID_LOCATION, 552 | } 553 | 554 | got, err := m.WriteToString() 555 | require.NoError(t, err) 556 | 557 | testfixtures.CompareFixture(t, "fixtures/location.mpd", got) 558 | } 559 | 560 | func TestReadWriteIdentical(t *testing.T) { 561 | const fixtures = "fixtures/" 562 | var ( 563 | skipFixtures = map[string]struct{}{"invalid.mpd": {}} 564 | matches, err = filepath.Glob(fixtures + "*.mpd") 565 | ) 566 | require.NoError(t, err) 567 | for _, file := range matches { 568 | file := filepath.Base(file) 569 | if _, ok := skipFixtures[file]; ok { 570 | continue 571 | } 572 | t.Run(file, func(t *testing.T) { 573 | m, err := ReadFromFile(fixtures + file) 574 | require.NoError(t, err) 575 | got, err := m.WriteToString() 576 | require.NoError(t, err) 577 | testfixtures.CompareFixture(t, fixtures+file, got) 578 | }) 579 | } 580 | } 581 | 582 | func TestSetInbandEventStream(t *testing.T) { 583 | var ( 584 | err error 585 | got string 586 | ) 587 | 588 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 589 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 590 | err = videoAS.AddNewInbandEventStream(VALID_SCHEME_ID_URI, "0") 591 | 592 | require.NoError(t, err) 593 | 594 | r, _ := videoAS.AddNewRepresentationVideo(VALID_VIDEO_BITRATE, VALID_VIDEO_CODEC, VALID_VIDEO_ID, VALID_VIDEO_FRAMERATE, VALID_VIDEO_WIDTH, VALID_VIDEO_HEIGHT) 595 | err = r.AddNewInbandEventStream(VALID_SCHEME_ID_URI, "1") 596 | 597 | require.NoError(t, err) 598 | 599 | got, err = m.WriteToString() 600 | require.NoError(t, err) 601 | 602 | testfixtures.CompareFixture(t, "fixtures/inband_event_stream.mpd", got) 603 | } 604 | 605 | func TestSetInbandEventStreamError(t *testing.T) { 606 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME) 607 | videoAS, _ := m.AddNewAdaptationSetVideoWithID("7357", DASH_MIME_TYPE_VIDEO_MP4, VALID_SCAN_TYPE, VALID_SEGMENT_ALIGNMENT, VALID_START_WITH_SAP) 608 | err := videoAS.AddNewInbandEventStream("", "") 609 | 610 | require.EqualErr(t, ErrInbandEventStreamSchemeUriEmpty, err) 611 | } 612 | -------------------------------------------------------------------------------- /mpd/pssh.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | func MakePSSHBox(systemID, payload []byte) ([]byte, error) { 10 | if len(systemID) != 16 { 11 | return nil, fmt.Errorf("SystemID must be 16 bytes, was: %d", len(systemID)) 12 | } 13 | 14 | psshBuf := &bytes.Buffer{} 15 | size := uint32(12 + 16 + 4 + len(payload)) // 3 uint32s, systemID, "pssh" string and payload 16 | if err := binary.Write(psshBuf, binary.BigEndian, size); err != nil { 17 | return nil, err 18 | } 19 | 20 | if err := binary.Write(psshBuf, binary.BigEndian, []byte("pssh")); err != nil { 21 | return nil, err 22 | } 23 | 24 | if err := binary.Write(psshBuf, binary.BigEndian, uint32(0)); err != nil { 25 | return nil, err 26 | } 27 | 28 | if _, err := psshBuf.Write(systemID); err != nil { 29 | return nil, err 30 | } 31 | 32 | if err := binary.Write(psshBuf, binary.BigEndian, uint32(len(payload))); err != nil { 33 | return nil, err 34 | } 35 | 36 | if _, err := psshBuf.Write(payload); err != nil { 37 | return nil, err 38 | } 39 | 40 | return psshBuf.Bytes(), nil 41 | } 42 | -------------------------------------------------------------------------------- /mpd/pssh_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "github.com/zencoder/go-dash/v3/helpers/require" 7 | "testing" 8 | ) 9 | 10 | func TestMakePSSHBox_Widevine(t *testing.T) { 11 | expectedPSSH, err := base64.StdEncoding.DecodeString("AAAAYXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEEIARIQWr3VL1VKTyq40GH3YUJRVRoIY2FzdGxhYnMiGFdyM1ZMMVZLVHlxNDBHSDNZVUpSVlE9PTIHZGVmYXVsdA==") 12 | if err != nil { 13 | panic(err.Error()) 14 | } 15 | 16 | payload, err := base64.StdEncoding.DecodeString(VALID_WV_HEADER) 17 | if err != nil { 18 | panic(err.Error()) 19 | } 20 | 21 | wvSystemID, err := hex.DecodeString(CONTENT_PROTECTION_WIDEVINE_SCHEME_HEX) 22 | if err != nil { 23 | panic(err.Error()) 24 | } 25 | 26 | psshBox, err := MakePSSHBox(wvSystemID, payload) 27 | require.NoError(t, err) 28 | 29 | require.EqualString(t, string(expectedPSSH), string(psshBox)) 30 | } 31 | 32 | func TestMakePSSHBox_Playready(t *testing.T) { 33 | expectedPSSH, err := base64.StdEncoding.DecodeString("AAACJnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAgYGAgAAAQABAPwBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBMADkAVwA5AFcAawBwAFYASwBrACsANAAwAEcASAAzAFkAVQBKAFIAVgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEkASwB6AFkAMgBIAFoATABBAGwASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==") 34 | if err != nil { 35 | panic(err.Error()) 36 | } 37 | 38 | // Base64 PRO 39 | payload, err := base64.StdEncoding.DecodeString(VALID_PLAYREADY_PRO) 40 | if err != nil { 41 | panic(err.Error()) 42 | } 43 | 44 | wvSystemID, err := hex.DecodeString(CONTENT_PROTECTION_PLAYREADY_SCHEME_HEX) 45 | if err != nil { 46 | panic(err.Error()) 47 | } 48 | 49 | psshBox, err := MakePSSHBox(wvSystemID, payload) 50 | require.NoError(t, err) 51 | 52 | require.EqualString(t, string(expectedPSSH), string(psshBox)) 53 | } 54 | 55 | func TestMakePSSHBox_BadSystemID(t *testing.T) { 56 | _, err := MakePSSHBox([]byte("meaningless byte array"), nil) 57 | require.EqualError(t, err, "SystemID must be 16 bytes, was: 22") 58 | } 59 | 60 | func TestMakePSSHBox_NilSystemID(t *testing.T) { 61 | _, err := MakePSSHBox(nil, nil) 62 | require.EqualError(t, err, "SystemID must be 16 bytes, was: 0") 63 | } 64 | -------------------------------------------------------------------------------- /mpd/scte35.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "encoding/xml" 5 | "sort" 6 | 7 | "github.com/Comcast/scte35-go/pkg/scte35" 8 | . "github.com/zencoder/go-dash/v3/helpers/ptrs" 9 | ) 10 | 11 | const ( 12 | SCTE352014SchemeIdUri = "urn:scte:scte35:2014:xml+bin" 13 | SCTE35352016Namespace = "http://www.scte.org/schemas/35/2016" 14 | ) 15 | 16 | type Signal struct { 17 | XMLName xml.Name `xml:"Signal"` 18 | XMLNs *string `xml:"xmlns,attr,omitempty"` 19 | Namespace *string `xml:"namespace,attr,omitempty"` 20 | Binaries []Binary `xml:"Binary,omitempty"` 21 | } 22 | 23 | type Binary struct { 24 | XMLName xml.Name `xml:"Binary"` 25 | XMLNs *string `xml:"xmlns,attr,omitempty"` 26 | BinaryData *string `xml:",chardata"` 27 | } 28 | 29 | // SCTE35EventOption is used to create options to modify the scte35Break Event. 30 | type SCTE35EventOption func(scte35Break *Event) 31 | 32 | // AddNewSCTE35Break will create a new period with an empty SCTE-35 period 33 | // compliant with urn:scte:scte35:2014:xml+bin. This function accepts optional 34 | // SCTE35EventOptions to further modify the break. 35 | func (period *Period) AddNewSCTE35Break( 36 | timeScale uint, 37 | presentationTime uint64, 38 | id string, 39 | eventOptions ...SCTE35EventOption, 40 | ) { 41 | var eventStreams []EventStream 42 | if period.EventStreams != nil { 43 | eventStreams = period.EventStreams 44 | } 45 | 46 | var ( 47 | scte35EventStream EventStream 48 | eventStreamIndex = -1 49 | ) 50 | 51 | for i, eventStream := range eventStreams { 52 | if eventStream.SchemeIDURI == nil || *eventStream.SchemeIDURI != SCTE352014SchemeIdUri { 53 | continue 54 | } 55 | 56 | scte35EventStream = eventStream 57 | eventStreamIndex = i 58 | break 59 | } 60 | 61 | scte35Break := Event{ 62 | ID: Strptr(id), 63 | PresentationTime: Uint64ptr(presentationTime), 64 | } 65 | 66 | for _, eventOption := range eventOptions { 67 | eventOption(&scte35Break) 68 | } 69 | 70 | scte35EventStream.Events = append(scte35EventStream.Events, scte35Break) 71 | 72 | sort.Sort(ByPresentationTime(scte35EventStream.Events)) 73 | 74 | if eventStreamIndex != -1 { 75 | period.EventStreams[eventStreamIndex] = scte35EventStream 76 | return 77 | } 78 | 79 | scte35EventStream.SchemeIDURI = Strptr(SCTE352014SchemeIdUri) 80 | scte35EventStream.Timescale = Uintptr(timeScale) 81 | period.EventStreams = append(period.EventStreams, scte35EventStream) 82 | } 83 | 84 | func getBreakSignal(scte35Break *Event) Signal { 85 | var signal Signal 86 | if len(scte35Break.Signals) != 0 { 87 | signal = scte35Break.Signals[0] 88 | } 89 | 90 | return signal 91 | } 92 | 93 | // WithBodyBinary sets the provided body binary in the break. This will also 94 | // set the XMLNs schema to the SCTE-35 2014 Schema. 95 | func WithBodyBinary(bodyBinary string) SCTE35EventOption { 96 | return func(scte35Break *Event) { 97 | signal := getBreakSignal(scte35Break) 98 | 99 | signal.XMLNs = Strptr(SCTE352014SchemeIdUri) 100 | signal.Binaries = []Binary{ 101 | { 102 | XMLNs: Strptr(SCTE352014SchemeIdUri), 103 | BinaryData: Strptr(bodyBinary), 104 | }, 105 | } 106 | 107 | scte35Break.Signals = []Signal{signal} 108 | } 109 | } 110 | 111 | // WithNameSpace sets the provided namespace as the break's signal namespace. 112 | func WithNameSpace(nameSpace string) SCTE35EventOption { 113 | return func(scte35Break *Event) { 114 | signal := getBreakSignal(scte35Break) 115 | 116 | signal.Namespace = Strptr(nameSpace) 117 | 118 | scte35Break.Signals = []Signal{signal} 119 | } 120 | } 121 | 122 | // WithSpliceInfoSection takes the provided scte35.SpliceInfoSection, encodes it 123 | // and then sets it as the binary data for the break. This will also set the 124 | // XMLNs schema to the SCTE-35 2014 Schema. 125 | func WithSpliceInfoSection(spliceInfoSection *scte35.SpliceInfoSection) SCTE35EventOption { 126 | return func(scte35Break *Event) { 127 | signal := getBreakSignal(scte35Break) 128 | 129 | signal.XMLNs = Strptr(SCTE352014SchemeIdUri) 130 | signal.Binaries = []Binary{ 131 | { 132 | XMLNs: Strptr(SCTE352014SchemeIdUri), 133 | BinaryData: Strptr(spliceInfoSection.Base64()), 134 | }, 135 | } 136 | 137 | scte35Break.Signals = []Signal{signal} 138 | } 139 | } 140 | 141 | // WithSpliceInsertCommand creates a new scte35.SpliceInfoSection with the 142 | // splice_insert command type, and calls WithSpliceInfoSection. 143 | func WithSpliceInsertCommand() SCTE35EventOption { 144 | return func(scte35Break *Event) { 145 | spliceInfoSection := &scte35.SpliceInfoSection{ 146 | SpliceCommand: scte35.NewSpliceCommand(scte35.SpliceInsertType), 147 | } 148 | 149 | WithSpliceInfoSection(spliceInfoSection)(scte35Break) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /mpd/scte35_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/Comcast/scte35-go/pkg/scte35" 9 | "github.com/zencoder/go-dash/v3/helpers/require" 10 | ) 11 | 12 | func TestPeriod_AddNewSCTE35Break(t *testing.T) { 13 | m := NewMPD(DASH_PROFILE_LIVE, VALID_MEDIA_PRESENTATION_DURATION, VALID_MIN_BUFFER_TIME, 14 | AttrAvailabilityStartTime(VALID_AVAILABILITY_START_TIME)) 15 | require.NotNil(t, m) 16 | 17 | for i := 0; i < 3; i++ { 18 | period := m.AddNewPeriod() 19 | period.ID = strconv.Itoa(i) 20 | } 21 | 22 | m.Periods[1].AddNewSCTE35Break(VALID_EVENT_STREAM_TIMESCALE, 10000, "1") 23 | 24 | m.Periods[1].AddNewSCTE35Break(1, 20000, "3", WithNameSpace(SCTE35352016Namespace), WithBodyBinary("/DAgAAAAAAAAAP/wBQb+AABb0AAAABAAAAEAAQCA==")) 25 | m.Periods[1].AddNewSCTE35Break(1, 20001, "4", WithNameSpace(SCTE35352016Namespace), WithSpliceInsertCommand()) 26 | m.Periods[1].AddNewSCTE35Break(VALID_EVENT_STREAM_TIMESCALE, 15000, "2", WithSpliceInfoSection(&scte35.SpliceInfoSection{SpliceCommand: scte35.NewSpliceCommand(scte35.SpliceScheduleType)})) 27 | 28 | expected, err := os.ReadFile("fixtures/scte35.mpd") 29 | require.NoError(t, err) 30 | 31 | actual, err := m.WriteToString() 32 | require.NoError(t, err) 33 | 34 | require.EqualString(t, string(expected), actual) 35 | } 36 | -------------------------------------------------------------------------------- /mpd/segment.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | type SegmentBase struct { 4 | Initialization *URL `xml:"Initialization,omitempty"` 5 | RepresentationIndex *URL `xml:"RepresentationIndex,omitempty"` 6 | Timescale *uint32 `xml:"timescale,attr,omitempty"` 7 | PresentationTimeOffset *uint64 `xml:"presentationTimeOffset,attr,omitempty"` 8 | IndexRange *string `xml:"indexRange,attr,omitempty"` 9 | IndexRangeExact *bool `xml:"indexRangeExact,attr,omitempty"` 10 | AvailabilityTimeOffset *float32 `xml:"availabilityTimeOffset,attr,omitempty"` 11 | AvailabilityTimeComplete *bool `xml:"availabilityTimeComplete,attr,omitempty"` 12 | } 13 | 14 | type MultipleSegmentBase struct { 15 | SegmentBase 16 | SegmentTimeline *SegmentTimeline `xml:"SegmentTimeline,omitempty"` 17 | BitstreamSwitching *URL `xml:"BitstreamSwitching,omitempty"` 18 | Duration *uint32 `xml:"duration,attr,omitempty"` 19 | StartNumber *uint32 `xml:"startNumber,attr,omitempty"` 20 | } 21 | 22 | type SegmentList struct { 23 | MultipleSegmentBase 24 | SegmentURLs []*SegmentURL `xml:"SegmentURL,omitempty"` 25 | } 26 | 27 | type SegmentURL struct { 28 | Media *string `xml:"media,attr,omitempty"` 29 | MediaRange *string `xml:"mediaRange,attr,omitempty"` 30 | Index *string `xml:"index,attr,omitempty"` 31 | IndexRange *string `xml:"indexRange,attr,omitempty"` 32 | } 33 | 34 | type SegmentTimeline struct { 35 | Segments []*SegmentTimelineSegment `xml:"S,omitempty"` 36 | } 37 | 38 | type SegmentTimelineSegment struct { 39 | StartTime *uint64 `xml:"t,attr,omitempty"` 40 | Duration uint64 `xml:"d,attr"` 41 | RepeatCount *int `xml:"r,attr,omitempty"` 42 | } 43 | 44 | type URL struct { 45 | SourceURL *string `xml:"sourceURL,attr,omitempty"` 46 | Range *string `xml:"range,attr,omitempty"` 47 | } 48 | -------------------------------------------------------------------------------- /mpd/segment_list_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zencoder/go-dash/v3/helpers/ptrs" 7 | "github.com/zencoder/go-dash/v3/helpers/require" 8 | "github.com/zencoder/go-dash/v3/helpers/testfixtures" 9 | ) 10 | 11 | func TestSegmentListSerialization(t *testing.T) { 12 | m := getSegmentListMPD() 13 | xml, err := m.WriteToString() 14 | require.NoError(t, err) 15 | testfixtures.CompareFixture(t, "fixtures/segment_list.mpd", xml) 16 | } 17 | 18 | func TestSegmentListDeserialization(t *testing.T) { 19 | xml := testfixtures.LoadFixture("fixtures/segment_list.mpd") 20 | m, err := ReadFromString(xml) 21 | 22 | require.NoError(t, err) 23 | if err == nil { 24 | expected := getSegmentListMPD() 25 | 26 | require.EqualStringSlice(t, expected.Periods[0].BaseURL, m.Periods[0].BaseURL) 27 | 28 | expectedAudioSegList := expected.Periods[0].AdaptationSets[0].Representations[0].SegmentList 29 | audioSegList := m.Periods[0].AdaptationSets[0].Representations[0].SegmentList 30 | 31 | require.EqualUInt32(t, *expectedAudioSegList.Timescale, *audioSegList.Timescale) 32 | require.EqualUInt32(t, *expectedAudioSegList.Duration, *audioSegList.Duration) 33 | require.EqualStringPtr(t, expectedAudioSegList.Initialization.SourceURL, audioSegList.Initialization.SourceURL) 34 | require.EqualStringPtr(t, expectedAudioSegList.Initialization.Range, audioSegList.Initialization.Range) 35 | 36 | for i := range expectedAudioSegList.SegmentURLs { 37 | require.EqualStringPtr(t, expectedAudioSegList.SegmentURLs[i].Media, audioSegList.SegmentURLs[i].Media) 38 | require.EqualStringPtr(t, expectedAudioSegList.SegmentURLs[i].Index, audioSegList.SegmentURLs[i].Index) 39 | require.EqualStringPtr(t, expectedAudioSegList.SegmentURLs[i].IndexRange, audioSegList.SegmentURLs[i].IndexRange) 40 | require.EqualStringPtr(t, expectedAudioSegList.SegmentURLs[i].MediaRange, audioSegList.SegmentURLs[i].MediaRange) 41 | } 42 | 43 | expectedVideoSegList := expected.Periods[0].AdaptationSets[1].Representations[0].SegmentList 44 | videoSegList := m.Periods[0].AdaptationSets[1].Representations[0].SegmentList 45 | 46 | require.EqualUInt32(t, *expectedVideoSegList.Timescale, *videoSegList.Timescale) 47 | require.EqualUInt32(t, *expectedVideoSegList.Duration, *videoSegList.Duration) 48 | require.EqualStringPtr(t, expectedVideoSegList.Initialization.Range, videoSegList.Initialization.Range) 49 | require.EqualStringPtr(t, expectedVideoSegList.Initialization.SourceURL, videoSegList.Initialization.SourceURL) 50 | 51 | for i := range expectedVideoSegList.SegmentURLs { 52 | require.EqualStringPtr(t, expectedVideoSegList.SegmentURLs[i].Media, videoSegList.SegmentURLs[i].Media) 53 | require.EqualStringPtr(t, expectedVideoSegList.SegmentURLs[i].Index, videoSegList.SegmentURLs[i].Index) 54 | require.EqualStringPtr(t, expectedVideoSegList.SegmentURLs[i].IndexRange, videoSegList.SegmentURLs[i].IndexRange) 55 | require.EqualStringPtr(t, expectedVideoSegList.SegmentURLs[i].MediaRange, videoSegList.SegmentURLs[i].MediaRange) 56 | } 57 | } 58 | } 59 | 60 | func getSegmentListMPD() *MPD { 61 | m := NewMPD(DASH_PROFILE_LIVE, "PT30.016S", "PT2.000S") 62 | m.period.BaseURL = []string{"http://localhost:8002/dash/"} 63 | 64 | aas, _ := m.AddNewAdaptationSetAudioWithID("1", "audio/mp4", true, 1, "English") 65 | ra, _ := aas.AddNewRepresentationAudio(48000, 255000, "mp4a.40.2", "audio_1") 66 | 67 | asl := new(SegmentList) 68 | asl.Timescale = ptrs.Uint32ptr(48000) 69 | asl.Duration = ptrs.Uint32ptr(479232) 70 | asl.Initialization = &URL{SourceURL: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/dcb11457-9092-4410-b204-67b3c6d9a9e2/init.m4f")} 71 | 72 | asegs := []*SegmentURL{} 73 | asegs = append(asegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/dcb11457-9092-4410-b204-67b3c6d9a9e2/segment0.m4f")}) 74 | asegs = append(asegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/dcb11457-9092-4410-b204-67b3c6d9a9e2/segment1.m4f")}) 75 | asegs = append(asegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/dcb11457-9092-4410-b204-67b3c6d9a9e2/segment2.m4f")}) 76 | asegs = append(asegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/dcb11457-9092-4410-b204-67b3c6d9a9e2/segment3.m4f")}) 77 | asl.SegmentURLs = asegs 78 | 79 | ra.SegmentList = asl 80 | 81 | vas, _ := m.AddNewAdaptationSetVideoWithID("2", "video/mp4", "progressive", true, 1) 82 | va, _ := vas.AddNewRepresentationVideo(int64(4172274), "avc1.640028", "video_1", "30000/1001", int64(1280), int64(720)) 83 | 84 | vsl := new(SegmentList) 85 | vsl.Timescale = ptrs.Uint32ptr(30000) 86 | vsl.Duration = ptrs.Uint32ptr(225120) 87 | vsl.Initialization = &URL{SourceURL: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/f2ad47b2-5362-46e6-ad1d-dff7b10f00b8/init.m4f")} 88 | 89 | vsegs := []*SegmentURL{} 90 | vsegs = append(vsegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/f2ad47b2-5362-46e6-ad1d-dff7b10f00b8/segment0.m4f")}) 91 | vsegs = append(vsegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/f2ad47b2-5362-46e6-ad1d-dff7b10f00b8/segment1.m4f")}) 92 | vsegs = append(vsegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/f2ad47b2-5362-46e6-ad1d-dff7b10f00b8/segment2.m4f")}) 93 | vsegs = append(vsegs, &SegmentURL{Media: ptrs.Strptr("b4324d65-ad06-4735-9535-5cd4af84ebb6/f2ad47b2-5362-46e6-ad1d-dff7b10f00b8/segment3.m4f")}) 94 | vsl.SegmentURLs = vsegs 95 | 96 | va.SegmentList = vsl 97 | 98 | return m 99 | } 100 | -------------------------------------------------------------------------------- /mpd/segment_timeline_test.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | 8 | "github.com/zencoder/go-dash/v3/helpers/ptrs" 9 | "github.com/zencoder/go-dash/v3/helpers/require" 10 | "github.com/zencoder/go-dash/v3/helpers/testfixtures" 11 | ) 12 | 13 | func TestSegmentTimelineSerialization(t *testing.T) { 14 | testcases := []struct { 15 | In *MPD 16 | Out string 17 | }{ 18 | {In: getSegmentTimelineMPD(), Out: "segment_timeline.mpd"}, 19 | {In: getMultiPeriodSegmentTimelineMPD(), Out: "segment_timeline_multi_period.mpd"}, 20 | } 21 | for _, tc := range testcases { 22 | t.Run(tc.Out, func(t *testing.T) { 23 | found, err := tc.In.WriteToString() 24 | require.NoError(t, err) 25 | testfixtures.CompareFixture(t, "fixtures/"+tc.Out, found) 26 | }) 27 | } 28 | } 29 | 30 | func TestSegmentTimelineDeserialization(t *testing.T) { 31 | xml := testfixtures.LoadFixture("fixtures/segment_timeline.mpd") 32 | m, err := ReadFromString(xml) 33 | require.NoError(t, err) 34 | expected := getSegmentTimelineMPD() 35 | require.EqualStringSlice(t, expected.Periods[0].BaseURL, m.Periods[0].BaseURL) 36 | 37 | expectedAudioSegTimeline := expected.Periods[0].AdaptationSets[0].Representations[0].SegmentTemplate.SegmentTimeline 38 | audioSegTimeline := m.Periods[0].AdaptationSets[0].Representations[0].SegmentTemplate.SegmentTimeline 39 | 40 | for i := range expectedAudioSegTimeline.Segments { 41 | require.EqualUInt64(t, expectedAudioSegTimeline.Segments[i].Duration, audioSegTimeline.Segments[i].Duration) 42 | require.EqualUInt64Ptr(t, expectedAudioSegTimeline.Segments[i].StartTime, audioSegTimeline.Segments[i].StartTime) 43 | require.EqualIntPtr(t, expectedAudioSegTimeline.Segments[i].RepeatCount, audioSegTimeline.Segments[i].RepeatCount) 44 | } 45 | 46 | expectedVideoSegTimeline := expected.Periods[0].AdaptationSets[1].Representations[0].SegmentTemplate.SegmentTimeline 47 | videoSegTimeline := m.Periods[0].AdaptationSets[1].Representations[0].SegmentTemplate.SegmentTimeline 48 | 49 | for i := range expectedVideoSegTimeline.Segments { 50 | require.EqualUInt64(t, expectedVideoSegTimeline.Segments[i].Duration, videoSegTimeline.Segments[i].Duration) 51 | require.EqualUInt64Ptr(t, expectedVideoSegTimeline.Segments[i].StartTime, videoSegTimeline.Segments[i].StartTime) 52 | require.EqualIntPtr(t, expectedVideoSegTimeline.Segments[i].RepeatCount, videoSegTimeline.Segments[i].RepeatCount) 53 | } 54 | } 55 | 56 | func getMultiPeriodSegmentTimelineMPD() *MPD { 57 | m := NewMPD(DASH_PROFILE_LIVE, "PT65.063S", "PT2.000S") 58 | for i := 0; i < 4; i++ { 59 | if i > 0 { 60 | m.AddNewPeriod() 61 | } 62 | p := m.GetCurrentPeriod() 63 | p.ID = strconv.Itoa(i) 64 | p.Duration = Duration(30 * time.Second) 65 | aas, _ := p.AddNewAdaptationSetAudioWithID("1", "audio/mp4", true, 1, "en") 66 | _, _ = aas.AddNewRepresentationAudio(48000, 92000, "mp4a.40.2", "audio_1") 67 | aas.SegmentTemplate = &SegmentTemplate{ 68 | Timescale: ptrs.Int64ptr(48000), 69 | Initialization: ptrs.Strptr("audio/init.m4f"), 70 | Media: ptrs.Strptr("audio/segment$Number$.m4f"), 71 | SegmentTimeline: &SegmentTimeline{ 72 | Segments: []*SegmentTimelineSegment{ 73 | {Duration: 95232, RepeatCount: ptrs.Intptr(14)}, 74 | {Duration: 15360}, 75 | }, 76 | }, 77 | } 78 | vas, _ := p.AddNewAdaptationSetVideoWithID("2", "video/mp4", "progressive", true, 1) 79 | _, _ = vas.AddNewRepresentationVideo(3532000, "avc1.640028", "video_1", "2997/100", 2048, 854) 80 | _, _ = vas.AddNewRepresentationVideo(453000, "avc1.420016", "video_2", "2997/100", 648, 270) 81 | vas.SegmentTemplate = &SegmentTemplate{ 82 | Timescale: ptrs.Int64ptr(30000), 83 | Initialization: ptrs.Strptr("video/$RepresentationID$/init.m4f"), 84 | Media: ptrs.Strptr("video/$RepresentationID$/segment$Number$.m4f"), 85 | SegmentTimeline: &SegmentTimeline{ 86 | Segments: []*SegmentTimelineSegment{ 87 | {Duration: 58058, RepeatCount: ptrs.Intptr(14)}, 88 | {Duration: 31031}, 89 | }, 90 | }, 91 | } 92 | // Add Special flags on 3rd Period, to simulate cutting a piece of content midway 93 | if i == 2 { 94 | aas.SegmentTemplate.StartNumber = ptrs.Int64ptr(17) 95 | aas.SegmentTemplate.PresentationTimeOffset = ptrs.Uint64ptr(743424) 96 | vas.SegmentTemplate.StartNumber = ptrs.Int64ptr(17) 97 | vas.SegmentTemplate.PresentationTimeOffset = ptrs.Uint64ptr(464464) 98 | } 99 | } 100 | return m 101 | } 102 | 103 | func getSegmentTimelineMPD() *MPD { 104 | m := NewMPD(DASH_PROFILE_LIVE, "PT65.063S", "PT2.000S") 105 | m.period.BaseURL = []string{"http://localhost:8002/public/"} 106 | 107 | aas, _ := m.AddNewAdaptationSetAudioWithID("1", "audio/mp4", true, 1, "English") 108 | ra, _ := aas.AddNewRepresentationAudio(48000, 255000, "mp4a.40.2", "audio_1") 109 | 110 | ra.SegmentTemplate = &SegmentTemplate{ 111 | Timescale: ptrs.Int64ptr(48000), 112 | Initialization: ptrs.Strptr("audio/init.m4f"), 113 | Media: ptrs.Strptr("audio/segment$Number$.m4f"), 114 | SegmentTimeline: &SegmentTimeline{ 115 | Segments: []*SegmentTimelineSegment{ 116 | {StartTime: ptrs.Uint64ptr(0), Duration: 231424}, 117 | {RepeatCount: ptrs.Intptr(2), Duration: 479232}, 118 | {Duration: 10240}, 119 | {Duration: 247808}, 120 | {RepeatCount: ptrs.Intptr(1), Duration: 479232}, 121 | {Duration: 3072}, 122 | }, 123 | }, 124 | } 125 | 126 | vas, _ := m.AddNewAdaptationSetVideoWithID("2", "video/mp4", "progressive", true, 1) 127 | va, _ := vas.AddNewRepresentationVideo(int64(4172274), "avc1.640028", "video_1", "30000/1001", int64(1280), int64(720)) 128 | 129 | va.SegmentTemplate = &SegmentTemplate{ 130 | Timescale: ptrs.Int64ptr(30000), 131 | Initialization: ptrs.Strptr("video/init.m4f"), 132 | Media: ptrs.Strptr("video/segment$Number$.m4f"), 133 | SegmentTimeline: &SegmentTimeline{ 134 | Segments: []*SegmentTimelineSegment{ 135 | {StartTime: ptrs.Uint64ptr(0), Duration: 145145}, 136 | {RepeatCount: ptrs.Intptr(2), Duration: 270270}, 137 | {Duration: 91091}, 138 | {Duration: 125125}, 139 | {RepeatCount: ptrs.Intptr(1), Duration: 270270}, 140 | {Duration: 88088}, 141 | }, 142 | }, 143 | } 144 | return m 145 | } 146 | -------------------------------------------------------------------------------- /mpd/validate.go: -------------------------------------------------------------------------------- 1 | package mpd 2 | 3 | // Validate checks for incomplete MPD object 4 | func (m *MPD) Validate() error { 5 | if m.Profiles == nil { 6 | return ErrNoDASHProfileSet 7 | } 8 | return nil 9 | } 10 | --------------------------------------------------------------------------------