├── .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")
--------------------------------------------------------------------------------