├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── pylint.yml │ ├── pytest.yml │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── manifests ├── aws-media-tailor-vod-personalized-response-manifest.mpd ├── bigBuckBunny-onDemend.mpd ├── bigBuckBunny-simple.mpd ├── bitmovin-sample.mpd ├── chunked-single-bitrate.mpd ├── client_manifest-720p-stereo-events.mpd ├── client_manifest-audio.mpd ├── client_manifest-ctv-events.mpd ├── client_manifest-events-multilang.mpd ├── client_manifest-events-nodvb.mpd ├── client_manifest-events.mpd ├── client_manifest-nointerlace-events.mpd ├── client_manifest-nosurround-ctv-multilang.mpd ├── client_manifest-pto_both-events.mpd ├── client_manifest.mpd └── multirate.mpd ├── pylintrc ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── setup.cfg ├── src └── mpd_parser │ ├── __init__.py │ ├── attribute_parsers.py │ ├── constants.py │ ├── exceptions.py │ ├── models │ ├── __init__.py │ ├── base_tags.py │ ├── composite_tags.py │ └── segment_tags.py │ ├── parser.py │ └── timeline_utils.py └── tests ├── __init__.py ├── conftest.py ├── test_assignment.py ├── test_attribute_parsers.py ├── test_base_tags.py ├── test_composite_tags.py └── test_manifets.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] bug oneliner here" 5 | labels: bug 6 | assignees: avishaycohen 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Setup** 14 | 1. Python version 15 | 2. mpd-parser version 16 | 3. any other system info you think is relevent 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Actual behavior** 22 | what happend instead of the expected behavior. 23 | 24 | **Stacktrace of the error** 25 | if applicable 26 | ```bash 27 | please add the python traceback here 28 | ``` 29 | 30 | **Screenshots** 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r ./requirements.txt 21 | - name: Analysing the code with pylint 22 | run: | 23 | pylint $(git ls-files -- '*.py') --fail-under 9 24 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Pytest 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r ./requirements.txt 21 | - name: Testing the code with pytest 22 | run: | 23 | cd tests 24 | pytest $(git ls-files -- '*.py') -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Setup Python 26 | uses: actions/setup-python@v4.3.0 27 | with: 28 | python-version: '3.11' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # visual studio code 132 | .vscode 133 | 134 | # Pycharm 135 | .idea 136 | 137 | # Windows python venv 138 | /Scripts/ 139 | pyvenv.cfg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 avishaycohen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpd-parser 2 | 3 | ## Installation 4 | ```shell 5 | $ python -m pip install mpd-parser 6 | ``` 7 | 8 | ## Usage 9 | ### Importing 10 | 11 | ```python 12 | from mpd_parser.parser import Parser 13 | ``` 14 | ### parse from string 15 | ```python 16 | with open("path/to/file.mpd", mode="r") as manifest_file: 17 | mpd_string = manifest_file.read() 18 | parsed_mpd = Parser.from_string(mpd_string) 19 | ``` 20 | 21 | ### parse from file 22 | ```python 23 | input_file = "path/to/file.mpd" 24 | mpd = Parser.from_file(input_file) 25 | ``` 26 | 27 | ### convert back to string 28 | ```python 29 | mpd_as_xml_string = Parser.to_string(parsed_mpd) 30 | ``` 31 | 32 | ## Overview 33 | A utility to parse mpeg dash mpd files quickly 34 | This package is heavily inspired by [mpegdash package](https://github.com/sangwonl/python-mpegdash) the main difference is that I choose to relay on lxml for parsing, and not the standard xml library. 35 | 36 | The decision to implement it with lxml is for two reasons: 37 | 1. lxml is faster then minidom 38 | 2. lxml mimics the ElementTree API which is a more pythonic approach to XMLs 39 | 40 | mpegdash package has two distinct advantages over this package: 41 | 1. it does not require third party libraries. 42 | 2. it uses the classic DOM approach to parsing XML files. it is a well known standard. 43 | 44 | ## Benchmarks 45 | TBA 46 | 47 | ## Example manifests 48 | Taken from https://ottverse.com/free-mpeg-dash-mpd-manifest-example-test-urls/ 49 | These are what I used to test and benchmark the package. 50 | 51 | ## Missing unit-tests 52 | 1. tags 53 | 2. attribute parsers 54 | 3. full manifest testing 55 | 56 | ## Contributing 57 | TBA 58 | 59 | ### Build locally 60 | ```shell 61 | python -m build 62 | ``` 63 | ### Run pylint locally 64 | I try to keep the pylint score above 9. 65 | ```shell 66 | python -m pylint ./mpd_parser/ 67 | ``` 68 | 69 | ## TODO 70 | 1. ~~finish working on periods and sub tags~~ 71 | 1. ~~periods~~ 72 | 2. ~~adapt-sets~~ 73 | 3. ~~segment bases~~ 74 | 4. ~~segment lists~~ 75 | 5. ~~segment templates~~ 76 | 6. ~~asset ids~~ 77 | 7. ~~event streams~~ 78 | 8. ~~subsets~~ 79 | 2. ~~create package locally~~ 80 | 3. ~~test it~~ 81 | 4. complete readme 82 | 1. ~~installation~~ 83 | 2. ~~usage~~ 84 | 3. Benchmarks 85 | 4. contributing 86 | 5. ~~push to github~~ 87 | 6. ~~push package to pypi~~ 88 | 7. add github actions 89 | 1. ~~pylint~~ 90 | 2. ~~pytest~~ 91 | 3. ~~build package~~ 92 | 4. ~~push package~~ 93 | 8. complete unit-tests 94 | 9. refactor tags to multiple files 95 | 10. Parsing: 96 | 1. ~~parsing from string~~ 97 | 2. ~~parsing from file~~ 98 | 3. parsing from URL 99 | 11. save mpd object: 100 | 1. ~~object to string~~ 101 | 2. object to file 102 | -------------------------------------------------------------------------------- /manifests/aws-media-tailor-vod-personalized-response-manifest.mpd: -------------------------------------------------------------------------------- 1 | 2 | https://10380e91fda5e303.mediapackage.us-west-2.amazonaws.com/out/v1/5f6a2197815e444a967f0c12f8325a11/ 3 | https://12345.mediatailor.us-west-2.amazonaws.com/v1/dashsegment/0d598fad40f42c4644d1c5b7674438772ee23b12/dash-vod-insertion/a5a7cf24-ee56-40e9-a0a2-82b483cf8650/8778696_PT0S/8778696_PT0S_0/ 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 | https://12345.mediatailor.us-west-2.amazonaws.com/v1/dashsegment/0d598fad40f42c4644d1c5b7674438772ee23b12/dash-vod-insertion/a5a7cf24-ee56-40e9-a0a2-82b483cf8650/8778696_PT0S/8778696_PT0S_1/ 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | https://12345.mediatailor.us-west-2.amazonaws.com/v1/dashsegment/0d598fad40f42c4644d1c5b7674438772ee23b12/dash-vod-insertion/a5a7cf24-ee56-40e9-a0a2-82b483cf8650/8778696_PT25S/8778696_PT25S_0/ 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | https://12345.mediatailor.us-west-2.amazonaws.com/v1/dashsegment/0d598fad40f42c4644d1c5b7674438772ee23b12/dash-vod-insertion/a5a7cf24-ee56-40e9-a0a2-82b483cf8650/8778696_PT25S/8778696_PT25S_1/ 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | -------------------------------------------------------------------------------- /manifests/bigBuckBunny-onDemend.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dashed/BigBuckBunny_1s_onDemand_2014_05_09.mpd generated by GPAC 6 | 7 | 8 | 9 | 10 | bunny_46980bps/BigBuckBunny_1snonSeg.mp4 11 | 12 | 13 | 14 | 15 | 16 | bunny_91917bps/BigBuckBunny_1snonSeg.mp4 17 | 18 | 19 | 20 | 21 | 22 | bunny_135410bps/BigBuckBunny_1snonSeg.mp4 23 | 24 | 25 | 26 | 27 | 28 | bunny_182366bps/BigBuckBunny_1snonSeg.mp4 29 | 30 | 31 | 32 | 33 | 34 | bunny_226106bps/BigBuckBunny_1snonSeg.mp4 35 | 36 | 37 | 38 | 39 | 40 | bunny_270316bps/BigBuckBunny_1snonSeg.mp4 41 | 42 | 43 | 44 | 45 | 46 | bunny_352546bps/BigBuckBunny_1snonSeg.mp4 47 | 48 | 49 | 50 | 51 | 52 | bunny_424520bps/BigBuckBunny_1snonSeg.mp4 53 | 54 | 55 | 56 | 57 | 58 | bunny_537825bps/BigBuckBunny_1snonSeg.mp4 59 | 60 | 61 | 62 | 63 | 64 | bunny_620705bps/BigBuckBunny_1snonSeg.mp4 65 | 66 | 67 | 68 | 69 | 70 | bunny_808057bps/BigBuckBunny_1snonSeg.mp4 71 | 72 | 73 | 74 | 75 | 76 | bunny_1071529bps/BigBuckBunny_1snonSeg.mp4 77 | 78 | 79 | 80 | 81 | 82 | bunny_1312787bps/BigBuckBunny_1snonSeg.mp4 83 | 84 | 85 | 86 | 87 | 88 | bunny_1662809bps/BigBuckBunny_1snonSeg.mp4 89 | 90 | 91 | 92 | 93 | 94 | bunny_2234145bps/BigBuckBunny_1snonSeg.mp4 95 | 96 | 97 | 98 | 99 | 100 | bunny_2617284bps/BigBuckBunny_1snonSeg.mp4 101 | 102 | 103 | 104 | 105 | 106 | bunny_3305118bps/BigBuckBunny_1snonSeg.mp4 107 | 108 | 109 | 110 | 111 | 112 | bunny_3841983bps/BigBuckBunny_1snonSeg.mp4 113 | 114 | 115 | 116 | 117 | 118 | bunny_4242923bps/BigBuckBunny_1snonSeg.mp4 119 | 120 | 121 | 122 | 123 | 124 | bunny_4726737bps/BigBuckBunny_1snonSeg.mp4 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /manifests/bigBuckBunny-simple.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dashed/BigBuckBunny_1s_simple_2014_05_09.mpd generated by GPAC 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 | -------------------------------------------------------------------------------- /manifests/bitmovin-sample.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /manifests/chunked-single-bitrate.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://livesim.dashif.org/livesim/sts_1645023533/sid_c2d9e37b/chunkdur_1/ato_7/testpic4_8s/ 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 | -------------------------------------------------------------------------------- /manifests/client_manifest-720p-stereo-events.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | 18 | avc3-events/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | audio/ 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /manifests/client_manifest-audio.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development - No events 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | 18 | audio/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | audio/ 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /manifests/client_manifest-ctv-events.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | 18 | avc3-events/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | audio/ 32 | 33 | 34 | 35 | 36 | 37 | 38 | audio/ 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /manifests/client_manifest-events-multilang.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | 18 | avc3-events/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | audio/ 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | audio/ 46 | 47 | 48 | 49 | 50 | 51 | 52 | audio/ 53 | 54 | 55 | 56 | 57 | 58 | 59 | audio/ 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | audio/ 68 | 69 | 70 | 71 | 72 | 73 | 74 | audio/ 75 | 76 | 77 | 78 | 79 | 80 | 81 | audio/ 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | audio/ 90 | 91 | 92 | 93 | 94 | 95 | 96 | audio/ 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /manifests/client_manifest-events-nodvb.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | 14 | 15 | 16 | 17 | avc3-events/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | audio/ 37 | 38 | 39 | 40 | 41 | 42 | 43 | audio/ 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /manifests/client_manifest-events.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | 18 | avc3-events/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | audio/ 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | audio/ 46 | 47 | 48 | 49 | 50 | 51 | 52 | audio/ 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /manifests/client_manifest-nointerlace-events.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | 18 | avc3-events/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | audio/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | audio/ 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /manifests/client_manifest-nosurround-ctv-multilang.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development - No events 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | avc3/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | audio/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | audio/ 38 | 39 | 40 | 41 | 42 | 43 | 44 | audio/ 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /manifests/client_manifest-pto_both-events.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development - with presentation time offset on both components 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | 18 | avc3-events-pto/ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | audio-pto/ 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /manifests/client_manifest.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adaptive Bitrate Test Stream from BBC Research and Development - No events 10 | BBC Research and Development 11 | British Broadcasting Corporation 2014 12 | 13 | http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/ 14 | 15 | 16 | 17 | avc3/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | audio/ 37 | 38 | 39 | 40 | 41 | 42 | 43 | audio/ 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /manifests/multirate.mpd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ../DASHed/2b/11/MultiRate.mpd generated by GPAC 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list=lxml 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist= 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | fail-on= 40 | 41 | # Specify a score threshold under which the program will exit with error. 42 | fail-under=10 43 | 44 | # Interpret the stdin as a python script, whose filename needs to be passed as 45 | # the module_or_package argument. 46 | #from-stdin= 47 | 48 | # Files or directories to be skipped. They should be base names, not paths. 49 | ignore=CVS 50 | 51 | # Add files or directories matching the regular expressions patterns to the 52 | # ignore-list. The regex matches against paths and can be in Posix or Windows 53 | # format. Because '\\' represents the directory delimiter on Windows systems, 54 | # it can't be used as an escape character. 55 | ignore-paths= 56 | 57 | # Files or directories matching the regular expression patterns are skipped. 58 | # The regex matches against base names, not paths. The default value ignores 59 | # Emacs file locks 60 | ignore-patterns=^\.# 61 | 62 | # List of module names for which member attributes should not be checked and 63 | # will not be imported (useful for modules/projects where namespaces are 64 | # manipulated during runtime and thus existing member attributes cannot be 65 | # deduced by static analysis). It supports qualified module names, as well as 66 | # Unix pattern matching. 67 | ignored-modules= 68 | 69 | # Python code to execute, usually for sys.path manipulation such as 70 | # pygtk.require(). 71 | #init-hook= 72 | 73 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 74 | # number of processors available to use, and will cap the count on Windows to 75 | # avoid hangs. 76 | jobs=2 77 | 78 | # Control the amount of potential inferred values when inferring a single 79 | # object. This can help the performance when dealing with large functions or 80 | # complex, nested conditions. 81 | limit-inference-results=100 82 | 83 | # List of plugins (as comma separated values of python module names) to load, 84 | # usually to register additional checkers. 85 | load-plugins= 86 | 87 | # Pickle collected data for later comparisons. 88 | persistent=yes 89 | 90 | # Resolve imports to .pyi stubs if available. May reduce no-member messages and 91 | # increase not-an-iterable messages. 92 | prefer-stubs=no 93 | 94 | # Minimum Python version to use for version dependent checks. Will default to 95 | # the version used to run pylint. 96 | py-version=3.11 97 | 98 | # Discover python modules and packages in the file system subtree. 99 | recursive=no 100 | 101 | # Add paths to the list of the source roots. Supports globbing patterns. The 102 | # source root is an absolute path or a path relative to the current working 103 | # directory used to determine a package namespace for modules located under the 104 | # source root. 105 | source-roots= 106 | 107 | # When enabled, pylint would attempt to guess common misconfiguration and emit 108 | # user-friendly hints instead of false-positive error messages. 109 | suggestion-mode=yes 110 | 111 | # Allow loading of arbitrary C extensions. Extensions are imported into the 112 | # active Python interpreter and may run arbitrary code. 113 | unsafe-load-any-extension=no 114 | 115 | # In verbose mode, extra non-checker-related info will be displayed. 116 | #verbose= 117 | 118 | 119 | [BASIC] 120 | 121 | # Naming style matching correct argument names. 122 | argument-naming-style=snake_case 123 | 124 | # Regular expression matching correct argument names. Overrides argument- 125 | # naming-style. If left empty, argument names will be checked with the set 126 | # naming style. 127 | #argument-rgx= 128 | 129 | # Naming style matching correct attribute names. 130 | attr-naming-style=snake_case 131 | 132 | # Regular expression matching correct attribute names. Overrides attr-naming- 133 | # style. If left empty, attribute names will be checked with the set naming 134 | # style. 135 | #attr-rgx= 136 | 137 | # Bad variable names which should always be refused, separated by a comma. 138 | bad-names=foo, 139 | bar, 140 | baz, 141 | toto, 142 | tutu, 143 | tata 144 | 145 | # Bad variable names regexes, separated by a comma. If names match any regex, 146 | # they will always be refused 147 | bad-names-rgxs= 148 | 149 | # Naming style matching correct class attribute names. 150 | class-attribute-naming-style=any 151 | 152 | # Regular expression matching correct class attribute names. Overrides class- 153 | # attribute-naming-style. If left empty, class attribute names will be checked 154 | # with the set naming style. 155 | #class-attribute-rgx= 156 | 157 | # Naming style matching correct class constant names. 158 | class-const-naming-style=UPPER_CASE 159 | 160 | # Regular expression matching correct class constant names. Overrides class- 161 | # const-naming-style. If left empty, class constant names will be checked with 162 | # the set naming style. 163 | #class-const-rgx= 164 | 165 | # Naming style matching correct class names. 166 | class-naming-style=PascalCase 167 | 168 | # Regular expression matching correct class names. Overrides class-naming- 169 | # style. If left empty, class names will be checked with the set naming style. 170 | #class-rgx= 171 | 172 | # Naming style matching correct constant names. 173 | const-naming-style=UPPER_CASE 174 | 175 | # Regular expression matching correct constant names. Overrides const-naming- 176 | # style. If left empty, constant names will be checked with the set naming 177 | # style. 178 | #const-rgx= 179 | 180 | # Minimum line length for functions/classes that require docstrings, shorter 181 | # ones are exempt. 182 | docstring-min-length=-1 183 | 184 | # Naming style matching correct function names. 185 | function-naming-style=snake_case 186 | 187 | # Regular expression matching correct function names. Overrides function- 188 | # naming-style. If left empty, function names will be checked with the set 189 | # naming style. 190 | #function-rgx= 191 | 192 | # Good variable names which should always be accepted, separated by a comma. 193 | good-names=i, 194 | j, 195 | k, 196 | ex, 197 | Run, 198 | _ 199 | 200 | # Good variable names regexes, separated by a comma. If names match any regex, 201 | # they will always be accepted 202 | good-names-rgxs= 203 | 204 | # Include a hint for the correct naming format with invalid-name. 205 | include-naming-hint=no 206 | 207 | # Naming style matching correct inline iteration names. 208 | inlinevar-naming-style=any 209 | 210 | # Regular expression matching correct inline iteration names. Overrides 211 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 212 | # with the set naming style. 213 | #inlinevar-rgx= 214 | 215 | # Naming style matching correct method names. 216 | method-naming-style=snake_case 217 | 218 | # Regular expression matching correct method names. Overrides method-naming- 219 | # style. If left empty, method names will be checked with the set naming style. 220 | #method-rgx= 221 | 222 | # Naming style matching correct module names. 223 | module-naming-style=snake_case 224 | 225 | # Regular expression matching correct module names. Overrides module-naming- 226 | # style. If left empty, module names will be checked with the set naming style. 227 | #module-rgx= 228 | 229 | # Colon-delimited sets of names that determine each other's naming style when 230 | # the name regexes allow several styles. 231 | name-group= 232 | 233 | # Regular expression which should only match function or class names that do 234 | # not require a docstring. 235 | no-docstring-rgx=^_ 236 | 237 | # List of decorators that produce properties, such as abc.abstractproperty. Add 238 | # to this list to register other decorators that produce valid properties. 239 | # These decorators are taken in consideration only for invalid-name. 240 | property-classes=abc.abstractproperty 241 | 242 | # Regular expression matching correct type alias names. If left empty, type 243 | # alias names will be checked with the set naming style. 244 | #typealias-rgx= 245 | 246 | # Regular expression matching correct type variable names. If left empty, type 247 | # variable names will be checked with the set naming style. 248 | #typevar-rgx= 249 | 250 | # Naming style matching correct variable names. 251 | variable-naming-style=snake_case 252 | 253 | # Regular expression matching correct variable names. Overrides variable- 254 | # naming-style. If left empty, variable names will be checked with the set 255 | # naming style. 256 | #variable-rgx= 257 | 258 | 259 | [CLASSES] 260 | 261 | # Warn about protected attribute access inside special methods 262 | check-protected-access-in-special-methods=no 263 | 264 | # List of method names used to declare (i.e. assign) instance attributes. 265 | defining-attr-methods=__init__, 266 | __new__, 267 | setUp, 268 | asyncSetUp, 269 | __post_init__ 270 | 271 | # List of member names, which should be excluded from the protected access 272 | # warning. 273 | exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit 274 | 275 | # List of valid names for the first argument in a class method. 276 | valid-classmethod-first-arg=cls 277 | 278 | # List of valid names for the first argument in a metaclass class method. 279 | valid-metaclass-classmethod-first-arg=mcs 280 | 281 | 282 | [DESIGN] 283 | 284 | # List of regular expressions of class ancestor names to ignore when counting 285 | # public methods (see R0903) 286 | exclude-too-few-public-methods= 287 | 288 | # List of qualified class names to ignore when counting class parents (see 289 | # R0901) 290 | ignored-parents= 291 | 292 | # Maximum number of arguments for function / method. 293 | max-args=5 294 | 295 | # Maximum number of attributes for a class (see R0902). 296 | max-attributes=7 297 | 298 | # Maximum number of boolean expressions in an if statement (see R0916). 299 | max-bool-expr=5 300 | 301 | # Maximum number of branch for function / method body. 302 | max-branches=12 303 | 304 | # Maximum number of locals for function / method body. 305 | max-locals=15 306 | 307 | # Maximum number of parents for a class (see R0901). 308 | max-parents=7 309 | 310 | # Maximum number of public methods for a class (see R0904). 311 | max-public-methods=20 312 | 313 | # Maximum number of return / yield for function / method body. 314 | max-returns=6 315 | 316 | # Maximum number of statements in function / method body. 317 | max-statements=50 318 | 319 | # Minimum number of public methods for a class (see R0903). 320 | min-public-methods=2 321 | 322 | 323 | [EXCEPTIONS] 324 | 325 | # Exceptions that will emit a warning when caught. 326 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 327 | 328 | 329 | [FORMAT] 330 | 331 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 332 | expected-line-ending-format= 333 | 334 | # Regexp for a line that is allowed to be longer than the limit. 335 | ignore-long-lines=^\s*(# )??$ 336 | 337 | # Number of spaces of indent required inside a hanging or continued line. 338 | indent-after-paren=4 339 | 340 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 341 | # tab). 342 | indent-string=' ' 343 | 344 | # Maximum number of characters on a single line. 345 | max-line-length=120 346 | 347 | # Maximum number of lines in a module. 348 | max-module-lines=1000 349 | 350 | # Allow the body of a class to be on the same line as the declaration if body 351 | # contains single statement. 352 | single-line-class-stmt=no 353 | 354 | # Allow the body of an if to be on the same line as the test if there is no 355 | # else. 356 | single-line-if-stmt=no 357 | 358 | 359 | [IMPORTS] 360 | 361 | # List of modules that can be imported at any level, not just the top level 362 | # one. 363 | allow-any-import-level= 364 | 365 | # Allow explicit reexports by alias from a package __init__. 366 | allow-reexport-from-package=no 367 | 368 | # Allow wildcard imports from modules that define __all__. 369 | allow-wildcard-with-all=no 370 | 371 | # Deprecated modules which should not be used, separated by a comma. 372 | deprecated-modules= 373 | 374 | # Output a graph (.gv or any supported image format) of external dependencies 375 | # to the given file (report RP0402 must not be disabled). 376 | ext-import-graph= 377 | 378 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 379 | # external) dependencies to the given file (report RP0402 must not be 380 | # disabled). 381 | import-graph= 382 | 383 | # Output a graph (.gv or any supported image format) of internal dependencies 384 | # to the given file (report RP0402 must not be disabled). 385 | int-import-graph= 386 | 387 | # Force import order to recognize a module as part of the standard 388 | # compatibility libraries. 389 | known-standard-library= 390 | 391 | # Force import order to recognize a module as part of a third party library. 392 | known-third-party=enchant 393 | 394 | # Couples of modules and preferred modules, separated by a comma. 395 | preferred-modules= 396 | 397 | 398 | [LOGGING] 399 | 400 | # The type of string formatting that logging methods do. `old` means using % 401 | # formatting, `new` is for `{}` formatting. 402 | logging-format-style=old 403 | 404 | # Logging modules to check that the string format arguments are in logging 405 | # function parameter format. 406 | logging-modules=logging 407 | 408 | 409 | [MESSAGES CONTROL] 410 | 411 | # Only show warnings with the listed confidence levels. Leave empty to show 412 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 413 | # UNDEFINED. 414 | confidence=HIGH, 415 | CONTROL_FLOW, 416 | INFERENCE, 417 | INFERENCE_FAILURE, 418 | UNDEFINED 419 | 420 | # Disable the message, report, category or checker with the given id(s). You 421 | # can either give multiple identifiers separated by comma (,) or put this 422 | # option multiple times (only on the command line, not in the configuration 423 | # file where it should appear only once). You can also use "--disable=all" to 424 | # disable everything first and then re-enable specific checks. For example, if 425 | # you want to run only the similarities checker, you can use "--disable=all 426 | # --enable=similarities". If you want to run only the classes checker, but have 427 | # no Warning level messages displayed, use "--disable=all --enable=classes 428 | # --disable=W". 429 | disable=raw-checker-failed, 430 | bad-inline-option, 431 | locally-disabled, 432 | file-ignored, 433 | suppressed-message, 434 | useless-suppression, 435 | deprecated-pragma, 436 | use-implicit-booleaness-not-comparison-to-string, 437 | use-implicit-booleaness-not-comparison-to-zero, 438 | use-symbolic-message-instead 439 | 440 | # Enable the message, report, category or checker with the given id(s). You can 441 | # either give multiple identifier separated by comma (,) or put this option 442 | # multiple time (only on the command line, not in the configuration file where 443 | # it should appear only once). See also the "--disable" option for examples. 444 | enable= 445 | 446 | 447 | [METHOD_ARGS] 448 | 449 | # List of qualified names (i.e., library.method) which require a timeout 450 | # parameter e.g. 'requests.api.get,requests.api.post' 451 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 452 | 453 | 454 | [MISCELLANEOUS] 455 | 456 | # List of note tags to take in consideration, separated by a comma. 457 | notes=FIXME, 458 | XXX, 459 | TODO 460 | 461 | # Regular expression of note tags to take in consideration. 462 | notes-rgx= 463 | 464 | 465 | [REFACTORING] 466 | 467 | # Maximum number of nested blocks for function / method body 468 | max-nested-blocks=5 469 | 470 | # Complete name of functions that never returns. When checking for 471 | # inconsistent-return-statements if a never returning function is called then 472 | # it will be considered as an explicit return statement and no message will be 473 | # printed. 474 | never-returning-functions=sys.exit,argparse.parse_error 475 | 476 | # Let 'consider-using-join' be raised when the separator to join on would be 477 | # non-empty (resulting in expected fixes of the type: ``"- " + " - 478 | # ".join(items)``) 479 | suggest-join-with-non-empty-separator=yes 480 | 481 | 482 | [REPORTS] 483 | 484 | # Python expression which should return a score less than or equal to 10. You 485 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 486 | # 'convention', and 'info' which contain the number of messages in each 487 | # category, as well as 'statement' which is the total number of statements 488 | # analyzed. This score is used by the global evaluation report (RP0004). 489 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 490 | 491 | # Template used to display messages. This is a python new-style format string 492 | # used to format the message information. See doc for all details. 493 | msg-template= 494 | 495 | # Set the output format. Available formats are: text, parseable, colorized, 496 | # json2 (improved json format), json (old json format) and msvs (visual 497 | # studio). You can also give a reporter class, e.g. 498 | # mypackage.mymodule.MyReporterClass. 499 | #output-format= 500 | 501 | # Tells whether to display a full report or only the messages. 502 | reports=no 503 | 504 | # Activate the evaluation score. 505 | score=yes 506 | 507 | 508 | [SIMILARITIES] 509 | 510 | # Comments are removed from the similarity computation 511 | ignore-comments=yes 512 | 513 | # Docstrings are removed from the similarity computation 514 | ignore-docstrings=yes 515 | 516 | # Imports are removed from the similarity computation 517 | ignore-imports=yes 518 | 519 | # Signatures are removed from the similarity computation 520 | ignore-signatures=yes 521 | 522 | # Minimum lines number of a similarity. 523 | min-similarity-lines=4 524 | 525 | 526 | [SPELLING] 527 | 528 | # Limits count of emitted suggestions for spelling mistakes. 529 | max-spelling-suggestions=4 530 | 531 | # Spelling dictionary name. No available dictionaries : You need to install 532 | # both the python package and the system dependency for enchant to work. 533 | spelling-dict= 534 | 535 | # List of comma separated words that should be considered directives if they 536 | # appear at the beginning of a comment and should not be checked. 537 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 538 | 539 | # List of comma separated words that should not be checked. 540 | spelling-ignore-words= 541 | 542 | # A path to a file that contains the private dictionary; one word per line. 543 | spelling-private-dict-file= 544 | 545 | # Tells whether to store unknown words to the private dictionary (see the 546 | # --spelling-private-dict-file option) instead of raising a message. 547 | spelling-store-unknown-words=no 548 | 549 | 550 | [STRING] 551 | 552 | # This flag controls whether inconsistent-quotes generates a warning when the 553 | # character used as a quote delimiter is used inconsistently within a module. 554 | check-quote-consistency=no 555 | 556 | # This flag controls whether the implicit-str-concat should generate a warning 557 | # on implicit string concatenation in sequences defined over several lines. 558 | check-str-concat-over-line-jumps=no 559 | 560 | 561 | [TYPECHECK] 562 | 563 | # List of decorators that produce context managers, such as 564 | # contextlib.contextmanager. Add to this list to register other decorators that 565 | # produce valid context managers. 566 | contextmanager-decorators=contextlib.contextmanager 567 | 568 | # List of members which are set dynamically and missed by pylint inference 569 | # system, and so shouldn't trigger E1101 when accessed. Python regular 570 | # expressions are accepted. 571 | generated-members= 572 | 573 | # Tells whether to warn about missing members when the owner of the attribute 574 | # is inferred to be None. 575 | ignore-none=yes 576 | 577 | # This flag controls whether pylint should warn about no-member and similar 578 | # checks whenever an opaque object is returned when inferring. The inference 579 | # can return multiple potential results while evaluating a Python object, but 580 | # some branches might not be evaluated, which results in partial inference. In 581 | # that case, it might be useful to still emit no-member and other checks for 582 | # the rest of the inferred objects. 583 | ignore-on-opaque-inference=yes 584 | 585 | # List of symbolic message names to ignore for Mixin members. 586 | ignored-checks-for-mixins=no-member, 587 | not-async-context-manager, 588 | not-context-manager, 589 | attribute-defined-outside-init 590 | 591 | # List of class names for which member attributes should not be checked (useful 592 | # for classes with dynamically set attributes). This supports the use of 593 | # qualified names. 594 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 595 | 596 | # Show a hint with possible names when a member name was not found. The aspect 597 | # of finding the hint is based on edit distance. 598 | missing-member-hint=yes 599 | 600 | # The minimum edit distance a name should have in order to be considered a 601 | # similar match for a missing member name. 602 | missing-member-hint-distance=1 603 | 604 | # The total number of similar names that should be taken in consideration when 605 | # showing a hint for a missing member. 606 | missing-member-max-choices=1 607 | 608 | # Regex pattern to define which classes are considered mixins. 609 | mixin-class-rgx=.*[Mm]ixin 610 | 611 | # List of decorators that change the signature of a decorated function. 612 | signature-mutators= 613 | 614 | 615 | [VARIABLES] 616 | 617 | # List of additional names supposed to be defined in builtins. Remember that 618 | # you should avoid defining new builtins when possible. 619 | additional-builtins= 620 | 621 | # Tells whether unused global variables should be treated as a violation. 622 | allow-global-unused-variables=yes 623 | 624 | # List of names allowed to shadow builtins 625 | allowed-redefined-builtins= 626 | 627 | # List of strings which can identify a callback function by name. A callback 628 | # name must start or end with one of those strings. 629 | callbacks=cb_, 630 | _cb 631 | 632 | # A regular expression matching the name of dummy variables (i.e. expected to 633 | # not be used). 634 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 635 | 636 | # Argument names that match this expression will be ignored. 637 | ignored-argument-names=_.*|^ignored_|^unused_ 638 | 639 | # Tells whether we should check for unused import in __init__ files. 640 | init-import=no 641 | 642 | # List of qualified module names which can have objects that can redefine 643 | # builtins. 644 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 645 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61.0.0", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "mpd-parser" 10 | version = "0.1.5" 11 | description = "lxml based parser for DASH manifests (mpd files)" 12 | readme = "README.md" 13 | authors = [{ name = "Avishay Cohen", email = "avishay.c@gmail.com" }] 14 | license = { file = "LICENSE" } 15 | classifiers = [ 16 | "License :: OSI Approved :: MIT License", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3", 19 | ] 20 | keywords = ["parser", "DASH", "manifest", "video", "files"] 21 | dependencies = [ 22 | "lxml", 23 | "isodate" 24 | ] 25 | requires-python = ">=3.8" 26 | 27 | [project.optional-dependencies] 28 | dev = ["pylint", "pytest", "mpegdash"] 29 | 30 | [project.urls] 31 | Homepage = "https://github.com/avishaycohen/mpd-parser" 32 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # pytest.ini 2 | [pytest] 3 | pythonpath = "src/" 4 | addopts = -rA -q 5 | testpaths = 6 | tests 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml==4.9.2 2 | pylint==2.17.0 3 | pytest==7.2.2 4 | isodate==0.7.2 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = mpd-parser 3 | version = 0.1.5 4 | author = Avishay Cohen 5 | author_email = avishay.c@gmail.com 6 | description = A parser for mpd manifest files 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/avishaycohen/mpd-parser 10 | project_urls = 11 | Bug Tracker = https://github.com/avishaycohen/mpd-parser/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: MIT License 15 | Operating System :: OS Independent 16 | 17 | [options] 18 | package_dir = 19 | = src 20 | packages = find: 21 | python_requires = >=3.8 22 | 23 | [options.packages.find] 24 | where = src -------------------------------------------------------------------------------- /src/mpd_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avishaycohen/mpd-parser/13a6b652b9e2b1984948dd52115b0bd5e813dc90/src/mpd_parser/__init__.py -------------------------------------------------------------------------------- /src/mpd_parser/attribute_parsers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for utility functions 3 | """ 4 | import math 5 | import re 6 | from typing import Optional, Type, Dict, List 7 | 8 | 9 | def organize_ns(namespace_mapping: Dict[Optional[str], str]) -> dict: 10 | """ 11 | xpath isn't compatible with None as key 12 | Args: 13 | namespace_mapping (dict): original namespace mapping 14 | 15 | Returns: 16 | dict: key values for the ns map without None as key 17 | """ 18 | new_mapping: Dict[Optional[str], str] = dict(namespace_mapping) 19 | new_mapping['ns'] = namespace_mapping[None] 20 | new_mapping.pop(None) 21 | return new_mapping 22 | 23 | 24 | def get_float_value(value: str) -> float: 25 | """ Helper to return a float from str """ 26 | if value is None: 27 | return None 28 | return float(value) if value != 'INF' else math.inf 29 | 30 | 31 | def get_bool_value(value: str) -> Optional[bool]: 32 | """ Helper to return a bool from str """ 33 | if value == 'true': 34 | return True 35 | if value == 'false': 36 | return False 37 | return None 38 | 39 | 40 | def get_int_value(value: str) -> Optional[int]: 41 | """ 42 | Helper to return int/none value 43 | this makes sure we are not returning a number when a tag/attrib 44 | was not included in the manifest file 45 | """ 46 | return value if value is None else int(value) 47 | 48 | 49 | def get_list_of_type(target_type: Type, attribute_value: str) -> List[str]: 50 | """ Helper to return a list of strings from the tag attribute """ 51 | if attribute_value is None: 52 | return [] 53 | return [target_type(item) for item in re.split(r"[, ]", attribute_value)] 54 | -------------------------------------------------------------------------------- /src/mpd_parser/constants.py: -------------------------------------------------------------------------------- 1 | """ Namespace for constants used across the lib """ 2 | 3 | # number constants 4 | DERIVED_ATTRIBUTES_CACHE_SIZE = 5 5 | ZERO_SECONDS = 0.0 6 | TWO_SECONDS = 2.0 7 | 8 | # parser constants 9 | KEYS_NOT_FOR_SETTING = ['element', 'tag_map', 'encoding'] 10 | 11 | # xpath constants 12 | LOOKUP_STR_FORMAT = './*[local-name(.) = "{target}" ]' 13 | ANCESTOR_LOOKUP_STR_FORMAT = 'ancestor::*[local-name(.) = "{target}" ][1]' 14 | -------------------------------------------------------------------------------- /src/mpd_parser/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions Module 3 | """ 4 | 5 | 6 | class UnicodeDeclaredError(Exception): 7 | """ Raised when the XML has an encoding declaration in it's manifest and the parser did not remove it """ 8 | description = "xml has encoding declaration, lxml cannot process it" 9 | 10 | class UnknownValueError(Exception): 11 | """ Raised when the XML parsing fails on unexpected issue, check error for more information """ 12 | description = "lxml failed to parse manifest, verify the input" 13 | 14 | class UnknownElementTreeParseError(Exception): 15 | """ Raised after a etree parse operation fails on an unexpected error """ 16 | description = "encountered unexpected error while using lxml parsing operation" 17 | 18 | class NoPeriodAncestorForTargetElement(Exception): 19 | """ Raised when trying to create a timeline from template without parent period """ 20 | description = "targeted segment template is not nested under a periods" 21 | -------------------------------------------------------------------------------- /src/mpd_parser/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avishaycohen/mpd-parser/13a6b652b9e2b1984948dd52115b0bd5e813dc90/src/mpd_parser/models/__init__.py -------------------------------------------------------------------------------- /src/mpd_parser/models/base_tags.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-function-docstring 2 | """ Module for the base class for tags, and other simple tags """ 3 | from functools import cached_property 4 | from typing import Any, Optional 5 | from xml.etree.ElementTree import Element 6 | 7 | from mpd_parser.attribute_parsers import get_bool_value, get_float_value, get_int_value, get_list_of_type 8 | from mpd_parser.constants import KEYS_NOT_FOR_SETTING, LOOKUP_STR_FORMAT 9 | 10 | 11 | class Tag: 12 | """Generic repr of mpd tag object""" 13 | 14 | def __init__(self, element: Element) -> None: 15 | self.element: Element = element 16 | self.tag_map: dict = {} 17 | 18 | def __setattr__(self, key: str, value: Any) -> None: 19 | """overload default setattr to make changes to the lxml element when attributes are changed by user""" 20 | super().__setattr__(key, value) 21 | # attributes that are not generated by lxml parsing 22 | if key in KEYS_NOT_FOR_SETTING: 23 | return 24 | 25 | # not an attribute, but part of the element 26 | if key == "text": 27 | self.element.text = value 28 | return 29 | 30 | # most attributes have a camel case name in DASH 31 | element_attrib_name = self.to_camel_case(key) 32 | 33 | # and those that don't, are mapped in tag_map 34 | if self.tag_map and key in self.tag_map: 35 | element_attrib_name = self.tag_map.get(key, element_attrib_name) 36 | 37 | # the value is None, remove attribute 38 | if not value: 39 | self.element.attrib.pop(element_attrib_name, None) 40 | return 41 | 42 | # list value should turn to a comma separated string 43 | if isinstance(value, list): 44 | self.element.attrib[element_attrib_name] = ",".join(map(str, value)) 45 | return 46 | 47 | self.element.attrib[element_attrib_name] = str(value) 48 | 49 | @classmethod 50 | def to_camel_case(cls, snake_case_string: str) -> str: 51 | """convert snake_case to lowerCamelCase""" 52 | lead, *follow = snake_case_string.split("_") 53 | return "".join([lead, *map(str.capitalize, follow)]) 54 | 55 | 56 | class TextTag(Tag): 57 | """A tag that uses the text member frequently""" 58 | 59 | @cached_property 60 | def text(self) -> str: 61 | return self.element.text 62 | 63 | 64 | class PSSH(Tag): 65 | """PSSH tag class""" 66 | 67 | @cached_property 68 | def pssh(self): 69 | return self.element.attrib.get("pssh") 70 | 71 | 72 | class ContentProtection(Tag): 73 | """Tag for content protection""" 74 | 75 | def __init__(self, element): 76 | super().__init__(element) 77 | self.tag_map = { 78 | "default_key_id": "default_KId", 79 | "ns2_key_id": "ns2:default_KID", 80 | "cenc_default_kid": "cenc:default_KID", 81 | } 82 | 83 | @cached_property 84 | def scheme_id_uri(self): 85 | return self.element.attrib.get("schemeIdUri") 86 | 87 | @cached_property 88 | def value(self): 89 | return self.element.attrib.get("value") 90 | 91 | @cached_property 92 | def id(self): 93 | return self.element.attrib.get("id") 94 | 95 | @cached_property 96 | def default_key_id(self): 97 | return self.element.attrib.get("default_KId") 98 | 99 | @cached_property 100 | def ns2_key_id(self): 101 | return self.element.attrib.get("ns2:default_KID") 102 | 103 | @cached_property 104 | def cenc_default_kid(self): 105 | return self.element.attrib.get("cenc:default_KID") 106 | 107 | @cached_property 108 | def pssh(self): 109 | return PSSH(self.element.attrib.get("cenc:pssh")) 110 | 111 | 112 | class Descriptor(Tag): 113 | """Reusable class for tag that have url, id and a value""" 114 | 115 | @cached_property 116 | def scheme_id_uri(self) -> str: 117 | return self.element.attrib.get("schemeIdUri") 118 | 119 | @cached_property 120 | def id(self) -> str: 121 | return self.element.attrib.get("id") 122 | 123 | @cached_property 124 | def value(self) -> str: 125 | return self.element.attrib.get("value") 126 | 127 | 128 | class UTCTiming(Descriptor): 129 | """UTC Timing information tag representation""" 130 | 131 | 132 | class AssetIdentifiers(Descriptor): 133 | """Asset Identifier tag""" 134 | 135 | 136 | class ContentComponent(Tag): 137 | """Content Compoenet tag representation""" 138 | 139 | @cached_property 140 | def id(self): 141 | return get_int_value(self.element.attrib.get("id")) 142 | 143 | @cached_property 144 | def lang(self): 145 | return self.element.attrib.get("lang") 146 | 147 | @cached_property 148 | def content_type(self): 149 | return self.element.attrib.get("contentType") 150 | 151 | @cached_property 152 | def par(self): 153 | return self.element.attrib.get("par") 154 | 155 | @cached_property 156 | def accessibilities(self): 157 | return [ 158 | Descriptor(member) 159 | for member in self.element.xpath( 160 | LOOKUP_STR_FORMAT.format(target="Accessibility") 161 | ) 162 | ] 163 | 164 | @cached_property 165 | def roles(self): 166 | return [ 167 | Descriptor(member) 168 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Role")) 169 | ] 170 | 171 | @cached_property 172 | def ratings(self): 173 | return [ 174 | Descriptor(member) 175 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Rating")) 176 | ] 177 | 178 | @cached_property 179 | def viewpoints(self): 180 | return [ 181 | Descriptor(member) 182 | for member in self.element.xpath( 183 | LOOKUP_STR_FORMAT.format(target="Viewpoint") 184 | ) 185 | ] 186 | 187 | 188 | class Title(TextTag): 189 | """Title tag representation""" 190 | 191 | 192 | class Source(TextTag): 193 | """Source tag representation""" 194 | 195 | 196 | class Location(TextTag): 197 | """Location tag representation""" 198 | 199 | 200 | class Copyright(TextTag): 201 | """Copyright tag representation""" 202 | 203 | 204 | class ProgramInfo(Tag): 205 | """Program information tag representation""" 206 | 207 | def __init__(self, element): 208 | super().__init__(element) 209 | self.tag_map = {"more_info_url": "moreInformationURL"} 210 | 211 | @cached_property 212 | def lang(self): 213 | return self.element.attrib.get("lang") 214 | 215 | @cached_property 216 | def more_info_url(self): 217 | return self.element.attrib.get("moreInformationURL") 218 | 219 | @cached_property 220 | def titles(self): 221 | return [ 222 | Title(member) 223 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Title")) 224 | ] 225 | 226 | @cached_property 227 | def sources(self): 228 | return [ 229 | Source(member) 230 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Source")) 231 | ] 232 | 233 | @cached_property 234 | def copy_rights(self): 235 | return [ 236 | Copyright(member) 237 | for member in self.element.xpath( 238 | LOOKUP_STR_FORMAT.format(target="Copyright") 239 | ) 240 | ] 241 | 242 | 243 | class BaseURL(Tag): 244 | """Base URL tag representation""" 245 | 246 | @cached_property 247 | def text(self) -> str: 248 | return self.element.text 249 | 250 | @cached_property 251 | def service_location(self) -> str: 252 | return self.element.attrib.get("serviceLocation") 253 | 254 | @cached_property 255 | def byte_range(self) -> str: 256 | return self.element.attrib.get("byteRange") 257 | 258 | @cached_property 259 | def availability_time_offset(self) -> float: 260 | return get_float_value(self.element.attrib.get("availabilityTimeOffset")) 261 | 262 | @cached_property 263 | def availability_time_complete(self) -> Optional[bool]: 264 | return get_bool_value(self.element.attrib.get("availabilityTimeComplete")) 265 | 266 | 267 | class URL(Tag): 268 | """Represent tags that have source-url and range attributes""" 269 | 270 | def __init__(self, element): 271 | super().__init__(element) 272 | self.tag_map = {"source_url": "sourceURL"} 273 | 274 | @cached_property 275 | def source_url(self): 276 | return self.element.attrib.get("sourceURL") 277 | 278 | @cached_property 279 | def range(self): 280 | return self.element.attrib.get("range") 281 | 282 | 283 | class Event(TextTag): 284 | """Single event tag""" 285 | 286 | @cached_property 287 | def message_data(self): 288 | return self.element.attrib.get("messageData") 289 | 290 | @cached_property 291 | def presentation_time(self): 292 | return int(self.element.attrib.get("presentationTime")) 293 | 294 | @cached_property 295 | def duration(self): 296 | return int(self.element.attrib.get("duration")) 297 | 298 | @cached_property 299 | def id(self): 300 | return int(self.element.attrib.get("id")) 301 | 302 | 303 | class EventStream(Descriptor): 304 | """Event Stream tag""" 305 | 306 | @cached_property 307 | def timescale(self): 308 | return self.element.attrib.get("timescale") 309 | 310 | @cached_property 311 | def events(self): 312 | return [ 313 | Event(member) 314 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Event")) 315 | ] 316 | 317 | 318 | class Subset(Tag): 319 | """Subset tag""" 320 | 321 | @cached_property 322 | def id(self): 323 | return self.element.attrib.get("id") 324 | 325 | @cached_property 326 | def contains(self): 327 | return get_list_of_type(int, self.element.attrib.get("contains")) 328 | -------------------------------------------------------------------------------- /src/mpd_parser/models/composite_tags.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-function-docstring 2 | """ Module for the compelex tags such as MPD, Period and others """ 3 | from functools import cached_property, lru_cache 4 | from xml.etree.ElementTree import Element 5 | 6 | from isodate import parse_datetime, parse_duration 7 | 8 | from mpd_parser.attribute_parsers import ( 9 | get_bool_value, 10 | get_float_value, 11 | get_int_value, 12 | get_list_of_type, 13 | organize_ns, 14 | ) 15 | from mpd_parser.constants import ( 16 | ANCESTOR_LOOKUP_STR_FORMAT, 17 | DERIVED_ATTRIBUTES_CACHE_SIZE, 18 | LOOKUP_STR_FORMAT, 19 | TWO_SECONDS, 20 | ZERO_SECONDS, 21 | ) 22 | from mpd_parser.models.base_tags import ( 23 | AssetIdentifiers, 24 | BaseURL, 25 | ContentComponent, 26 | ContentProtection, 27 | Descriptor, 28 | EventStream, 29 | Location, 30 | ProgramInfo, 31 | Subset, 32 | Tag, 33 | UTCTiming, 34 | ) 35 | from mpd_parser.models.segment_tags import MultipleSegmentBase, SegmentBase, SegmentList 36 | from mpd_parser.timeline_utils import SegmentTiming 37 | 38 | 39 | class Period(Tag): 40 | """Period class, represents a period tag in mpd manifest.""" 41 | 42 | @cached_property 43 | def id(self): 44 | return self.element.attrib.get("id") 45 | 46 | @cached_property 47 | def start(self): 48 | return self.element.attrib.get("start") 49 | 50 | @property 51 | @lru_cache(maxsize=DERIVED_ATTRIBUTES_CACHE_SIZE) 52 | def start_in_seconds(self) -> float: 53 | """Parsed and converted to seconds, 54 | Does not uses cached_property to block writes as it must be derived to maintain truthness 55 | """ 56 | return ( 57 | parse_duration(self.start).total_seconds() if self.start else ZERO_SECONDS 58 | ) 59 | 60 | @cached_property 61 | def duration(self): 62 | return self.element.attrib.get("duration") 63 | 64 | @property 65 | def duration_in_seconds(self) -> float: 66 | """Parsed and converted to seconds""" 67 | return ( 68 | parse_duration(self.duration).total_seconds() 69 | if self.duration 70 | else ZERO_SECONDS 71 | ) 72 | 73 | @cached_property 74 | def bitstream_switching(self): 75 | return get_bool_value(self.element.attrib.get("bitstreamSwitching")) 76 | 77 | @cached_property 78 | def base_urls(self): 79 | return [ 80 | BaseURL(member) 81 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="BaseURL")) 82 | ] 83 | 84 | @cached_property 85 | def segment_bases(self): 86 | return [ 87 | SegmentBase(member) 88 | for member in self.element.xpath( 89 | LOOKUP_STR_FORMAT.format(target="SegmentBase") 90 | ) 91 | ] 92 | 93 | @cached_property 94 | def segment_lists(self): 95 | return [ 96 | SegmentList(member) 97 | for member in self.element.xpath( 98 | LOOKUP_STR_FORMAT.format(target="SegmentList") 99 | ) 100 | ] 101 | 102 | @cached_property 103 | def segment_templates(self): 104 | return [ 105 | SegmentTemplate(member) 106 | for member in self.element.xpath( 107 | LOOKUP_STR_FORMAT.format(target="SegmentTemplate") 108 | ) 109 | ] 110 | 111 | @cached_property 112 | def asset_identifiers(self): 113 | return [ 114 | AssetIdentifiers(member) 115 | for member in self.element.xpath( 116 | LOOKUP_STR_FORMAT.format(target="AssetIdentifiers") 117 | ) 118 | ] 119 | 120 | @cached_property 121 | def event_streams(self): 122 | return [ 123 | EventStream(member) 124 | for member in self.element.xpath( 125 | LOOKUP_STR_FORMAT.format(target="EventStream") 126 | ) 127 | ] 128 | 129 | @cached_property 130 | def adaptation_sets(self): 131 | return [ 132 | AdaptationSet(member) 133 | for member in self.element.xpath( 134 | LOOKUP_STR_FORMAT.format(target="AdaptationSet") 135 | ) 136 | ] 137 | 138 | @cached_property 139 | def subsets(self): 140 | return [ 141 | Subset(member) 142 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Subset")) 143 | ] 144 | 145 | 146 | class MPD(Tag): # pylint: disable=too-many-public-methods 147 | """ 148 | MPD class represents the root of a mpd manifest, 149 | it is the top most tag of an XML file following the DASH format 150 | 151 | the element passed for MPD should be the root of the lxml.etree 152 | """ 153 | 154 | def __init__(self, element: Element, encoding: str = "utf-8"): 155 | super().__init__(element=element) 156 | self.encoding = encoding 157 | self.tag_map = {"cenc": "xlmns:cenc"} 158 | 159 | @cached_property 160 | def namespace(self): 161 | value = self.element.nsmap 162 | if None in self.element.nsmap.keys(): 163 | # can't use None as key in xpath 164 | value = organize_ns(self.element.nsmap) 165 | return value 166 | 167 | @cached_property 168 | def xmlns(self): 169 | return self.element.nsmap.get(None) 170 | 171 | @cached_property 172 | def id(self): 173 | return self.element.attrib.get("id") 174 | 175 | @cached_property 176 | def type(self): 177 | return self.element.attrib.get("type") 178 | 179 | @cached_property 180 | def profiles(self): 181 | return self.element.attrib.get("profiles") 182 | 183 | @cached_property 184 | def cenc(self): 185 | return self.element.attrib.get("xlmns:cenc") 186 | 187 | @cached_property 188 | def availability_start_time(self): 189 | return self.element.attrib.get("availabilityStartTime") 190 | 191 | @property 192 | @lru_cache 193 | def availability_start_time_in_seconds(self): 194 | return ( 195 | ( 196 | parse_datetime(self.availability_start_time) 197 | - parse_datetime("1970-01-01T00:00:00Z") 198 | ).total_seconds() 199 | if self.availability_start_time 200 | else None 201 | ) 202 | 203 | @cached_property 204 | def availability_end_time(self): 205 | return self.element.attrib.get("availabilityEndTime") 206 | 207 | @property 208 | @lru_cache 209 | def availability_end_time_in_seconds(self): 210 | return ( 211 | parse_duration(self.availability_end_time).total_seconds() 212 | if self.availability_end_time 213 | else None 214 | ) 215 | 216 | @cached_property 217 | def publish_time(self): 218 | return self.element.attrib.get("publishTime") 219 | 220 | @cached_property 221 | def media_presentation_duration(self): 222 | return self.element.attrib.get("mediaPresentationDuration") 223 | 224 | @cached_property 225 | def minimum_update_period(self): 226 | return self.element.attrib.get("minimumUpdatePeriod") 227 | 228 | @property 229 | @lru_cache 230 | def minimum_update_period_in_seconds(self): 231 | return ( 232 | parse_duration(self.minimum_update_period).total_seconds() 233 | if self.minimum_update_period 234 | else TWO_SECONDS # default for minimumUpdatePeriod 235 | ) 236 | 237 | @cached_property 238 | def min_buffer_time(self): 239 | return self.element.attrib.get("minBufferTime") 240 | 241 | @cached_property 242 | def time_shift_buffer_depth(self): 243 | return self.element.attrib.get("timeShiftBufferDepth") 244 | 245 | @property 246 | @lru_cache 247 | def time_shift_buffer_depth_in_seconds(self): 248 | return ( 249 | parse_duration(self.time_shift_buffer_depth).total_seconds() 250 | if self.time_shift_buffer_depth 251 | else ZERO_SECONDS 252 | ) 253 | 254 | @cached_property 255 | def suggested_presentation_delay(self): 256 | return self.element.attrib.get("suggestedPresentationDelay") 257 | 258 | @cached_property 259 | def max_segment_duration(self): 260 | return self.element.attrib.get("maxSegmentDuration") 261 | 262 | @cached_property 263 | def max_subsegment_duration(self): 264 | return self.element.attrib.get("maxSubsegmentDuration") 265 | 266 | @cached_property 267 | def program_informations(self): 268 | return [ 269 | ProgramInfo(member) 270 | for member in self.element.xpath( 271 | LOOKUP_STR_FORMAT.format(target="ProgramInformation") 272 | ) 273 | ] 274 | 275 | @cached_property 276 | def base_urls(self): 277 | return [ 278 | BaseURL(member) 279 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="BaseURL")) 280 | ] 281 | 282 | @cached_property 283 | def locations(self): 284 | return [ 285 | Location(member) 286 | for member in self.element.xpath( 287 | LOOKUP_STR_FORMAT.format(target="Location") 288 | ) 289 | ] 290 | 291 | @cached_property 292 | def utc_timings(self): 293 | return [ 294 | UTCTiming(member) 295 | for member in self.element.xpath( 296 | LOOKUP_STR_FORMAT.format(target="UTCTiming") 297 | ) 298 | ] 299 | 300 | @cached_property 301 | def periods(self): 302 | return [ 303 | Period(member) 304 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Period")) 305 | ] 306 | 307 | 308 | class SegmentTemplate(MultipleSegmentBase): 309 | """SegmentTemplate tag""" 310 | 311 | @cached_property 312 | def media(self): 313 | return self.element.attrib.get("media") 314 | 315 | @cached_property 316 | def index(self): 317 | return self.element.attrib.get("index") 318 | 319 | @cached_property 320 | def initialization(self): 321 | return self.element.attrib.get("initialization") 322 | 323 | @cached_property 324 | def bitstream_switching(self): 325 | return self.element.attrib.get("bitstreamSwitching") 326 | 327 | @property 328 | @lru_cache(maxsize=DERIVED_ATTRIBUTES_CACHE_SIZE) 329 | def parsed_segment_timeline(self): 330 | """Calculate timing information for segments based on SegmentTemplate 331 | 332 | Returns: 333 | List[SegmentTiming]: List of segment timing information. 334 | if there's no duration and timescale in the template, returns empty list []. 335 | 336 | Example: 337 | >>> template.duration = 2000 338 | >>> template.timescale = 1000 339 | >>> template.start_number = 1 340 | >>> timings = TimelineUtils.get_segment_timing(template, 0) 341 | >>> print(timings[0].duration) # 2.0 342 | >>> print(timings[0].start_time) # 0.0 343 | """ 344 | segments = [] 345 | if not self.duration or not self.timescale: 346 | return segments 347 | 348 | # Get segment duration in seconds 349 | segment_duration = self.duration / self.timescale 350 | 351 | # Get relevant variables 352 | start_number = self.start_number or 1 353 | period_ancestor = None 354 | manifest_mpd = None 355 | try: 356 | period_ancestor = Period( 357 | self.element.xpath(ANCESTOR_LOOKUP_STR_FORMAT.format(target="Period"))[ 358 | 0 359 | ] 360 | ) 361 | except IndexError: 362 | pass 363 | 364 | try: 365 | manifest_mpd = MPD( 366 | self.element.xpath(ANCESTOR_LOOKUP_STR_FORMAT.format(target="MPD"))[0] 367 | ) 368 | except IndexError: 369 | pass 370 | 371 | # Calculate segment count based on context 372 | if period_ancestor and period_ancestor.duration_in_seconds: 373 | # VOD content - use period duration 374 | segment_count = int(period_ancestor.duration_in_seconds / segment_duration) 375 | elif ( 376 | manifest_mpd 377 | and manifest_mpd.availability_start_time 378 | and manifest_mpd.time_shift_buffer_depth 379 | ): 380 | # Live content - use time shift buffer if available 381 | time_shift_buffer = manifest_mpd.time_shift_buffer_depth_in_seconds 382 | segment_count = int(time_shift_buffer / segment_duration) 383 | else: 384 | # Dynamic content - use MPD update period 385 | update_period = manifest_mpd.minimum_update_period_in_seconds 386 | segment_count = int(update_period / segment_duration) 387 | 388 | current_time = period_ancestor.start_in_seconds or 0 389 | for i in range(start_number, start_number + segment_count): 390 | segment = SegmentTiming( 391 | start_time=current_time, duration=segment_duration, number=i 392 | ) 393 | 394 | if manifest_mpd.availability_start_time: 395 | segment.availability_start = ( 396 | manifest_mpd.availability_start_time_in_seconds + current_time 397 | ) 398 | segment.availability_end = segment.availability_start + segment_duration 399 | 400 | segments.append(segment) 401 | current_time += segment_duration 402 | 403 | return segments 404 | 405 | 406 | class RepresentationBase(Tag): # pylint: disable=too-many-public-methods 407 | """Generic representation tag""" 408 | 409 | def __init__(self, element): 410 | super().__init__(element) 411 | self.tag_map = { 412 | "maximum_sap_period": "maximumSAPPeriod", 413 | "start_with_sap": "startWithSAP", 414 | } 415 | 416 | @cached_property 417 | def profile(self): 418 | return self.element.attrib.get("profile") 419 | 420 | @cached_property 421 | def profiles(self): 422 | return self.element.attrib.get("profiles") 423 | 424 | @cached_property 425 | def width(self): 426 | return get_int_value(self.element.attrib.get("width")) 427 | 428 | @cached_property 429 | def height(self): 430 | return get_int_value(self.element.attrib.get("height")) 431 | 432 | @cached_property 433 | def sar(self): 434 | return self.element.attrib.get("sar") 435 | 436 | @cached_property 437 | def frame_rate(self): 438 | return self.element.attrib.get("frameRate") 439 | 440 | @cached_property 441 | def audio_sampling_rate(self): 442 | return self.element.attrib.get("audioSamplingRate") 443 | 444 | @cached_property 445 | def mime_type(self): 446 | return self.element.attrib.get("mimeType") 447 | 448 | @cached_property 449 | def segment_profiles(self): 450 | return self.element.attrib.get("segmentProfiles") 451 | 452 | @cached_property 453 | def codecs(self): 454 | return self.element.attrib.get("codecs") 455 | 456 | @cached_property 457 | def maximum_sap_period(self): 458 | return get_float_value(self.element.attrib.get("maximumSAPPeriod")) 459 | 460 | @cached_property 461 | def start_with_sap(self): 462 | return get_int_value(self.element.attrib.get("startWithSAP")) 463 | 464 | @cached_property 465 | def max_playout_rate(self): 466 | return get_float_value(self.element.attrib.get("maxPlayoutRate")) 467 | 468 | @cached_property 469 | def coding_dependency(self): 470 | return get_bool_value(self.element.attrib.get("codingDependency")) 471 | 472 | @cached_property 473 | def scan_type(self): 474 | return self.element.attrib.get("scanType") 475 | 476 | @cached_property 477 | def frame_packings(self): 478 | return [ 479 | Descriptor(member) 480 | for member in self.element.xpath( 481 | LOOKUP_STR_FORMAT.format(target="FramePacking") 482 | ) 483 | ] 484 | 485 | @cached_property 486 | def audio_channel_configurations(self): 487 | return [ 488 | Descriptor(member) 489 | for member in self.element.xpath( 490 | LOOKUP_STR_FORMAT.format(target="AudioChannelConfiguration") 491 | ) 492 | ] 493 | 494 | @cached_property 495 | def content_protections(self): 496 | return [ 497 | ContentProtection(member) 498 | for member in self.element.xpath( 499 | LOOKUP_STR_FORMAT.format(target="ContentProtection") 500 | ) 501 | ] 502 | 503 | @cached_property 504 | def essential_properties(self): 505 | return [ 506 | Descriptor(member) 507 | for member in self.element.xpath( 508 | LOOKUP_STR_FORMAT.format(target="EssentialProperty") 509 | ) 510 | ] 511 | 512 | @cached_property 513 | def supplemental_properties(self): 514 | return [ 515 | Descriptor(member) 516 | for member in self.element.xpath( 517 | LOOKUP_STR_FORMAT.format(target="SupplementalProperty") 518 | ) 519 | ] 520 | 521 | @cached_property 522 | def inband_event_stream(self): 523 | return [ 524 | Descriptor(member) 525 | for member in self.element.xpath( 526 | LOOKUP_STR_FORMAT.format(target="InbandEventStream") 527 | ) 528 | ] 529 | 530 | 531 | class SubRepresentation(RepresentationBase): 532 | """A sub representation tag""" 533 | 534 | @cached_property 535 | def level(self): 536 | return get_int_value(self.element.attrib.get("level")) 537 | 538 | @cached_property 539 | def bandwidth(self): 540 | return get_int_value(self.element.attrib.get("bandwidth")) 541 | 542 | @cached_property 543 | def dependency_level(self): 544 | return get_list_of_type(int, self.element.attrib.get("dependencyLevel")) 545 | 546 | @cached_property 547 | def content_component(self): 548 | return get_list_of_type(str, self.element.attrib.get("contentComponent")) 549 | 550 | 551 | class Representation(RepresentationBase): 552 | """Representation tag""" 553 | 554 | @cached_property 555 | def id(self): 556 | return self.element.attrib.get("id") 557 | 558 | @cached_property 559 | def bandwidth(self): 560 | return get_int_value(self.element.attrib.get("bandwidth")) 561 | 562 | @cached_property 563 | def quality_ranking(self): 564 | return get_int_value(self.element.attrib.get("qualityRanking")) 565 | 566 | @cached_property 567 | def dependency_id(self): 568 | return get_list_of_type(str, self.element.attrib.get("dependencyId")) 569 | 570 | @cached_property 571 | def num_channels(self): 572 | return get_int_value(self.element.attrib.get("numChannels")) 573 | 574 | @cached_property 575 | def sample_rate(self): 576 | return get_int_value(self.element.attrib.get("sampleRate")) 577 | 578 | @cached_property 579 | def base_urls(self): 580 | return [ 581 | BaseURL(member) 582 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="BaseURL")) 583 | ] 584 | 585 | @cached_property 586 | def segment_bases(self): 587 | return [ 588 | SegmentBase(member) 589 | for member in self.element.xpath( 590 | LOOKUP_STR_FORMAT.format(target="SegmentBase") 591 | ) 592 | ] 593 | 594 | @cached_property 595 | def segment_lists(self): 596 | return [ 597 | SegmentList(member) 598 | for member in self.element.xpath( 599 | LOOKUP_STR_FORMAT.format(target="SegmentList") 600 | ) 601 | ] 602 | 603 | @cached_property 604 | def segment_templates(self): 605 | return [ 606 | SegmentTemplate(member) 607 | for member in self.element.xpath( 608 | LOOKUP_STR_FORMAT.format(target="SegmentTemplate") 609 | ) 610 | ] 611 | 612 | @cached_property 613 | def sub_representations(self): 614 | return [ 615 | SubRepresentation(member) 616 | for member in self.element.xpath( 617 | LOOKUP_STR_FORMAT.format(target="SubRepresentation") 618 | ) 619 | ] 620 | 621 | 622 | class AdaptationSet(RepresentationBase): # pylint: disable=too-many-public-methods 623 | """Adaptation Set tag representation""" 624 | 625 | def __init__(self, element): 626 | super().__init__(element) 627 | self.tag_map = {"subsegment_starts_with_sap": "subsegmentStartsWithSAP"} 628 | 629 | @cached_property 630 | def id(self): 631 | return get_int_value(self.element.attrib.get("id")) 632 | 633 | @cached_property 634 | def group(self): 635 | return get_int_value(self.element.attrib.get("group")) 636 | 637 | @cached_property 638 | def lang(self): 639 | return self.element.attrib.get("lang") 640 | 641 | @cached_property 642 | def label(self): 643 | return self.element.attrib.get("label") 644 | 645 | @cached_property 646 | def content_type(self): 647 | return self.element.attrib.get("contentType") 648 | 649 | @cached_property 650 | def par(self): 651 | return self.element.attrib.get("par") 652 | 653 | @cached_property 654 | def min_bandwidth(self): 655 | return get_int_value(self.element.attrib.get("minBandwidth")) 656 | 657 | @cached_property 658 | def max_bandwidth(self): 659 | return get_int_value(self.element.attrib.get("maxBandwidth")) 660 | 661 | @cached_property 662 | def min_width(self): 663 | return get_int_value(self.element.attrib.get("minWidth")) 664 | 665 | @cached_property 666 | def max_width(self): 667 | return get_int_value(self.element.attrib.get("maxWidth")) 668 | 669 | @cached_property 670 | def min_height(self): 671 | return get_int_value(self.element.attrib.get("minHeight")) 672 | 673 | @cached_property 674 | def max_height(self): 675 | return get_int_value(self.element.attrib.get("maxHeight")) 676 | 677 | @cached_property 678 | def min_frame_rate(self): 679 | return self.element.attrib.get("minFrameRate") 680 | 681 | @cached_property 682 | def max_frame_rate(self): 683 | return self.element.attrib.get("maxFrameRate") 684 | 685 | @cached_property 686 | def segment_alignment(self): 687 | return get_bool_value(self.element.attrib.get("segmentAlignment")) 688 | 689 | @cached_property 690 | def selection_priority(self): 691 | return get_int_value(self.element.attrib.get("selectionPriority")) 692 | 693 | @cached_property 694 | def subsegment_starts_with_sap(self): 695 | return get_int_value(self.element.attrib.get("subsegmentStartsWithSAP")) 696 | 697 | @cached_property 698 | def subsegment_alignment(self): 699 | return get_bool_value(self.element.attrib.get("subsegmentAlignment")) 700 | 701 | @cached_property 702 | def bitstream_switching(self): 703 | return get_bool_value(self.element.attrib.get("bitstreamSwitching")) 704 | 705 | @cached_property 706 | def accessibilities(self): 707 | return [ 708 | Descriptor(member) 709 | for member in self.element.xpath( 710 | LOOKUP_STR_FORMAT.format(target="Accessibility") 711 | ) 712 | ] 713 | 714 | @cached_property 715 | def roles(self): 716 | return [ 717 | Descriptor(member) 718 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Role")) 719 | ] 720 | 721 | @cached_property 722 | def ratings(self): 723 | return [ 724 | Descriptor(member) 725 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="Rating")) 726 | ] 727 | 728 | @cached_property 729 | def viewpoints(self): 730 | return [ 731 | Descriptor(member) 732 | for member in self.element.xpath( 733 | LOOKUP_STR_FORMAT.format(target="Viewpoint") 734 | ) 735 | ] 736 | 737 | @cached_property 738 | def content_components(self): 739 | return [ 740 | ContentComponent(member) 741 | for member in self.element.xpath( 742 | LOOKUP_STR_FORMAT.format(target="ContentComponent") 743 | ) 744 | ] 745 | 746 | @cached_property 747 | def base_urls(self): 748 | return [ 749 | BaseURL(member) 750 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="BaseURL")) 751 | ] 752 | 753 | @cached_property 754 | def segment_bases(self): 755 | return [ 756 | SegmentBase(member) 757 | for member in self.element.xpath( 758 | LOOKUP_STR_FORMAT.format(target="SegmentBase") 759 | ) 760 | ] 761 | 762 | @cached_property 763 | def segment_lists(self): 764 | return [ 765 | SegmentList(member) 766 | for member in self.element.xpath( 767 | LOOKUP_STR_FORMAT.format(target="SegmentList") 768 | ) 769 | ] 770 | 771 | @cached_property 772 | def segment_templates(self): 773 | return [ 774 | SegmentTemplate(member) 775 | for member in self.element.xpath( 776 | LOOKUP_STR_FORMAT.format(target="SegmentTemplate") 777 | ) 778 | ] 779 | 780 | @cached_property 781 | def representations(self): 782 | return [ 783 | Representation(member) 784 | for member in self.element.xpath( 785 | LOOKUP_STR_FORMAT.format(target="Representation") 786 | ) 787 | ] 788 | -------------------------------------------------------------------------------- /src/mpd_parser/models/segment_tags.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-function-docstring 2 | """ Segment and timeline related tags """ 3 | from functools import cached_property 4 | from mpd_parser.attribute_parsers import get_bool_value, get_float_value, get_int_value 5 | from mpd_parser.constants import LOOKUP_STR_FORMAT 6 | from mpd_parser.models.base_tags import URL, Tag 7 | 8 | 9 | class Initialization(URL): 10 | """Initialization tag representation""" 11 | 12 | 13 | class RepresentationIndex(URL): 14 | """Representation Index tag representation""" 15 | 16 | 17 | class BitstreamSwitchings(URL): 18 | """BitstreamSwitching tag representation""" 19 | 20 | 21 | class Segment(Tag): 22 | """S tag representation. A single segment of video""" 23 | 24 | @cached_property 25 | def t(self): 26 | """starting time of the segment""" 27 | return get_int_value(self.element.attrib.get("t")) 28 | 29 | @cached_property 30 | def d(self): 31 | """duration time of the segmeent""" 32 | return get_int_value(self.element.attrib.get("d")) 33 | 34 | @cached_property 35 | def r(self): 36 | """number of repeating segments""" 37 | return get_int_value(self.element.attrib.get("r")) 38 | 39 | 40 | class SegmentBase(Tag): 41 | """Basic Segment tag representation""" 42 | 43 | @cached_property 44 | def timescale(self): 45 | return get_int_value(self.element.attrib.get("timescale")) 46 | 47 | @cached_property 48 | def index_range(self): 49 | return self.element.attrib.get("indexRange") 50 | 51 | @cached_property 52 | def index_range_exact(self): 53 | return get_bool_value(self.element.attrib.get("indexRangeExact")) 54 | 55 | @cached_property 56 | def presentation_time_offset(self): 57 | return get_int_value(self.element.attrib.get("presentationTimeOffset")) 58 | 59 | @cached_property 60 | def availability_time_offset(self): 61 | return get_float_value(self.element.attrib.get("availabilityTimeOffset")) 62 | 63 | @cached_property 64 | def availability_time_complete(self): 65 | return get_bool_value(self.element.attrib.get("availabilityTimeComplete")) 66 | 67 | @cached_property 68 | def initializations(self): 69 | return [ 70 | Initialization(member) 71 | for member in self.element.xpath( 72 | LOOKUP_STR_FORMAT.format(target="Initialization") 73 | ) 74 | ] 75 | 76 | @cached_property 77 | def representation_indexes(self): 78 | return [ 79 | RepresentationIndex(member) 80 | for member in self.element.xpath( 81 | LOOKUP_STR_FORMAT.format(target="RepresentationIndex") 82 | ) 83 | ] 84 | 85 | class MultipleSegmentBase(SegmentBase): 86 | """Multiple segments tag""" 87 | 88 | @cached_property 89 | def duration(self): 90 | return get_int_value(self.element.attrib.get("duration")) 91 | 92 | @cached_property 93 | def start_number(self): 94 | return get_int_value(self.element.attrib.get("startNumber")) 95 | 96 | @cached_property 97 | def segment_timelines(self): 98 | return [ 99 | SegmentTimeline(member) 100 | for member in self.element.xpath( 101 | LOOKUP_STR_FORMAT.format(target="SegmentTimeline") 102 | ) 103 | ] 104 | 105 | @cached_property 106 | def bitstream_switchings(self): 107 | return [ 108 | BitstreamSwitchings(member) 109 | for member in self.element.xpath( 110 | './*[local-name(.) = "BitstreamSwitching" ]' 111 | ) 112 | ] 113 | 114 | 115 | class SegmentURL(Tag): 116 | """SegmentURL tag""" 117 | 118 | @cached_property 119 | def media(self): 120 | return self.element.attrib.get("media") 121 | 122 | @cached_property 123 | def media_range(self): 124 | return self.element.attrib.get("mediaRange") 125 | 126 | @cached_property 127 | def index(self): 128 | return self.element.attrib.get("index") 129 | 130 | @cached_property 131 | def index_range(self): 132 | return self.element.attrib.get("indexRange") 133 | 134 | 135 | class SegmentList(MultipleSegmentBase): 136 | """SegmentList tag""" 137 | 138 | @cached_property 139 | def segment_urls(self): 140 | return [ 141 | SegmentURL(member) 142 | for member in self.element.xpath( 143 | LOOKUP_STR_FORMAT.format(target="SegmentURL") 144 | ) 145 | ] 146 | 147 | class SegmentTimeline(Tag): 148 | """SegmentTimeline tag repr""" 149 | 150 | @cached_property 151 | def segments(self): 152 | return [ 153 | Segment(member) 154 | for member in self.element.xpath(LOOKUP_STR_FORMAT.format(target="S")) 155 | ] 156 | -------------------------------------------------------------------------------- /src/mpd_parser/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main module of the package, Parser class 3 | """ 4 | 5 | import logging 6 | from re import Match, sub 7 | from urllib.request import urlopen 8 | 9 | from lxml import etree 10 | 11 | from mpd_parser.exceptions import UnicodeDeclaredError, UnknownElementTreeParseError, UnknownValueError 12 | from mpd_parser.models.composite_tags import MPD 13 | 14 | # module level logger, application will configure formatting and handlers 15 | logger = logging.getLogger(__name__) 16 | 17 | # Regular expression to match encoding declaration in XML 18 | ENCODING_PATTERN = r"<\?.*?\s(encoding=\"\S*\").*\?>" 19 | 20 | 21 | class Parser: 22 | """ 23 | Parser class, holds factories to work with manifest files. 24 | can parse: 25 | 1. from_string 26 | 2. from_file 27 | 3. from_url 28 | """ 29 | 30 | @classmethod 31 | def from_string(cls, manifest_as_string: str) -> MPD: 32 | """generate a parsed mpd object from a given string 33 | 34 | Args: 35 | manifest_as_string (str): string repr of a manifest file. 36 | 37 | Returns: 38 | an object representing the MPD tag and all it's XML goodies 39 | """ 40 | # remove encoding declaration from manifest if exist 41 | encoding = [] 42 | if "encoding" in manifest_as_string: 43 | 44 | def cut_and_burn(match: Match) -> str: 45 | """Helper to save the removed encoding""" 46 | encoding.append(match) 47 | return "" 48 | 49 | manifest_as_string = sub(ENCODING_PATTERN, cut_and_burn, manifest_as_string) 50 | try: 51 | root = etree.fromstring(manifest_as_string) 52 | except ValueError as err: 53 | if "Unicode" in err.args[0]: 54 | raise UnicodeDeclaredError() from err 55 | logger.exception("Failed to parse manifest string") 56 | raise UnknownValueError() from err 57 | except Exception as err: 58 | logger.exception("Failed to parse manifest string") 59 | raise UnknownElementTreeParseError() from err 60 | if encoding: 61 | return MPD(root, encoding=encoding[0].groups()[0]) 62 | return MPD(root) 63 | 64 | @classmethod 65 | def from_file(cls, manifest_file_name: str) -> MPD: 66 | """ 67 | Generate a parsed mpd object from a given file name 68 | Args: 69 | manifest_file_name (str): file name to parse 70 | 71 | Returns: 72 | an object representing the MPD tag and all it's XML goodies 73 | """ 74 | try: 75 | tree = etree.parse(manifest_file_name) 76 | except ValueError as err: 77 | if "Unicode" in err.args[0]: 78 | raise UnicodeDeclaredError() from err 79 | logger.exception("Failed to parse manifest file %s", manifest_file_name) 80 | raise UnknownValueError() from err 81 | except Exception as err: 82 | logger.exception("Failed to parse manifest file %s", manifest_file_name) 83 | raise UnknownElementTreeParseError() from err 84 | return MPD(tree.getroot()) 85 | 86 | @classmethod 87 | def from_url(cls, url: str) -> MPD: 88 | """ 89 | Generate a parsed mpd object from a given URL 90 | Args: 91 | url (str): the url of the file to parse 92 | 93 | Returns: 94 | an object representing the MPD tag and all it's XML goodies 95 | """ 96 | try: 97 | with urlopen(url) as manifest_file: 98 | tree = etree.parse(manifest_file) 99 | except ValueError as err: 100 | if "Unicode" in err.args[0]: 101 | raise UnicodeDeclaredError() from err 102 | logger.exception("Failed to parse manifest from URL %s", url) 103 | raise UnknownValueError() from err 104 | except Exception as err: 105 | logger.exception("Failed to parse manifest from URL %s", url) 106 | raise UnknownElementTreeParseError() from err 107 | return MPD(tree.getroot()) 108 | 109 | @classmethod 110 | def to_string(cls, mpd: MPD) -> str: 111 | """generate a string xml from a given MPD tag object 112 | 113 | Args: 114 | mpd: MPD object created by one of the parser factories 115 | Returns: 116 | a string representation of the MPD object, xml formatted dash mpeg manifest 117 | """ 118 | return etree.tostring(mpd.element).decode("utf-8") 119 | -------------------------------------------------------------------------------- /src/mpd_parser/timeline_utils.py: -------------------------------------------------------------------------------- 1 | """ Utilities to parse, compute and handle time and duration related information in the mpd file """ 2 | 3 | from datetime import datetime 4 | from typing import Optional 5 | from dataclasses import dataclass 6 | 7 | 8 | @dataclass 9 | class SegmentTiming: 10 | """Represents timing information for a segment 11 | It is a parsed and extended version of `tags.Segment` class. 12 | """ 13 | 14 | start_time: float # in seconds from period start 15 | duration: float # in seconds 16 | number: int # segment number in sequence 17 | availability_start: Optional[datetime] = None # for live streams 18 | availability_end: Optional[datetime] = None # for live streams 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avishaycohen/mpd-parser/13a6b652b9e2b1984948dd52115b0bd5e813dc90/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conftest module for package testing 3 | """ 4 | 5 | from typing import Any 6 | 7 | from mpd_parser.models.base_tags import Tag 8 | 9 | MANIFESTS_DIR = "./../manifests/" 10 | 11 | 12 | def touch_attributes(obj: Any, verbose: bool = False): 13 | """ this function go over an object attributes and tries to see that they are populated """ 14 | attrib_list = [a for a in dir(obj) if not a.startswith('__')] 15 | for attrib in attrib_list: 16 | if getattr(obj, attrib) is None: 17 | continue 18 | if not isinstance(getattr(obj, attrib), list): 19 | print(f'{attrib}: {getattr(obj, attrib)}') 20 | continue 21 | for item in getattr(obj, attrib): 22 | if isinstance(item, Tag): 23 | touch_attributes(item) 24 | continue 25 | print(f'{attrib}: {getattr(obj, attrib) if verbose else "object"}') 26 | -------------------------------------------------------------------------------- /tests/test_assignment.py: -------------------------------------------------------------------------------- 1 | """ Test module for assignment functionallity """ 2 | 3 | import pytest 4 | from lxml import etree 5 | 6 | from mpd_parser.models.base_tags import Subset 7 | from mpd_parser.parser import Parser 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "input_file", 12 | [ 13 | "./../manifests/bigBuckBunny-onDemend.mpd", 14 | ], 15 | ) 16 | class TestTag: 17 | """generic test class for a tag""" 18 | 19 | 20 | class TestMPD(TestTag): 21 | """test attribute assignment for MPD tag""" 22 | 23 | @staticmethod 24 | def test_min_buffer_time(input_file): 25 | """test changes to min_buffer_time attribute""" 26 | with open(input_file, mode="r", encoding="UTF-8") as manifest_file: 27 | mpd_string = manifest_file.read() 28 | mpd = Parser.from_string(mpd_string) 29 | orig_value = mpd.min_buffer_time 30 | mpd.min_buffer_time = "something else" 31 | assert mpd.min_buffer_time != orig_value 32 | assert mpd.element.attrib["minBufferTime"] == "something else" 33 | 34 | 35 | class TestProgramInfo(TestTag): 36 | """test class for program information tag assignment""" 37 | 38 | @staticmethod 39 | def test_more_info_url(input_file): 40 | """this test an existing attrib value being changed""" 41 | with open(input_file, mode="r", encoding="UTF-8") as manifest_file: 42 | mpd_string = manifest_file.read() 43 | mpd = Parser.from_string(mpd_string) 44 | prog_info_list = mpd.program_informations 45 | orig_value = prog_info_list[0].more_info_url 46 | prog_info_list[0].more_info_url = "best-urls" 47 | assert prog_info_list[0].more_info_url != orig_value 48 | assert prog_info_list[0].element.attrib["moreInformationURL"] == "best-urls" 49 | assert ( 50 | mpd.program_informations[0].element.attrib["moreInformationURL"] 51 | == "best-urls" 52 | ) 53 | 54 | @staticmethod 55 | def test_lang(input_file): 56 | """this test a new attrib that is being set""" 57 | with open(input_file, mode="r", encoding="UTF-8") as manifest_file: 58 | mpd_string = manifest_file.read() 59 | mpd = Parser.from_string(mpd_string) 60 | prog_info_list = mpd.program_informations 61 | orig_value = prog_info_list[0].lang 62 | prog_info_list[0].lang = "eng" 63 | assert prog_info_list[0].lang != orig_value 64 | assert prog_info_list[0].element.attrib["lang"] == "eng" 65 | assert mpd.program_informations[0].element.attrib["lang"] == "eng" 66 | 67 | @staticmethod 68 | def test_titles(input_file): 69 | """test text value change""" 70 | with open(input_file, mode="r", encoding="UTF-8") as manifest_file: 71 | mpd_string = manifest_file.read() 72 | mpd = Parser.from_string(mpd_string) 73 | prog_info_list = mpd.program_informations 74 | title_list = prog_info_list[0].titles 75 | first_title = title_list[0] 76 | orig_value = first_title.text 77 | first_title.text = "new-text" 78 | assert first_title.text != orig_value 79 | assert first_title.element.text == "new-text" 80 | assert mpd.program_informations[0].titles[0].text == "new-text" 81 | 82 | 83 | class TestListValueAssignment: 84 | """test changing an attribute with a list type""" 85 | 86 | @staticmethod 87 | def test_list_value_assignment(): 88 | """test changing an attribute with a list type""" 89 | subset_xml = '' 90 | element = etree.fromstring(subset_xml) 91 | subset = Subset(element) 92 | assert subset.contains == [100, 101] 93 | subset.contains = [1, 2] 94 | assert subset.element.attrib["contains"] == "1,2" 95 | 96 | @staticmethod 97 | def test_list_remove_value_assignment(): 98 | """test removing an attribute""" 99 | subset_xml = '' 100 | element = etree.fromstring(subset_xml) 101 | subset = Subset(element) 102 | assert subset.contains == [100, 101] 103 | subset.contains = None 104 | assert subset.element.attrib.get("contains") is None 105 | 106 | @staticmethod 107 | def test_add_list_to_non_exist_value(): 108 | """test adding a list attribute to one that doesn't exist yet""" 109 | subset_xml = '' 110 | element = etree.fromstring(subset_xml) 111 | subset = Subset(element) 112 | subset.contains = [1, 2, 3] 113 | assert subset.element.attrib["contains"] == "1,2,3" 114 | -------------------------------------------------------------------------------- /tests/test_attribute_parsers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for attribute_parsers.py 3 | """ 4 | from math import inf 5 | from pytest import mark 6 | 7 | from mpd_parser.attribute_parsers import get_float_value, get_bool_value 8 | 9 | 10 | @mark.parametrize("input_value, expected_output", 11 | [ 12 | ("INF", inf), 13 | ("5", 5.0), 14 | ("10.000000", 10.0), 15 | ("24", 24.0), 16 | (None, None) 17 | ]) 18 | def test_get_float_value(input_value, expected_output): 19 | assert get_float_value(input_value) == expected_output 20 | 21 | 22 | @mark.parametrize("input_value, expected_output", 23 | [ 24 | ('true', True), 25 | ('false', False), 26 | ('None', None), 27 | ('1', None), 28 | (None, None), 29 | ('TRUE', None) 30 | ]) 31 | def test_get_bool_value(input_value, expected_output): 32 | assert get_bool_value(input_value) == expected_output 33 | -------------------------------------------------------------------------------- /tests/test_base_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the base tag classes as standalone classes 3 | """ 4 | 5 | import math 6 | from xml.etree.ElementTree import Element 7 | 8 | import pytest 9 | from lxml import etree 10 | 11 | from mpd_parser.models.base_tags import ( 12 | URL, 13 | AssetIdentifiers, 14 | BaseURL, 15 | ContentComponent, 16 | Descriptor, 17 | Event, 18 | EventStream, 19 | ProgramInfo, 20 | Subset, 21 | Tag, 22 | TextTag, 23 | UTCTiming, 24 | ) 25 | 26 | 27 | def test_tag_initialization(): 28 | """test tag init""" 29 | element = Element("test_tag") 30 | tag = Tag(element) 31 | assert tag.element == element 32 | 33 | 34 | def test_tag_setattr(): 35 | """test basic ability to set value in a tag""" 36 | element = Element("test_tag") 37 | tag = Tag(element) 38 | tag.new_attribute = "value" 39 | assert element.attrib["newAttribute"] == "value" 40 | 41 | 42 | def test_text_tag(): 43 | """test text tag""" 44 | tag_text_xml = "Hello, world!" 45 | element = etree.fromstring(tag_text_xml) 46 | text_tag = TextTag(element) 47 | assert text_tag.text == "Hello, world!" 48 | 49 | 50 | def test_event_tag(): 51 | """test event tag""" 52 | event_xml = 'Hello world' 53 | element = etree.fromstring(event_xml) 54 | event = Event(element) 55 | assert event.text == "Hello world" 56 | assert event.presentation_time == 5 57 | assert event.duration == 1 58 | assert event.id == 2 59 | 60 | 61 | def test_descriptor_tag(): 62 | """test descriptor tag""" 63 | desc_xml = '' 64 | element = etree.fromstring(desc_xml) 65 | desc = Descriptor(element) 66 | assert desc.scheme_id_uri == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" 67 | assert desc.value == "someval" 68 | assert desc.id == "desc1" 69 | 70 | 71 | def test_utc_timing_tag(): 72 | """test utc timing tag""" 73 | utc_xml = '' 74 | element = etree.fromstring(utc_xml) 75 | utc = UTCTiming(element) 76 | assert utc.scheme_id_uri == "urn:mpeg:dash:utc:http-head:2014" 77 | assert utc.value == "http://time.akamai.com" 78 | assert utc.id == "utc1" 79 | 80 | 81 | def test_asset_identifiers_tag(): 82 | """test asset identfiers tag""" 83 | ai_xml = '' 84 | element = etree.fromstring(ai_xml) 85 | ai = AssetIdentifiers(element) 86 | assert ai.scheme_id_uri == "urn:org:foo:asset-id" 87 | assert ai.value == "asset123" 88 | assert ai.id == "ai1" 89 | 90 | 91 | def test_content_component_tag(): 92 | """test content component tag""" 93 | cc_xml = """ 94 | 95 | 96 | 97 | 98 | 99 | 100 | """ 101 | element = etree.fromstring(cc_xml) 102 | cc = ContentComponent(element) 103 | assert cc.id == 1 104 | assert cc.lang == "en" 105 | assert cc.content_type == "video/mp4" 106 | assert cc.par == "16:9" 107 | assert len(cc.accessibilities) == 1 108 | assert cc.accessibilities[0].scheme_id_uri == "urn:foo:acc" 109 | assert cc.accessibilities[0].value == "desc" 110 | assert cc.accessibilities[0].id == "acc1" 111 | assert len(cc.roles) == 1 112 | assert cc.roles[0].scheme_id_uri == "urn:foo:role" 113 | assert cc.roles[0].value == "main" 114 | assert cc.roles[0].id == "role1" 115 | assert len(cc.ratings) == 1 116 | assert cc.ratings[0].scheme_id_uri == "urn:foo:rating" 117 | assert cc.ratings[0].value == "PG" 118 | assert cc.ratings[0].id == "rating1" 119 | assert len(cc.viewpoints) == 1 120 | assert cc.viewpoints[0].scheme_id_uri == "urn:foo:viewpoint" 121 | assert cc.viewpoints[0].value == "3d" 122 | assert cc.viewpoints[0].id == "view1" 123 | 124 | 125 | def test_program_info_tag(): 126 | """test program info tag""" 127 | pi_xml = """ 128 | 129 | My Program 130 | My Source 131 | Copyright Info 132 | 133 | """ 134 | element = etree.fromstring(pi_xml) 135 | pi = ProgramInfo(element) 136 | assert pi.lang == "en" 137 | assert pi.more_info_url == "https://example.com" 138 | assert len(pi.titles) == 1 139 | assert pi.titles[0].text == "My Program" 140 | assert len(pi.sources) == 1 141 | assert pi.sources[0].text == "My Source" 142 | assert len(pi.copy_rights) == 1 143 | assert pi.copy_rights[0].text == "Copyright Info" 144 | 145 | 146 | def test_url_tag(): 147 | """test url tag""" 148 | url_xml = '' 149 | element = etree.fromstring(url_xml) 150 | url = URL(element) 151 | assert url.source_url == "https://example.com/content" 152 | assert url.range == "100-200" 153 | 154 | 155 | def test_event_stream_tag(): 156 | """test event stream tag""" 157 | event_stream_xml = """ 158 | 159 | Hello 160 | World 161 | 162 | """ 163 | element = etree.fromstring(event_stream_xml) 164 | event_stream = EventStream(element) 165 | assert event_stream.scheme_id_uri == "urn:example:event" 166 | assert event_stream.value == "example" 167 | assert event_stream.timescale == "1000" 168 | assert len(event_stream.events) == 2 169 | assert event_stream.events[0].text == "Hello" 170 | assert event_stream.events[0].presentation_time == 5000 171 | assert event_stream.events[0].duration == 1000 172 | assert event_stream.events[0].id == 1 173 | assert event_stream.events[1].text == "World" 174 | assert event_stream.events[1].presentation_time == 6000 175 | assert event_stream.events[1].duration == 1000 176 | assert event_stream.events[1].id == 2 177 | 178 | 179 | def test_subset_tag(): 180 | """test subset tag""" 181 | subset_xml = '' 182 | element = etree.fromstring(subset_xml) 183 | subset = Subset(element) 184 | assert subset.id == "subset1" 185 | assert subset.contains == [1, 2, 3] 186 | 187 | 188 | @pytest.mark.parametrize( 189 | "xml_snippet,expected", 190 | [ 191 | ( 192 | """bunny_46980bps/BigBuckBunny_1snonSeg.mp4""", 193 | { 194 | "base_url_value": "bunny_46980bps/BigBuckBunny_1snonSeg.mp4", 195 | "availability_time_offset": math.inf, 196 | }, 197 | ), 198 | ( 199 | '' 200 | "some-other-url", 201 | { 202 | "base_url_value": "some-other-url", 203 | "availability_time_offset": 7.000000, 204 | "availability_time_complete": False, 205 | }, 206 | ), 207 | ], 208 | ) 209 | def test_base_url_tag(xml_snippet, expected): 210 | """test base url tag""" 211 | element = etree.fromstring(xml_snippet) 212 | base_url = BaseURL(element) 213 | assert base_url.text == expected.get("base_url_value") 214 | assert base_url.availability_time_offset == expected.get("availability_time_offset") 215 | assert base_url.availability_time_complete == expected.get( 216 | "availability_time_complete" 217 | ) 218 | -------------------------------------------------------------------------------- /tests/test_composite_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the composite tags classes as standalone classes 3 | """ 4 | 5 | from lxml import etree 6 | 7 | from mpd_parser.models.composite_tags import SegmentTemplate 8 | 9 | 10 | def test_segment_template_tag(): 11 | """test segment template tag""" 12 | segment_template_xml = ( 13 | "' 18 | ) 19 | element = etree.fromstring(segment_template_xml) 20 | segment_template = SegmentTemplate(element) 21 | assert segment_template.timescale == 48000 22 | assert segment_template.initialization == "audio-7-lav/init.mp4" 23 | assert segment_template.media == "audio-7-lav/$Number%05d$.mp4" 24 | assert segment_template.start_number == 1 25 | -------------------------------------------------------------------------------- /tests/test_manifets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the parsing of full manifests 3 | """ 4 | import io 5 | import os 6 | 7 | from pytest import mark, raises 8 | from mpd_parser.exceptions import UnicodeDeclaredError, UnknownElementTreeParseError, UnknownValueError 9 | from mpd_parser.parser import Parser 10 | 11 | from tests.conftest import touch_attributes, MANIFESTS_DIR 12 | 13 | class DummyFile(io.BytesIO): 14 | def __enter__(self): 15 | return self 16 | def __exit__(self, exc_type, exc_val, exc_tb): 17 | self.close() 18 | 19 | def dummy_urlopen(url): 20 | return DummyFile(b"") 21 | 22 | 23 | @mark.parametrize("input_file", [ 24 | "./../manifests/bigBuckBunny-onDemend.mpd", 25 | ]) 26 | def test_mpd_tag(input_file): 27 | """ Test that parser works and create MPD object """ 28 | with open(input_file, mode="r", encoding='UTF-8') as manifest_file: 29 | mpd_string = manifest_file.read() 30 | mpd = Parser.from_string(mpd_string) 31 | assert mpd.id is None 32 | assert mpd.type == "static" 33 | assert mpd.min_buffer_time == "PT1.500000S" 34 | assert mpd.media_presentation_duration == "PT0H9M55.46S" 35 | assert mpd.profiles == "urn:mpeg:dash:profile:isoff-on-demand:2011" 36 | assert len(mpd.program_informations) == 1 37 | assert mpd.program_informations[0].titles[0].text == \ 38 | "dashed/BigBuckBunny_1s_onDemand_2014_05_09.mpd generated by GPAC" 39 | assert mpd.program_informations[0].more_info_url == "http://gpac.sourceforge.net" 40 | assert len(mpd.periods) == 1 41 | assert mpd.periods[0].id is None 42 | assert mpd.periods[0].duration == "PT0H9M55.46S" 43 | 44 | 45 | @mark.parametrize("input_file", [ 46 | "./../manifests/bigBuckBunny-onDemend.mpd", 47 | ]) 48 | def test_parse_from_file_mpd_tag(input_file): 49 | """ Test that parser works and create MPD object """ 50 | mpd = Parser.from_file(input_file) 51 | assert mpd.id is None 52 | assert mpd.type == "static" 53 | assert mpd.min_buffer_time == "PT1.500000S" 54 | assert mpd.media_presentation_duration == "PT0H9M55.46S" 55 | assert mpd.profiles == "urn:mpeg:dash:profile:isoff-on-demand:2011" 56 | assert len(mpd.program_informations) == 1 57 | assert mpd.program_informations[0].titles[0].text == \ 58 | "dashed/BigBuckBunny_1s_onDemand_2014_05_09.mpd generated by GPAC" 59 | assert mpd.program_informations[0].more_info_url == "http://gpac.sourceforge.net" 60 | assert len(mpd.periods) == 1 61 | assert mpd.periods[0].id is None 62 | assert mpd.periods[0].duration == "PT0H9M55.46S" 63 | 64 | @mark.parametrize("url", [ 65 | "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd", 66 | ]) 67 | def test_parse_from_url_mpd_tag(url): 68 | """ Test that parser works and create MPD object from URL target """ 69 | mpd = Parser.from_url(url) 70 | assert mpd.id == "f08e80da-bf1d-4e3d-8899-f0f6155f6efa" 71 | assert mpd.type == "static" 72 | assert mpd.min_buffer_time == "P0Y0M0DT0H0M1.000S" 73 | assert mpd.media_presentation_duration == "P0Y0M0DT0H3M30.000S" 74 | assert mpd.profiles == "urn:mpeg:dash:profile:isoff-main:2011" 75 | assert mpd.program_informations == [] 76 | assert len(mpd.periods) == 1 77 | assert mpd.periods[0].id is None 78 | assert mpd.periods[0].duration is None 79 | assert mpd.publish_time == "2015-08-04T10:47:32.000Z" 80 | assert len(mpd.periods[0].adaptation_sets) == 2 81 | assert mpd.periods[0].adaptation_sets[0].mime_type == "video/mp4" 82 | assert mpd.periods[0].adaptation_sets[0].codecs == "avc1.42c00d" 83 | 84 | 85 | @mark.parametrize("parsing_type", [Parser.from_file, Parser.from_string]) 86 | @mark.parametrize("input_file", [f"{MANIFESTS_DIR}{name}" for name in os.listdir(MANIFESTS_DIR)]) 87 | def test_touch_all_manifest_properties(input_file, parsing_type): 88 | """ 89 | Test each manifest by walking over it's xml tree. 90 | Does not verify values. 91 | """ 92 | if parsing_type == Parser.from_file: 93 | mpd = parsing_type(input_file) 94 | touch_attributes(mpd) 95 | return 96 | 97 | with open(input_file, mode="r", encoding='UTF-8') as manifest_file: 98 | mpd_string = manifest_file.read() 99 | mpd = parsing_type(mpd_string) 100 | touch_attributes(mpd) 101 | 102 | 103 | @mark.parametrize("input_file", [ 104 | "./../manifests/bigBuckBunny-onDemend.mpd", 105 | ]) 106 | def test_to_string(input_file): 107 | """ test the converter from MPD object to string xml """ 108 | with open(input_file, mode="r") as manifest_file: 109 | mpd_string = manifest_file.read() 110 | orig_mpd = Parser.from_string(mpd_string) 111 | mpd_result = Parser.to_string(orig_mpd) 112 | transformed_mpd = Parser.from_string(mpd_result) 113 | assert orig_mpd.program_informations[0].more_info_url == transformed_mpd.program_informations[0].more_info_url 114 | assert orig_mpd.periods[0].adaptation_sets[0].representations[0].segment_bases[0].initializations[ 115 | 0].range == \ 116 | transformed_mpd.periods[0].adaptation_sets[0].representations[0].segment_bases[0].initializations[ 117 | 0].range 118 | 119 | @mark.parametrize( 120 | "exception,patch_func", 121 | [ 122 | (UnicodeDeclaredError, lambda: ValueError("Unicode something")), 123 | (UnknownValueError, lambda: ValueError("Some other value error")), 124 | (UnknownElementTreeParseError, lambda: RuntimeError("Some runtime error")), 125 | ] 126 | ) 127 | def test_from_string_error_handling(monkeypatch, exception, patch_func): 128 | def fake_parse(*args, **kwargs): 129 | raise patch_func() 130 | monkeypatch.setattr("mpd_parser.parser.etree.fromstring", fake_parse) 131 | with raises(exception): 132 | Parser.from_string("") 133 | 134 | @mark.parametrize( 135 | "exception,patch_func", 136 | [ 137 | (UnicodeDeclaredError, lambda: ValueError("Unicode something")), 138 | (UnknownValueError, lambda: ValueError("Some other value error")), 139 | (UnknownElementTreeParseError, lambda: RuntimeError("Some runtime error")), 140 | ] 141 | ) 142 | def test_from_file_error_handling(monkeypatch, exception, patch_func): 143 | def fake_parse(*args, **kwargs): 144 | raise patch_func() 145 | monkeypatch.setattr("mpd_parser.parser.etree.parse", fake_parse) 146 | with raises(exception): 147 | Parser.from_file("dummy_file.mpd") 148 | 149 | @mark.parametrize( 150 | "exception,patch_func", 151 | [ 152 | (UnicodeDeclaredError, lambda: ValueError("Unicode something")), 153 | (UnknownValueError, lambda: ValueError("Some other value error")), 154 | (UnknownElementTreeParseError, lambda: RuntimeError("Some runtime error")), 155 | ] 156 | ) 157 | def test_from_url_error_handling(monkeypatch, exception, patch_func): 158 | def fake_parse(*args, **kwargs): 159 | raise patch_func() 160 | monkeypatch.setattr("mpd_parser.parser.etree.parse", fake_parse) 161 | monkeypatch.setattr("mpd_parser.parser.urlopen", dummy_urlopen) 162 | with raises(exception): 163 | Parser.from_url("http://dummy.url/manifest.mpd") --------------------------------------------------------------------------------