├── .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 | [![Build status](https://github.com/beardypig/pymp4/actions/workflows/ci.yml/badge.svg)](https://github.com/beardypig/pymp4/actions/workflows/ci.yml) 4 | [![License](https://img.shields.io/pypi/l/pymp4)](LICENSE) 5 | [![Python versions](https://img.shields.io/pypi/pyversions/pymp4)](https://pypi.org/project/pymp4) 6 | [![Coverage](https://codecov.io/gh/beardypig/pymp4/branch/master/graph/badge.svg)](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 | --------------------------------------------------------------------------------