├── .github
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── poetry.lock
├── pyproject.toml
├── src
└── pymp4
│ ├── __init__.py
│ ├── cli.py
│ ├── exceptions.py
│ ├── parser.py
│ ├── tools
│ └── __init__.py
│ └── util.py
└── tests
├── __init__.py
├── test_box.py
├── test_dashboxes.py
├── test_util.py
└── test_webvtt_boxes.py
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: cd
2 | permissions:
3 | contents: "write"
4 | id-token: "write"
5 | packages: "write"
6 | pull-requests: "read"
7 |
8 | on:
9 | push:
10 | tags:
11 | - "v*"
12 |
13 | jobs:
14 | tagged-release:
15 | name: Tagged Release
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Set up Python
20 | uses: actions/setup-python@v4
21 | with:
22 | python-version: '3.8.x'
23 | - name: Install Poetry
24 | uses: abatilo/actions-poetry@v2.3.0
25 | with:
26 | poetry-version: '1.4.1'
27 | - name: Install dependencies
28 | run: poetry install --no-dev
29 | - name: Build project
30 | run: poetry build
31 | - name: Upload wheel
32 | uses: actions/upload-artifact@v3
33 | with:
34 | name: Python Wheel
35 | path: "dist/*.whl"
36 | - name: Deploy release
37 | uses: marvinpinto/action-automatic-releases@latest
38 | with:
39 | prerelease: false
40 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
41 | files: |
42 | dist/*.whl
43 | - name: Publish to PyPI
44 | env:
45 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
46 | run: poetry publish
47 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Set up Python ${{ matrix.python-version }}
20 | uses: actions/setup-python@v4
21 | with:
22 | python-version: ${{ matrix.python-version }}
23 | - name: Install poetry
24 | uses: abatilo/actions-poetry@v2.3.0
25 | with:
26 | poetry-version: 1.4.1
27 | - name: Install project
28 | run: poetry install
29 | - name: Run tests
30 | run: poetry run pytest tests/
31 | - name: Run coverage
32 | run: |
33 | poetry run coverage run -m pytest tests/
34 | poetry run coverage xml
35 | - name: Upload coverage to Codecov
36 | uses: codecov/codecov-action@v3
37 | with:
38 | name: "Python-${{ matrix.python-version }}"
39 | fail_ci_if_error: true
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 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 beardypig
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pymp4
2 |
3 | [](https://github.com/beardypig/pymp4/actions/workflows/ci.yml)
4 | [](LICENSE)
5 | [](https://pypi.org/project/pymp4)
6 | [](https://app.codecov.io/github/beardypig/pymp4)
7 |
8 | Python MP4 box parser and toolkit based on the [construct](https://github.com/construct/construct) library.
9 |
10 | ## Usage
11 |
12 | ```python
13 | >>> from pymp4.parser import Box
14 | >>> from io import BytesIO
15 |
16 | >>> Box.build(dict(
17 | type=b"ftyp",
18 | major_brand="iso5",
19 | minor_version=1,
20 | compatible_brands=["iso5", "avc1"]))
21 | b'\x00\x00\x00\x18ftypiso5\x00\x00\x00\x01iso5avc1'
22 |
23 | >>> ftyp = Box.parse(b'\x00\x00\x00\x18ftypiso5\x00\x00\x00\x01iso5avc1')
24 | >>> print(ftyp)
25 | Container:
26 | type = ftyp
27 | major_brand = iso5
28 | minor_version = 1
29 | compatible_brands = ListContainer:
30 | iso5
31 | avc1
32 |
33 | ```
34 |
35 | ## Contributors
36 |
37 |
38 |
39 |
40 |
41 |
42 | ## License
43 |
44 | [Apache License, Version 2.0](LICENSE)
45 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "attrs"
5 | version = "22.2.0"
6 | description = "Classes Without Boilerplate"
7 | category = "dev"
8 | optional = false
9 | python-versions = ">=3.6"
10 | files = [
11 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
12 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
13 | ]
14 |
15 | [package.extras]
16 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
17 | dev = ["attrs[docs,tests]"]
18 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
19 | tests = ["attrs[tests-no-zope]", "zope.interface"]
20 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
21 |
22 | [[package]]
23 | name = "colorama"
24 | version = "0.4.6"
25 | description = "Cross-platform colored terminal text."
26 | category = "dev"
27 | optional = false
28 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
29 | files = [
30 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
31 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
32 | ]
33 |
34 | [[package]]
35 | name = "construct"
36 | version = "2.8.8"
37 | description = "A powerful declarative parser/builder for binary data"
38 | category = "main"
39 | optional = false
40 | python-versions = "*"
41 | files = [
42 | {file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"},
43 | ]
44 |
45 | [[package]]
46 | name = "coverage"
47 | version = "7.2.3"
48 | description = "Code coverage measurement for Python"
49 | category = "dev"
50 | optional = false
51 | python-versions = ">=3.7"
52 | files = [
53 | {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"},
54 | {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"},
55 | {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"},
56 | {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"},
57 | {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"},
58 | {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"},
59 | {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"},
60 | {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"},
61 | {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"},
62 | {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"},
63 | {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"},
64 | {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"},
65 | {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"},
66 | {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"},
67 | {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"},
68 | {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"},
69 | {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"},
70 | {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"},
71 | {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"},
72 | {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"},
73 | {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"},
74 | {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"},
75 | {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"},
76 | {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"},
77 | {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"},
78 | {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"},
79 | {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"},
80 | {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"},
81 | {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"},
82 | {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"},
83 | {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"},
84 | {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"},
85 | {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"},
86 | {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"},
87 | {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"},
88 | {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"},
89 | {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"},
90 | {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"},
91 | {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"},
92 | {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"},
93 | {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"},
94 | {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"},
95 | {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"},
96 | {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"},
97 | {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"},
98 | {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"},
99 | {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"},
100 | {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"},
101 | {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"},
102 | {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"},
103 | {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"},
104 | ]
105 |
106 | [package.dependencies]
107 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
108 |
109 | [package.extras]
110 | toml = ["tomli"]
111 |
112 | [[package]]
113 | name = "exceptiongroup"
114 | version = "1.1.1"
115 | description = "Backport of PEP 654 (exception groups)"
116 | category = "dev"
117 | optional = false
118 | python-versions = ">=3.7"
119 | files = [
120 | {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
121 | {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
122 | ]
123 |
124 | [package.extras]
125 | test = ["pytest (>=6)"]
126 |
127 | [[package]]
128 | name = "importlib-metadata"
129 | version = "6.2.0"
130 | description = "Read metadata from Python packages"
131 | category = "dev"
132 | optional = false
133 | python-versions = ">=3.7"
134 | files = [
135 | {file = "importlib_metadata-6.2.0-py3-none-any.whl", hash = "sha256:8388b74023a138c605fddd0d47cb81dd706232569f56c9aca7d9c7fdb54caeba"},
136 | {file = "importlib_metadata-6.2.0.tar.gz", hash = "sha256:9127aad2f49d7203e7112098c12b92e4fd1061ccd18548cdfdc49171a8c073cc"},
137 | ]
138 |
139 | [package.dependencies]
140 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
141 | zipp = ">=0.5"
142 |
143 | [package.extras]
144 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
145 | perf = ["ipython"]
146 | testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
147 |
148 | [[package]]
149 | name = "iniconfig"
150 | version = "2.0.0"
151 | description = "brain-dead simple config-ini parsing"
152 | category = "dev"
153 | optional = false
154 | python-versions = ">=3.7"
155 | files = [
156 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
157 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
158 | ]
159 |
160 | [[package]]
161 | name = "packaging"
162 | version = "23.0"
163 | description = "Core utilities for Python packages"
164 | category = "dev"
165 | optional = false
166 | python-versions = ">=3.7"
167 | files = [
168 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
169 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
170 | ]
171 |
172 | [[package]]
173 | name = "pluggy"
174 | version = "1.0.0"
175 | description = "plugin and hook calling mechanisms for python"
176 | category = "dev"
177 | optional = false
178 | python-versions = ">=3.6"
179 | files = [
180 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
181 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
182 | ]
183 |
184 | [package.dependencies]
185 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
186 |
187 | [package.extras]
188 | dev = ["pre-commit", "tox"]
189 | testing = ["pytest", "pytest-benchmark"]
190 |
191 | [[package]]
192 | name = "pytest"
193 | version = "7.2.2"
194 | description = "pytest: simple powerful testing with Python"
195 | category = "dev"
196 | optional = false
197 | python-versions = ">=3.7"
198 | files = [
199 | {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
200 | {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
201 | ]
202 |
203 | [package.dependencies]
204 | attrs = ">=19.2.0"
205 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
206 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
207 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
208 | iniconfig = "*"
209 | packaging = "*"
210 | pluggy = ">=0.12,<2.0"
211 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
212 |
213 | [package.extras]
214 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
215 |
216 | [[package]]
217 | name = "pytest-cov"
218 | version = "4.0.0"
219 | description = "Pytest plugin for measuring coverage."
220 | category = "dev"
221 | optional = false
222 | python-versions = ">=3.6"
223 | files = [
224 | {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
225 | {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
226 | ]
227 |
228 | [package.dependencies]
229 | coverage = {version = ">=5.2.1", extras = ["toml"]}
230 | pytest = ">=4.6"
231 |
232 | [package.extras]
233 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
234 |
235 | [[package]]
236 | name = "tomli"
237 | version = "2.0.1"
238 | description = "A lil' TOML parser"
239 | category = "dev"
240 | optional = false
241 | python-versions = ">=3.7"
242 | files = [
243 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
244 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
245 | ]
246 |
247 | [[package]]
248 | name = "typing-extensions"
249 | version = "4.5.0"
250 | description = "Backported and Experimental Type Hints for Python 3.7+"
251 | category = "dev"
252 | optional = false
253 | python-versions = ">=3.7"
254 | files = [
255 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
256 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
257 | ]
258 |
259 | [[package]]
260 | name = "zipp"
261 | version = "3.15.0"
262 | description = "Backport of pathlib-compatible object wrapper for zip files"
263 | category = "dev"
264 | optional = false
265 | python-versions = ">=3.7"
266 | files = [
267 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
268 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
269 | ]
270 |
271 | [package.extras]
272 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
273 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
274 |
275 | [metadata]
276 | lock-version = "2.0"
277 | python-versions = ">=3.7,<4.0"
278 | content-hash = "9a2baac6978e2ce64197980dda6b562347469f581c3881b1fe9d59432d47bc51"
279 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["poetry-core"]
3 | build-backend = "poetry.core.masonry.api"
4 |
5 | [tool.poetry]
6 | name = "pymp4"
7 | version = "1.4.0"
8 | description = "Python parser for MP4 boxes"
9 | authors = ["beardypig "]
10 | license = "Apache-2.0"
11 | readme = "README.md"
12 | homepage = "https://github.com/beardypig/pymp4"
13 | repository = "https://github.com/beardypig/pymp4"
14 | classifiers = [
15 | "Development Status :: 4 - Beta",
16 | "Environment :: Console",
17 | "Intended Audience :: Developers",
18 | "Natural Language :: English",
19 | "Operating System :: OS Independent",
20 | "Topic :: Multimedia :: Sound/Audio",
21 | "Topic :: Multimedia :: Video",
22 | "Topic :: Utilities",
23 | ]
24 |
25 | [tool.poetry.dependencies]
26 | python = ">=3.7,<4.0"
27 | construct = "2.8.8"
28 |
29 | [tool.poetry.group.dev.dependencies]
30 | coverage = { version="^7.2.3", extras=["toml"] }
31 | pytest = "^7.2.2"
32 | pytest-cov = "^4.0.0"
33 |
34 | [tool.poetry.scripts]
35 | mp4dump = "pymp4.cli:dump"
36 |
37 | [tool.coverage.run]
38 | source = ["src/pymp4"]
39 | omit = [".*", "*/site-packages/*", "*/python?.?/*"]
40 |
41 | [tool.coverage.report]
42 | exclude_lines = [
43 | "pragma: no cover",
44 | "def __repr__",
45 | "raise NotImplementedError",
46 | "if __name__ == .__main__.:"
47 | ]
48 |
--------------------------------------------------------------------------------
/src/pymp4/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pymp4/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import print_function
3 | import io
4 | import logging
5 | import argparse
6 |
7 | from pymp4.parser import Box
8 | from construct import setglobalfullprinting
9 |
10 | log = logging.getLogger(__name__)
11 | setglobalfullprinting(True)
12 |
13 |
14 | def dump():
15 | parser = argparse.ArgumentParser(description='Dump all the boxes from an MP4 file')
16 | parser.add_argument("input_file", type=argparse.FileType("rb"), metavar="FILE", help="Path to the MP4 file to open")
17 |
18 | args = parser.parse_args()
19 |
20 | fd = args.input_file
21 | fd.seek(0, io.SEEK_END)
22 | eof = fd.tell()
23 | fd.seek(0)
24 |
25 | while fd.tell() < eof:
26 | box = Box.parse_stream(fd)
27 | print(box)
28 |
--------------------------------------------------------------------------------
/src/pymp4/exceptions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Copyright 2016 beardypig
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | """
17 |
18 |
19 | class BoxNotFound(Exception):
20 | pass
21 |
--------------------------------------------------------------------------------
/src/pymp4/parser.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Copyright 2016 beardypig
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | """
17 | import logging
18 | from uuid import UUID
19 |
20 | from construct import *
21 | import construct.core
22 | from construct.lib import *
23 |
24 | log = logging.getLogger(__name__)
25 |
26 | UNITY_MATRIX = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000]
27 |
28 |
29 | class PrefixedIncludingSize(Subconstruct):
30 | __slots__ = ["name", "lengthfield", "subcon"]
31 |
32 | def __init__(self, lengthfield, subcon):
33 | super(PrefixedIncludingSize, self).__init__(subcon)
34 | self.lengthfield = lengthfield
35 |
36 | def _parse(self, stream, context, path):
37 | try:
38 | lengthfield_size = self.lengthfield.sizeof()
39 | length = self.lengthfield._parse(stream, context, path)
40 | except SizeofError:
41 | offset_start = stream.tell()
42 | length = self.lengthfield._parse(stream, context, path)
43 | lengthfield_size = stream.tell() - offset_start
44 |
45 | stream2 = BoundBytesIO(stream, length - lengthfield_size)
46 | obj = self.subcon._parse(stream2, context, path)
47 | return obj
48 |
49 | def _build(self, obj, stream, context, path):
50 | try:
51 | # needs to be both fixed size, seekable and tellable (third not checked)
52 | self.lengthfield.sizeof()
53 | if not stream.seekable:
54 | raise SizeofError
55 | offset_start = stream.tell()
56 | self.lengthfield._build(0, stream, context, path)
57 | self.subcon._build(obj, stream, context, path)
58 | offset_end = stream.tell()
59 | stream.seek(offset_start)
60 | self.lengthfield._build(offset_end - offset_start, stream, context, path)
61 | stream.seek(offset_end)
62 | except SizeofError:
63 | data = self.subcon.build(obj, context)
64 | sl, p_sl = 0, 0
65 | dlen = len(data)
66 | # do..while
67 | i = 0
68 | while True:
69 | i += 1
70 | p_sl = sl
71 | sl = len(self.lengthfield.build(dlen + sl))
72 | if p_sl == sl: break
73 |
74 | self.lengthfield._build(dlen + sl, stream, context, path)
75 | else:
76 | self.lengthfield._build(len(data), stream, context, path)
77 | construct.core._write_stream(stream, len(data), data)
78 |
79 | def _sizeof(self, context, path):
80 | return self.lengthfield._sizeof(context, path) + self.subcon._sizeof(context, path)
81 |
82 |
83 | # Header box
84 |
85 | FileTypeBox = Struct(
86 | "type" / Const(b"ftyp"),
87 | "major_brand" / String(4),
88 | "minor_version" / Int32ub,
89 | "compatible_brands" / GreedyRange(String(4)),
90 | )
91 |
92 | SegmentTypeBox = Struct(
93 | "type" / Const(b"styp"),
94 | "major_brand" / String(4),
95 | "minor_version" / Int32ub,
96 | "compatible_brands" / GreedyRange(String(4)),
97 | )
98 |
99 | # Catch find boxes
100 |
101 | RawBox = Struct(
102 | "type" / String(4, padchar=b" ", paddir="right"),
103 | "data" / Default(GreedyBytes, b"")
104 | )
105 |
106 | FreeBox = Struct(
107 | "type" / Const(b"free"),
108 | "data" / GreedyBytes
109 | )
110 |
111 | SkipBox = Struct(
112 | "type" / Const(b"skip"),
113 | "data" / GreedyBytes
114 | )
115 |
116 | # Movie boxes, contained in a moov Box
117 |
118 | MovieHeaderBox = Struct(
119 | "type" / Const(b"mvhd"),
120 | "version" / Default(Int8ub, 0),
121 | "flags" / Default(Int24ub, 0),
122 | Embedded(Switch(this.version, {
123 | 1: Struct(
124 | "creation_time" / Default(Int64ub, 0),
125 | "modification_time" / Default(Int64ub, 0),
126 | "timescale" / Default(Int32ub, 10000000),
127 | "duration" / Int64ub
128 | ),
129 | 0: Struct(
130 | "creation_time" / Default(Int32ub, 0),
131 | "modification_time" / Default(Int32ub, 0),
132 | "timescale" / Default(Int32ub, 10000000),
133 | "duration" / Int32ub,
134 | ),
135 | })),
136 | "rate" / Default(Int32sb, 65536),
137 | "volume" / Default(Int16sb, 256),
138 | # below could be just Padding(10) but why not
139 | Const(Int16ub, 0),
140 | Const(Int32ub, 0),
141 | Const(Int32ub, 0),
142 | "matrix" / Default(Int32sb[9], UNITY_MATRIX),
143 | "pre_defined" / Default(Int32ub[6], [0] * 6),
144 | "next_track_ID" / Default(Int32ub, 0xffffffff)
145 | )
146 |
147 | # Track boxes, contained in trak box
148 |
149 | TrackHeaderBox = Struct(
150 | "type" / Const(b"tkhd"),
151 | "version" / Default(Int8ub, 0),
152 | "flags" / Default(Int24ub, 1),
153 | Embedded(Switch(this.version, {
154 | 1: Struct(
155 | "creation_time" / Default(Int64ub, 0),
156 | "modification_time" / Default(Int64ub, 0),
157 | "track_ID" / Default(Int32ub, 1),
158 | Padding(4),
159 | "duration" / Default(Int64ub, 0),
160 | ),
161 | 0: Struct(
162 | "creation_time" / Default(Int32ub, 0),
163 | "modification_time" / Default(Int32ub, 0),
164 | "track_ID" / Default(Int32ub, 1),
165 | Padding(4),
166 | "duration" / Default(Int32ub, 0),
167 | ),
168 | })),
169 | Padding(8),
170 | "layer" / Default(Int16sb, 0),
171 | "alternate_group" / Default(Int16sb, 0),
172 | "volume" / Default(Int16sb, 0),
173 | Padding(2),
174 | "matrix" / Default(Array(9, Int32sb), UNITY_MATRIX),
175 | "width" / Default(Int32ub, 0),
176 | "height" / Default(Int32ub, 0),
177 | )
178 |
179 | HDSSegmentBox = Struct(
180 | "type" / Const(b"abst"),
181 | "version" / Default(Int8ub, 0),
182 | "flags" / Default(Int24ub, 0),
183 | "info_version" / Int32ub,
184 | EmbeddedBitStruct(
185 | Padding(1),
186 | "profile" / Flag,
187 | "live" / Flag,
188 | "update" / Flag,
189 | Padding(4)
190 | ),
191 | "time_scale" / Int32ub,
192 | "current_media_time" / Int64ub,
193 | "smpte_time_code_offset" / Int64ub,
194 | "movie_identifier" / CString(),
195 | "server_entry_table" / PrefixedArray(Int8ub, CString()),
196 | "quality_entry_table" / PrefixedArray(Int8ub, CString()),
197 | "drm_data" / CString(),
198 | "metadata" / CString(),
199 | "segment_run_table" / PrefixedArray(Int8ub, LazyBound(lambda x: Box)),
200 | "fragment_run_table" / PrefixedArray(Int8ub, LazyBound(lambda x: Box))
201 | )
202 |
203 | HDSSegmentRunBox = Struct(
204 | "type" / Const(b"asrt"),
205 | "version" / Default(Int8ub, 0),
206 | "flags" / Default(Int24ub, 0),
207 | "quality_entry_table" / PrefixedArray(Int8ub, CString()),
208 | "segment_run_enteries" / PrefixedArray(Int32ub, Struct(
209 | "first_segment" / Int32ub,
210 | "fragments_per_segment" / Int32ub
211 | ))
212 | )
213 |
214 | HDSFragmentRunBox = Struct(
215 | "type" / Const(b"afrt"),
216 | "version" / Default(Int8ub, 0),
217 | "flags" / BitStruct(
218 | Padding(23),
219 | "update" / Flag
220 | ),
221 | "time_scale" / Int32ub,
222 | "quality_entry_table" / PrefixedArray(Int8ub, CString()),
223 | "fragment_run_enteries" / PrefixedArray(Int32ub, Struct(
224 | "first_fragment" / Int32ub,
225 | "first_fragment_timestamp" / Int64ub,
226 | "fragment_duration" / Int32ub,
227 | "discontinuity" / If(this.fragment_duration == 0, Int8ub)
228 | ))
229 | )
230 |
231 |
232 | # Boxes contained by Media Box
233 |
234 | class ISO6392TLanguageCode(Adapter):
235 | def _decode(self, obj, context):
236 | """
237 | Get the python representation of the obj
238 | """
239 | return b''.join(map(int2byte, [c + 0x60 for c in bytearray(obj)])).decode("utf8")
240 |
241 | def _encode(self, obj, context):
242 | """
243 | Get the bytes representation of the obj
244 | """
245 | return [c - 0x60 for c in bytearray(obj.encode("utf8"))]
246 |
247 |
248 | MediaHeaderBox = Struct(
249 | "type" / Const(b"mdhd"),
250 | "version" / Default(Int8ub, 0),
251 | "flags" / Const(Int24ub, 0),
252 | "creation_time" / IfThenElse(this.version == 1, Int64ub, Int32ub),
253 | "modification_time" / IfThenElse(this.version == 1, Int64ub, Int32ub),
254 | "timescale" / Int32ub,
255 | "duration" / IfThenElse(this.version == 1, Int64ub, Int32ub),
256 | Embedded(BitStruct(
257 | Padding(1),
258 | "language" / ISO6392TLanguageCode(BitsInteger(5)[3]),
259 | )),
260 | Padding(2, pattern=b"\x00"),
261 | )
262 |
263 | HandlerReferenceBox = Struct(
264 | "type" / Const(b"hdlr"),
265 | "version" / Const(Int8ub, 0),
266 | "flags" / Const(Int24ub, 0),
267 | Padding(4, pattern=b"\x00"),
268 | "handler_type" / String(4),
269 | Padding(12, pattern=b"\x00"), # Int32ub[3]
270 | "name" / CString(encoding="utf8")
271 | )
272 |
273 | # Boxes contained by Media Info Box
274 |
275 | VideoMediaHeaderBox = Struct(
276 | "type" / Const(b"vmhd"),
277 | "version" / Default(Int8ub, 0),
278 | "flags" / Const(Int24ub, 1),
279 | "graphics_mode" / Default(Int16ub, 0),
280 | "opcolor" / Struct(
281 | "red" / Default(Int16ub, 0),
282 | "green" / Default(Int16ub, 0),
283 | "blue" / Default(Int16ub, 0),
284 | ),
285 | )
286 |
287 | DataEntryUrlBox = PrefixedIncludingSize(Int32ub, Struct(
288 | "type" / Const(b"url "),
289 | "version" / Const(Int8ub, 0),
290 | "flags" / BitStruct(
291 | Padding(23), "self_contained" / Rebuild(Flag, ~this._.location)
292 | ),
293 | "location" / If(~this.flags.self_contained, CString(encoding="utf8")),
294 | ))
295 |
296 | DataEntryUrnBox = PrefixedIncludingSize(Int32ub, Struct(
297 | "type" / Const(b"urn "),
298 | "version" / Const(Int8ub, 0),
299 | "flags" / BitStruct(
300 | Padding(23), "self_contained" / Rebuild(Flag, ~(this._.name & this._.location))
301 | ),
302 | "name" / If(this.flags == 0, CString(encoding="utf8")),
303 | "location" / If(this.flags == 0, CString(encoding="utf8")),
304 | ))
305 |
306 | DataReferenceBox = Struct(
307 | "type" / Const(b"dref"),
308 | "version" / Const(Int8ub, 0),
309 | "flags" / Default(Int24ub, 0),
310 | "data_entries" / PrefixedArray(Int32ub, Select(DataEntryUrnBox, DataEntryUrlBox)),
311 | )
312 |
313 | # Sample Table boxes (stbl)
314 |
315 | MP4ASampleEntryBox = Struct(
316 | "version" / Default(Int16ub, 0),
317 | "revision" / Const(Int16ub, 0),
318 | "vendor" / Const(Int32ub, 0),
319 | "channels" / Default(Int16ub, 2),
320 | "bits_per_sample" / Default(Int16ub, 16),
321 | "compression_id" / Default(Int16sb, 0),
322 | "packet_size" / Const(Int16ub, 0),
323 | "sampling_rate" / Int16ub,
324 | Padding(2)
325 | )
326 |
327 |
328 | class MaskedInteger(Adapter):
329 | def _decode(self, obj, context):
330 | return obj & 0x1F
331 |
332 | def _encode(self, obj, context):
333 | return obj & 0x1F
334 |
335 |
336 | AAVC = Struct(
337 | "version" / Const(Int8ub, 1),
338 | "profile" / Int8ub,
339 | "compatibility" / Int8ub,
340 | "level" / Int8ub,
341 | EmbeddedBitStruct(
342 | Padding(6, pattern=b'\x01'),
343 | "nal_unit_length_field" / Default(BitsInteger(2), 3),
344 | ),
345 | "sps" / Default(PrefixedArray(MaskedInteger(Int8ub), PascalString(Int16ub)), []),
346 | "pps" / Default(PrefixedArray(Int8ub, PascalString(Int16ub)), [])
347 | )
348 |
349 | HVCC = Struct(
350 | EmbeddedBitStruct(
351 | "version" / Const(BitsInteger(8), 1),
352 | "profile_space" / BitsInteger(2),
353 | "general_tier_flag" / BitsInteger(1),
354 | "general_profile" / BitsInteger(5),
355 | "general_profile_compatibility_flags" / BitsInteger(32),
356 | "general_constraint_indicator_flags" / BitsInteger(48),
357 | "general_level" / BitsInteger(8),
358 | Padding(4, pattern=b'\xff'),
359 | "min_spatial_segmentation" / BitsInteger(12),
360 | Padding(6, pattern=b'\xff'),
361 | "parallelism_type" / BitsInteger(2),
362 | Padding(6, pattern=b'\xff'),
363 | "chroma_format" / BitsInteger(2),
364 | Padding(5, pattern=b'\xff'),
365 | "luma_bit_depth" / BitsInteger(3),
366 | Padding(5, pattern=b'\xff'),
367 | "chroma_bit_depth" / BitsInteger(3),
368 | "average_frame_rate" / BitsInteger(16),
369 | "constant_frame_rate" / BitsInteger(2),
370 | "num_temporal_layers" / BitsInteger(3),
371 | "temporal_id_nested" / BitsInteger(1),
372 | "nalu_length_size" / BitsInteger(2),
373 | ),
374 | # TODO: parse NALUs
375 | "raw_bytes" / GreedyBytes
376 | )
377 |
378 | AVC1SampleEntryBox = Struct(
379 | "version" / Default(Int16ub, 0),
380 | "revision" / Const(Int16ub, 0),
381 | "vendor" / Default(String(4, padchar=b" "), b"brdy"),
382 | "temporal_quality" / Default(Int32ub, 0),
383 | "spatial_quality" / Default(Int32ub, 0),
384 | "width" / Int16ub,
385 | "height" / Int16ub,
386 | "horizontal_resolution" / Default(Int16ub, 72), # TODO: actually a fixed point decimal
387 | Padding(2),
388 | "vertical_resolution" / Default(Int16ub, 72), # TODO: actually a fixed point decimal
389 | Padding(2),
390 | "data_size" / Const(Int32ub, 0),
391 | "frame_count" / Default(Int16ub, 1),
392 | "compressor_name" / Default(String(32, padchar=b" "), ""),
393 | "depth" / Default(Int16ub, 24),
394 | "color_table_id" / Default(Int16sb, -1),
395 | "avc_data" / PrefixedIncludingSize(Int32ub, Struct(
396 | "type" / String(4, padchar=b" ", paddir="right"),
397 | Embedded(Switch(this.type, {
398 | b"avcC": AAVC,
399 | b"hvcC": HVCC,
400 | }, Struct("data" / GreedyBytes)))
401 | )),
402 | "sample_info" / LazyBound(lambda _: GreedyRange(Box))
403 | )
404 |
405 | SampleEntryBox = PrefixedIncludingSize(Int32ub, Struct(
406 | "format" / String(4, padchar=b" ", paddir="right"),
407 | Padding(6, pattern=b"\x00"),
408 | "data_reference_index" / Default(Int16ub, 1),
409 | Embedded(Switch(this.format, {
410 | b"ec-3": MP4ASampleEntryBox,
411 | b"mp4a": MP4ASampleEntryBox,
412 | b"enca": MP4ASampleEntryBox,
413 | b"avc1": AVC1SampleEntryBox,
414 | b"encv": AVC1SampleEntryBox,
415 | b"wvtt": Struct("children" / LazyBound(lambda ctx: GreedyRange(Box)))
416 | }, Struct("data" / GreedyBytes)))
417 | ))
418 |
419 | BitRateBox = Struct(
420 | "type" / Const(b"btrt"),
421 | "bufferSizeDB" / Int32ub,
422 | "maxBitrate" / Int32ub,
423 | "avgBirate" / Int32ub,
424 | )
425 |
426 | SampleDescriptionBox = Struct(
427 | "type" / Const(b"stsd"),
428 | "version" / Default(Int8ub, 0),
429 | "flags" / Const(Int24ub, 0),
430 | "entries" / PrefixedArray(Int32ub, SampleEntryBox)
431 | )
432 |
433 | SampleSizeBox = Struct(
434 | "type" / Const(b"stsz"),
435 | "version" / Int8ub,
436 | "flags" / Const(Int24ub, 0),
437 | "sample_size" / Int32ub,
438 | "sample_count" / Int32ub,
439 | "entry_sizes" / If(this.sample_size == 0, Array(this.sample_count, Int32ub))
440 | )
441 |
442 | SampleSizeBox2 = Struct(
443 | "type" / Const(b"stz2"),
444 | "version" / Int8ub,
445 | "flags" / Const(Int24ub, 0),
446 | Padding(3, pattern=b"\x00"),
447 | "field_size" / Int8ub,
448 | "sample_count" / Int24ub,
449 | "entries" / Array(this.sample_count, Struct(
450 | "entry_size" / LazyBound(lambda ctx: globals()["Int%dub" % ctx.field_size])
451 | ))
452 | )
453 |
454 | SampleDegradationPriorityBox = Struct(
455 | "type" / Const(b"stdp"),
456 | "version" / Const(Int8ub, 0),
457 | "flags" / Const(Int24ub, 0),
458 | )
459 |
460 | TimeToSampleBox = Struct(
461 | "type" / Const(b"stts"),
462 | "version" / Const(Int8ub, 0),
463 | "flags" / Const(Int24ub, 0),
464 | "entries" / Default(PrefixedArray(Int32ub, Struct(
465 | "sample_count" / Int32ub,
466 | "sample_delta" / Int32ub,
467 | )), [])
468 | )
469 |
470 | SyncSampleBox = Struct(
471 | "type" / Const(b"stss"),
472 | "version" / Const(Int8ub, 0),
473 | "flags" / Const(Int24ub, 0),
474 | "entries" / Default(PrefixedArray(Int32ub, Struct(
475 | "sample_number" / Int32ub,
476 | )), [])
477 | )
478 |
479 | SampleToChunkBox = Struct(
480 | "type" / Const(b"stsc"),
481 | "version" / Const(Int8ub, 0),
482 | "flags" / Const(Int24ub, 0),
483 | "entries" / Default(PrefixedArray(Int32ub, Struct(
484 | "first_chunk" / Int32ub,
485 | "samples_per_chunk" / Int32ub,
486 | "sample_description_index" / Int32ub,
487 | )), [])
488 | )
489 |
490 | ChunkOffsetBox = Struct(
491 | "type" / Const(b"stco"),
492 | "version" / Const(Int8ub, 0),
493 | "flags" / Const(Int24ub, 0),
494 | "entries" / Default(PrefixedArray(Int32ub, Struct(
495 | "chunk_offset" / Int32ub,
496 | )), [])
497 | )
498 |
499 | ChunkLargeOffsetBox = Struct(
500 | "type" / Const(b"co64"),
501 | "version" / Const(Int8ub, 0),
502 | "flags" / Const(Int24ub, 0),
503 | "entries" / PrefixedArray(Int32ub, Struct(
504 | "chunk_offset" / Int64ub,
505 | ))
506 | )
507 |
508 | # Movie Fragment boxes, contained in moof box
509 |
510 | MovieFragmentHeaderBox = Struct(
511 | "type" / Const(b"mfhd"),
512 | "version" / Const(Int8ub, 0),
513 | "flags" / Const(Int24ub, 0),
514 | "sequence_number" / Int32ub
515 | )
516 |
517 | TrackFragmentBaseMediaDecodeTimeBox = Struct(
518 | "type" / Const(b"tfdt"),
519 | "version" / Int8ub,
520 | "flags" / Const(Int24ub, 0),
521 | "baseMediaDecodeTime" / Switch(this.version, {1: Int64ub, 0: Int32ub})
522 | )
523 |
524 | TrackSampleFlags = BitStruct(
525 | Padding(4),
526 | "is_leading" / Default(Enum(BitsInteger(2), UNKNOWN=0, LEADINGDEP=1, NOTLEADING=2, LEADINGNODEP=3, default=0), 0),
527 | "sample_depends_on" / Default(Enum(BitsInteger(2), UNKNOWN=0, DEPENDS=1, NOTDEPENDS=2, RESERVED=3, default=0), 0),
528 | "sample_is_depended_on" / Default(Enum(BitsInteger(2), UNKNOWN=0, NOTDISPOSABLE=1, DISPOSABLE=2, RESERVED=3, default=0), 0),
529 | "sample_has_redundancy" / Default(Enum(BitsInteger(2), UNKNOWN=0, REDUNDANT=1, NOTREDUNDANT=2, RESERVED=3, default=0), 0),
530 | "sample_padding_value" / Default(BitsInteger(3), 0),
531 | "sample_is_non_sync_sample" / Default(Flag, False),
532 | "sample_degradation_priority" / Default(BitsInteger(16), 0),
533 | )
534 |
535 | TrackRunBox = Struct(
536 | "type" / Const(b"trun"),
537 | "version" / Int8ub,
538 | "flags" / BitStruct(
539 | Padding(12),
540 | "sample_composition_time_offsets_present" / Flag,
541 | "sample_flags_present" / Flag,
542 | "sample_size_present" / Flag,
543 | "sample_duration_present" / Flag,
544 | Padding(5),
545 | "first_sample_flags_present" / Flag,
546 | Padding(1),
547 | "data_offset_present" / Flag,
548 | ),
549 | "sample_count" / Int32ub,
550 | "data_offset" / Default(If(this.flags.data_offset_present, Int32sb), None),
551 | "first_sample_flags" / Default(If(this.flags.first_sample_flags_present, Int32ub), None),
552 | "sample_info" / Array(this.sample_count, Struct(
553 | "sample_duration" / If(this._.flags.sample_duration_present, Int32ub),
554 | "sample_size" / If(this._.flags.sample_size_present, Int32ub),
555 | "sample_flags" / If(this._.flags.sample_flags_present, TrackSampleFlags),
556 | "sample_composition_time_offsets" / If(
557 | this._.flags.sample_composition_time_offsets_present,
558 | IfThenElse(this._.version == 0, Int32ub, Int32sb)
559 | ),
560 | ))
561 | )
562 |
563 | TrackFragmentHeaderBox = Struct(
564 | "type" / Const(b"tfhd"),
565 | "version" / Int8ub,
566 | "flags" / BitStruct(
567 | Padding(6),
568 | "default_base_is_moof" / Flag,
569 | "duration_is_empty" / Flag,
570 | Padding(10),
571 | "default_sample_flags_present" / Flag,
572 | "default_sample_size_present" / Flag,
573 | "default_sample_duration_present" / Flag,
574 | Padding(1),
575 | "sample_description_index_present" / Flag,
576 | "base_data_offset_present" / Flag,
577 | ),
578 | "track_ID" / Int32ub,
579 | "base_data_offset" / Default(If(this.flags.base_data_offset_present, Int64ub), None),
580 | "sample_description_index" / Default(If(this.flags.sample_description_index_present, Int32ub), None),
581 | "default_sample_duration" / Default(If(this.flags.default_sample_duration_present, Int32ub), None),
582 | "default_sample_size" / Default(If(this.flags.default_sample_size_present, Int32ub), None),
583 | "default_sample_flags" / Default(If(this.flags.default_sample_flags_present, TrackSampleFlags), None),
584 | )
585 |
586 | MovieExtendsHeaderBox = Struct(
587 | "type" / Const(b"mehd"),
588 | "version" / Default(Int8ub, 0),
589 | "flags" / Const(Int24ub, 0),
590 | "fragment_duration" / IfThenElse(this.version == 1,
591 | Default(Int64ub, 0),
592 | Default(Int32ub, 0))
593 | )
594 |
595 | TrackExtendsBox = Struct(
596 | "type" / Const(b"trex"),
597 | "version" / Const(Int8ub, 0),
598 | "flags" / Const(Int24ub, 0),
599 | "track_ID" / Int32ub,
600 | "default_sample_description_index" / Default(Int32ub, 1),
601 | "default_sample_duration" / Default(Int32ub, 0),
602 | "default_sample_size" / Default(Int32ub, 0),
603 | "default_sample_flags" / Default(TrackSampleFlags, Container()),
604 | )
605 |
606 | SegmentIndexBox = Struct(
607 | "type" / Const(b"sidx"),
608 | "version" / Int8ub,
609 | "flags" / Const(Int24ub, 0),
610 | "reference_ID" / Int32ub,
611 | "timescale" / Int32ub,
612 | "earliest_presentation_time" / IfThenElse(this.version == 0, Int32ub, Int64ub),
613 | "first_offset" / IfThenElse(this.version == 0, Int32ub, Int64ub),
614 | Padding(2),
615 | "reference_count" / Int16ub,
616 | "references" / Array(this.reference_count, BitStruct(
617 | "reference_type" / Enum(BitsInteger(1), INDEX=1, MEDIA=0),
618 | "referenced_size" / BitsInteger(31),
619 | "segment_duration" / BitsInteger(32),
620 | "starts_with_SAP" / Flag,
621 | "SAP_type" / BitsInteger(3),
622 | "SAP_delta_time" / BitsInteger(28),
623 | ))
624 | )
625 |
626 | SampleAuxiliaryInformationSizesBox = Struct(
627 | "type" / Const(b"saiz"),
628 | "version" / Const(Int8ub, 0),
629 | "flags" / BitStruct(
630 | Padding(23),
631 | "has_aux_info_type" / Flag,
632 | ),
633 | # Optional fields
634 | "aux_info_type" / Default(If(this.flags.has_aux_info_type, Int32ub), None),
635 | "aux_info_type_parameter" / Default(If(this.flags.has_aux_info_type, Int32ub), None),
636 | "default_sample_info_size" / Int8ub,
637 | "sample_count" / Int32ub,
638 | # only if sample default_sample_info_size is 0
639 | "sample_info_sizes" / If(this.default_sample_info_size == 0,
640 | Array(this.sample_count, Int8ub))
641 | )
642 |
643 | SampleAuxiliaryInformationOffsetsBox = Struct(
644 | "type" / Const(b"saio"),
645 | "version" / Int8ub,
646 | "flags" / BitStruct(
647 | Padding(23),
648 | "has_aux_info_type" / Flag,
649 | ),
650 | # Optional fields
651 | "aux_info_type" / Default(If(this.flags.has_aux_info_type, Int32ub), None),
652 | "aux_info_type_parameter" / Default(If(this.flags.has_aux_info_type, Int32ub), None),
653 | # Short offsets in version 0, long in version 1
654 | "offsets" / PrefixedArray(Int32ub, Switch(this.version, {0: Int32ub, 1: Int64ub}))
655 | )
656 |
657 | # Movie data box
658 |
659 | MovieDataBox = Struct(
660 | "type" / Const(b"mdat"),
661 | "data" / GreedyBytes
662 | )
663 |
664 | # Media Info Box
665 |
666 | SoundMediaHeaderBox = Struct(
667 | "type" / Const(b"smhd"),
668 | "version" / Const(Int8ub, 0),
669 | "flags" / Const(Int24ub, 0),
670 | "balance" / Default(Int16sb, 0),
671 | "reserved" / Const(Int16ub, 0)
672 | )
673 |
674 |
675 | # DASH Boxes
676 |
677 | class UUIDBytes(Adapter):
678 | def _decode(self, obj, context):
679 | return UUID(bytes=obj)
680 |
681 | def _encode(self, obj, context):
682 | return obj.bytes
683 |
684 |
685 | ProtectionSystemHeaderBox = Struct(
686 | "type" / If(this._.type != b"uuid", Const(b"pssh")),
687 | "version" / Rebuild(Int8ub, lambda ctx: 1 if (hasattr(ctx, "key_IDs") and ctx.key_IDs) else 0),
688 | "flags" / Const(Int24ub, 0),
689 | "system_ID" / UUIDBytes(Bytes(16)),
690 | "key_IDs" / Default(If(this.version == 1,
691 | PrefixedArray(Int32ub, UUIDBytes(Bytes(16)))),
692 | None),
693 | "init_data" / Prefixed(Int32ub, GreedyBytes)
694 | )
695 |
696 | TrackEncryptionBox = Struct(
697 | "type" / If(this._.type != b"uuid", Const(b"tenc")),
698 | "version" / Default(OneOf(Int8ub, (0, 1)), 0),
699 | "flags" / Default(Int24ub, 0),
700 | "_reserved" / Const(Int8ub, 0),
701 | "default_byte_blocks" / Default(IfThenElse(
702 | this.version > 0,
703 | BitStruct(
704 | # count of encrypted blocks in the protection pattern, where each block is 16-bytes
705 | "crypt" / Nibble,
706 | # count of unencrypted blocks in the protection pattern
707 | "skip" / Nibble
708 | ),
709 | Const(Int8ub, 0)
710 | ), 0),
711 | "is_encrypted" / OneOf(Int8ub, (0, 1)),
712 | "iv_size" / OneOf(Int8ub, (0, 8, 16)),
713 | "key_ID" / UUIDBytes(Bytes(16)),
714 | "constant_iv" / Default(If(
715 | this.is_encrypted and this.iv_size == 0,
716 | PrefixedArray(Int8ub, Byte)
717 | ), None)
718 | )
719 |
720 | SampleEncryptionBox = Struct(
721 | "type" / If(this._.type != b"uuid", Const(b"senc")),
722 | "version" / Const(Int8ub, 0),
723 | "flags" / BitStruct(
724 | Padding(22),
725 | "has_subsample_encryption_info" / Flag,
726 | Padding(1)
727 | ),
728 | "sample_encryption_info" / PrefixedArray(Int32ub, Struct(
729 | "iv" / Bytes(8),
730 | # include the sub sample encryption information
731 | "subsample_encryption_info" / Default(If(this.flags.has_subsample_encryption_info, PrefixedArray(Int16ub, Struct(
732 | "clear_bytes" / Int16ub,
733 | "cipher_bytes" / Int32ub
734 | ))), None)
735 | ))
736 | )
737 |
738 | OriginalFormatBox = Struct(
739 | "type" / Const(b"frma"),
740 | "original_format" / Default(String(4), b"avc1")
741 | )
742 |
743 | SchemeTypeBox = Struct(
744 | "type" / Const(b"schm"),
745 | "version" / Default(Int8ub, 0),
746 | "flags" / Default(Int24ub, 0),
747 | "scheme_type" / Default(String(4), b"cenc"),
748 | "scheme_version" / Default(Int32ub, 0x00010000),
749 | "schema_uri" / Default(If(this.flags & 1 == 1, CString()), None)
750 | )
751 |
752 | ProtectionSchemeInformationBox = Struct(
753 | "type" / Const(b"sinf"),
754 | # TODO: define which children are required 'schm', 'schi' and 'tenc'
755 | "children" / LazyBound(lambda _: GreedyRange(Box))
756 | )
757 |
758 | # PIFF boxes
759 |
760 | UUIDBox = Struct(
761 | "type" / Const(b"uuid"),
762 | "extended_type" / UUIDBytes(Bytes(16)),
763 | "data" / Switch(this.extended_type, {
764 | UUID("A2394F52-5A9B-4F14-A244-6C427C648DF4"): SampleEncryptionBox,
765 | UUID("D08A4F18-10F3-4A82-B6C8-32D8ABA183D3"): ProtectionSystemHeaderBox,
766 | UUID("8974DBCE-7BE7-4C51-84F9-7148F9882554"): TrackEncryptionBox
767 | }, GreedyBytes)
768 | )
769 |
770 | # WebVTT boxes
771 |
772 | CueIDBox = Struct(
773 | "type" / Const(b"iden"),
774 | "cue_id" / GreedyString("utf8")
775 | )
776 |
777 | CueSettingsBox = Struct(
778 | "type" / Const(b"sttg"),
779 | "settings" / GreedyString("utf8")
780 | )
781 |
782 | CuePayloadBox = Struct(
783 | "type" / Const(b"payl"),
784 | "cue_text" / GreedyString("utf8")
785 | )
786 |
787 | WebVTTConfigurationBox = Struct(
788 | "type" / Const(b"vttC"),
789 | "config" / GreedyString("utf8")
790 | )
791 |
792 | WebVTTSourceLabelBox = Struct(
793 | "type" / Const(b"vlab"),
794 | "label" / GreedyString("utf8")
795 | )
796 |
797 | ContainerBoxLazy = LazyBound(lambda ctx: ContainerBox)
798 |
799 |
800 | class TellMinusSizeOf(Subconstruct):
801 | def __init__(self, subcon):
802 | super(TellMinusSizeOf, self).__init__(subcon)
803 | self.flagbuildnone = True
804 |
805 | def _parse(self, stream, context, path):
806 | return stream.tell() - self.subcon.sizeof(context)
807 |
808 | def _build(self, obj, stream, context, path):
809 | return b""
810 |
811 | def sizeof(self, context=None, **kw):
812 | return 0
813 |
814 |
815 | Box = PrefixedIncludingSize(Int32ub, Struct(
816 | "offset" / TellMinusSizeOf(Int32ub),
817 | "type" / Peek(String(4, padchar=b" ", paddir="right")),
818 | Embedded(Switch(this.type, {
819 | b"ftyp": FileTypeBox,
820 | b"styp": SegmentTypeBox,
821 | b"mvhd": MovieHeaderBox,
822 | b"moov": ContainerBoxLazy,
823 | b"moof": ContainerBoxLazy,
824 | b"mfhd": MovieFragmentHeaderBox,
825 | b"tfdt": TrackFragmentBaseMediaDecodeTimeBox,
826 | b"trun": TrackRunBox,
827 | b"tfhd": TrackFragmentHeaderBox,
828 | b"traf": ContainerBoxLazy,
829 | b"mvex": ContainerBoxLazy,
830 | b"mehd": MovieExtendsHeaderBox,
831 | b"trex": TrackExtendsBox,
832 | b"trak": ContainerBoxLazy,
833 | b"mdia": ContainerBoxLazy,
834 | b"tkhd": TrackHeaderBox,
835 | b"mdat": MovieDataBox,
836 | b"free": FreeBox,
837 | b"skip": SkipBox,
838 | b"mdhd": MediaHeaderBox,
839 | b"hdlr": HandlerReferenceBox,
840 | b"minf": ContainerBoxLazy,
841 | b"vmhd": VideoMediaHeaderBox,
842 | b"dinf": ContainerBoxLazy,
843 | b"dref": DataReferenceBox,
844 | b"stbl": ContainerBoxLazy,
845 | b"stsd": SampleDescriptionBox,
846 | b"stsz": SampleSizeBox,
847 | b"stz2": SampleSizeBox2,
848 | b"stts": TimeToSampleBox,
849 | b"stss": SyncSampleBox,
850 | b"stsc": SampleToChunkBox,
851 | b"stco": ChunkOffsetBox,
852 | b"co64": ChunkLargeOffsetBox,
853 | b"smhd": SoundMediaHeaderBox,
854 | b"sidx": SegmentIndexBox,
855 | b"saiz": SampleAuxiliaryInformationSizesBox,
856 | b"saio": SampleAuxiliaryInformationOffsetsBox,
857 | b"btrt": BitRateBox,
858 | # dash
859 | b"tenc": TrackEncryptionBox,
860 | b"pssh": ProtectionSystemHeaderBox,
861 | b"senc": SampleEncryptionBox,
862 | b"sinf": ProtectionSchemeInformationBox,
863 | b"frma": OriginalFormatBox,
864 | b"schm": SchemeTypeBox,
865 | b"schi": ContainerBoxLazy,
866 | # piff
867 | b"uuid": UUIDBox,
868 | # HDS boxes
869 | b'abst': HDSSegmentBox,
870 | b'asrt': HDSSegmentRunBox,
871 | b'afrt': HDSFragmentRunBox,
872 | # WebVTT
873 | b"vttC": WebVTTConfigurationBox,
874 | b"vlab": WebVTTSourceLabelBox,
875 | b"vttc": ContainerBoxLazy,
876 | b"vttx": ContainerBoxLazy,
877 | b"iden": CueIDBox,
878 | b"sttg": CueSettingsBox,
879 | b"payl": CuePayloadBox
880 | }, default=RawBox)),
881 | "end" / Tell
882 | ))
883 |
884 | ContainerBox = Struct(
885 | "type" / String(4, padchar=b" ", paddir="right"),
886 | "children" / GreedyRange(Box)
887 | )
888 |
889 | MP4 = GreedyRange(Box)
890 |
--------------------------------------------------------------------------------
/src/pymp4/tools/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import logging
3 |
4 | log = logging.getLogger(__name__)
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/pymp4/util.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Copyright 2016-2019 beardypig
4 | Copyright 2017-2019 truedread
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 | """
18 | import logging
19 |
20 | from pymp4.exceptions import BoxNotFound
21 |
22 | log = logging.getLogger(__name__)
23 |
24 |
25 | class BoxUtil(object):
26 | @classmethod
27 | def first(cls, box, type_):
28 | if box.type == type_:
29 | return box
30 | if hasattr(box, "children"):
31 | for sbox in box.children:
32 | try:
33 | return cls.first(sbox, type_)
34 | except BoxNotFound:
35 | # ignore the except when the box is not found in sub-boxes
36 | pass
37 |
38 | raise BoxNotFound("could not find box of type: {}".format(type_))
39 |
40 | @classmethod
41 | def index(cls, box, type_):
42 | if hasattr(box, "children"):
43 | for i, box in enumerate(box.children):
44 | if box.type == type_:
45 | return i
46 |
47 | @classmethod
48 | def find(cls, box, type_):
49 | if box.type == type_:
50 | yield box
51 | elif hasattr(box, "children"):
52 | for sbox in box.children:
53 | for fbox in cls.find(sbox, type_):
54 | yield fbox
55 |
56 | @classmethod
57 | def find_extended(cls, box, extended_type_):
58 | if hasattr(box, "extended_type"):
59 | if box.extended_type == extended_type_:
60 | yield box
61 | elif hasattr(box, "children"):
62 | for sbox in box.children:
63 | for fbox in cls.find_extended(sbox, extended_type_):
64 | yield fbox
65 | elif hasattr(box, "children"):
66 | for sbox in box.children:
67 | for fbox in cls.find_extended(sbox, extended_type_):
68 | yield fbox
69 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beardypig/pymp4/8e0045511adc212ea0427435a58a6887948bbd8d/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_box.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Copyright 2016 beardypig
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | """
17 | import logging
18 | import unittest
19 |
20 | from construct import Container
21 | from pymp4.parser import Box
22 |
23 | log = logging.getLogger(__name__)
24 |
25 |
26 | class BoxTests(unittest.TestCase):
27 | def test_ftyp_parse(self):
28 | self.assertEqual(
29 | Box.parse(b'\x00\x00\x00\x18ftypiso5\x00\x00\x00\x01iso5avc1'),
30 | Container(offset=0)
31 | (type=b"ftyp")
32 | (major_brand=b"iso5")
33 | (minor_version=1)
34 | (compatible_brands=[b"iso5", b"avc1"])
35 | (end=24)
36 | )
37 |
38 | def test_ftyp_build(self):
39 | self.assertEqual(
40 | Box.build(dict(
41 | type=b"ftyp",
42 | major_brand=b"iso5",
43 | minor_version=1,
44 | compatible_brands=[b"iso5", b"avc1"])),
45 | b'\x00\x00\x00\x18ftypiso5\x00\x00\x00\x01iso5avc1')
46 |
47 | def test_mdhd_parse(self):
48 | self.assertEqual(
49 | Box.parse(
50 | b'\x00\x00\x00\x20mdhd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0fB@\x00\x00\x00\x00U\xc4\x00\x00'),
51 | Container(offset=0)
52 | (type=b"mdhd")(version=0)(flags=0)
53 | (creation_time=0)
54 | (modification_time=0)
55 | (timescale=1000000)
56 | (duration=0)
57 | (language="und")
58 | (end=32)
59 | )
60 |
61 | def test_mdhd_build(self):
62 | mdhd_data = Box.build(dict(
63 | type=b"mdhd",
64 | creation_time=0,
65 | modification_time=0,
66 | timescale=1000000,
67 | duration=0,
68 | language=u"und"))
69 | self.assertEqual(len(mdhd_data), 32)
70 | self.assertEqual(mdhd_data,
71 | b'\x00\x00\x00\x20mdhd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0fB@\x00\x00\x00\x00U\xc4\x00\x00')
72 |
73 | mdhd_data64 = Box.build(dict(
74 | type=b"mdhd",
75 | version=1,
76 | creation_time=0,
77 | modification_time=0,
78 | timescale=1000000,
79 | duration=0,
80 | language=u"und"))
81 | self.assertEqual(len(mdhd_data64), 44)
82 | self.assertEqual(mdhd_data64,
83 | b'\x00\x00\x00,mdhd\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0fB@\x00\x00\x00\x00\x00\x00\x00\x00U\xc4\x00\x00')
84 |
85 | def test_moov_build(self):
86 | moov = \
87 | Container(type=b"moov")(children=[ # 96 bytes
88 | Container(type=b"mvex")(children=[ # 88 bytes
89 | Container(type=b"mehd")(version=0)(flags=0)(fragment_duration=0), # 16 bytes
90 | Container(type=b"trex")(track_ID=1), # 32 bytes
91 | Container(type=b"trex")(track_ID=2), # 32 bytes
92 | ])
93 | ])
94 |
95 | moov_data = Box.build(moov)
96 |
97 | self.assertEqual(len(moov_data), 96)
98 | self.assertEqual(
99 | moov_data,
100 | b'\x00\x00\x00\x60moov'
101 | b'\x00\x00\x00\x58mvex'
102 | b'\x00\x00\x00\x10mehd\x00\x00\x00\x00\x00\x00\x00\x00'
103 | b'\x00\x00\x00\x20trex\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
104 | b'\x00\x00\x00\x20trex\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
105 | )
106 |
107 | def test_smhd_parse(self):
108 | in_bytes = b'\x00\x00\x00\x10smhd\x00\x00\x00\x00\x00\x00\x00\x00'
109 | self.assertEqual(
110 | Box.parse(in_bytes + b'padding'),
111 | Container(offset=0)
112 | (type=b"smhd")(version=0)(flags=0)
113 | (balance=0)(reserved=0)(end=len(in_bytes))
114 | )
115 |
116 | def test_smhd_build(self):
117 | smhd_data = Box.build(dict(
118 | type=b"smhd",
119 | balance=0))
120 | self.assertEqual(len(smhd_data), 16),
121 | self.assertEqual(smhd_data, b'\x00\x00\x00\x10smhd\x00\x00\x00\x00\x00\x00\x00\x00')
122 |
123 | def test_stsd_parse(self):
124 | tx3g_data = b'\x00\x00\x00\x00\x01\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x12\xFF\xFF\xFF\xFF\x00\x00\x00\x12ftab\x00\x01\x00\x01\x05Serif'
125 | in_bytes = b'\x00\x00\x00\x50stsd\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x40tx3g\x00\x00\x00\x00\x00\x00\x00\x01' + tx3g_data
126 | self.assertEqual(
127 | Box.parse(in_bytes + b'padding'),
128 | Container(offset=0)
129 | (type=b"stsd")(version=0)(flags=0)
130 | (entries=[Container(format=b'tx3g')(data_reference_index=1)(data=tx3g_data)])
131 | (end=len(in_bytes))
132 | )
133 |
--------------------------------------------------------------------------------
/tests/test_dashboxes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Copyright 2016 beardypig
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | """
17 | import logging
18 | import unittest
19 | from uuid import UUID
20 |
21 | from construct import Container
22 | from pymp4.parser import Box
23 |
24 | log = logging.getLogger(__name__)
25 |
26 |
27 | class BoxTests(unittest.TestCase):
28 | def test_tenc_parse(self):
29 | self.assertEqual(
30 | Box.parse(b'\x00\x00\x00 tenc\x00\x00\x00\x00\x00\x00\x01\x083{\x96C!\xb6CU\x9eY>\xcc\xb4l~\xf7'),
31 | Container(offset=0)
32 | (type=b"tenc")
33 | (version=0)
34 | (flags=0)
35 | (_reserved=0)
36 | (default_byte_blocks=0)
37 | (is_encrypted=1)
38 | (iv_size=8)
39 | (key_ID=UUID('337b9643-21b6-4355-9e59-3eccb46c7ef7'))
40 | (constant_iv=None)
41 | (end=32)
42 | )
43 |
44 | def test_tenc_build(self):
45 | self.assertEqual(
46 | Box.build(dict(
47 | type=b"tenc",
48 | key_ID=UUID('337b9643-21b6-4355-9e59-3eccb46c7ef7'),
49 | iv_size=8,
50 | is_encrypted=1)),
51 | b'\x00\x00\x00 tenc\x00\x00\x00\x00\x00\x00\x01\x083{\x96C!\xb6CU\x9eY>\xcc\xb4l~\xf7')
52 |
--------------------------------------------------------------------------------
/tests/test_util.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Copyright 2016-2019 beardypig
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | """
17 | import logging
18 | import unittest
19 |
20 | from construct import Container
21 |
22 | from pymp4.exceptions import BoxNotFound
23 | from pymp4.util import BoxUtil
24 |
25 | log = logging.getLogger(__name__)
26 |
27 |
28 | class BoxTests(unittest.TestCase):
29 | box_data = Container(type=b"demo")(children=[
30 | Container(type=b"a ")(id=1),
31 | Container(type=b"b ")(id=2),
32 | Container(type=b"c ")(children=[
33 | Container(type=b"a ")(id=3),
34 | Container(type=b"b ")(id=4),
35 | ]),
36 | Container(type=b"d ")(id=5),
37 | ])
38 |
39 | box_extended_data = Container(type=b"test")(children=[
40 | Container(type=b"a ")(id=1, extended_type=b"e--a"),
41 | Container(type=b"b ")(id=2, extended_type=b"e--b"),
42 | ])
43 |
44 | def test_find(self):
45 | self.assertListEqual(
46 | list(BoxUtil.find(self.box_data, b"b ")),
47 | [Container(type=b"b ")(id=2), Container(type=b"b ")(id=4)]
48 | )
49 |
50 | def test_find_after_nest(self):
51 | self.assertListEqual(
52 | list(BoxUtil.find(self.box_data, b"d ")),
53 | [Container(type=b"d ")(id=5)]
54 | )
55 |
56 | def test_find_nested_type(self):
57 | self.assertListEqual(
58 | list(BoxUtil.find(self.box_data, b"c ")),
59 | [Container(type=b"c ")(children=[
60 | Container(type=b"a ")(id=3),
61 | Container(type=b"b ")(id=4),
62 | ])]
63 | )
64 |
65 | def test_find_empty(self):
66 | self.assertListEqual(
67 | list(BoxUtil.find(self.box_data, b"f ")),
68 | []
69 | )
70 |
71 | def test_first(self):
72 | self.assertEqual(
73 | BoxUtil.first(self.box_data, b"b "),
74 | Container(type=b"b ")(id=2)
75 | )
76 |
77 | def test_first_missing(self):
78 | self.assertRaises(
79 | BoxNotFound,
80 | BoxUtil.first, self.box_data, b"f ",
81 | )
82 |
83 | def test_find_extended(self):
84 | self.assertListEqual(
85 | list(BoxUtil.find_extended(self.box_extended_data, b"e--a")),
86 | [Container(type=b"a ")(id=1, extended_type=b"e--a")]
87 | )
88 |
--------------------------------------------------------------------------------
/tests/test_webvtt_boxes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import logging
3 | import unittest
4 |
5 | from construct import Container
6 | from pymp4.parser import Box
7 |
8 | log = logging.getLogger(__name__)
9 |
10 |
11 | class BoxTests(unittest.TestCase):
12 | def test_iden_parse(self):
13 | self.assertEqual(
14 | Box.parse(b'\x00\x00\x00\x27iden2 - this is the second subtitle'),
15 | Container(offset=0)
16 | (type=b"iden")
17 | (cue_id="2 - this is the second subtitle")
18 | (end=39)
19 | )
20 |
21 | def test_iden_build(self):
22 | self.assertEqual(
23 | Box.build(dict(
24 | type=b"iden",
25 | cue_id="1 - first subtitle")),
26 | b'\x00\x00\x00\x1aiden1 - first subtitle')
27 |
28 | def test_sttg_parse(self):
29 | self.assertEqual(
30 | Box.parse(b'\x00\x00\x003sttgline:10% position:50% size:48% align:center'),
31 | Container(offset=0)
32 | (type=b"sttg")
33 | (settings="line:10% position:50% size:48% align:center")
34 | (end=51)
35 | )
36 |
37 | def test_sttg_build(self):
38 | self.assertEqual(
39 | Box.build(dict(
40 | type=b"sttg",
41 | settings="line:75% position:20% size:2em align:right")),
42 | b'\x00\x00\x002sttgline:75% position:20% size:2em align:right')
43 |
44 | def test_payl_parse(self):
45 | self.assertEqual(
46 | Box.parse(b'\x00\x00\x00\x13payl[chuckling]'),
47 | Container(offset=0)
48 | (type=b"payl")
49 | (cue_text="[chuckling]")
50 | (end=19)
51 | )
52 |
53 | def test_payl_build(self):
54 | self.assertEqual(
55 | Box.build(dict(
56 | type=b"payl",
57 | cue_text="I have a bad feeling about- [boom]")),
58 | b'\x00\x00\x00*paylI have a bad feeling about- [boom]')
59 |
60 | def test_vttC_parse(self):
61 | self.assertEqual(
62 | Box.parse(b'\x00\x00\x00\x0evttCWEBVTT'),
63 | Container(offset=0)
64 | (type=b"vttC")
65 | (config="WEBVTT")
66 | (end=14)
67 | )
68 |
69 | def test_vttC_build(self):
70 | self.assertEqual(
71 | Box.build(dict(
72 | type=b"vttC",
73 | config="WEBVTT with a text header\n\nSTYLE\n::cue {\ncolor: red;\n}")),
74 | b'\x00\x00\x00>vttCWEBVTT with a text header\n\nSTYLE\n::cue {\ncolor: red;\n}')
75 |
76 | def test_vlab_parse(self):
77 | self.assertEqual(
78 | Box.parse(b'\x00\x00\x00\x14vlabsource_label'),
79 | Container(offset=0)
80 | (type=b"vlab")
81 | (label="source_label")
82 | (end=20)
83 | )
84 |
85 | def test_vlab_build(self):
86 | self.assertEqual(
87 | Box.build(dict(
88 | type=b"vlab",
89 | label="1234 \n test_label \n\n")),
90 | b'\x00\x00\x00\x1cvlab1234 \n test_label \n\n')
91 |
--------------------------------------------------------------------------------