├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── deploy_package.yaml │ └── docs.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.txt ├── README.md ├── codecov.yml ├── docs ├── Makefile ├── _static │ └── .empty ├── _templates │ └── autosummary │ │ └── module.rst ├── api │ └── otio-plugins.md ├── conf.py ├── gen_api_docs.py ├── index.rst ├── make.bat └── requirements.txt ├── pyproject.toml ├── src └── otio_aaf_adapter │ ├── __init__.py │ ├── adapters │ ├── __init__.py │ ├── aaf_adapter │ │ ├── __init__.py │ │ ├── aaf_writer.py │ │ └── hooks.py │ └── advanced_authoring_format.py │ └── plugin_manifest.json └── tests ├── hooks_plugin_example ├── plugin_manifest.json ├── post_aaf_read_transcribe_hook.py ├── post_aaf_write_transcribe_hook.py ├── pre_aaf_read_transcribe_hook.py └── pre_aaf_write_transcribe_hook.py ├── sample_data ├── 2997fps-DFTC.aaf ├── 2997fps.aaf ├── 30fps.aaf ├── avid_data_track_example.aaf ├── bad_marker_track_from_avid.aaf ├── composite.aaf ├── duplicates.aaf ├── essence_group.aaf ├── gaps.otio ├── keyframed_properties.aaf ├── linear_speed_effects.aaf ├── linear_speed_effects_aaf.mov ├── marker-over-audio.aaf ├── marker-over-transition.aaf ├── misc_speed_effects.aaf ├── misc_speed_effects_aaf.mov ├── multiple-markers-over-transitions.aaf ├── multiple-markers-over-transitions.txt ├── multiple_markers.aaf ├── multiple_timecode_objects.aaf ├── multiple_top_level_mobs.aaf ├── multitrack.aaf ├── nested_audio_dissolve.aaf ├── nested_stack.aaf ├── nesting_test.aaf ├── nesting_test_preflattened.aaf ├── no_metadata.otio ├── normalclip_sourceclip_references_compositionmob_has_also_mastermob_usercomments.aaf ├── normalclip_sourceclip_references_compositionmob_with_usercomments_no_mastermob_usercomments.aaf ├── not_aaf.otio ├── one_audio_clip.aaf ├── one_clip.aaf ├── picchu_seq0100_snippet_dnx.dnx ├── picchu_seq0100_snippet_dnx.mov ├── picchu_seq0100_snippet_embedded.aaf ├── precheckfail.otio ├── preflattened.aaf ├── simple.aaf ├── subclip_sourceclip_references_compositionmob_with_mastermob.aaf ├── test_muted_clip.aaf ├── time_warp_test.avid_media_composer.aaf ├── timecode_test.aaf ├── transitions.aaf ├── trims.aaf └── utf8.aaf └── test_aaf_adapter.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | *.cfg 4 | *.txt 5 | *.md 6 | *.in 7 | .github 8 | .pytest_cache 9 | docs 10 | build 11 | dist 12 | *.egg-info 13 | venv 14 | select = E,W,F 15 | max-line-length = 88 16 | # @TODO: for now, ignoring both line continuation before OR after binary 17 | # operators. The pep8 style seems to have adopted the W504 style as 18 | # correct, which we've been doing conventionally but not 100% uniformly. 19 | # At some point in the future, we should remove W504 from this list and 20 | # conform all the continuation to be the same. 21 | ignore = 22 | W503 23 | W504 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Problem or regression with an existing feature 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Before opening a bug report, please search open and closed issues to see if the problem has already been addressed. If there is such a report, and your issue is not adequately addressed there, please reference the issues in your new report. 11 | 12 | ## Bug Report 13 | 14 | Please pick one of the following categories: 15 | 16 | ### Build Problem 17 | 18 | Please provide your complete command line invocation, and attach a log of the console output. 19 | 20 | ### Incorrect Functionality and General Questions 21 | 22 | Describe the issue here. 23 | 24 | ## To Reproduce 25 | 26 | 1. Operating System 27 | 2. Python version 28 | 3. Example snippet that demonstrates the issue - if it's a build issue, 29 | 4. OpenTimelineIO release version or commit hash 30 | 31 | ## Expected Behavior 32 | 33 | Description of the expected behavior. 34 | 35 | ## Screenshots 36 | 37 | If applicable, add screenshots to help explain your problem. 38 | 39 | ## Logs 40 | 41 | If applicable, attach a complete console log, that show a reproduction of the problem entirely. 42 | 43 | ## Additional Context 44 | 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for otio-aaf-adapter 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Request 11 | 12 | Is this a New Feature, or a Proposed Change to existing Behavior? 13 | 14 | ## Description 15 | 16 | Describe the new feature, or describe the modification to existing behavior, and provide context as necessary to help with understanding how the feature relates to a workflow or functionality. 17 | 18 | ## Context 19 | 20 | Add any other context here, such as simulated output, or screenshots, that might help clarify the request. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Link the Issue(s) this Pull Request is related to.** 2 | 3 | Each PR should link at least one issue, in the form: 4 | 5 | Fixes #123 6 | 7 | Use one line for each Issue. This allows auto-closing the related issue when the fix is merged. 8 | 9 | **Summarize your change.** 10 | 11 | Describe the reason for the change. 12 | 13 | Add a list of changes, and note any that might need special attention during review. 14 | 15 | **Reference associated tests.** 16 | 17 | If no new tests are introduced as part of this PR, note the tests that are providing coverage. 18 | 19 | 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "pip" 10 | directory: "/" 11 | schedule: 12 | day: "monday" 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | env: 4 | GH_COV_PY: 3.10 5 | GH_COV_OS: ubuntu-latest 6 | GH_COV_OTIO: main 7 | GH_DEPENDABOT: dependabot 8 | on: 9 | push: 10 | branches: [ '*' ] 11 | paths-ignore: 12 | - "*.md" 13 | - "*.in" 14 | - "*.txt" 15 | 16 | pull_request: 17 | branches: [ main ] 18 | paths-ignore: 19 | - "*.md" 20 | - "*.in" 21 | - "*.txt" 22 | 23 | jobs: 24 | build-wheel: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - name: Set up Python 3.10 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.10" 33 | 34 | - name: Install dependencies 35 | run: | 36 | pip install flake8 37 | 38 | - name: Lint with flake8 39 | run: | 40 | flake8 --show-source --statistics 41 | 42 | - name: Install pypa build 43 | run: python -m pip install build 44 | 45 | - name: Create wheel and sdist 46 | run: python -m build -s -w --outdir dist . 47 | 48 | - name: Upload sdist 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: sdist 52 | path: dist/*.tar.gz 53 | 54 | - name: Upload wheel 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: wheel 58 | path: dist/*.whl 59 | 60 | test-plugin: 61 | needs: 62 | - build-wheel 63 | env: 64 | plugin_name: "otio_aaf_plugin" 65 | strategy: 66 | matrix: 67 | # Use macos-13 so we'll be on intel hardware and can pull a pre-built wheel 68 | # When OTIO has an Apple Silicon build we can switch back to macos-latest for that version 69 | os: [ubuntu-latest, macos-13, windows-latest, macos-latest] 70 | otio-version: ["main", "0.17.0"] 71 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 72 | include: 73 | - { os: ubuntu-latest, shell: bash } 74 | - { os: ubuntu-22.04, shell: bash, python-version: 3.7 } 75 | - { os: macos-latest, shell: bash } 76 | - { os: macos-13, shell: bash } 77 | - { os: windows-latest, shell: pwsh } 78 | exclude: 79 | - { os: macos-latest, python-version: 3.7 } 80 | - { os: macos-latest, python-version: 3.8 } 81 | - { os: macos-latest, python-version: 3.9 } 82 | - { os: ubuntu-latest, python-version: 3.7 } 83 | 84 | name: ${{ matrix.os }} py-${{ matrix.python-version }} otio-${{ matrix.otio-version }} 85 | runs-on: ${{ matrix.os }} 86 | 87 | steps: 88 | - uses: actions/checkout@v4 89 | 90 | - name: Set up Python ${{ matrix.python-version }} 91 | uses: actions/setup-python@v5 92 | with: 93 | python-version: ${{ matrix.python-version }} 94 | 95 | - name: Download wheel 96 | uses: actions/download-artifact@v4 97 | with: 98 | name: wheel 99 | path: dist/ 100 | 101 | - name: Install dependencies 102 | shell: bash 103 | run: | 104 | python -m pip install --upgrade pip 105 | pip install pytest pytest-cov wheel -V pyaaf2 106 | if [[ "${{ matrix.otio-version }}" == "main" ]]; then 107 | pip install "git+https://github.com/AcademySoftwareFoundation/OpenTimelineIO.git" 108 | else 109 | pip install OpenTimelineIO>=${{ matrix.otio-version }} --only-binary :all: 110 | fi 111 | 112 | - name: Run Unit Tests 113 | shell: bash 114 | run: | 115 | python -m pip install dist/*.whl --no-index 116 | pytest -v tests 117 | 118 | - name: Upload coverage to Codecov 119 | if: | 120 | matrix.python-version == env.GH_COV_PY && 121 | matrix.otio-version == env.GH_COV_OTIO && 122 | matrix.os == env.GH_COV_OS && 123 | github.actor != env.GH_DEPENDABOT 124 | uses: codecov/codecov-action@v3 125 | with: 126 | flags: unittests 127 | name: otio-aaf-adapter-codecov 128 | fail_ci_if_error: true 129 | 130 | latest-release: 131 | needs: test-plugin 132 | runs-on: ubuntu-latest 133 | steps: 134 | - name: Download sdist 135 | uses: actions/download-artifact@v4 136 | with: 137 | name: sdist 138 | path: dist 139 | 140 | - name: Download wheel 141 | uses: actions/download-artifact@v4 142 | with: 143 | name: wheel 144 | path: dist 145 | 146 | - uses: "marvinpinto/action-automatic-releases@latest" 147 | if: ${{ github.ref == 'refs/heads/main' }} 148 | with: 149 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 150 | automatic_release_tag: "latest" 151 | prerelease: true 152 | title: "Development Build" 153 | files: | 154 | dist/*.tar.gz 155 | dist/*.whl 156 | -------------------------------------------------------------------------------- /.github/workflows/deploy_package.yaml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package to PyPi 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install build 22 | - name: Build dist package 23 | run: | 24 | python -m build 25 | - name: Upload Built Artifacts 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: dist 29 | path: | 30 | ./dist/*.whl 31 | ./dist/*.gz 32 | - name: Publish package distributions to PyPI 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | check-links: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: 'recursive' 16 | 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.10' 20 | 21 | - name: Create virtualenv 22 | run: python3 -m venv .venv 23 | 24 | - name: Install dependencies 25 | run: | 26 | source .venv/bin/activate 27 | python -m pip install . 28 | python -m pip install -r docs/requirements.txt 29 | 30 | - name: Linkcheck 31 | working-directory: docs 32 | run: | 33 | source ../.venv/bin/activate 34 | 35 | set +e 36 | make linkcheck 37 | exit_code=$? 38 | 39 | set -e 40 | 41 | if [ $exit_code -eq 0 ]; then 42 | echo -e "\n\n=================\nAll links are valid!" 43 | 44 | echo "# :heavy_check_mark: Sphinx links" >> $GITHUB_STEP_SUMMARY 45 | echo "All links are valid!" >> $GITHUB_STEP_SUMMARY 46 | else 47 | echo -e "\n\n=================\nFound broken links. Look at the build logs.\n" 48 | 49 | echo "# :x: Sphinx links" >> $GITHUB_STEP_SUMMARY 50 | echo "Found broken links. Look at the build logs for additional information." >> $GITHUB_STEP_SUMMARY 51 | echo '```' >> $GITHUB_STEP_SUMMARY 52 | cat _build/linkcheck/output.txt >> $GITHUB_STEP_SUMMARY 53 | echo '```' >> $GITHUB_STEP_SUMMARY 54 | fi 55 | 56 | exit $exit_code 57 | 58 | check-warnings: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | with: 63 | submodules: 'recursive' 64 | 65 | - uses: actions/setup-python@v5 66 | with: 67 | python-version: '3.10' 68 | 69 | - name: Create virtualenv 70 | run: python3 -m venv .venv 71 | 72 | - name: Install dependencies 73 | run: | 74 | source .venv/bin/activate 75 | python -m pip install . 76 | python -m pip install -r docs/requirements.txt 77 | 78 | - name: Check warnings/errors 79 | working-directory: docs 80 | run: | 81 | source ../.venv/bin/activate 82 | 83 | set +e 84 | make htmlstrict 85 | 86 | exit_code=$? 87 | 88 | set -e 89 | 90 | if [ $exit_code -eq 0 ]; then 91 | echo -e "\n\n=================\nNo warnings or errors detected!" 92 | echo "# :heavy_check_mark: Sphinx warnings/errors" >> $GITHUB_STEP_SUMMARY 93 | echo "No errors or warnings detected!" >> $GITHUB_STEP_SUMMARY 94 | else 95 | echo -e "\n\n=================\nWarnings and or errors detected; See the summary bellow:\n" 96 | cat _build/htmlstrict/output.txt 97 | 98 | echo "# :x: Sphinx warnings/errors" >> $GITHUB_STEP_SUMMARY 99 | echo "Found some warnings or errors:" >> $GITHUB_STEP_SUMMARY 100 | echo '```' >> $GITHUB_STEP_SUMMARY 101 | cat _build/htmlstrict/output.txt >> $GITHUB_STEP_SUMMARY 102 | echo '```' >> $GITHUB_STEP_SUMMARY 103 | fi 104 | 105 | exit $exit_code 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/CODE_OF_CONDUCT.md 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/CONTRIBUTING.md 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | This plugin is part of the OpenTimelineIO project, whose contributors are 2 | listed at this URL: 3 | https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/CONTRIBUTORS.md 4 | 5 | This is a list of people who have contributed code to the 6 | otio-aaf-adapter project, sorted alphabetically by first name. 7 | 8 | If you know of anyone missing from this list, please contact us: 9 | https://lists.aswf.io/g/otio-discussion 10 | 11 | * Andrew Moore ([andrewmoore-nz](https://github.com/andrewmoore-nz)) 12 | * Eric Reinecke ([reinecke](https://github.com/reinecke)) 13 | * Freeson Wang ([freesonluxo](https://github.com/freesonluxo)) 14 | * Josh Burnell ([JoshBurnell](https://github.com/JoshBurnell)) 15 | * Joshua Minor ([jminor](https://github.com/jminor)) 16 | * Julian Yu-Chung Chen ([jchen9](https://github.com/jchen9)) 17 | * Mark Reid ([markreidvfx](https://github.com/markreidvfx)) 18 | * Shahbaz Khan ([shahbazk8194](https://github.com/shahbazk8194)) 19 | * Stefan Schulze ([stefanschulze](https://github.com/stefanschulze)) 20 | * Stephan Steinbach ([ssteinbach](https://github.com/ssteinbach)) 21 | * Tim Lehr ([timlehr](https://github.com/timlehr)) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTimelineIO Advanced Authoring Format (AAF) Adapter 2 | 3 | [![Supported VFX Platform Versions](https://img.shields.io/badge/vfx%20platform-2020--2023-lightgrey.svg)](http://www.vfxplatform.com/) 4 | ![Dynamic YAML Badge](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FOpenTimelineIO%2Fotio-aaf-adapter%2Fmain%2F.github%2Fworkflows%2Fci.yaml&query=%24.jobs%5B%22test-plugin%22%5D.strategy.matrix%5B%22otio-version%22%5D&label=OpenTimelineIO) 5 | ![Dynamic YAML Badge](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FOpenTimelineIO%2Fotio-aaf-adapter%2Fmain%2F.github%2Fworkflows%2Fci.yaml&query=%24.jobs%5B%22test-plugin%22%5D.strategy.matrix%5B%22python-version%22%5D&label=Python) 6 | 7 | ## Overview 8 | 9 | This project is a [OpenTimelineIO](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) adapter for reading and writing Advanced Authoring Format (AAF) files. 10 | This adapter was originally included with OpenTimelineIO as a contrib adapter. It is in the process of being separated into this project to improve maintainability and reduced the dependencies of both projects. 11 | 12 | ## Feature Matrix 13 | 14 | | Feature | Read | Write | 15 | | ------- | ---- | ----- | 16 | | Single Track of Clips | ✔ | ✔ | 17 | | Multiple Video Tracks | ✔ | ✔ | 18 | | Audio Tracks & Clips | ✔ | ✔ | 19 | | Gap/Filler | ✔ | ✔ | 20 | | Markers | ✔ | ✔ | 21 | | Nesting | ✔ | ✔ | 22 | | Transitions | ✔ | ✔ | 23 | | Audio/Video Effects | ✖ | ✖ | 24 | | Linear Speed Effects | ✔ | ✖ | 25 | | Fancy Speed Effects | ✖ | ✖ | 26 | | Color Decision List | ✖ | ✖ | 27 | | Image Sequence Reference | ✖ | ✖ | 28 | 29 | ## Requirements 30 | 31 | * [OpenTimelineIO](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) 32 | * [pyaaf2](https://github.com/markreidvfx/pyaaf2) 33 | 34 | 35 | ## Licensing 36 | 37 | This repository is licensed under the [Apache License, Version 2.0](LICENSE.md). 38 | 39 | ## Testing for Development 40 | 41 | ```bash 42 | # In the root folder of the repo 43 | pip install -e . 44 | 45 | # Test adapter 46 | otioconvert -i some_timeline.aaf -o some_timeline.ext 47 | ``` 48 | 49 | If you are using a version of OpentimelineIO that still has the AAF contrib adapter you may need to add the path of [plugin_manifest.json](./src/otio_aaf_adapter/plugin_manifest.json) to your `OTIO_PLUGIN_MANIFEST_PATH` [environment variable.](https://opentimelineio.readthedocs.io/en/latest/tutorials/otio-env-variables.html) This should override the contrib version. 50 | 51 | ## Contributions 52 | 53 | If you have any suggested changes to the otio-aaf-adapter, 54 | please provide them via [pull request](../../pulls) or [create an issue](../../issues) as appropriate. 55 | 56 | All contributions to this repository must align with the contribution 57 | [guidelines](https://opentimelineio.readthedocs.io/en/latest/tutorials/contributing.html) 58 | of the OpenTimelineIO project. 59 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - "*/site-packages/::src/" 3 | 4 | # Let's make the coverage threshold reports "informational" instead of blocking for now. 5 | # Details here: https://docs.codecov.com/docs/quick-start#tips-and-tricks 6 | coverage: 7 | status: 8 | project: 9 | default: 10 | informational: true 11 | patch: 12 | default: 13 | informational: true 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -n -j8 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " htmlstrict to make standalone HTML files and fails if any warnings or errors are produced" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " applehelp to make an Apple Help Book" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " epub3 to make an epub3" 32 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 33 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 34 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 35 | @echo " text to make text files" 36 | @echo " man to make manual pages" 37 | @echo " texinfo to make Texinfo files" 38 | @echo " info to make Texinfo files and run them through makeinfo" 39 | @echo " gettext to make PO message catalogs" 40 | @echo " changes to make an overview of all changed/added/deprecated items" 41 | @echo " xml to make Docutils-native XML files" 42 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 43 | @echo " linkcheck to check all external links for integrity" 44 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 45 | @echo " coverage to run coverage check of the documentation (if enabled)" 46 | @echo " dummy to check syntax errors of document sources" 47 | 48 | .PHONY: clean 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | .PHONY: html 53 | html: 54 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 55 | @echo 56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 57 | 58 | .PHONY: htmlstrict 59 | htmlstrict: 60 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/htmlstrict -W --keep-going -w $(BUILDDIR)/htmlstrict/output.txt 61 | @echo 62 | @echo "Warnings check complete." 63 | 64 | .PHONY: dirhtml 65 | dirhtml: 66 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 67 | @echo 68 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 69 | 70 | .PHONY: singlehtml 71 | singlehtml: 72 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 73 | @echo 74 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 75 | 76 | .PHONY: pickle 77 | pickle: 78 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 79 | @echo 80 | @echo "Build finished; now you can process the pickle files." 81 | 82 | .PHONY: json 83 | json: 84 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 85 | @echo 86 | @echo "Build finished; now you can process the JSON files." 87 | 88 | .PHONY: htmlhelp 89 | htmlhelp: 90 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 91 | @echo 92 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 93 | ".hhp project file in $(BUILDDIR)/htmlhelp." 94 | 95 | .PHONY: qthelp 96 | qthelp: 97 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 98 | @echo 99 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 100 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 101 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OpenTimelineIO.qhcp" 102 | @echo "To view the help file:" 103 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OpenTimelineIO.qhc" 104 | 105 | .PHONY: applehelp 106 | applehelp: 107 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 108 | @echo 109 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 110 | @echo "N.B. You won't be able to view it unless you put it in" \ 111 | "~/Library/Documentation/Help or install it in your application" \ 112 | "bundle." 113 | 114 | .PHONY: devhelp 115 | devhelp: 116 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 117 | @echo 118 | @echo "Build finished." 119 | @echo "To view the help file:" 120 | @echo "# mkdir -p $$HOME/.local/share/devhelp/OpenTimelineIO" 121 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OpenTimelineIO" 122 | @echo "# devhelp" 123 | 124 | .PHONY: epub 125 | epub: 126 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 127 | @echo 128 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 129 | 130 | .PHONY: epub3 131 | epub3: 132 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 133 | @echo 134 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 135 | 136 | .PHONY: latex 137 | latex: 138 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 139 | @echo 140 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 141 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 142 | "(use \`make latexpdf' here to do that automatically)." 143 | 144 | .PHONY: latexpdf 145 | latexpdf: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through pdflatex..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: latexpdfja 152 | latexpdfja: 153 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 154 | @echo "Running LaTeX files through platex and dvipdfmx..." 155 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 156 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 157 | 158 | .PHONY: text 159 | text: 160 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 161 | @echo 162 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 163 | 164 | .PHONY: man 165 | man: 166 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 167 | @echo 168 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 169 | 170 | .PHONY: texinfo 171 | texinfo: 172 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 173 | @echo 174 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 175 | @echo "Run \`make' in that directory to run these through makeinfo" \ 176 | "(use \`make info' here to do that automatically)." 177 | 178 | .PHONY: info 179 | info: 180 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 181 | @echo "Running Texinfo files through makeinfo..." 182 | make -C $(BUILDDIR)/texinfo info 183 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 184 | 185 | .PHONY: gettext 186 | gettext: 187 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 188 | @echo 189 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 190 | 191 | .PHONY: changes 192 | changes: 193 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 194 | @echo 195 | @echo "The overview file is in $(BUILDDIR)/changes." 196 | 197 | .PHONY: linkcheck 198 | linkcheck: 199 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 200 | @echo 201 | @echo "Link check complete; look for any errors in the above output " \ 202 | "or in $(BUILDDIR)/linkcheck/output.txt." 203 | 204 | .PHONY: doctest 205 | doctest: 206 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 207 | @echo "Testing of doctests in the sources finished, look at the " \ 208 | "results in $(BUILDDIR)/doctest/output.txt." 209 | 210 | .PHONY: coverage 211 | coverage: 212 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 213 | @echo "Testing of coverage in the sources finished, look at the " \ 214 | "results in $(BUILDDIR)/coverage/python.txt." 215 | 216 | .PHONY: xml 217 | xml: 218 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 219 | @echo 220 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 221 | 222 | .PHONY: pseudoxml 223 | pseudoxml: 224 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 225 | @echo 226 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 227 | 228 | .PHONY: dummy 229 | dummy: 230 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 231 | @echo 232 | @echo "Build finished. Dummy builder generates no files." 233 | -------------------------------------------------------------------------------- /docs/_static/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/docs/_static/.empty -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {{ fullname }} 2 | {{ underline }} 3 | 4 | .. automodule:: {{ fullname }} 5 | :members: 6 | 7 | {% block modules %} 8 | {% if modules %} 9 | .. rubric:: Modules 10 | 11 | .. autosummary:: 12 | :toctree: 13 | :recursive: 14 | {% for item in modules %} 15 | {{ item }} 16 | {%- endfor %} 17 | {% endif %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /docs/api/otio-plugins.md: -------------------------------------------------------------------------------- 1 | 2 | # AAF 3 | 4 | ``` 5 | OpenTimelineIO Advanced Authoring Format (AAF) Adapter 6 | 7 | Depending on if/where PyAAF is installed, you may need to set this env var: 8 | OTIO_AAF_PYTHON_LIB - should point at the PyAAF module. 9 | ``` 10 | 11 | *source*: `otio_aaf_adapter/adapters/advanced_authoring_format.py` 12 | 13 | 14 | *Supported Features (with arguments)*: 15 | 16 | - read_from_file: 17 | ``` 18 | Reads AAF content from `filepath` and outputs an OTIO 19 | timeline object. 20 | 21 | Args: 22 | filepath (str): AAF filepath 23 | simplify (bool, optional): simplify timeline structure by stripping empty 24 | items 25 | transcribe_log (bool, optional): log activity as items are getting 26 | transcribed 27 | attach_markers (bool, optional): attaches markers to their appropriate items 28 | like clip, gap. etc on the track 29 | bake_keyframed_properties (bool, optional): bakes animated property values 30 | for each frame in a source clip 31 | Returns: 32 | otio.schema.Timeline 33 | ``` 34 | - filepath 35 | - simplify 36 | - transcribe_log 37 | - attach_markers 38 | - bake_keyframed_properties 39 | - write_to_file: 40 | - input_otio 41 | - filepath 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # Copyright Contributors to the OpenTimelineIO project 5 | import re 6 | 7 | import sphinx_rtd_theme 8 | import opentimelineio 9 | import otio_aaf_adapter 10 | 11 | # -- Project information --------------------------------------------------------------- 12 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 13 | 14 | project = 'otio-aaf-adapter' 15 | copyright = "Copyright Contributors to the OpenTimelineIO project" 16 | author = 'Contributors to the OpenTimelineIO project' 17 | 18 | try: 19 | RELEASE = opentimelineio.__version__ 20 | except AttributeError: 21 | RELEASE = 'unknown' 22 | 23 | # The short X.Y version. 24 | version = RELEASE.split('-')[0] 25 | # The full version, including alpha/beta/rc tags. 26 | release = RELEASE 27 | 28 | # -- General configuration ------------------------------------------------------------- 29 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 30 | 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.autosummary', 34 | 'sphinx.ext.intersphinx', 35 | 'myst_parser', # This plugin is used to format our markdown correctly 36 | ] 37 | 38 | templates_path = ['_templates'] 39 | 40 | exclude_patterns = ['_build', '_templates', '.venv'] 41 | 42 | pygments_style = 'sphinx' 43 | 44 | 45 | # -- Options for HTML output ----------------------------------------------------------- 46 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 47 | 48 | html_theme = "sphinx_rtd_theme" 49 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 50 | 51 | htmlhelp_basename = '{}doc'.format(project.lower()) 52 | 53 | 54 | # -- Options for LaTeX output ---------------------------------------------------------- 55 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output 56 | 57 | latex_documents = [ 58 | ('index', '{}.tex'.format(project.lower()), 59 | u'{} Documentation'.format(project), 60 | author, 'manual'), 61 | ] 62 | 63 | # -- Options for manual page output ---------------------------------------------------- 64 | # sphinx-doc.org/en/master/usage/configuration.html#options-for-manual-page-output 65 | 66 | man_pages = [ 67 | ('index', project.lower(), '{} Documentation'.format(project), 68 | [author], 1) 69 | ] 70 | 71 | # -- Options for Texinfo output -------------------------------------------------------- 72 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-texinfo-output 73 | 74 | texinfo_documents = [ 75 | ('index', project.lower(), '{} Documentation'.format(project), 76 | author, project, 'One line description of project.', 77 | 'Miscellaneous'), 78 | ] 79 | 80 | # -- Options for intersphinx ----------------------------------------------------------- 81 | # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration 82 | 83 | intersphinx_mapping = { 84 | "python": ("https://docs.python.org/3", None), 85 | } 86 | 87 | # -- Options for Autodoc --------------------------------------------------------------- 88 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration 89 | 90 | # Both the class’ and the __init__ method’s docstring are concatenated and inserted. 91 | # Pybind11 generates class signatures on the __init__ method. 92 | autoclass_content = "both" 93 | 94 | autodoc_default_options = { 95 | 'undoc-members': True 96 | } 97 | 98 | # -- Options for linkcheck ------------------------------------------------------------- 99 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder 100 | 101 | linkcheck_exclude_documents = [ 102 | r'cxx/cxx' 103 | ] 104 | 105 | # -- Options for MySt-Parser ----------------------------------------------------------- 106 | # https://myst-parser.readthedocs.io/en/latest/sphinx/reference.html 107 | 108 | myst_heading_anchors = 5 109 | 110 | # -- Custom ---------------------------------------------------------------------------- 111 | 112 | def process_signature( 113 | app, 114 | what: str, 115 | name: str, 116 | obj: object, 117 | options: dict[str, str], 118 | signature: str, 119 | return_annotation, 120 | ): 121 | """This does several things: 122 | * Removes "self" argument from a signature. Pybind11 adds self to 123 | method arguments, which is useless in a python reference documentation. 124 | * Handles overloaded methods/functions by using the docstrings generated 125 | by Pybind11. Pybind11 adds the signature of each overload in the first function's 126 | signature. So the idea is to generate a new signature for each one instead. 127 | """ 128 | signatures = [] 129 | isClass = what == "class" 130 | 131 | # This block won't be necessary once https://github.com/pybind/pybind11/pull/2621 132 | # gets merged in Pybind11. 133 | if signature or isClass: 134 | docstrLines = obj.__doc__ and obj.__doc__.split("\n") or [] 135 | if not docstrLines or isClass: 136 | # A class can have part of its doc in its docstr or in the __init__ docstr. 137 | docstrLines += ( 138 | obj.__init__.__doc__ and obj.__init__.__doc__.split("\n") or [] 139 | ) 140 | 141 | # This could be solidified by using a regex on the reconstructed docstr? 142 | if len(docstrLines) > 1 and "Overloaded function." in docstrLines: 143 | # Overloaded function detected. Extract each signature and create a new 144 | # signature for each of them. 145 | for line in docstrLines: 146 | nameToMatch = name.split(".")[-1] if not isClass else "__init__" 147 | 148 | # Maybe get use sphinx.util.inspect.signature_from_str ? 149 | if match := re.search(f"^\d+\.\s{nameToMatch}(\(.*)", line): 150 | signatures.append(match.group(1)) 151 | elif signature: 152 | signatures.append(signature) 153 | 154 | signature = "" 155 | 156 | # Remove self from signatures. 157 | for index, sig in enumerate(signatures): 158 | newsig = re.sub("self\: [a-zA-Z0-9._]+(,\s)?", "", sig) 159 | signatures[index] = newsig 160 | 161 | signature = "\n".join(signatures) 162 | return signature, return_annotation 163 | 164 | 165 | def process_docstring( 166 | app, 167 | what: str, 168 | name: str, 169 | obj: object, 170 | options: dict[str, str], 171 | lines: list[str], 172 | ): 173 | for index, line in enumerate(lines): 174 | # Remove "self" from docstrings of overloaded functions/methods. 175 | # For overloaded functions/methods/classes, pybind11 176 | # creates docstrings that look like: 177 | # 178 | # Overloaded function. 179 | # 1. func_name(self: , param2: int) 180 | # 1. func_name(self: , param2: float) 181 | # 182 | # "self" is a distraction that can be removed to improve readability. 183 | # This should be removed once https://github.com/pybind/pybind11/pull/2621 is merged. 184 | if re.match(f'\d+\. {name.split("."[0])}', line): 185 | line = re.sub("self\: [a-zA-Z0-9._]+(,\s)?", "", line) 186 | lines[index] = line 187 | 188 | 189 | def setup(app): 190 | app.connect("autodoc-process-signature", process_signature) 191 | app.connect("autodoc-process-docstring", process_docstring) 192 | -------------------------------------------------------------------------------- /docs/gen_api_docs.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import tempfile 3 | import textwrap 4 | import os 5 | 6 | import opentimelineio as otio 7 | import otio_aaf_adapter 8 | 9 | PLUGIN_TEMPLATE = """ 10 | # {name} 11 | 12 | ``` 13 | {doc} 14 | ``` 15 | 16 | *source*: `{path}` 17 | 18 | {other} 19 | 20 | """ 21 | 22 | ADAPTER_TEMPLATE = """ 23 | *Supported Features (with arguments)*: 24 | 25 | {} 26 | 27 | """ 28 | 29 | 30 | def _format_plugin(plugin_map, extra_stuff, sanitized_paths): 31 | # XXX: always force unix path separator so that the output is consistent 32 | # between every platform. 33 | PATH_SEP = "/" 34 | 35 | path = plugin_map['path'] 36 | 37 | # force using PATH_SEP in place of os.path.sep 38 | path = path.replace("\\", PATH_SEP) 39 | 40 | if sanitized_paths: 41 | path = PATH_SEP.join(path.split(PATH_SEP)[-3:]) 42 | return PLUGIN_TEMPLATE.format( 43 | name=plugin_map['name'], 44 | doc=plugin_map['doc'], 45 | path=path, 46 | other=extra_stuff, 47 | ) 48 | 49 | 50 | def _format_doc(docstring, prefix): 51 | """Use textwrap to format a docstring for markdown.""" 52 | 53 | initial_indent = prefix 54 | # subsequent_indent = " " * len(prefix) 55 | subsequent_indent = " " * 2 56 | 57 | block = docstring.split("\n") 58 | fmt_block = [] 59 | for line in block: 60 | line = textwrap.fill( 61 | line, 62 | initial_indent=initial_indent, 63 | subsequent_indent=subsequent_indent, 64 | width=len(subsequent_indent) + 80, 65 | ) 66 | initial_indent = subsequent_indent 67 | fmt_block.append(line) 68 | 69 | return "\n".join(fmt_block) 70 | 71 | 72 | def _format_adapters(plugin_map): 73 | feature_lines = [] 74 | 75 | for feature, feature_data in plugin_map['supported features'].items(): 76 | doc = feature_data['doc'] 77 | if doc: 78 | feature_lines.append( 79 | _format_doc(doc, "- {}: \n```\n".format(feature)) + "\n```" 80 | ) 81 | else: 82 | feature_lines.append( 83 | "- {}:".format(feature) 84 | ) 85 | 86 | for arg in feature_data["args"]: 87 | feature_lines.append(" - {}".format(arg)) 88 | 89 | return ADAPTER_TEMPLATE.format("\n".join(feature_lines)) 90 | 91 | 92 | def _parsed_args(): 93 | """ parse commandline arguments with argparse """ 94 | 95 | parser = argparse.ArgumentParser( 96 | description=__doc__, 97 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 98 | ) 99 | group = parser.add_mutually_exclusive_group() 100 | group.add_argument( 101 | "-d", 102 | "--dryrun", 103 | action="store_true", 104 | default=False, 105 | help="Dryrun mode - print out instead of perform actions" 106 | ) 107 | group.add_argument( 108 | "-o", 109 | "--output", 110 | type=str, 111 | default=None, 112 | help="Update the baseline with the current version" 113 | ) 114 | 115 | return parser.parse_args() 116 | 117 | 118 | def main(): 119 | args = _parsed_args() 120 | 121 | manifest_path = os.path.abspath(os.path.join(otio_aaf_adapter.__file__, 122 | '..', 'plugin_manifest.json')) 123 | manifest = otio.plugins.manifest_from_file(manifest_path) 124 | plugin_info_map = manifest.adapters[0].plugin_info_map() 125 | 126 | docs = _format_plugin(plugin_info_map, 127 | _format_adapters(plugin_info_map), True) 128 | 129 | # print it out somewhere 130 | if args.dryrun: 131 | print(docs) 132 | return 133 | 134 | output = args.output 135 | if not output: 136 | output = tempfile.NamedTemporaryFile( 137 | 'w', 138 | suffix="otio_serialized_schema.md", 139 | delete=False 140 | ).name 141 | 142 | with open(output, 'w') as fo: 143 | fo.write(docs) 144 | 145 | print("wrote documentation to {}.".format(output)) 146 | 147 | 148 | if __name__ == "__main__": 149 | main() 150 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to otio-aaf-adapter's documentation! 2 | ================================================== 3 | 4 | Reads and writes AAF compositions 5 | 6 | - includes clip, gaps, transitions but not markers or effects 7 | - This adapter is still in progress, see the ongoing work here: `AAF Project `_ 8 | - `Spec `_ 9 | - `Protocol `_ 10 | 11 | - Depends on the `PyAAF2 `_ module, so either: 12 | 13 | - ``pip install pyaaf2`` 14 | - ...or set ``${OTIO_AAF_PYTHON_LIB}`` to point the location of the PyAAF2 module 15 | 16 | 17 | Plugin Reference 18 | ---------------- 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | api/otio-plugins 24 | 25 | Links 26 | ----- 27 | 28 | - `OpenTimelineIO Home Page `_ 29 | - `OpenTimelineIO Discussion Group `_ 30 | 31 | 32 | Indices and tables 33 | ------------------ 34 | 35 | * :ref:`genindex` 36 | * :ref:`modindex` 37 | * :ref:`search` 38 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-n -j8 -d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\OpenTimelineIO.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\OpenTimelineIO.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==5.1.1 2 | readthedocs-sphinx-ext==2.1.8 3 | sphinx-rtd-theme 4 | myst-parser==0.18.0 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright Contributors to the OpenTimelineIO project 3 | [build-system] 4 | requires = ["hatchling"] 5 | build-backend = "hatchling.build" 6 | 7 | [project] 8 | name = "otio-aaf-adapter" 9 | version = "1.1.0" 10 | description = "OpenTimelineIO AAF Adapter" 11 | authors = [ 12 | { name="Contributors to the OpenTimelineIO project", email="otio-discussion@lists.aswf.io" }, 13 | ] 14 | license = { file="LICENSE.txt" } 15 | readme = "README.md" 16 | requires-python = ">=3.7" 17 | dependencies = [ 18 | "opentimelineio >= 0.17.0", 19 | "pyaaf2>=1.4.0" 20 | ] 21 | 22 | classifiers = [ 23 | "Development Status :: 4 - Beta", 24 | "Intended Audience :: Developers", 25 | "Topic :: Multimedia :: Video", 26 | "Topic :: Multimedia :: Video :: Display", 27 | "Topic :: Multimedia :: Video :: Non-Linear Editor", 28 | "Topic :: Software Development :: Libraries :: Python Modules", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Programming Language :: Python :: 3.11", 35 | "Operating System :: OS Independent", 36 | "License :: OSI Approved :: Apache Software License", 37 | "Natural Language :: English" 38 | ] 39 | keywords = ["film", "tv", "editing", "editorial", "edit", "non-linear", "aaf", "time", "otio", "otio-adapter"] 40 | 41 | [project.urls] 42 | Homepage = "https://github.com/OpenTimelineIO/otio-aaf-adapter" 43 | Tracker = "https://github.com/OpenTimelineIO/otio-aaf-adapter/issues" 44 | 45 | [project.entry-points."opentimelineio.plugins"] 46 | otio_aaf_adapter = "otio_aaf_adapter" 47 | 48 | [tool.hatch.build.targets.sdist] 49 | # Ensure the sdist includes a setup.py for older pip versions 50 | support-legacy = true 51 | exclude = [".github"] 52 | 53 | [tool.pytest.ini_options] 54 | addopts = "--cov=otio_aaf_adapter" 55 | -------------------------------------------------------------------------------- /src/otio_aaf_adapter/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # Copyright Contributors to the OpenTimelineIO project 5 | 6 | """Unsupported contrib code for OpenTimelineIO.""" 7 | 8 | # flake8: noqa 9 | 10 | from . import ( 11 | adapters 12 | ) 13 | -------------------------------------------------------------------------------- /src/otio_aaf_adapter/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright Contributors to the OpenTimelineIO project 3 | -------------------------------------------------------------------------------- /src/otio_aaf_adapter/adapters/aaf_adapter/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright Contributors to the OpenTimelineIO project 3 | -------------------------------------------------------------------------------- /src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright Contributors to the OpenTimelineIO project 3 | 4 | """AAF Adapter Transcriber 5 | 6 | Specifies how to transcribe an OpenTimelineIO file into an AAF file. 7 | """ 8 | from . import hooks 9 | 10 | from pathlib import Path 11 | from typing import Tuple 12 | from typing import List 13 | from numbers import Rational 14 | 15 | import aaf2 16 | import aaf2.mobs 17 | import abc 18 | import uuid 19 | import opentimelineio as otio 20 | import os 21 | import copy 22 | import re 23 | import logging 24 | 25 | from typing import Dict, Any 26 | 27 | 28 | AAF_PARAMETERDEF_PAN = aaf2.auid.AUID("e4962322-2267-11d3-8a4c-0050040ef7d2") 29 | AAF_OPERATIONDEF_MONOAUDIOPAN = aaf2.auid.AUID("9d2ea893-0968-11d3-8a38-0050040ef7d2") 30 | AAF_PARAMETERDEF_AVIDPARAMETERBYTEORDER = uuid.UUID( 31 | "c0038672-a8cf-11d3-a05b-006094eb75cb") 32 | AAF_PARAMETERDEF_AVIDEFFECTID = uuid.UUID( 33 | "93994bd6-a81d-11d3-a05b-006094eb75cb") 34 | AAF_PARAMETERDEF_AFX_FG_KEY_OPACITY_U = uuid.UUID( 35 | "8d56813d-847e-11d5-935a-50f857c10000") 36 | AAF_PARAMETERDEF_LEVEL = uuid.UUID("e4962320-2267-11d3-8a4c-0050040ef7d2") 37 | AAF_VVAL_EXTRAPOLATION_ID = uuid.UUID("0e24dd54-66cd-4f1a-b0a0-670ac3a7a0b3") 38 | AAF_OPERATIONDEF_SUBMASTER = uuid.UUID("f1db0f3d-8d64-11d3-80df-006008143e6f") 39 | 40 | logger = logging.getLogger(__name__) 41 | 42 | 43 | def _is_considered_gap(thing): 44 | """Returns whether or not thiing can be considered gap. 45 | 46 | TODO: turns generators w/ kind "Slug" inito gap. Should probably generate 47 | opaque black instead. 48 | """ 49 | if isinstance(thing, otio.schema.Gap): 50 | return True 51 | 52 | if isinstance(thing, otio.schema.Clip) and isinstance( 53 | thing.media_reference, 54 | otio.schema.GeneratorReference 55 | ): 56 | if thing.media_reference.generator_kind in ("Slug",): 57 | return True 58 | else: 59 | raise otio.exceptions.NotSupportedError( 60 | "AAF adapter does not support generator references of kind" 61 | " '{}'".format(thing.media_reference.generator_kind) 62 | ) 63 | 64 | return False 65 | 66 | 67 | def _nearest_timecode(rate): 68 | supported_rates = (24.0, 69 | 25.0, 70 | 30.0, 71 | 60.0) 72 | nearest_rate = 0.0 73 | min_diff = float("inf") 74 | for valid_rate in supported_rates: 75 | if valid_rate == rate: 76 | return rate 77 | 78 | diff = abs(rate - valid_rate) 79 | if diff >= min_diff: 80 | continue 81 | 82 | min_diff = diff 83 | nearest_rate = valid_rate 84 | 85 | return nearest_rate 86 | 87 | 88 | class AAFAdapterError(otio.exceptions.OTIOError): 89 | pass 90 | 91 | 92 | class AAFValidationError(AAFAdapterError): 93 | pass 94 | 95 | 96 | class AAFFileTranscriber: 97 | """ 98 | AAFFileTranscriber 99 | 100 | AAFFileTranscriber manages the file-level knowledge during a conversion from 101 | otio to aaf. This includes keeping track of unique tapemobs and mastermobs. 102 | """ 103 | 104 | def __init__(self, input_otio, aaf_file, embed_essence, create_edgecode, **kwargs): 105 | """ 106 | AAFFileTranscriber requires an input timeline and an output pyaaf2 file handle. 107 | 108 | Args: 109 | input_otio(otio.schema.Timeline): an input OpenTimelineIO timeline 110 | aaf_file(aaf2.file.AAFFile): a pyaaf2 file handle to an output file 111 | embed_essence(bool): if `True`, media references will be embedded into AAF 112 | create_edgecode(bool): if `True` each clip will get an EdgeCode slot 113 | assigned that defines the Avid Frame Count Start / End. 114 | """ 115 | self.aaf_file = aaf_file 116 | self.embed_essence = embed_essence 117 | self.create_edgecode = create_edgecode 118 | self.compositionmob = self.aaf_file.create.CompositionMob() 119 | self.compositionmob.name = input_otio.name 120 | self.compositionmob.usage = "Usage_TopLevel" 121 | self.aaf_file.content.mobs.append(self.compositionmob) 122 | self._unique_mastermobs = {} 123 | self._unique_tapemobs = {} 124 | self._clip_mob_ids_map = _gather_clip_mob_ids(input_otio, **kwargs) 125 | 126 | # transcribe timeline comments onto composition mob 127 | self._transcribe_user_comments(input_otio, self.compositionmob) 128 | self._transcribe_mob_attributes(input_otio, self.compositionmob) 129 | 130 | def _unique_mastermob(self, otio_clip): 131 | """Get a unique mastermob, identified by clip metadata mob id.""" 132 | mob_id = self._clip_mob_ids_map.get(otio_clip) 133 | mastermob = self._unique_mastermobs.get(mob_id) 134 | if not mastermob: 135 | mastermob = self.aaf_file.create.MasterMob() 136 | mastermob.name = otio_clip.name 137 | mastermob.mob_id = aaf2.mobid.MobID(mob_id) 138 | self.aaf_file.content.mobs.append(mastermob) 139 | self._unique_mastermobs[mob_id] = mastermob 140 | 141 | # transcribe clip comments / mob attributes onto master mob 142 | self._transcribe_user_comments(otio_clip, mastermob) 143 | self._transcribe_mob_attributes(otio_clip, mastermob) 144 | 145 | # transcribe media reference comments / mob attributes onto master mob. 146 | # this might overwrite clip comments / attributes. 147 | self._transcribe_user_comments(otio_clip.media_reference, mastermob) 148 | self._transcribe_mob_attributes(otio_clip.media_reference, mastermob) 149 | 150 | return mastermob 151 | 152 | def _unique_tapemob(self, otio_clip): 153 | """Get a unique tapemob, identified by clip metadata mob id.""" 154 | mob_id = self._clip_mob_ids_map.get(otio_clip) 155 | tapemob = self._unique_tapemobs.get(mob_id) 156 | if not tapemob: 157 | tapemob = self.aaf_file.create.SourceMob() 158 | tapemob.name = otio_clip.name 159 | tapemob.descriptor = self.aaf_file.create.ImportDescriptor() 160 | # If the edit_rate is not an integer, we need 161 | # to use drop frame with a nominal integer fps. 162 | edit_rate = otio_clip.visible_range().duration.rate 163 | timecode_fps = round(edit_rate) 164 | tape_slot, tape_timecode_slot = tapemob.create_tape_slots( 165 | otio_clip.name, 166 | edit_rate=otio_clip.visible_range().duration.rate, 167 | timecode_fps=round(otio_clip.visible_range().duration.rate), 168 | drop_frame=(edit_rate != timecode_fps) 169 | ) 170 | timecode_start = int( 171 | otio_clip.media_reference.available_range.start_time.value 172 | ) 173 | timecode_length = int( 174 | otio_clip.media_reference.available_range.duration.value 175 | ) 176 | 177 | tape_timecode_slot.segment.start = int(timecode_start) 178 | tape_timecode_slot.segment.length = int(timecode_length) 179 | self.aaf_file.content.mobs.append(tapemob) 180 | self._unique_tapemobs[mob_id] = tapemob 181 | 182 | media = otio_clip.media_reference 183 | if isinstance(media, otio.schema.ExternalReference) and media.target_url: 184 | locator = self.aaf_file.create.NetworkLocator() 185 | locator['URLString'].value = media.target_url 186 | tapemob.descriptor["Locator"].append(locator) 187 | 188 | return tapemob 189 | 190 | def track_transcriber(self, otio_track): 191 | """Return an appropriate _TrackTranscriber given an otio track.""" 192 | if otio_track.kind == otio.schema.TrackKind.Video: 193 | transcriber = VideoTrackTranscriber(self, otio_track, 194 | embed_essence=self.embed_essence, 195 | create_edgecode=self.create_edgecode) 196 | elif otio_track.kind == otio.schema.TrackKind.Audio: 197 | transcriber = AudioTrackTranscriber(self, otio_track, 198 | embed_essence=self.embed_essence, 199 | create_edgecode=self.create_edgecode) 200 | else: 201 | raise otio.exceptions.NotSupportedError( 202 | f"Unsupported track kind: {otio_track.kind}") 203 | return transcriber 204 | 205 | def add_timecode(self, input_otio, default_edit_rate): 206 | """ 207 | Add CompositionMob level timecode track base on global_start_time 208 | if available, otherwise start is set to 0. 209 | """ 210 | if input_otio.global_start_time: 211 | edit_rate = input_otio.global_start_time.rate 212 | start = int(input_otio.global_start_time.value) 213 | else: 214 | edit_rate = default_edit_rate 215 | start = 0 216 | 217 | slot = self.compositionmob.create_timeline_slot(edit_rate) 218 | slot.name = "TC" 219 | 220 | # indicated that this is the primary timecode track 221 | slot['PhysicalTrackNumber'].value = 1 222 | 223 | # timecode.start is in edit_rate units NOT timecode fps 224 | # timecode.fps is only really a hint for a NLE displays on 225 | # how to display the start frame index to the user. 226 | # currently only selects basic non drop frame rates 227 | timecode = self.aaf_file.create.Timecode() 228 | timecode.fps = int(_nearest_timecode(edit_rate)) 229 | timecode.drop = False 230 | timecode.start = start 231 | slot.segment = timecode 232 | 233 | def _transcribe_user_comments(self, otio_item, target_mob): 234 | """Transcribes user comments on `otio_item` onto `target_mob` in AAF.""" 235 | 236 | user_comments = otio_item.metadata.get("AAF", {}).get("UserComments", {}) 237 | for key, val in user_comments.items(): 238 | if isinstance(val, (int, str)): 239 | target_mob.comments[key] = val 240 | elif isinstance(val, (float, Rational)): 241 | target_mob.comments[key] = aaf2.rational.AAFRational(val) 242 | else: 243 | logger.warning( 244 | f"Skip transcribing unsupported comment value of type " 245 | f"'{type(val)}' for key '{key}'." 246 | ) 247 | 248 | def _transcribe_mob_attributes(self, otio_item, target_mob): 249 | """Transcribes mob attribute list onto the `target_mob`. 250 | This can be used to roundtrip specific mob config values, like audio channel 251 | settings. 252 | """ 253 | mob_attr_map = otio_item.metadata.get("AAF", {}).get("MobAttributeList", {}) 254 | mob_attr_list = aaf2.misc.TaggedValueHelper(target_mob['MobAttributeList']) 255 | for key, val in mob_attr_map.items(): 256 | if isinstance(val, (int, str)): 257 | mob_attr_list[key] = val 258 | elif isinstance(val, (float, Rational)): 259 | mob_attr_list[key] = aaf2.rational.AAFRational(val) 260 | else: 261 | raise ValueError(f"Unsupported mob attribute type '{type(val)}' for " 262 | f"key '{key}'.") 263 | 264 | 265 | def validate_metadata(timeline): 266 | """Print a check of necessary metadata requirements for an otio timeline.""" 267 | 268 | all_checks = [__check(timeline, "duration().rate")] 269 | edit_rate = __check(timeline, "duration().rate").value 270 | 271 | for child in timeline.find_children(): 272 | checks = [] 273 | if _is_considered_gap(child): 274 | checks = [ 275 | __check(child, "duration().rate").equals(edit_rate) 276 | ] 277 | if isinstance(child, otio.schema.Clip): 278 | checks = [ 279 | __check(child, "duration().rate").equals(edit_rate), 280 | __check(child, "media_reference.available_range.duration.rate" 281 | ).equals(edit_rate), 282 | __check(child, "media_reference.available_range.start_time.rate" 283 | ).equals(edit_rate) 284 | ] 285 | if isinstance(child, otio.schema.Transition): 286 | checks = [ 287 | __check(child, "duration().rate").equals(edit_rate), 288 | __check(child, "metadata['AAF']['PointList']"), 289 | __check(child, "metadata['AAF']['OperationGroup']['Operation']" 290 | "['DataDefinition']['Name']"), 291 | __check(child, "metadata['AAF']['OperationGroup']['Operation']" 292 | "['Description']"), 293 | __check(child, "metadata['AAF']['OperationGroup']['Operation']" 294 | "['Name']"), 295 | __check(child, "metadata['AAF']['CutPoint']") 296 | ] 297 | all_checks.extend(checks) 298 | 299 | if any(check.errors for check in all_checks): 300 | raise AAFValidationError("\n" + "\n".join( 301 | sum([check.errors for check in all_checks], []))) 302 | 303 | 304 | def _gather_clip_mob_ids(input_otio, 305 | prefer_file_mob_id=False, 306 | use_empty_mob_ids=False, 307 | **kwargs): 308 | """ 309 | Create dictionary of otio clips with their corresponding mob ids. 310 | """ 311 | 312 | def _from_clip_metadata(clip): 313 | """Get the MobID from the clip.metadata.""" 314 | return clip.metadata.get("AAF", {}).get("SourceID") 315 | 316 | def _from_media_reference_metadata(clip): 317 | """Get the MobID from the media_reference.metadata.""" 318 | return (clip.media_reference.metadata.get("AAF", {}).get("MobID") or 319 | clip.media_reference.metadata.get("AAF", {}).get("SourceID")) 320 | 321 | def _from_aaf_file(clip): 322 | """ Get the MobID from the AAF file itself.""" 323 | mob_id = None 324 | if isinstance(clip.media_reference, otio.schema.ExternalReference): 325 | target_url = clip.media_reference.target_url 326 | if os.path.isfile(target_url) and target_url.endswith("aaf"): 327 | with aaf2.open(clip.media_reference.target_url) as aaf_file: 328 | mastermobs = list(aaf_file.content.mastermobs()) 329 | if len(mastermobs) == 1: 330 | mob_id = mastermobs[0].mob_id 331 | return mob_id 332 | 333 | def _generate_empty_mobid(clip): 334 | """Generate a meaningless MobID.""" 335 | return aaf2.mobid.MobID.new() 336 | 337 | strategies = [ 338 | _from_clip_metadata, 339 | _from_media_reference_metadata, 340 | _from_aaf_file 341 | ] 342 | 343 | if prefer_file_mob_id: 344 | strategies.remove(_from_aaf_file) 345 | strategies.insert(0, _from_aaf_file) 346 | 347 | if use_empty_mob_ids: 348 | strategies.append(_generate_empty_mobid) 349 | 350 | clip_mob_ids = {} 351 | 352 | for otio_clip in input_otio.find_clips(): 353 | if _is_considered_gap(otio_clip): 354 | continue 355 | for strategy in strategies: 356 | mob_id = strategy(otio_clip) 357 | if mob_id: 358 | clip_mob_ids[otio_clip] = mob_id 359 | break 360 | else: 361 | raise AAFAdapterError(f"Cannot find mob ID for clip {otio_clip}") 362 | 363 | return clip_mob_ids 364 | 365 | 366 | def _stackify_nested_groups(timeline): 367 | """ 368 | Ensure that all nesting in a given timeline is in a stack container. 369 | This conforms with how AAF thinks about nesting, there needs 370 | to be an outer container, even if it's just one object. 371 | """ 372 | copied = copy.deepcopy(timeline) 373 | for track in copied.tracks: 374 | for i, child in enumerate(track.find_children()): 375 | is_nested = isinstance(child, otio.schema.Track) 376 | is_parent_in_stack = isinstance(child.parent(), otio.schema.Stack) 377 | if is_nested and not is_parent_in_stack: 378 | stack = otio.schema.Stack() 379 | track.remove(child) 380 | stack.append(child) 381 | track.insert(i, stack) 382 | return copied 383 | 384 | 385 | class _TrackTranscriber: 386 | """ 387 | _TrackTranscriber is the base class for the conversion of a given otio track. 388 | 389 | _TrackTranscriber is not meant to be used by itself. It provides the common 390 | functionality to inherit from. We need an abstract base class because Audio and 391 | Video are handled differently. 392 | """ 393 | __metaclass__ = abc.ABCMeta 394 | 395 | def __init__(self, root_file_transcriber, otio_track, 396 | embed_essence, create_edgecode): 397 | """ 398 | _TrackTranscriber 399 | 400 | Args: 401 | root_file_transcriber(AAFFileTranscriber): the corresponding 'parent' 402 | AAFFileTranscriber object 403 | otio_track(otio.schema.Track): the given otio_track to convert 404 | embed_essence(bool): if `True`, referenced media files in clips will be 405 | embedded into the AAF file 406 | create_edgecode(bool): if `True` each clip will get an EdgeCode slot 407 | assigned that defines the Avid Frame Count Start / End. 408 | """ 409 | self.root_file_transcriber = root_file_transcriber 410 | self.compositionmob = root_file_transcriber.compositionmob 411 | self.aaf_file = root_file_transcriber.aaf_file 412 | self.otio_track = otio_track 413 | self.edit_rate = self.otio_track.find_children()[0].duration().rate 414 | self.embed_essence = embed_essence 415 | self.create_edgecode = create_edgecode 416 | self.timeline_mobslot, self.sequence = self._create_timeline_mobslot() 417 | self.timeline_mobslot.name = self.otio_track.name 418 | 419 | def transcribe(self, otio_child): 420 | """Transcribe otio child to corresponding AAF object""" 421 | if _is_considered_gap(otio_child): 422 | filler = self.aaf_filler(otio_child) 423 | return filler 424 | elif isinstance(otio_child, otio.schema.Transition): 425 | transition = self.aaf_transition(otio_child) 426 | return transition 427 | elif isinstance(otio_child, otio.schema.Clip): 428 | source_clip = self.aaf_sourceclip(otio_child) 429 | return source_clip 430 | elif isinstance(otio_child, otio.schema.Track): 431 | sequence = self.aaf_sequence(otio_child) 432 | return sequence 433 | elif isinstance(otio_child, otio.schema.Stack): 434 | operation_group = self.aaf_operation_group(otio_child) 435 | return operation_group 436 | else: 437 | raise otio.exceptions.NotSupportedError( 438 | f"Unsupported otio child type: {type(otio_child)}") 439 | 440 | @property 441 | @abc.abstractmethod 442 | def media_kind(self) -> str: 443 | """Return the string for what kind of track this is.""" 444 | pass 445 | 446 | @property 447 | @abc.abstractmethod 448 | def _master_mob_slot_id(self) -> int: 449 | """ 450 | Return the MasterMob Slot ID for the corresponding track media kind 451 | """ 452 | # MasterMob's and MasterMob slots have to be unique. We handle unique 453 | # MasterMob's with _unique_mastermob(). We also need to protect against 454 | # duplicate MasterMob slots. As of now, we mandate all picture clips to 455 | # be created in MasterMob slot 1 and all sound clips to be created in 456 | # MasterMob slot 2. While this is a little inadequate, it works for now 457 | pass 458 | 459 | @abc.abstractmethod 460 | def _create_timeline_mobslot(self) \ 461 | -> Tuple[aaf2.mobslots.TimelineMobSlot, aaf2.components.Sequence]: 462 | """ 463 | Return a timeline_mobslot and sequence for this track. 464 | 465 | In AAF, a TimelineMobSlot is a container for the Sequence. A Sequence is 466 | analogous to an otio track. 467 | 468 | Returns: 469 | Returns a tuple of (TimelineMobSlot, Sequence) 470 | """ 471 | pass 472 | 473 | @abc.abstractmethod 474 | def default_descriptor(self, otio_clip) -> aaf2.essence.EssenceDescriptor: 475 | pass 476 | 477 | @abc.abstractmethod 478 | def _transition_parameters(self) -> \ 479 | Tuple[List[aaf2.dictionary.ParameterDef], aaf2.misc.Parameter]: 480 | pass 481 | 482 | @abc.abstractmethod 483 | def _import_essence_for_clip(self, 484 | otio_clip: otio.schema.Clip, 485 | essence_path: Path) \ 486 | -> Tuple[aaf2.mobs.MasterMob, aaf2.mobslots.TimelineMobSlot]: 487 | pass 488 | 489 | def aaf_network_locator(self, otio_external_ref): 490 | locator = self.aaf_file.create.NetworkLocator() 491 | locator['URLString'].value = otio_external_ref.target_url 492 | return locator 493 | 494 | def _copy_essence_for_clip(self, 495 | otio_clip: otio.schema.Clip, 496 | aaf_file_path: Path) \ 497 | -> Tuple[aaf2.mobs.MasterMob, aaf2.mobslots.TimelineMobSlot]: 498 | # get Mob ID and make make sure it's a valid MobID object type 499 | mob_id = self.root_file_transcriber._clip_mob_ids_map.get(otio_clip) 500 | if isinstance(mob_id, str): 501 | urn_str = mob_id 502 | mob_id = aaf2.mobs.MobID() 503 | mob_id.urn = urn_str 504 | 505 | # open source AAF file and copy essence 506 | with aaf2.open(str(aaf_file_path), "r") as src_aaf: 507 | # copy over master mob and essence from source AAF to target AAF 508 | for src_master_mob in src_aaf.content.mastermobs(): 509 | if src_master_mob.mob_id != mob_id: 510 | continue 511 | 512 | # copy the essence data from file src_aaf to target aaf 513 | for i, slot in enumerate(src_master_mob.slots): 514 | if isinstance( 515 | slot, aaf2.mobslots.TimelineMobSlot 516 | ): 517 | # copy essence from file aaf_file_path to target aaf 518 | src_source_mob = slot.segment.mob 519 | essence_data_copy = src_source_mob.essence.copy( 520 | root=self.aaf_file 521 | ) 522 | self.aaf_file.content.essencedata.append(essence_data_copy) 523 | 524 | # copy source mob from file aaf_file_path to target aaf 525 | src_source_mob = slot.segment.mob 526 | source_mob_copy = src_source_mob.copy(root=self.aaf_file) 527 | self.aaf_file.content.mobs.append(source_mob_copy) 528 | break 529 | else: 530 | raise AAFAdapterError( 531 | f"No essence data to copy for MasterMob with " 532 | f"ID '{mob_id}' in media reference AAF file: {aaf_file_path}" 533 | ) 534 | 535 | # copy master mob from file aaf_file_path to target aaf 536 | master_mob_copy = src_master_mob.copy(root=self.aaf_file) 537 | self.aaf_file.content.mobs.append(master_mob_copy) 538 | 539 | # get timeline slot for master mob 540 | for slot in master_mob_copy.slots: 541 | if isinstance( 542 | slot, aaf2.mobslots.TimelineMobSlot 543 | ): 544 | master_mob_copy_tl_slot = slot 545 | break 546 | else: 547 | raise AAFAdapterError(f"No TimelineMobSlot for MasterMob " 548 | f"with ID '{mob_id}'.") 549 | 550 | break 551 | else: 552 | raise AAFAdapterError(f"No matching MasterMob with ID '{mob_id}' " 553 | f"in media reference AAF file: {aaf_file_path}") 554 | 555 | return master_mob_copy, master_mob_copy_tl_slot 556 | 557 | def aaf_filler(self, otio_gap): 558 | """Convert an otio Gap into an aaf Filler""" 559 | length = int(otio_gap.visible_range().duration.value) 560 | filler = self.aaf_file.create.Filler(self.media_kind, length) 561 | return filler 562 | 563 | def aaf_sourceclip(self, otio_clip): 564 | """Convert an OTIO Clip into a pyaaf SourceClip. 565 | If `self.embed_essence` is `True`, we attempt to import / embed 566 | the media reference target URL file into the new AAF as media essence. 567 | 568 | Args: 569 | otio_clip(otio.schema.Clip): input OTIO clip 570 | 571 | Returns: 572 | `aaf2.components.SourceClip` 573 | 574 | """ 575 | if self.embed_essence and not otio_clip.media_reference.is_missing_reference: 576 | # embed essence for clip media 577 | target_path = Path( 578 | otio.url_utils.filepath_from_url(otio_clip.media_reference.target_url) 579 | ) 580 | if not target_path.is_file(): 581 | raise FileNotFoundError(f"Cannot find file to embed essence from: " 582 | f"'{target_path}'") 583 | 584 | if target_path.suffix == ".aaf": 585 | # copy over mobs and essence from existing AAF file 586 | mastermob, mastermob_slot = self._copy_essence_for_clip( 587 | otio_clip, target_path 588 | ) 589 | elif target_path.suffix in (".dnx", ".wav"): 590 | # import essence from clip media reference 591 | mastermob, mastermob_slot = self._import_essence_for_clip( 592 | otio_clip=otio_clip, essence_path=target_path 593 | ) 594 | else: 595 | raise AAFAdapterError( 596 | f"Cannot embed media reference at: '{target_path}'." 597 | f"Only .aaf / .dnx / .wav files are supported." 598 | f"You can add logic to transcode your media for " 599 | f"embedding by implementing a " 600 | f"'{hooks.HOOK_PRE_WRITE_TRANSCRIBE}' hook.") 601 | else: 602 | tapemob, tapemob_slot = self._create_tapemob(otio_clip) 603 | filemob, filemob_slot = self._create_filemob(otio_clip, tapemob, 604 | tapemob_slot) 605 | mastermob, mastermob_slot = self._create_mastermob(otio_clip, 606 | filemob, 607 | filemob_slot) 608 | 609 | # We need both `start_time` and `duration` 610 | # Here `start` is the offset between `first` and `in` values. 611 | 612 | offset = (otio_clip.visible_range().start_time - 613 | otio_clip.available_range().start_time) 614 | start = int(offset.value) 615 | length = int(otio_clip.visible_range().duration.value) 616 | 617 | compmob_clip = self.compositionmob.create_source_clip( 618 | slot_id=self.timeline_mobslot.slot_id, 619 | # XXX: Python3 requires these to be passed as explicit ints 620 | start=int(start), 621 | length=int(length), 622 | media_kind=self.media_kind 623 | ) 624 | compmob_clip.mob = mastermob 625 | compmob_clip.slot = mastermob_slot 626 | compmob_clip.slot_id = mastermob_slot.slot_id 627 | 628 | # create edgecode for Avid Frame Count properties 629 | if self.create_edgecode: 630 | ec_tl_slot = self._create_edgecode_timeline_slot( 631 | edit_rate=self.edit_rate, 632 | start=int(otio_clip.available_range().start_time.value), 633 | length=int(otio_clip.available_range().duration.value) 634 | ) 635 | mastermob.slots.append(ec_tl_slot) 636 | 637 | # check if we need to set mark-in / mark-out 638 | if otio_clip.visible_range() != otio_clip.available_range(): 639 | mastermob_slot["MarkIn"].value = int( 640 | otio_clip.visible_range().start_time.value 641 | ) 642 | mastermob_slot["MarkOut"].value = int( 643 | otio_clip.visible_range().end_time_exclusive().value 644 | ) 645 | 646 | return compmob_clip 647 | 648 | def aaf_transition(self, otio_transition): 649 | """Convert an otio Transition into an aaf Transition""" 650 | if (otio_transition.transition_type != 651 | otio.schema.TransitionTypes.SMPTE_Dissolve): 652 | print( 653 | "Unsupported transition type: {}".format( 654 | otio_transition.transition_type)) 655 | return None 656 | 657 | transition_params, varying_value = self._transition_parameters() 658 | 659 | interpolation_def = self.aaf_file.create.InterpolationDef( 660 | aaf2.misc.LinearInterp, "LinearInterp", "Linear keyframe interpolation") 661 | self.aaf_file.dictionary.register_def(interpolation_def) 662 | varying_value["Interpolation"].value = ( 663 | self.aaf_file.dictionary.lookup_interperlationdef("LinearInterp")) 664 | 665 | pointlist = otio_transition.metadata["AAF"]["PointList"] 666 | 667 | c1 = self.aaf_file.create.ControlPoint() 668 | c1["EditHint"].value = "Proportional" 669 | c1.value = pointlist[0]["Value"] 670 | c1.time = pointlist[0]["Time"] 671 | 672 | c2 = self.aaf_file.create.ControlPoint() 673 | c2["EditHint"].value = "Proportional" 674 | c2.value = pointlist[1]["Value"] 675 | c2.time = pointlist[1]["Time"] 676 | 677 | varying_value["PointList"].extend([c1, c2]) 678 | 679 | op_group_metadata = otio_transition.metadata["AAF"]["OperationGroup"] 680 | effect_id = op_group_metadata["Operation"].get("Identification") 681 | is_time_warp = op_group_metadata["Operation"].get("IsTimeWarp") 682 | by_pass = op_group_metadata["Operation"].get("Bypass") 683 | number_inputs = op_group_metadata["Operation"].get("NumberInputs") 684 | operation_category = op_group_metadata["Operation"].get("OperationCategory") 685 | data_def_name = op_group_metadata["Operation"]["DataDefinition"]["Name"] 686 | data_def = self.aaf_file.dictionary.lookup_datadef(str(data_def_name)) 687 | description = op_group_metadata["Operation"]["Description"] 688 | op_def_name = otio_transition.metadata["AAF"][ 689 | "OperationGroup" 690 | ]["Operation"]["Name"] 691 | 692 | # Create OperationDefinition 693 | op_def = self.aaf_file.create.OperationDef(uuid.UUID(effect_id), op_def_name) 694 | self.aaf_file.dictionary.register_def(op_def) 695 | op_def.media_kind = self.media_kind 696 | datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind) 697 | op_def["IsTimeWarp"].value = is_time_warp 698 | op_def["Bypass"].value = by_pass 699 | op_def["NumberInputs"].value = number_inputs 700 | op_def["OperationCategory"].value = str(operation_category) 701 | op_def["ParametersDefined"].extend(transition_params) 702 | op_def["DataDefinition"].value = data_def 703 | op_def["Description"].value = str(description) 704 | 705 | # Create OperationGroup 706 | length = int(otio_transition.duration().value) 707 | operation_group = self.aaf_file.create.OperationGroup(op_def, length) 708 | operation_group["DataDefinition"].value = datadef 709 | operation_group["Parameters"].append(varying_value) 710 | 711 | # Create Transition 712 | transition = self.aaf_file.create.Transition(self.media_kind, length) 713 | transition["OperationGroup"].value = operation_group 714 | transition["CutPoint"].value = otio_transition.metadata["AAF"]["CutPoint"] 715 | transition["DataDefinition"].value = datadef 716 | return transition 717 | 718 | def aaf_sequence(self, otio_track): 719 | """Convert an otio Track into an aaf Sequence""" 720 | sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind) 721 | sequence.components.value = [] 722 | length = 0 723 | for nested_otio_child in otio_track: 724 | result = self.transcribe(nested_otio_child) 725 | length += result.length 726 | sequence.components.append(result) 727 | sequence.length = length 728 | return sequence 729 | 730 | def aaf_operation_group(self, otio_stack): 731 | """ 732 | Create and return an OperationGroup which will contain other AAF objects 733 | to support OTIO nesting 734 | """ 735 | # Create OperationDefinition 736 | op_def = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_SUBMASTER, 737 | "Submaster") 738 | self.aaf_file.dictionary.register_def(op_def) 739 | op_def.media_kind = self.media_kind 740 | datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind) 741 | 742 | # These values are necessary for pyaaf2 OperationDefinitions 743 | op_def["IsTimeWarp"].value = False 744 | op_def["Bypass"].value = 0 745 | op_def["NumberInputs"].value = -1 746 | op_def["OperationCategory"].value = "OperationCategory_Effect" 747 | op_def["DataDefinition"].value = datadef 748 | 749 | # Create OperationGroup 750 | operation_group = self.aaf_file.create.OperationGroup(op_def) 751 | operation_group.media_kind = self.media_kind 752 | operation_group["DataDefinition"].value = datadef 753 | 754 | length = 0 755 | for nested_otio_child in otio_stack: 756 | result = self.transcribe(nested_otio_child) 757 | length += result.length 758 | operation_group.segments.append(result) 759 | operation_group.length = length 760 | return operation_group 761 | 762 | def _create_tapemob(self, otio_clip): 763 | """ 764 | Return a physical sourcemob for an otio Clip based on the MobID. 765 | 766 | Returns: 767 | Returns a tuple of (TapeMob, TapeMobSlot) 768 | """ 769 | tapemob = self.root_file_transcriber._unique_tapemob(otio_clip) 770 | tapemob_slot = tapemob.create_empty_slot(self.edit_rate, self.media_kind) 771 | tapemob_slot.segment.length = int( 772 | otio_clip.media_reference.available_range.duration.value) 773 | return tapemob, tapemob_slot 774 | 775 | def transcribe_otio_aaf_descriptor( 776 | self, 777 | descriptor: aaf2.essence.FileDescriptor, 778 | otio_aaf_descriptor: Dict[str, Any], 779 | ) -> aaf2.essence.FileDescriptor: 780 | """ 781 | Transcribe the properties of AAF descriptor if the are not yet mapped. 782 | 783 | Args: 784 | descriptor (_type_): AAF descriptor to be transcribed. 785 | otio_aaf_descriptor (_type_): OTIO representation of the descriptor. 786 | 787 | Returns: 788 | descriptor: The default-transcribed AAF descriptor extended with 789 | the properties from otio_aaf_descriptor. 790 | """ 791 | for key, value in otio_aaf_descriptor.items(): 792 | # ClassName is not an AAF property 793 | if key == "ClassName": 794 | continue 795 | 796 | # Don't overwrite already set properties 797 | if key in descriptor: 798 | continue 799 | 800 | try: 801 | key_typedef = descriptor[key].typedef 802 | key_native_type = self.aaf_file.metadict.lookup_class( 803 | key_typedef.class_id 804 | ) 805 | 806 | if ( 807 | key_native_type is aaf2.types.TypeDefRecord 808 | and key_typedef.type_name == "AUID" 809 | ): 810 | key_native_value = aaf2.types.AUID(value) 811 | descriptor[key].value = key_native_value 812 | else: 813 | descriptor[key].value = value 814 | except KeyError as e: 815 | logger.warning(f'Translation of "{key}" is impossible: {e}') 816 | 817 | return descriptor 818 | 819 | def _create_filemob(self, otio_clip, tapemob, tapemob_slot): 820 | """ 821 | Return a file sourcemob for an otio Clip. Needs a tapemob and tapemob slot. 822 | 823 | Returns: 824 | Returns a tuple of (FileMob, FileMobSlot) 825 | """ 826 | filemob = self.aaf_file.create.SourceMob() 827 | self.aaf_file.content.mobs.append(filemob) 828 | 829 | filemob.descriptor = self.default_descriptor(otio_clip) 830 | filemob_slot = filemob.create_timeline_slot(self.edit_rate) 831 | filemob_clip = filemob.create_source_clip( 832 | slot_id=filemob_slot.slot_id, 833 | length=tapemob_slot.segment.length, 834 | media_kind=tapemob_slot.segment.media_kind) 835 | filemob_clip.mob = tapemob 836 | filemob_clip.slot = tapemob_slot 837 | filemob_clip.slot_id = tapemob_slot.slot_id 838 | filemob_slot.segment = filemob_clip 839 | return filemob, filemob_slot 840 | 841 | def _create_mastermob(self, otio_clip, filemob, filemob_slot): 842 | """ 843 | Return a mastermob for an otio Clip. Needs a filemob and filemob slot. 844 | 845 | Returns: 846 | Returns a tuple of (MasterMob, MasterMobSlot) 847 | """ 848 | mastermob = self.root_file_transcriber._unique_mastermob(otio_clip) 849 | timecode_length = int(otio_clip.media_reference.available_range.duration.value) 850 | 851 | try: 852 | mastermob_slot = mastermob.slot_at(self._master_mob_slot_id) 853 | except IndexError: 854 | mastermob_slot = ( 855 | mastermob.create_timeline_slot(edit_rate=self.edit_rate, 856 | slot_id=self._master_mob_slot_id)) 857 | mastermob_clip = mastermob.create_source_clip( 858 | slot_id=mastermob_slot.slot_id, 859 | length=timecode_length, 860 | media_kind=self.media_kind) 861 | mastermob_clip.mob = filemob 862 | mastermob_clip.slot = filemob_slot 863 | mastermob_clip.slot_id = filemob_slot.slot_id 864 | mastermob_slot.segment = mastermob_clip 865 | return mastermob, mastermob_slot 866 | 867 | def _create_edgecode_timeline_slot(self, edit_rate, start, length): 868 | """Creates and edgecode timeline mob slot, which is needed 869 | to set Frame Count Start and Frame Count End values in Avid. 870 | 871 | Args: 872 | aaf_file(aaf2.AAFFile): AAF file handle 873 | edit_rate(Fraction): fractional edit rate 874 | start(int): Frame Count Start frame number 875 | length(int): clip length 876 | 877 | Returns: 878 | aaf2.TimelineMobSlot: edgecode TL mob slot 879 | 880 | """ 881 | edgecode = self.aaf_file.create.EdgeCode() 882 | edgecode.media_kind = "Edgecode" 883 | edgecode["Start"].value = start 884 | edgecode["Length"].value = length 885 | edgecode["AvEdgeType"].value = 3 886 | edgecode["AvFilmType"].value = 0 887 | edgecode["FilmKind"].value = "Ft35MM" 888 | edgecode["CodeFormat"].value = "EtNull" 889 | 890 | ec_tl_slot = self.aaf_file.create.TimelineMobSlot(slot_id=20, 891 | edit_rate=edit_rate) 892 | ec_tl_slot.name = "EC1" 893 | ec_tl_slot.segment = edgecode 894 | 895 | # Important magic number from Avid, 896 | # track number has to be 6 otherwise MC will ignore it 897 | ec_tl_slot["PhysicalTrackNumber"].value = 6 898 | 899 | return ec_tl_slot 900 | 901 | 902 | class VideoTrackTranscriber(_TrackTranscriber): 903 | """Video track kind specialization of TrackTranscriber.""" 904 | 905 | @property 906 | def media_kind(self): 907 | return "picture" 908 | 909 | @property 910 | def _master_mob_slot_id(self): 911 | return 1 912 | 913 | def _create_timeline_mobslot(self): 914 | """ 915 | Create a Sequence container (TimelineMobSlot) and Sequence. 916 | 917 | TimelineMobSlot --> Sequence 918 | """ 919 | timeline_mobslot = self.compositionmob.create_timeline_slot( 920 | edit_rate=self.edit_rate) 921 | sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind) 922 | sequence.components.value = [] 923 | timeline_mobslot.segment = sequence 924 | return timeline_mobslot, sequence 925 | 926 | def default_descriptor(self, otio_clip): 927 | descriptor_dict = otio_clip.media_reference.metadata.get("AAF", {}).get( 928 | "EssenceDescription", {}) 929 | 930 | descriptor_class = descriptor_dict.get("ClassName") 931 | if descriptor_class: 932 | descriptor = getattr(self.aaf_file.create, descriptor_class)() 933 | else: 934 | descriptor = self.aaf_file.create.CDCIDescriptor() 935 | descriptor_class = "CDCIDescriptor" 936 | 937 | video_linemap = descriptor_dict.get("VideoLineMap", [42, 0]) 938 | video_linemap = [int(x) for x in video_linemap] 939 | 940 | if isinstance(descriptor, aaf2.essence.CDCIDescriptor): 941 | descriptor["ComponentWidth"].value = int( 942 | descriptor_dict.get("ComponentWidth", 8) 943 | ) 944 | descriptor["HorizontalSubsampling"].value = int( 945 | descriptor_dict.get("HorizontalSubsampling", 2) 946 | ) 947 | elif isinstance(descriptor, aaf2.essence.RGBADescriptor): 948 | # This is a hack for aaf2's inability of dealing with 949 | # empty pixel layout list that OTIO has 950 | default_pixel_layout = [ 951 | {'Code': 'CompRed', 'Size': 8}, 952 | {'Code': 'CompGreen', 'Size': 8}, 953 | {'Code': 'CompBlue', 'Size': 8} 954 | ] 955 | pixel_layout = descriptor_dict.get("PixelLayout", default_pixel_layout) 956 | if (len(pixel_layout) == 0): 957 | pixel_layout = default_pixel_layout 958 | 959 | descriptor["PixelLayout"].value = pixel_layout 960 | 961 | descriptor["ImageAspectRatio"].value = descriptor_dict.get( 962 | "ImageAspectRatio", "16/9" 963 | ) 964 | descriptor["StoredWidth"].value = int( 965 | descriptor_dict.get("StoredWidth", 1920) 966 | ) 967 | descriptor["StoredHeight"].value = int( 968 | descriptor_dict.get("StoredHeight", 1080) 969 | ) 970 | descriptor["FrameLayout"].value = descriptor_dict.get( 971 | "FrameLayout", "FullFrame" 972 | ) 973 | descriptor["VideoLineMap"].value = video_linemap 974 | 975 | # aaf2 Rational follows python's fractions logic, 976 | # thus able to construct from anything 977 | descriptor["SampleRate"].value = str(aaf2.rational.AAFRational( 978 | descriptor_dict.get("SampleRate", 24))) 979 | descriptor["Length"].value = int(descriptor_dict.get("Length", 1)) 980 | 981 | media = otio_clip.media_reference 982 | if isinstance(media, otio.schema.ExternalReference): 983 | if media.target_url: 984 | locator = self.aaf_network_locator(media) 985 | descriptor["Locator"].append(locator) 986 | if media.available_range: 987 | descriptor['SampleRate'].value = media.available_range.duration.rate 988 | descriptor["Length"].value = int(media.available_range.duration.value) 989 | 990 | # Finalize the descriptor with the rest of the properties 991 | descriptor = self.transcribe_otio_aaf_descriptor(descriptor, descriptor_dict) 992 | 993 | return descriptor 994 | 995 | def _transition_parameters(self): 996 | """ 997 | Return video transition parameters 998 | """ 999 | # Create ParameterDef for AvidParameterByteOrder 1000 | byteorder_typedef = self.aaf_file.dictionary.lookup_typedef("aafUInt16") 1001 | param_byteorder = self.aaf_file.create.ParameterDef( 1002 | AAF_PARAMETERDEF_AVIDPARAMETERBYTEORDER, 1003 | "AvidParameterByteOrder", 1004 | "", 1005 | byteorder_typedef) 1006 | self.aaf_file.dictionary.register_def(param_byteorder) 1007 | 1008 | # Create ParameterDef for AvidEffectID 1009 | avid_effect_typdef = self.aaf_file.dictionary.lookup_typedef("AvidBagOfBits") 1010 | param_effect_id = self.aaf_file.create.ParameterDef( 1011 | AAF_PARAMETERDEF_AVIDEFFECTID, 1012 | "AvidEffectID", 1013 | "", 1014 | avid_effect_typdef) 1015 | self.aaf_file.dictionary.register_def(param_effect_id) 1016 | 1017 | # Create ParameterDef for AFX_FG_KEY_OPACITY_U 1018 | opacity_param_def = self.aaf_file.dictionary.lookup_typedef("Rational") 1019 | opacity_param = self.aaf_file.create.ParameterDef( 1020 | AAF_PARAMETERDEF_AFX_FG_KEY_OPACITY_U, 1021 | "AFX_FG_KEY_OPACITY_U", 1022 | "", 1023 | opacity_param_def) 1024 | self.aaf_file.dictionary.register_def(opacity_param) 1025 | 1026 | # Create VaryingValue 1027 | opacity_u = self.aaf_file.create.VaryingValue() 1028 | opacity_u.parameterdef = self.aaf_file.dictionary.lookup_parameterdef( 1029 | "AFX_FG_KEY_OPACITY_U") 1030 | opacity_u["VVal_Extrapolation"].value = AAF_VVAL_EXTRAPOLATION_ID 1031 | opacity_u["VVal_FieldCount"].value = 1 1032 | 1033 | return [param_byteorder, param_effect_id], opacity_u 1034 | 1035 | def _import_essence_for_clip(self, otio_clip, essence_path): 1036 | """Implements DNX video essence import""" 1037 | available_range = otio_clip.media_reference.available_range 1038 | start = int(available_range.start_time.value) 1039 | length = int(available_range.duration.value) 1040 | edit_rate = round(available_range.duration.rate) 1041 | 1042 | # create master mobs 1043 | mastermob = self.root_file_transcriber._unique_mastermob(otio_clip) 1044 | tape_mob = self.root_file_transcriber._unique_tapemob(otio_clip) 1045 | tape_clip = tape_mob.create_source_clip(self._master_mob_slot_id, start=start) 1046 | 1047 | # import video essence 1048 | mastermob_slot = mastermob.import_dnxhd_essence(path=str(essence_path), 1049 | edit_rate=edit_rate, 1050 | tape=tape_clip, 1051 | length=length, 1052 | offline=False) 1053 | return mastermob, mastermob_slot 1054 | 1055 | 1056 | class AudioTrackTranscriber(_TrackTranscriber): 1057 | """Audio track kind specialization of TrackTranscriber.""" 1058 | 1059 | @property 1060 | def media_kind(self): 1061 | return "sound" 1062 | 1063 | @property 1064 | def _master_mob_slot_id(self): 1065 | return 2 1066 | 1067 | def aaf_sourceclip(self, otio_clip): 1068 | # Parameter Definition 1069 | typedef = self.aaf_file.dictionary.lookup_typedef("Rational") 1070 | param_def = self.aaf_file.create.ParameterDef(AAF_PARAMETERDEF_PAN, 1071 | "Pan", 1072 | "Pan", 1073 | typedef) 1074 | self.aaf_file.dictionary.register_def(param_def) 1075 | interp_def = self.aaf_file.create.InterpolationDef(aaf2.misc.LinearInterp, 1076 | "LinearInterp", 1077 | "LinearInterp") 1078 | self.aaf_file.dictionary.register_def(interp_def) 1079 | 1080 | # generate PointList for pan 1081 | varying_value = self.aaf_file.create.VaryingValue() 1082 | varying_value.parameterdef = param_def 1083 | varying_value["Interpolation"].value = interp_def 1084 | 1085 | length = int(otio_clip.duration().value) 1086 | 1087 | # default pan points are mid pan 1088 | default_points = [ 1089 | { 1090 | "ControlPointSource": 2, 1091 | "Time": f"0/{length}", 1092 | "Value": "1/2", 1093 | }, 1094 | { 1095 | "ControlPointSource": 2, 1096 | "Time": f"{length - 1}/{length}", 1097 | "Value": "1/2", 1098 | } 1099 | ] 1100 | cp_dict_list = otio_clip.metadata.get("AAF", {}).get("Pan", {}).get( 1101 | "ControlPoints", default_points) 1102 | 1103 | for cp_dict in cp_dict_list: 1104 | point = self.aaf_file.create.ControlPoint() 1105 | point["Time"].value = aaf2.rational.AAFRational(cp_dict["Time"]) 1106 | point["Value"].value = aaf2.rational.AAFRational(cp_dict["Value"]) 1107 | point["ControlPointSource"].value = cp_dict["ControlPointSource"] 1108 | varying_value["PointList"].append(point) 1109 | 1110 | opgroup = self.timeline_mobslot.segment 1111 | opgroup.parameters.append(varying_value) 1112 | 1113 | return super().aaf_sourceclip(otio_clip) 1114 | 1115 | def _create_timeline_mobslot(self): 1116 | """ 1117 | Create a Sequence container (TimelineMobSlot) and Sequence. 1118 | Sequence needs to be in an OperationGroup. 1119 | 1120 | TimelineMobSlot --> OperationGroup --> Sequence 1121 | """ 1122 | # TimelineMobSlot 1123 | timeline_mobslot = self.compositionmob.create_sound_slot( 1124 | edit_rate=self.edit_rate) 1125 | # OperationDefinition 1126 | opdef = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_MONOAUDIOPAN, 1127 | "Audio Pan") 1128 | opdef.media_kind = self.media_kind 1129 | opdef["NumberInputs"].value = 1 1130 | self.aaf_file.dictionary.register_def(opdef) 1131 | # OperationGroup 1132 | total_length = int(sum([t.duration().value for t in self.otio_track])) 1133 | opgroup = self.aaf_file.create.OperationGroup(opdef) 1134 | opgroup.media_kind = self.media_kind 1135 | opgroup.length = total_length 1136 | timeline_mobslot.segment = opgroup 1137 | # Sequence 1138 | sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind) 1139 | sequence.components.value = [] 1140 | sequence.length = total_length 1141 | opgroup.segments.append(sequence) 1142 | return timeline_mobslot, sequence 1143 | 1144 | def default_descriptor(self, otio_clip): 1145 | descriptor = self.aaf_file.create.PCMDescriptor() 1146 | descriptor_dict = otio_clip.media_reference.metadata.get("AAF", {}).get( 1147 | "EssenceDescription", {}) 1148 | 1149 | sample_rate = float(aaf2.rational.AAFRational( 1150 | descriptor_dict.get("SampleRate", 48000))) 1151 | descriptor_dict["AverageBPS"] = int(descriptor_dict.get("AverageBPS", 96000)) 1152 | descriptor_dict["BlockAlign"] = int(descriptor_dict.get("BlockAlign", 2)) 1153 | descriptor_dict["QuantizationBits"] = int( 1154 | descriptor_dict.get("QuantizationBits", 16) 1155 | ) 1156 | 1157 | if isinstance(otio_clip.media_reference, otio.schema.ExternalReference): 1158 | locator = self.aaf_network_locator(otio_clip.media_reference) 1159 | descriptor["Locator"].append(locator) 1160 | 1161 | descriptor_dict["AudioSamplingRate"] = float(aaf2.rational.AAFRational( 1162 | descriptor_dict.get("AudioSamplingRate", 48000)) 1163 | ) 1164 | descriptor_dict["Channels"] = int(descriptor_dict.get("Channels", 1)) 1165 | descriptor_dict["SampleRate"] = sample_rate 1166 | descriptor_dict["Length"] = int(descriptor_dict.get("Length", int( 1167 | otio_clip.media_reference.available_range.duration.rescaled_to( 1168 | sample_rate).value 1169 | ))) 1170 | 1171 | # Finalize the descriptor with the rest of the properties 1172 | descriptor = self.transcribe_otio_aaf_descriptor(descriptor, descriptor_dict) 1173 | 1174 | return descriptor 1175 | 1176 | def _transition_parameters(self): 1177 | """ 1178 | Return audio transition parameters 1179 | """ 1180 | # Create ParameterDef for ParameterDef_Level 1181 | def_level_typedef = self.aaf_file.dictionary.lookup_typedef("Rational") 1182 | param_def_level = self.aaf_file.create.ParameterDef(AAF_PARAMETERDEF_LEVEL, 1183 | "ParameterDef_Level", 1184 | "", 1185 | def_level_typedef) 1186 | self.aaf_file.dictionary.register_def(param_def_level) 1187 | 1188 | # Create VaryingValue 1189 | level = self.aaf_file.create.VaryingValue() 1190 | level.parameterdef = ( 1191 | self.aaf_file.dictionary.lookup_parameterdef("ParameterDef_Level")) 1192 | 1193 | return [param_def_level], level 1194 | 1195 | 1196 | class __check: 1197 | """ 1198 | __check is a private helper class that safely gets values given to check 1199 | for existence and equality 1200 | """ 1201 | 1202 | def __init__(self, obj, tokenpath): 1203 | self.orig = obj 1204 | self.value = obj 1205 | self.errors = [] 1206 | self.tokenpath = tokenpath 1207 | try: 1208 | for token in re.split(r"[\.\[]", tokenpath): 1209 | if token.endswith("()"): 1210 | self.value = getattr(self.value, token.replace("()", ""))() 1211 | elif "]" in token: 1212 | self.value = self.value[token.strip("[]'\"")] 1213 | else: 1214 | self.value = getattr(self.value, token) 1215 | except Exception as e: 1216 | self.value = None 1217 | self.errors.append("{}{} {}.{} does not exist, {}".format( 1218 | self.orig.name if hasattr(self.orig, "name") else "", 1219 | type(self.orig), 1220 | type(self.orig).__name__, 1221 | self.tokenpath, e)) 1222 | 1223 | def equals(self, val): 1224 | """Check if the retrieved value is equal to a given value.""" 1225 | if self.value is not None and self.value != val: 1226 | self.errors.append( 1227 | "{}{} {}.{} not equal to {} (expected) != {} (actual)".format( 1228 | self.orig.name if hasattr(self.orig, "name") else "", 1229 | type(self.orig), 1230 | type(self.orig).__name__, self.tokenpath, val, self.value)) 1231 | return self 1232 | -------------------------------------------------------------------------------- /src/otio_aaf_adapter/adapters/aaf_adapter/hooks.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright Contributors to the OpenTimelineIO project 3 | 4 | import aaf2 5 | import opentimelineio as otio 6 | 7 | 8 | # Plugin custom hook names 9 | HOOK_PRE_READ_TRANSCRIBE = "otio_aaf_pre_read_transcribe" 10 | HOOK_POST_READ_TRANSCRIBE = "otio_aaf_post_read_transcribe" 11 | HOOK_PRE_WRITE_TRANSCRIBE = "otio_aaf_pre_write_transcribe" 12 | HOOK_POST_WRITE_TRANSCRIBE = "otio_aaf_post_write_transcribe" 13 | 14 | 15 | def run_pre_write_transcribe_hook( 16 | timeline: otio.schema.Timeline, 17 | write_filepath: str, 18 | aaf_handle: aaf2.file.AAFFile, 19 | embed_essence: bool, 20 | extra_kwargs: dict 21 | ) -> otio.schema.Timeline: 22 | """This hook runs on write, just before the timeline got translated to pyaaf2 23 | data.""" 24 | if HOOK_PRE_WRITE_TRANSCRIBE in otio.hooks.names(): 25 | extra_kwargs.update({ 26 | "write_filepath": write_filepath, 27 | "aaf_handle": aaf_handle, 28 | "embed_essence": embed_essence, 29 | }) 30 | return otio.hooks.run(HOOK_PRE_WRITE_TRANSCRIBE, timeline, extra_kwargs) 31 | return timeline 32 | 33 | 34 | def run_post_write_transcribe_hook( 35 | timeline: otio.schema.Timeline, 36 | write_filepath: str, 37 | aaf_handle: aaf2.file.AAFFile, 38 | embed_essence: bool, 39 | extra_kwargs: dict 40 | ) -> otio.schema.Timeline: 41 | """This hook runs on write, just after the timeline gets translated to pyaaf2 data. 42 | """ 43 | if HOOK_POST_WRITE_TRANSCRIBE in otio.hooks.names(): 44 | extra_kwargs.update({ 45 | "write_filepath": write_filepath, 46 | "aaf_handle": aaf_handle, 47 | "embed_essence": embed_essence, 48 | }) 49 | return otio.hooks.run(HOOK_POST_WRITE_TRANSCRIBE, timeline, extra_kwargs) 50 | return timeline 51 | 52 | 53 | def run_pre_read_transcribe_hook( 54 | read_filepath: str, 55 | aaf_handle: aaf2.file.AAFFile, 56 | extra_kwargs: dict 57 | ) -> None: 58 | """This hook runs on read, just before the timeline gets translated from pyaaf2 59 | to OTIO data. It can be useful to manipulate the AAF data directly before the 60 | transcribing occurs. The hook doesn't return a timeline, since it runs before the 61 | Timeline object has been transcribed.""" 62 | if HOOK_PRE_WRITE_TRANSCRIBE in otio.hooks.names(): 63 | extra_kwargs.update({ 64 | "read_filepath": read_filepath, 65 | "aaf_handle": aaf_handle, 66 | }) 67 | otio.hooks.run(HOOK_PRE_READ_TRANSCRIBE, tl=None, extra_args=extra_kwargs) 68 | 69 | 70 | def run_post_read_transcribe_hook( 71 | timeline: otio.schema.Timeline, 72 | read_filepath: str, 73 | aaf_handle: aaf2.file.AAFFile, 74 | extra_kwargs: dict 75 | ) -> otio.schema.Timeline: 76 | """This hook runs on read, just after the timeline got translated to OTIO data, 77 | but before it is simplified. Possible use cases could be logic to extract and 78 | transcode media from the AAF. 79 | """ 80 | if HOOK_POST_WRITE_TRANSCRIBE in otio.hooks.names(): 81 | extra_kwargs.update({ 82 | "read_filepath": read_filepath, 83 | "aaf_handle": aaf_handle 84 | }) 85 | return otio.hooks.run(HOOK_POST_WRITE_TRANSCRIBE, 86 | tl=timeline, 87 | extra_args=extra_kwargs) 88 | return timeline 89 | -------------------------------------------------------------------------------- /src/otio_aaf_adapter/plugin_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA" : "PluginManifest.1", 3 | "adapters" : [ 4 | { 5 | "OTIO_SCHEMA" : "Adapter.1", 6 | "name" : "AAF", 7 | "execution_scope" : "in process", 8 | "filepath" : "adapters/advanced_authoring_format.py", 9 | "suffixes" : ["aaf"] 10 | } 11 | ], 12 | "hooks" : { 13 | "otio_aaf_pre_read_transcribe": [], 14 | "otio_aaf_post_read_transcribe": [], 15 | "otio_aaf_pre_write_transcribe": [], 16 | "otio_aaf_post_write_transcribe": [] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/hooks_plugin_example/plugin_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA" : "PluginManifest.1", 3 | "hook_scripts" : [ 4 | { 5 | "OTIO_SCHEMA" : "HookScript.1", 6 | "name" : "pre_aaf_write_transcribe_hook", 7 | "execution_scope" : "in process", 8 | "filepath" : "pre_aaf_write_transcribe_hook.py" 9 | }, 10 | { 11 | "OTIO_SCHEMA" : "HookScript.1", 12 | "name" : "post_aaf_write_transcribe_hook", 13 | "execution_scope" : "in process", 14 | "filepath" : "post_aaf_write_transcribe_hook.py" 15 | }, 16 | { 17 | "OTIO_SCHEMA" : "HookScript.1", 18 | "name" : "pre_aaf_read_transcribe_hook", 19 | "execution_scope" : "in process", 20 | "filepath" : "pre_aaf_read_transcribe_hook.py" 21 | }, 22 | { 23 | "OTIO_SCHEMA" : "HookScript.1", 24 | "name" : "post_aaf_read_transcribe_hook", 25 | "execution_scope" : "in process", 26 | "filepath" : "post_aaf_read_transcribe_hook.py" 27 | } 28 | ], 29 | "hooks" : { 30 | "otio_aaf_pre_write_transcribe": ["pre_aaf_write_transcribe_hook"], 31 | "otio_aaf_post_write_transcribe": ["post_aaf_write_transcribe_hook"], 32 | "otio_aaf_pre_read_transcribe": ["pre_aaf_read_transcribe_hook"], 33 | "otio_aaf_post_read_transcribe": ["post_aaf_read_transcribe_hook"] 34 | } 35 | } -------------------------------------------------------------------------------- /tests/hooks_plugin_example/post_aaf_read_transcribe_hook.py: -------------------------------------------------------------------------------- 1 | """Example hook that runs post-transcription on a read operation. 2 | This hook could be used to extract and transcode essence data from the AAF for 3 | consumption outside of Avid MC. 4 | """ 5 | from otio_aaf_adapter.adapters.aaf_adapter.aaf_writer import AAFAdapterError 6 | 7 | 8 | def hook_function(in_timeline, argument_map=None): 9 | if argument_map.get("test_post_hook_raise", False): 10 | raise AAFAdapterError() 11 | 12 | return in_timeline 13 | -------------------------------------------------------------------------------- /tests/hooks_plugin_example/post_aaf_write_transcribe_hook.py: -------------------------------------------------------------------------------- 1 | """Example hook that runs post-transcription on a write operation. 2 | This hook is useful to clean up temporary files or metadata post-write. 3 | """ 4 | from otio_aaf_adapter.adapters.aaf_adapter.aaf_writer import AAFAdapterError 5 | 6 | 7 | def hook_function(in_timeline, argument_map=None): 8 | if argument_map.get("test_post_hook_raise", False): 9 | raise AAFAdapterError() 10 | 11 | if not argument_map.get("embed_essence", False): 12 | # no essence embedding requested, skip the hook 13 | return in_timeline 14 | 15 | for clip in in_timeline.find_clips(): 16 | # reset target URL to pre-conversion media, remove metadata 17 | original_url = clip.media_reference.metadata.pop("original_target_url") 18 | if original_url: 19 | clip.media_reference.target_url = original_url 20 | 21 | return in_timeline 22 | -------------------------------------------------------------------------------- /tests/hooks_plugin_example/pre_aaf_read_transcribe_hook.py: -------------------------------------------------------------------------------- 1 | """Example hook that runs pre-transcription on a read operation. 2 | This can be useful for just-in-time modification of the AAF structure prior to 3 | transcription. 4 | """ 5 | from otio_aaf_adapter.adapters.aaf_adapter.aaf_writer import AAFAdapterError 6 | 7 | 8 | def hook_function(in_timeline, argument_map=None): 9 | if argument_map.get("test_pre_hook_raise", False): 10 | raise AAFAdapterError() 11 | 12 | return in_timeline 13 | -------------------------------------------------------------------------------- /tests/hooks_plugin_example/pre_aaf_write_transcribe_hook.py: -------------------------------------------------------------------------------- 1 | """Example hook that runs pre-transcription on a write operation. 2 | This can be useful for just-in-time transcoding of media references to DNX data / 3 | WAVE audio files. 4 | """ 5 | import os 6 | from pathlib import Path 7 | from otio_aaf_adapter.adapters.aaf_adapter.aaf_writer import AAFAdapterError 8 | 9 | 10 | def hook_function(in_timeline, argument_map=None): 11 | if argument_map.get("test_pre_hook_raise", False): 12 | raise AAFAdapterError() 13 | 14 | if not argument_map.get("embed_essence", False): 15 | # no essence embedding requested, skip the hook 16 | return in_timeline 17 | 18 | for clip in in_timeline.find_clips(): 19 | # mock convert video media references, this could be done with ffmpeg 20 | if Path(clip.media_reference.target_url).suffix == ".mov": 21 | converted_url = Path(clip.media_reference.target_url).with_suffix(".dnx") 22 | clip.media_reference.metadata[ 23 | "original_target_url" 24 | ] = clip.media_reference.target_url 25 | clip.media_reference.target_url = os.fspath(converted_url) 26 | 27 | return in_timeline 28 | -------------------------------------------------------------------------------- /tests/sample_data/2997fps-DFTC.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/2997fps-DFTC.aaf -------------------------------------------------------------------------------- /tests/sample_data/2997fps.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/2997fps.aaf -------------------------------------------------------------------------------- /tests/sample_data/30fps.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/30fps.aaf -------------------------------------------------------------------------------- /tests/sample_data/avid_data_track_example.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/avid_data_track_example.aaf -------------------------------------------------------------------------------- /tests/sample_data/bad_marker_track_from_avid.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/bad_marker_track_from_avid.aaf -------------------------------------------------------------------------------- /tests/sample_data/composite.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/composite.aaf -------------------------------------------------------------------------------- /tests/sample_data/duplicates.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/duplicates.aaf -------------------------------------------------------------------------------- /tests/sample_data/essence_group.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/essence_group.aaf -------------------------------------------------------------------------------- /tests/sample_data/gaps.otio: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA": "Timeline.1", 3 | "global_start_time": null, 4 | "metadata": {}, 5 | "name": "gaps", 6 | "tracks": { 7 | "OTIO_SCHEMA": "Stack.1", 8 | "children": [ 9 | { 10 | "OTIO_SCHEMA": "Track.1", 11 | "children": [ 12 | { 13 | "OTIO_SCHEMA": "Gap.1", 14 | "effects": [], 15 | "markers": [], 16 | "metadata": {}, 17 | "name": "gap", 18 | "source_range": { 19 | "OTIO_SCHEMA": "TimeRange.1", 20 | "duration": { 21 | "OTIO_SCHEMA": "RationalTime.1", 22 | "rate": 24.0, 23 | "value": 60 24 | }, 25 | "start_time": { 26 | "OTIO_SCHEMA": "RationalTime.1", 27 | "rate": 24.0, 28 | "value": 0 29 | } 30 | } 31 | } 32 | ], 33 | "effects": [], 34 | "kind": "Video", 35 | "markers": [], 36 | "metadata": {}, 37 | "name": null, 38 | "source_range": null 39 | }, 40 | { 41 | "OTIO_SCHEMA": "Track.1", 42 | "children": [ 43 | { 44 | "OTIO_SCHEMA": "Gap.1", 45 | "effects": [], 46 | "markers": [], 47 | "metadata": {}, 48 | "name": "gap", 49 | "source_range": { 50 | "OTIO_SCHEMA": "TimeRange.1", 51 | "duration": { 52 | "OTIO_SCHEMA": "RationalTime.1", 53 | "rate": 24.0, 54 | "value": 600 55 | }, 56 | "start_time": { 57 | "OTIO_SCHEMA": "RationalTime.1", 58 | "rate": 24.0, 59 | "value": 0 60 | } 61 | } 62 | }, 63 | { 64 | "OTIO_SCHEMA": "Gap.1", 65 | "effects": [], 66 | "markers": [], 67 | "metadata": {}, 68 | "name": "gap", 69 | "source_range": { 70 | "OTIO_SCHEMA": "TimeRange.1", 71 | "duration": { 72 | "OTIO_SCHEMA": "RationalTime.1", 73 | "rate": 24.0, 74 | "value": 480 75 | }, 76 | "start_time": { 77 | "OTIO_SCHEMA": "RationalTime.1", 78 | "rate": 24.0, 79 | "value": 0 80 | } 81 | } 82 | }, 83 | { 84 | "OTIO_SCHEMA": "Gap.1", 85 | "effects": [], 86 | "markers": [], 87 | "metadata": {}, 88 | "name": "gap", 89 | "source_range": { 90 | "OTIO_SCHEMA": "TimeRange.1", 91 | "duration": { 92 | "OTIO_SCHEMA": "RationalTime.1", 93 | "rate": 24.0, 94 | "value": 300 95 | }, 96 | "start_time": { 97 | "OTIO_SCHEMA": "RationalTime.1", 98 | "rate": 24.0, 99 | "value": 0 100 | } 101 | } 102 | } 103 | ], 104 | "effects": [], 105 | "kind": "Video", 106 | "markers": [], 107 | "metadata": {}, 108 | "name": null, 109 | "source_range": null 110 | } 111 | ], 112 | "effects": [], 113 | "markers": [], 114 | "metadata": {}, 115 | "name": "tracks", 116 | "source_range": null 117 | } 118 | } -------------------------------------------------------------------------------- /tests/sample_data/keyframed_properties.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/keyframed_properties.aaf -------------------------------------------------------------------------------- /tests/sample_data/linear_speed_effects.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/linear_speed_effects.aaf -------------------------------------------------------------------------------- /tests/sample_data/linear_speed_effects_aaf.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/linear_speed_effects_aaf.mov -------------------------------------------------------------------------------- /tests/sample_data/marker-over-audio.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/marker-over-audio.aaf -------------------------------------------------------------------------------- /tests/sample_data/marker-over-transition.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/marker-over-transition.aaf -------------------------------------------------------------------------------- /tests/sample_data/misc_speed_effects.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/misc_speed_effects.aaf -------------------------------------------------------------------------------- /tests/sample_data/misc_speed_effects_aaf.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/misc_speed_effects_aaf.mov -------------------------------------------------------------------------------- /tests/sample_data/multiple-markers-over-transitions.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/multiple-markers-over-transitions.aaf -------------------------------------------------------------------------------- /tests/sample_data/multiple-markers-over-transitions.txt: -------------------------------------------------------------------------------- 1 | username 0 V1 red 0 1 2 | username 5 V1 red 5 1 3 | username 6 V1 red 0 1 4 | username 16 V1 red 10 1 5 | username 17 V1 red 11 1 6 | username 28 V1 red 22 1 7 | username 39 V1 red 33 1 8 | username 40 V1 red 23 1 9 | username 64 V1 red 47 1 10 | username 65 V1 red 48 1 11 | username 69 V1 red 52 1 12 | username 74 V1 red 57 1 13 | username 75 V1 red 0 1 14 | username 83 V1 red 8 1 15 | username 89 V1 red 14 1 16 | username 90 V1 red 0 1 17 | username 101 V1 red 11 1 18 | username 113 V1 red 23 1 19 | username 114 V1 red 24 1 20 | username 125 V1 red 35 1 21 | username 137 V1 red 47 1 22 | username 138 V1 red 0 1 23 | username 141 V1 red 3 1 24 | username 144 V1 red 6 1 25 | username 145 V1 red 0 1 26 | username 152 V1 red 7 1 27 | username 159 V1 red 14 1 28 | username 160 V1 red 15 1 29 | username 172 V1 red 27 1 30 | username 192 V1 red 47 1 31 | username 193 V1 red 0 1 32 | username 200 V1 red 7 1 33 | username 207 V1 red 14 1 34 | username 208 V1 red 0 1 35 | username 218 V1 red 10 1 36 | username 230 V1 red 22 1 37 | username 231 V1 red 23 1 38 | username 242 V1 red 34 1 39 | username 254 V1 red 46 1 40 | username 255 V1 red 24 1 41 | username 263 V1 red 32 1 42 | username 275 V1 red 44 1 43 | username 276 V1 red 0 1 44 | username 288 V1 red 12 1 45 | username 298 V1 red 22 1 46 | username 299 V1 red 23 1 47 | username 310 V1 red 34 1 48 | username 311 V1 red 35 1 49 | username 322 V1 red 46 1 50 | username 334 V1 red 58 1 51 | username 335 V1 red 24 1 52 | username 338 V1 red 27 1 53 | username 343 V1 red 32 1 54 | username 344 V1 red 0 1 55 | username 355 V1 red 11 1 56 | username 366 V1 red 22 1 57 | username 367 V1 red 23 1 58 | username 378 V1 red 34 1 59 | username 391 V1 red 47 1 60 | username 392 V1 red 25 1 61 | username 398 V1 red 31 1 62 | username 404 V1 red 37 1 63 | -------------------------------------------------------------------------------- /tests/sample_data/multiple_markers.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/multiple_markers.aaf -------------------------------------------------------------------------------- /tests/sample_data/multiple_timecode_objects.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/multiple_timecode_objects.aaf -------------------------------------------------------------------------------- /tests/sample_data/multiple_top_level_mobs.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/multiple_top_level_mobs.aaf -------------------------------------------------------------------------------- /tests/sample_data/multitrack.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/multitrack.aaf -------------------------------------------------------------------------------- /tests/sample_data/nested_audio_dissolve.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/nested_audio_dissolve.aaf -------------------------------------------------------------------------------- /tests/sample_data/nested_stack.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/nested_stack.aaf -------------------------------------------------------------------------------- /tests/sample_data/nesting_test.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/nesting_test.aaf -------------------------------------------------------------------------------- /tests/sample_data/nesting_test_preflattened.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/nesting_test_preflattened.aaf -------------------------------------------------------------------------------- /tests/sample_data/no_metadata.otio: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA": "Timeline.1", 3 | "metadata": {}, 4 | "name": "OTIO_Test_ppjoshm1.Exported.01", 5 | "tracks": { 6 | "OTIO_SCHEMA": "Stack.1", 7 | "children": [ 8 | { 9 | "OTIO_SCHEMA": "Track.1", 10 | "children": [ 11 | { 12 | "OTIO_SCHEMA": "Clip.1", 13 | "effects": [], 14 | "markers": [], 15 | "media_reference": { 16 | "OTIO_SCHEMA": "ExternalReference.1", 17 | "available_range": { 18 | "OTIO_SCHEMA": "TimeRange.1", 19 | "duration": { 20 | "OTIO_SCHEMA": "RationalTime.1", 21 | "rate": 24, 22 | "value": 192 23 | }, 24 | "start_time": { 25 | "OTIO_SCHEMA": "RationalTime.1", 26 | "rate": 24, 27 | "value": 1 28 | } 29 | }, 30 | "metadata": {}, 31 | "name": null, 32 | "target_url": "sample_data/one_clip.aaf" 33 | }, 34 | "metadata": { 35 | "example_studio": { 36 | "OTIO_SCHEMA": "ExampleStudioMetadata.1", 37 | "cache": { 38 | "hitech": { 39 | "OTIO_SCHEMA": "ExampleDatabase.1", 40 | "shot": null, 41 | "take": null 42 | } 43 | }, 44 | "take": { 45 | "OTIO_SCHEMA": "ExampleStudioTake.1", 46 | "globaltake": 1, 47 | "prod": "ppjoshm", 48 | "shot": "ppjoshm_1", 49 | "unit": "none" 50 | } 51 | } 52 | }, 53 | "name": "ppjoshm_1 (SIM1)", 54 | "source_range": { 55 | "OTIO_SCHEMA": "TimeRange.1", 56 | "duration": { 57 | "OTIO_SCHEMA": "RationalTime.1", 58 | "rate": 24.0, 59 | "value": 10 60 | }, 61 | "start_time": { 62 | "OTIO_SCHEMA": "RationalTime.1", 63 | "rate": 24.0, 64 | "value": 101 65 | } 66 | } 67 | } 68 | ], 69 | "effects": [], 70 | "kind": "Video", 71 | "markers": [], 72 | "metadata": {}, 73 | "name": "TimelineMobSlot", 74 | "source_range": null 75 | }, 76 | { 77 | "OTIO_SCHEMA": "Track.1", 78 | "children": [ 79 | { 80 | "OTIO_SCHEMA": "Clip.1", 81 | "effects": [], 82 | "markers": [], 83 | "media_reference": { 84 | "OTIO_SCHEMA": "ExternalReference.1", 85 | "available_range": { 86 | "OTIO_SCHEMA": "TimeRange.1", 87 | "duration": { 88 | "OTIO_SCHEMA": "RationalTime.1", 89 | "rate": 24, 90 | "value": 192 91 | }, 92 | "start_time": { 93 | "OTIO_SCHEMA": "RationalTime.1", 94 | "rate": 24, 95 | "value": 1 96 | } 97 | }, 98 | "metadata": {}, 99 | "name": null, 100 | "target_url": "sample_data/one_clip.aaf" 101 | }, 102 | "metadata": { 103 | "example_studio": { 104 | "OTIO_SCHEMA": "ExampleStudioMetadata.1", 105 | "cache": { 106 | "hitech": { 107 | "OTIO_SCHEMA": "ExampleDatabase.1", 108 | "shot": null, 109 | "take": null 110 | } 111 | }, 112 | "take": { 113 | "OTIO_SCHEMA": "ExampleStudioTake.1", 114 | "globaltake": 1, 115 | "prod": "ppjoshm", 116 | "shot": "ppjoshm_1", 117 | "unit": "none" 118 | } 119 | } 120 | }, 121 | "name": "ppjoshm_1 (SIM1)", 122 | "source_range": { 123 | "OTIO_SCHEMA": "TimeRange.1", 124 | "duration": { 125 | "OTIO_SCHEMA": "RationalTime.1", 126 | "rate": 24.0, 127 | "value": 10 128 | }, 129 | "start_time": { 130 | "OTIO_SCHEMA": "RationalTime.1", 131 | "rate": 24.0, 132 | "value": 0 133 | } 134 | } 135 | } 136 | ], 137 | "effects": [], 138 | "kind": "Audio", 139 | "markers": [], 140 | "metadata": {}, 141 | "name": "TimelineMobSlot", 142 | "source_range": null 143 | } 144 | ], 145 | "effects": [], 146 | "markers": [], 147 | "metadata": {}, 148 | "name": "tracks", 149 | "source_range": null 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/sample_data/normalclip_sourceclip_references_compositionmob_has_also_mastermob_usercomments.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/normalclip_sourceclip_references_compositionmob_has_also_mastermob_usercomments.aaf -------------------------------------------------------------------------------- /tests/sample_data/normalclip_sourceclip_references_compositionmob_with_usercomments_no_mastermob_usercomments.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/normalclip_sourceclip_references_compositionmob_with_usercomments_no_mastermob_usercomments.aaf -------------------------------------------------------------------------------- /tests/sample_data/not_aaf.otio: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA": "Timeline.1", 3 | "metadata": {}, 4 | "name": "OTIO_Test_ppjoshm1.Exported.01", 5 | "tracks": { 6 | "OTIO_SCHEMA": "Stack.1", 7 | "children": [ 8 | { 9 | "OTIO_SCHEMA": "Track.1", 10 | "children": [ 11 | { 12 | "OTIO_SCHEMA": "Clip.1", 13 | "effects": [], 14 | "markers": [], 15 | "media_reference": { 16 | "OTIO_SCHEMA": "ExternalReference.1", 17 | "available_range": { 18 | "OTIO_SCHEMA": "TimeRange.1", 19 | "duration": { 20 | "OTIO_SCHEMA": "RationalTime.1", 21 | "rate": 24, 22 | "value": 192 23 | }, 24 | "start_time": { 25 | "OTIO_SCHEMA": "RationalTime.1", 26 | "rate": 24, 27 | "value": 1 28 | } 29 | }, 30 | "metadata": {}, 31 | "name": null, 32 | "target_url": "sample_data/one_clip.mov" 33 | }, 34 | "metadata": { 35 | "example_studio": { 36 | "OTIO_SCHEMA": "ExampleStudioMetadata.1", 37 | "cache": { 38 | "hitech": { 39 | "OTIO_SCHEMA": "ExampleDatabase.1", 40 | "shot": null, 41 | "take": null 42 | } 43 | }, 44 | "take": { 45 | "OTIO_SCHEMA": "ExampleStudioTake.1", 46 | "globaltake": 1, 47 | "prod": "ppjoshm", 48 | "shot": "ppjoshm_1", 49 | "unit": "none" 50 | } 51 | } 52 | }, 53 | "name": "ppjoshm_1 (SIM1)", 54 | "source_range": { 55 | "OTIO_SCHEMA": "TimeRange.1", 56 | "duration": { 57 | "OTIO_SCHEMA": "RationalTime.1", 58 | "rate": 24.0, 59 | "value": 10 60 | }, 61 | "start_time": { 62 | "OTIO_SCHEMA": "RationalTime.1", 63 | "rate": 24.0, 64 | "value": 101 65 | } 66 | } 67 | } 68 | ], 69 | "effects": [], 70 | "kind": "Video", 71 | "markers": [], 72 | "metadata": {}, 73 | "name": "TimelineMobSlot", 74 | "source_range": null 75 | }, 76 | { 77 | "OTIO_SCHEMA": "Track.1", 78 | "children": [ 79 | { 80 | "OTIO_SCHEMA": "Clip.1", 81 | "effects": [], 82 | "markers": [], 83 | "media_reference": { 84 | "OTIO_SCHEMA": "ExternalReference.1", 85 | "available_range": { 86 | "OTIO_SCHEMA": "TimeRange.1", 87 | "duration": { 88 | "OTIO_SCHEMA": "RationalTime.1", 89 | "rate": 24, 90 | "value": 192 91 | }, 92 | "start_time": { 93 | "OTIO_SCHEMA": "RationalTime.1", 94 | "rate": 24, 95 | "value": 1 96 | } 97 | }, 98 | "metadata": {}, 99 | "name": null, 100 | "target_url": "sample_data/one_clip.mov" 101 | }, 102 | "metadata": { 103 | "example_studio": { 104 | "OTIO_SCHEMA": "ExampleStudioMetadata.1", 105 | "cache": { 106 | "hitech": { 107 | "OTIO_SCHEMA": "ExampleDatabase.1", 108 | "shot": null, 109 | "take": null 110 | } 111 | }, 112 | "take": { 113 | "OTIO_SCHEMA": "ExampleStudioTake.1", 114 | "globaltake": 1, 115 | "prod": "ppjoshm", 116 | "shot": "ppjoshm_1", 117 | "unit": "none" 118 | } 119 | } 120 | }, 121 | "name": "ppjoshm_1 (SIM1)", 122 | "source_range": { 123 | "OTIO_SCHEMA": "TimeRange.1", 124 | "duration": { 125 | "OTIO_SCHEMA": "RationalTime.1", 126 | "rate": 24.0, 127 | "value": 10 128 | }, 129 | "start_time": { 130 | "OTIO_SCHEMA": "RationalTime.1", 131 | "rate": 24.0, 132 | "value": 0 133 | } 134 | } 135 | } 136 | ], 137 | "effects": [], 138 | "kind": "Audio", 139 | "markers": [], 140 | "metadata": {}, 141 | "name": "TimelineMobSlot", 142 | "source_range": null 143 | } 144 | ], 145 | "effects": [], 146 | "markers": [], 147 | "metadata": {}, 148 | "name": "tracks", 149 | "source_range": null 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/sample_data/one_audio_clip.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/one_audio_clip.aaf -------------------------------------------------------------------------------- /tests/sample_data/one_clip.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/one_clip.aaf -------------------------------------------------------------------------------- /tests/sample_data/picchu_seq0100_snippet_dnx.dnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/picchu_seq0100_snippet_dnx.dnx -------------------------------------------------------------------------------- /tests/sample_data/picchu_seq0100_snippet_dnx.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/picchu_seq0100_snippet_dnx.mov -------------------------------------------------------------------------------- /tests/sample_data/picchu_seq0100_snippet_embedded.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/picchu_seq0100_snippet_embedded.aaf -------------------------------------------------------------------------------- /tests/sample_data/precheckfail.otio: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA": "Timeline.1", 3 | "global_start_time": { 4 | "OTIO_SCHEMA": "RationalTime.1", 5 | "rate": 24.0, 6 | "value": 86400 7 | }, 8 | "metadata": { 9 | "AAF": { 10 | "ClassName": "CompositionMob", 11 | "CreationTime": "2019-03-29 18:55:55", 12 | "LastModified": "2019-03-29 18:55:14", 13 | "MobAttributeList": { 14 | "AudioPluginWindowTrack": 1, 15 | "PRJ_BOUNDARY_FRAMES": 1, 16 | "SEQUERNCE_FORMAT_STRING": "HD 1080p/24", 17 | "SEQUERNCE_FORMAT_TYPE": 10, 18 | "_IMAGE_BOUNDS_OVERRIDE": " -800/1 -450/1 1600/1 900/1", 19 | "_USER_POS": 10, 20 | "_VERSION": 2 21 | }, 22 | "MobID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6a3b.ace913a2", 23 | "Name": "OTIO_Test_ppjoshm1.Exported.01", 24 | "Slots": {}, 25 | "UsageCode": "Usage_TopLevel" 26 | } 27 | }, 28 | "name": "OTIO_Test_ppjoshm1.Exported.01", 29 | "tracks": { 30 | "OTIO_SCHEMA": "Stack.1", 31 | "children": [ 32 | { 33 | "OTIO_SCHEMA": "Track.1", 34 | "children": [ 35 | { 36 | "OTIO_SCHEMA": "Clip.1", 37 | "effects": [], 38 | "markers": [], 39 | "media_reference": { 40 | "OTIO_SCHEMA": "MissingReference.1", 41 | "available_range": null, 42 | "metadata": { 43 | "AAF": { 44 | "ClassName": "MasterMob", 45 | "ConvertFrameRate": false, 46 | "CreationTime": "2019-03-29 18:52:18", 47 | "LastModified": "2019-03-29 18:54:01", 48 | "MobAttributeList": { 49 | "_GEN": 1553885640, 50 | "_IMPORTSETTING": "__AttributeList", 51 | "_SAVED_AAF_AUDIO_LENGTH": 0, 52 | "_SAVED_AAF_AUDIO_RATE_DEN": 1, 53 | "_SAVED_AAF_AUDIO_RATE_NUM": 24, 54 | "_USER_POS": 0, 55 | "_VERSION": 2 56 | }, 57 | "MobID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5", 58 | "Name": "ppjoshm_1 (SIM1)", 59 | "Slots": {} 60 | } 61 | }, 62 | "name": null 63 | }, 64 | "metadata": { 65 | "AAF": { 66 | "ClassName": "SourceClip", 67 | "ComponentAttributeList": { 68 | "_IMAGE_BOUNDS_OVERRIDE": " -800/1 -450/1 1600/1 900/1 -800/1 -450/1 1600/1 900/1 -800/1 -450/1 1600/1 900/1 -800/1 -450/1 1600/1 900/1" 69 | }, 70 | "DataDefinition": { 71 | "Description": "Picture Essence", 72 | "Identification": "01030202-0100-0000-060e-2b3404010101", 73 | "Name": "Picture" 74 | }, 75 | "Length": 10, 76 | "Name": "ppjoshm_1 (SIM1)", 77 | "SourceID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5", 78 | "SourceMobSlotID": 1, 79 | "StartTime": 0 80 | } 81 | }, 82 | "name": "ppjoshm_1 (SIM1)", 83 | "source_range": { 84 | "OTIO_SCHEMA": "TimeRange.1", 85 | "duration": { 86 | "OTIO_SCHEMA": "RationalTime.1", 87 | "rate": 24.0, 88 | "value": 10 89 | }, 90 | "start_time": { 91 | "OTIO_SCHEMA": "RationalTime.1", 92 | "rate": 24.0, 93 | "value": 86501 94 | } 95 | } 96 | } 97 | ], 98 | "effects": [], 99 | "kind": "Video", 100 | "markers": [], 101 | "metadata": { 102 | "AAF": { 103 | "ClassName": "TimelineMobSlot", 104 | "EditRate": "24", 105 | "MediaKind": "Picture", 106 | "Name": "TimelineMobSlot", 107 | "Origin": 0, 108 | "PhysicalTrackNumber": 1, 109 | "Segment": { 110 | "Components": {}, 111 | "DataDefinition": { 112 | "Description": "Picture Essence", 113 | "Identification": "01030202-0100-0000-060e-2b3404010101", 114 | "Name": "Picture" 115 | }, 116 | "Length": 10 117 | }, 118 | "SlotID": 9, 119 | "SlotName": "" 120 | } 121 | }, 122 | "name": "TimelineMobSlot", 123 | "source_range": null 124 | }, 125 | { 126 | "OTIO_SCHEMA": "Track.1", 127 | "children": [ 128 | { 129 | "OTIO_SCHEMA": "Clip.1", 130 | "effects": [], 131 | "markers": [], 132 | "media_reference": { 133 | "OTIO_SCHEMA": "MissingReference.1", 134 | "available_range": { 135 | "OTIO_SCHEMA": "TimeRange.1", 136 | "duration": { 137 | "OTIO_SCHEMA": "RationalTime.1", 138 | "rate": 48.0, 139 | "value": 10 140 | }, 141 | "start_time": { 142 | "OTIO_SCHEMA": "RationalTime.1", 143 | "rate": 48.0, 144 | "value": 0 145 | } 146 | }, 147 | "metadata": { 148 | "AAF": { 149 | "ClassName": "MasterMob", 150 | "ConvertFrameRate": false, 151 | "CreationTime": "2019-03-29 18:52:18", 152 | "LastModified": "2019-03-29 18:54:01", 153 | "MobAttributeList": { 154 | "_GEN": 1553885640, 155 | "_IMPORTSETTING": "__AttributeList", 156 | "_SAVED_AAF_AUDIO_LENGTH": 0, 157 | "_SAVED_AAF_AUDIO_RATE_DEN": 1, 158 | "_SAVED_AAF_AUDIO_RATE_NUM": 24, 159 | "_USER_POS": 0, 160 | "_VERSION": 2 161 | }, 162 | "MobID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5", 163 | "Name": "ppjoshm_1 (SIM1)", 164 | "Slots": {} 165 | } 166 | }, 167 | "name": null 168 | }, 169 | "metadata": { 170 | "AAF": { 171 | "ClassName": "SourceClip", 172 | "DataDefinition": { 173 | "Description": "Sound Essence", 174 | "Identification": "01030202-0200-0000-060e-2b3404010101", 175 | "Name": "Sound" 176 | }, 177 | "Length": 10, 178 | "Name": "ppjoshm_1 (SIM1)", 179 | "SourceID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5", 180 | "SourceMobSlotID": 2, 181 | "StartTime": 0 182 | } 183 | }, 184 | "name": "ppjoshm_1 (SIM1)", 185 | "source_range": { 186 | "OTIO_SCHEMA": "TimeRange.1", 187 | "duration": { 188 | "OTIO_SCHEMA": "RationalTime.1", 189 | "rate": 24.0, 190 | "value": 10 191 | }, 192 | "start_time": { 193 | "OTIO_SCHEMA": "RationalTime.1", 194 | "rate": 24.0, 195 | "value": 0 196 | } 197 | } 198 | } 199 | ], 200 | "effects": [], 201 | "kind": "Audio", 202 | "markers": [], 203 | "metadata": { 204 | "AAF": { 205 | "ClassName": "TimelineMobSlot", 206 | "EditRate": "24", 207 | "MediaKind": "Sound", 208 | "Name": "TimelineMobSlot", 209 | "Origin": 0, 210 | "PhysicalTrackNumber": 1, 211 | "Segment": { 212 | "Components": {}, 213 | "DataDefinition": { 214 | "Description": "Sound Essence", 215 | "Identification": "01030202-0200-0000-060e-2b3404010101", 216 | "Name": "Sound" 217 | }, 218 | "Length": 10 219 | }, 220 | "SlotID": 10, 221 | "SlotName": "" 222 | } 223 | }, 224 | "name": "TimelineMobSlot", 225 | "source_range": null 226 | } 227 | ], 228 | "effects": [], 229 | "markers": [], 230 | "metadata": {}, 231 | "name": "tracks", 232 | "source_range": null 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /tests/sample_data/preflattened.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/preflattened.aaf -------------------------------------------------------------------------------- /tests/sample_data/simple.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/simple.aaf -------------------------------------------------------------------------------- /tests/sample_data/subclip_sourceclip_references_compositionmob_with_mastermob.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/subclip_sourceclip_references_compositionmob_with_mastermob.aaf -------------------------------------------------------------------------------- /tests/sample_data/test_muted_clip.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/test_muted_clip.aaf -------------------------------------------------------------------------------- /tests/sample_data/time_warp_test.avid_media_composer.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/time_warp_test.avid_media_composer.aaf -------------------------------------------------------------------------------- /tests/sample_data/timecode_test.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/timecode_test.aaf -------------------------------------------------------------------------------- /tests/sample_data/transitions.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/transitions.aaf -------------------------------------------------------------------------------- /tests/sample_data/trims.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/trims.aaf -------------------------------------------------------------------------------- /tests/sample_data/utf8.aaf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/otio-aaf-adapter/acbbe48a621fdef2688e514701f5e30296e7c189/tests/sample_data/utf8.aaf --------------------------------------------------------------------------------