diff --git a/.github/workflows/CIlinux.yml b/.github/workflows/CIlinux.yml
index 6e897d3a..890d5dbf 100644
--- a/.github/workflows/CIlinux.yml
+++ b/.github/workflows/CIlinux.yml
@@ -39,13 +39,13 @@ on:
jobs:
test:
name: CI Linux
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install APT Dependencies
@@ -60,22 +60,23 @@ jobs:
sudo chmod +x scripts/bash/prepare_dataset.sh
- name: Install Pip Dependencies
run: |
- sudo pip install -U pip wheel numpy
+ sudo pip install -U numpy
sudo pip install -U .
sudo pip install -U opencv-python-headless
sudo pip install -U vidgear[core]
- sudo pip install -U flake8 six codecov pytest pytest-cov
+ sudo pip install -U ruff six codecov pytest pytest-cov
if: success()
- name: Run prepare_dataset Bash script
run: bash scripts/bash/prepare_dataset.sh
shell: bash
- - name: Run pytest and flake8
+ - name: Run pytest and ruff
run: |
- timeout 1200 sudo python -m pytest -sv --cov=deffcode --cov-report=xml --cov-report term-missing tests/ || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; else echo "EXIT_CODE=$code" >>$GITHUB_ENV; fi
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+ timeout 1200 sudo pytest -sv --cov=deffcode --cov-report=xml --cov-report term-missing tests/ || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; else echo "EXIT_CODE=$code" >>$GITHUB_ENV; fi
+ ruff check .
+ ruff format --check .
if: success()
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
with:
name: ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/docs_deployer.yml b/.github/workflows/docs_deployer.yml
index cfd896ca..6ba65570 100644
--- a/.github/workflows/docs_deployer.yml
+++ b/.github/workflows/docs_deployer.yml
@@ -23,7 +23,7 @@ on:
types: [published]
env:
- PYTHON_VERSION: 3.9
+ PYTHON_VERSION: 3.11
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
GIT_NAME: ${{ secrets.GIT_NAME }}
GIT_EMAIL: ${{ secrets.GIT_EMAIL }}
@@ -34,10 +34,10 @@ jobs:
if: github.event_name == 'release' && github.event.action == 'published'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install_deffcode
@@ -48,8 +48,7 @@ jobs:
- name: install_docs_deps
run: |
pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin
- pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy
- pip install jinja2==3.0.*
+ pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python
if: success()
- name: git configure
run: |
@@ -82,10 +81,10 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install_deffcode
@@ -96,8 +95,7 @@ jobs:
- name: install_docs_deps
run: |
pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin
- pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy
- pip install jinja2==3.0.*
+ pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python
if: success()
- name: git configure
run: |
@@ -131,11 +129,11 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- run: git checkout dev
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install_deffcode
@@ -146,8 +144,7 @@ jobs:
- name: install_docs_deps
run: |
pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin
- pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy
- pip install jinja2==3.0.*
+ pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python
if: success()
- name: git configure
run: |
diff --git a/README.md b/README.md
index 9f3baf73..6780699c 100644
--- a/README.md
+++ b/README.md
@@ -67,12 +67,13 @@ Here are some key features that stand out:
- Curated list of well-documented recipes ranging from [**Basic**][basic-recipes] to [**Advanced**][advanced-recipes] skill levels.
- Hands down the easiest [**Index based Camera Device Capturing**][decoding-camera-devices-using-indexes], similar to OpenCV.
- Easy to code **Real-time [Simple][transcoding-live-simple-filtergraphs] & [Complex][transcoding-live-complex-filtergraphs] Filtergraphs**. _(Yes, You read it correctly "Real-time"!)_
+- Native **[Multi-Input Source Configurations][multi-input-source-configurations]** support for decoding complex topologies.
- Lightning fast dedicated **GPU-Accelerated Video [Decoding][hardware-accelerated-video-decoding] & [Transcoding][hardware-accelerated-video-transcoding]**.
- Enables precise FFmpeg [**Key-frame Seeking**][extracting-key-frames-as-png-image] with pinpoint accuracy.
- Effortless [**Metadata Extraction**][extracting-video-metadata] from all streams available in the source.
- Maintains the standard easy to learn [**OpenCV-Python**](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html) coding syntax.
- Out-of-the-box support for all prominent Computer Vision libraries.
-- Cross-platform, runs on Python 3.7+, and easy to install.
+- Cross-platform, runs on Python 3.10+, and easy to install.
-# Submitting Pull Request(PR) Guidelines:
+# Submitting Pull Request (PR) Guidelines
+These guidelines outline how to submit a high-quality Pull Request (PR) to **DeFFcode**.
-The following guidelines tells you how to submit a valid PR for DeFFcode:
+## :material-rocket-launch: Before You Start
-!!! question "Working on your first Pull Request for DeFFcode?"
+??? question "First time contributing to DeFFcode?"
- * You can learn about "**How to contribute to an Open Source Project on GitHub**" from [this doc ➶](https://opensource.guide/how-to-contribute/)
- * If you're stuck at something, please join our [Gitter community channel](https://gitter.im/DeFFcode/community). We will help you get started!
+ - Learn how open-source contributions work from [this guide ➶](https://opensource.guide/how-to-contribute/)
+ - Need help? Join our [Gitter community](https://gitter.im/DeFFcode/community) and we’ll assist you
-
+
-## Clone branch for PR
+## :material-source-branch-plus: Create a Working Branch
-You can clone your [**Forked**](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) remote git to local and create your PR working branch as a sub-branch of latest [`master`](https://github.com/abhiTronix/deffcode/tree/master) branch as follows:
+Start by cloning your fork and creating a feature branch from the latest `master`:
-!!! alert "Make sure the [`master`](https://github.com/abhiTronix/deffcode/tree/master) branch of your Forked repository is up-to-date with DeFFcode, before starting working on a Pull Request."
+!!! danger "Keep your fork up to date"
+
+ Ensure your fork’s `master` branch is synced with the upstream repository before starting.
```sh
-# clone your forked repository(change with your username) and get inside
-git clone https://github.com/{YOUR USERNAME}/DeFFcode.git && cd DeFFcode
+# Clone your fork (replace with your username)
+git clone https://github.com/{YOUR_USERNAME}/DeFFcode.git
+cd DeFFcode
-# pull any recent updates
+# Sync latest changes
git pull
-# Now create your new branch with suitable name(such as "subbranch_of_master")
-git checkout -b subbranch_of_master
+# Create a new branch
+git checkout -b feature/your-branch-name
```
-Now after working with this newly created branch for your Pull Request, you can commit and push or merge it locally or remotely as usual.
+Work on this branch and push changes as usual.
-
+
-
+## :octicons-checklist-24: PR Submission Checklist
-## PR Submission Checklist
+### 1. Open an Issue First
-There are some important checks you need to perform while submitting your Pull Request(s) for DeFFcode library:
+* Start by creating an issue using the [proposal template](https://github.com/abhiTronix/deffcode/issues/new?labels=issue%3A+proposal&template=proposal.md)
+* This helps align your work with project goals and avoids duplicate effort
-- [x] **Submit a Related Issue:**
-
- * The first thing you do is submit an issue with a [proposal template](https://github.com/abhiTronix/deffcode/issues/new?labels=issue%3A+proposal&template=proposal.md) for your work first and then work on your Pull Request.
+### 2. Open a Draft PR Early
+* Create a **draft PR** from the beginning of your work
+* Add:
+ * A clear and descriptive title
+ * Summary of what the PR fixes/adds/improves
+ * Screenshots or outputs (if applicable)
+* For bug fixes:
+ * Include a **failing test case** that your fix resolves
+* Ensure all CI checks pass
+* Mark as **Ready for Review** once complete
-- [x] **Submit a Draft Pull Request:**
+### 3. Test, Format & Lint Locally
- * Submit the [draft pull request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) from the first day of your development.
- * Add a brief but descriptive title for your PR.
- * Explain what the PR adds, fixes, or improves.
- * In case of bug fixes, add a new unit test case that would fail against your bug fix.
- * Provide output or screenshots, if you can.
- * Make sure your pull request passed all the CI checks _(triggers automatically on pushing commits against `master` branch)_. If it's somehow failing, then ask the maintainer for a review.
- * Click "**ready for review**" when finished.
+* Run tests and ensure everything passes
+* Format and lint your code before committing
+* See [Testing & Linting](#testing-formatting-linting) section below
-- [x] **Test, Format & lint code locally:**
+### 4. Write Clear Commit Messages
- * Make sure to test, format, and lint the modified code locally before every commit. The details are discussed [below ➶](#formatting-linting)
+* Keep messages concise and meaningful
+* Link issues using keywords like `#!sh resolves #123`
+* Use `git commit --amend` to refine commits when needed
-- [x] **Make sensible commit messages:**
+### 5. Perform Integrity Checks
- * If your pull request fixes a separate issue number, remember to include `"resolves #issue_number"` in the commit message. Learn more about it [here ➶](https://help.github.com/articles/closing-issues-using-keywords/).
- * Keep the commit message concisely as much as possible at every submit. You can make a supplement to the previous commit with `git commit --amend` command.
+!!! warning "Duplicate PRs will be rejected"
-- [x] **Perform Integrity Checks:**
+* Check for existing related PRs/issues
+* Ensure your changes align with DeFFcode’s design and goals
+* By contributing, you agree your code will be licensed under the [Apache 2.0 License ➶](https://github.com/abhiTronix/deffcode/blob/master/LICENSE)
- !!! warning "Any duplicate pull request will be Rejected!"
+### 6. Link Your Issue
- * Search GitHub if there's a similar open or closed PR that relates to your submission.
- * Check if your purpose code matches the overall direction of the DeFFcode APIs and improves it.
- * Retain copyright for your contributions, but also agree to license them for usage by the project and author(s) under the [**Apache 2.0 license ➶**](https://github.com/abhiTronix/deffcode/blob/master/LICENSE).
+!!! tip
-- [x] **Link your Issues:**
+ Learn more about linking PRs to issues [here ➶](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- !!! tip "For more information on Linking a pull request to an issue, See [this doc➶](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue)"
+* Link your PR to the relevant issue
+* This helps track progress and avoid duplication
- * Finally, when you're confident enough, make your pull request public.
- * You can link an issue to a pull request manually or using a supported keyword in the pull request description. It helps collaborators see that someone is working on the issue. For more information, see [this doc➶](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
+
-
+## :material-test-tube: Testing, Formatting & Linting
-
+All PRs must pass testing and code quality checks.
-## Testing, Formatting & Linting
+### Requirements
-All Pull Request(s) must be tested, formatted & linted against our library standards as discussed below:
+!!! info "Python 3.10+ required"
-### Requirements
+Install dependencies:
-Testing DeFFcode requires additional test dependencies and dataset, which can be handled manually as follows:
+```sh
+# Install OpenCV (if not already installed)
+pip install opencv-python
+
+# Install remaining dependencies
+pip install --upgrade ruff pytest vidgear[core]
+```
-- [x] **Install additional python libraries:**
-
- You can easily install these dependencies via pip:
+### Test Dataset Setup
+
+Download required test data:
+
+=== "Linux :material-linux:/macOS :material-apple:"
```sh
- # Install opencv(only if not installed previously)
- $ pip install opencv-python
+ chmod +x scripts/bash/prepare_dataset.sh
+ ./scripts/bash/prepare_dataset.sh
+ ```
- # install rest of dependencies
- $ pip install --upgrade flake8 black pytest vidgear[core]
+=== "Windows :material-microsoft-windows:"
+
+ ```sh
+ sh scripts/bash/prepare_dataset.sh
```
-- [x] **Download Tests Dataset:**
+### Run Tests
- To perform tests, you also need to download additional dataset *(to your temp dir)* by running [`prepare_dataset.sh`](https://github.com/abhiTronix/deffcode/blob/master/scripts/bash/prepare_dataset.sh) bash script as follows:
+From the project root:
- === "On Linux/MacOS"
+```sh
+pytest -sv
+```
- ```sh
- $ chmod +x scripts/bash/prepare_dataset.sh
- $ ./scripts/bash/prepare_dataset.sh
- ```
+
- === "On Windows"
+### Formatting & Linting (Ruff)
- ```sh
- $ sh scripts/bash/prepare_dataset.sh
- ```
+DeFFcode uses **[Ruff](https://docs.astral.sh/ruff/)** for both linting and formatting.
-### Running Tests
+#### Lint Code
-All tests can be run with [`pytest`](https://docs.pytest.org/en/stable/)(*in DeFFcode's root folder*) as follows:
+```sh
+# Check for issues
+ruff check {path}
- ```sh
- $ pytest -sv #-sv for verbose output.
- ```
+# Auto-fix issues
+ruff check --fix {path}
+```
-### Formatting & Linting
+#### Format Code
-For formatting and linting, following libraries are used:
+```sh
+# Apply formatting
+ruff format {path}
-- [x] **Flake8:** You must run [`flake8`](https://flake8.pycqa.org/en/latest/manpage.html) linting for checking the code base against the coding style (PEP8), programming errors and other cyclomatic complexity:
+# Check formatting only
+ruff format --check {path}
+```
- ```sh
- $ flake8 {source_file_or_directory} --count --select=E9,F63,F7,F82 --show-source --statistics
- ```
+!!! tip "These checks run in CI—running them locally saves time during review."
-- [x] **Black:** DeFFcode follows [`black`](https://github.com/psf/black) formatting to make code review faster by producing the smallest diffs possible. You must run it with sensible defaults as follows:
- ```sh
- $ black {source_file_or_directory}
- ```
+
-
+## :material-chat-question: Frequently Asked Questions
-
+### Q1. Why is my PR taking time to be reviewed?
-## Frequently Asked Questions
+!!! success "After your PR is merged"
+ * You can delete your branch safely
+ * Changes are first merged into `dev`, then into `master` during release
+ * Active contributors may receive faster reviews over time
-**Q1. Why do my changes taking so long to be Reviewed and/or Merged?**
+PRs are reviewed by maintainers based on priority and availability. You may be asked to make changes before approval.
-!!! success "Submission Aftermaths"
- * After your PR is merged, you can safely delete your branch and pull the changes from the main (upstream) repository.
- * The changes will remain in `dev` branch until next DeFFcode version is released, then it will be merged into `master` branch.
- * After a successful Merge, your newer contributions will be given priority over others.
+### Q2. Can I submit a large PR?
-Pull requests will be reviewed by the maintainers and the rationale behind the maintainer’s decision to accept or deny the changes will be posted in the pull request. Please wait for our code review and approval, possibly enhancing your change on request.
+* Yes—but ensure changes are **focused and related**
+* For major changes:
+ - [x] Open an issue first for discussion
+ - [x] Large, unrelated changes should be split into smaller PRs
+This helps speed up review and increases the chances of acceptance.
-**Q2. Would you accept a huge Pull Request with Lots of Changes?**
-First, make sure that the changes are somewhat related. Otherwise, please create separate pull requests. Anyway, before submitting a huge change, it's probably a good idea to [open an issue](../../contribution/issue) in the DeFFcode Github repository to ask the maintainers if they agree with your proposed changes. Otherwise, they could refuse your proposal after you put all that hard work into making the changes. We definitely don't want you to waste your time!
+Thanks for contributing to **DeFFcode** 🚀
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/contribution/issue.md b/docs/contribution/issue.md
index 56033472..3e13c645 100644
--- a/docs/contribution/issue.md
+++ b/docs/contribution/issue.md
@@ -20,38 +20,68 @@ limitations under the License.
# Submitting an Issue Guidelines
-If you've found a new bug or you've come up with some new feature which can improve the quality of the DeFFcode, then related issues are welcomed! But, Before you do, please read the following guidelines:
+If you've discovered a bug or have an idea that could improve **DeFFcode**, we’d love to hear from you. Before opening an issue, please review the guidelines below—they help us triage faster and resolve issues more efficiently.
-??? question "First Issue on GitHub?"
+## :material-rocket-launch: Before You Start
+
+??? question "First issue on GitHub?"
+
+ You can learn how to create one from GitHub’s official guide on [creating an issue](https://help.github.com/en/github/managing-your-work-on-github/creating-an-issue).
+
+!!! info
+
+ Issues can usually be resolved much faster when they include clear reproduction steps, environment details, and a small demo.
- You can easily learn about it from [creating an issue](https://help.github.com/en/github/managing-your-work-on-github/creating-an-issue) wiki.
+ If you're short on time, feel free to submit a brief report—but please note that incomplete reports may take longer to investigate.
+
+
+
+## :material-tab-search: Search the Documentation and Existing Issues
+
+Before opening a new issue, please check the following first:
+
+- [x] Search for an existing [open or closed issue](https://github.com/abhiTronix/deffcode/issues?q=is%3Aissue) that matches your problem.
+- [x] Review the [FAQ & Troubleshooting section](../../help/get_help/#frequently-asked-questions).
+- [x] For quick questions, use our [Gitter community](https://gitter.im/deffcode-python/community) instead of opening an issue.
+
+You may find that your question has already been answered or that a workaround already exists.
+
+
+
+## :material-folder-edit: Gather Required Information
-!!! Info
+Please include the following information with your report whenever possible:
- Please note that your issue will be fixed much faster if you spend about half an hour preparing it, including the exact reproduction steps and a demo. If you're in a hurry or don't feel confident, it's fine to report issues with less details, but this makes it less likely they'll get fixed soon.
+- [x] Enable the `verbose=True` flag in the relevant API to collect debug logs.
+- [x] Provide a **minimal reproducible example** that demonstrates the issue.
+- [x] Include the installed DeFFcode version using command: `#!sh python -c "import deffcode; print(deffcode.__version__)"` and also:
+ * Python version
+ * Operating system
+ * FFmpeg version (`ffmpeg -version`)
-### Search the Docs and Previous Issues
+
- * Remember to first search GitHub for a [open or closed issue](https://github.com/abhiTronix/deffcode/issues?q=is%3Aissue) that relates to your submission or already been reported. You may find related information and the discussion might inform you of workarounds that may help to resolve the issue.
- * For quick questions, please refrain from opening an issue, as you can reach us on [Gitter](https://gitter.im/deffcode-python/community) community channel.
- * Also, go comprehensively through our dedicated [FAQ & Troubleshooting section](../../help/get_help/#frequently-asked-questions).
+## :octicons-repo-template-24: Follow the Issue Template
-### Gather Required Information
+- [x] Select the correct issue template before submitting.
+- [x] Complete all relevant sections in the template.
+- [x] Reports with insufficient information may be marked **Invalid ⛔**
+- [x] If no follow-up details are provided, the issue may be closed.
-* All DeFFcode APIs provides a `verbose` boolean flag in parameters, to log debugged output to terminal. Kindly turn this parameter `True` in the respective API for getting debug output, and paste it with your Issue.
-* In order to reproduce bugs we will systematically ask you to provide a minimal reproduction code for your report.
-* Check and paste, exact DeFFcode version by running command `#!python python -c "import deffcode; print(deffcode.__version__)"`.
+
-### Follow the Issue Template
+## :fontawesome-solid-fist-raised: Raise the Issue
-* Please format your issue by choosing the appropriate template.
-* Any improper/insufficient reports will be marked **Invalid ⛔**, and if we don't hear back from you we may close the issue.
+Before submitting:
-### Raise the Issue
+- [x] Write a short but descriptive title
+- [x] Keep the report focused on one issue
+- [x] Attach relevant logs, screenshots, or source code when available
-* Add a brief but descriptive title for your issue.
-* Keep the issue phrasing in context of the problem.
-* Attach source-code/screenshots if you have one.
-* Finally, raise it by choosing the appropriate Issue Template: [**Bug report 🐞**](https://github.com/abhiTronix/deffcode/issues/new?assignees=abhiTronix&labels=Bug+%3Alady_beetle%3A%2CNeeds+Triage+%3Amonocle_face%3A&template=bug_report.yaml&title=%5BBug%5D%3A+), [Idea 💡](https://github.com/abhiTronix/deffcode/issues/new?assignees=&labels=Idea+%3Abulb%3A&template=idea.yaml&title=%5BIdea%5D%3A+), [Question ❔](https://github.com/abhiTronix/deffcode/issues/new?assignees=&labels=Question+%3Agrey_question%3A&template=question.yaml&title=%5BQuestion%5D%3A+).
+Choose the appropriate template below:
-
\ No newline at end of file
+* [**Bug Report 🐞**](https://github.com/abhiTronix/deffcode/issues/new?assignees=abhiTronix&labels=Bug+%3Alady_beetle%3A%2CNeeds+Triage+%3Amonocle_face%3A&template=bug_report.yaml&title=%5BBug%5D%3A+)
+* [**Feature Idea 💡**](https://github.com/abhiTronix/deffcode/issues/new?assignees=&labels=Idea+%3Abulb%3A&template=idea.yaml&title=%5BIdea%5D%3A+)
+* [**Question ❔**](https://github.com/abhiTronix/deffcode/issues/new?assignees=&labels=Question+%3Agrey_question%3A&template=question.yaml&title=%5BQuestion%5D%3A+)
+
+
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 34c61f70..7ccfc613 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -23,7 +23,7 @@ limitations under the License.
{ loading=lazy }
{ loading=lazy }
-
A cross-platform **:fontawesome-solid-gauge-high: High-performance Video Frames Decoder** that flexibly executes FFmpeg pipeline inside a subprocess pipe for generating real-time, low-overhead, lightning fast video frames with robust error-handling in just a few lines of python code :fontawesome-solid-fire-flame-curved:
+> A cross-platform **:fontawesome-solid-gauge-high: High-performance Video Frames Decoder** that flexibly executes FFmpeg pipeline inside a subprocess pipe for generating real-time, low-overhead, lightning fast video frames with robust error-handling in just a few lines of python code :fontawesome-solid-fire-flame-curved:
@@ -45,12 +45,13 @@ Here are some key features that stand out:
- [x] Curated list of well-documented recipes ranging from [**Basic**](recipes/basic/) to [**Advanced**](recipes/advanced/) skill levels.
- [x] Hands down the easiest [**Index based Camera Device Capturing**](recipes/basic/decode-camera-devices), similar to OpenCV.
- [x] Memory efficient **Live [Simple](recipes/basic/transcode-live-frames-simplegraphs/#transcoding-live-simple-filtergraphs) & [Complex](recipes/advanced/transcode-live-frames-complexgraphs/#transcoding-live-complex-filtergraphs) Filtergraphs**. _(Yes, You read it correctly "Live"!)_
+- [x] Native **[Multi-Input Source Configurations](recipes/advanced/multi_input/#multi-input-source-configurations)** support for decoding complex topologies.
- [x] Lightning fast dedicated **:fontawesome-solid-microchip: GPU-Accelerated Video [Decoding](recipes/advanced/decode-hw-acceleration/#hardware-accelerated-video-decoding) & [Transcoding](recipes/advanced/transcode-hw-acceleration/#hardware-accelerated-video-transcoding)**.
- [x] Enables precise FFmpeg [**Frame Seeking**](recipes/basic/save-keyframe-image/#extracting-key-frames-as-png-image) with pinpoint accuracy.
- [x] Effortless [**Metadata Extraction**](recipes/basic/extract-video-metadata/#extracting-video-metadata) from all streams available in the source.
- [x] Maintains the standard easy to learn [**OpenCV-Python**](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html) coding syntax.
- [x] Out-of-the-box support for all prominent Computer Vision libraries.
-- [x] Cross-platform, runs on Python 3.7+, and easy to install.
+- [x] Cross-platform, runs on Python 3.10+, and easy to install.
??? question "Still missing a key feature in DeFFcode?"
@@ -197,18 +198,18 @@ It is something I am doing with my own free time. But so much more needs to be d
Here is a Bibtex entry you can use to cite this project in a publication:
-[](https://doi.org/10.5281/zenodo.7523792)
+[](https://doi.org/10.5281/zenodo.12689394)
```BibTeX
@software{deffcode,
author = {Abhishek Thakur},
- title = {abhiTronix/deffcode: v0.2.5},
- month = jan,
- year = 2023,
+ title = {abhiTronix/deffcode: v0.2.6},
+ month = jul,
+ year = 2024,
publisher = {Zenodo},
- version = {v0.2.5},
- doi = {10.5281/zenodo.7523792},
- url = {https://doi.org/10.5281/zenodo.7523792}
+ version = {v0.2.6},
+ doi = {10.5281/zenodo.12689394},
+ url = {https://doi.org/10.5281/zenodo.12689394},
}
```
diff --git a/docs/installation/index.md b/docs/installation/index.md
index 89f62023..97d1d84d 100644
--- a/docs/installation/index.md
+++ b/docs/installation/index.md
@@ -29,7 +29,39 @@ limitations under the License.
## Supported Systems
-DeFFcode is well-tested and supported on the following systems(but not limited to), with [python 3.7+](https://www.python.org/downloads/) and [pip](https://pip.pypa.io/en/stable/installing/#do-i-need-to-install-pip) installed:
+DeFFcode is well-tested and supported on the following systems(but not limited to):
+
+* Any :material-linux: Linux distro released in 2016 or later
+* :fontawesome-brands-windows: Windows 7 or later
+* :material-apple: macOS 10.12.6 (Sierra) or later
+
+
+
+## Supported Python legacies
+
+:fontawesome-brands-python: [**Python 3.10+**](https://www.python.org/downloads/) are only supported legacies for installing DeFFcode `v0.2.7` and above.
+
+
+
+## Prerequisites
+
+==:warning: **DeFFcode APIs requires FFmpeg binaries to be installed for all of its core functionality.**==
+
+### FFmpeg
+
+When installing DeFFcode, [**FFmpeg**][ffmpeg] is the only prerequisites you need to configure/install manually. **You could easily do it by referring [**FFmpeg Installation doc**](../installation/ffmpeg_install/)**.
+
+
+
+## Installation
+
+### A. Installation using pip (Recommended)
+
+
+> _Best option for easily getting stable DeFFcode installed._
+
+
+**Installation is as simple as:**
??? alert ":fontawesome-brands-python: Upgrade your `pip`"
@@ -46,14 +78,14 @@ DeFFcode is well-tested and supported on the following systems(but not limited t
* Download the script, from https://bootstrap.pypa.io/get-pip.py.
* Open a terminal/command prompt, `cd` to the folder containing the `get-pip.py` file and run:
- === "Linux/MacOS"
+ === "Linux :material-linux:/macOS :material-apple:"
```sh
python get-pip.py
```
- === "Windows"
+ === "Windows :material-microsoft-windows:"
```sh
py get-pip.py
@@ -94,38 +126,6 @@ DeFFcode is well-tested and supported on the following systems(but not limited t
```
-* Any :material-linux: Linux distro released in 2016 or later
-* :fontawesome-brands-windows: Windows 7 or later
-* :material-apple: MacOS 10.12.6 (Sierra) or later
-
-
-
-## Supported Python legacies
-
-:fontawesome-brands-python: [**Python 3.7+**](https://www.python.org/downloads/) are only supported legacies for installing DeFFcode `v0.1.0` and above.
-
-
-
-## Prerequisites
-
-==:warning: **DeFFcode APIs requires FFmpeg binaries to be installed for all of its core functionality.**==
-
-### FFmpeg
-
-When installing DeFFcode, [**FFmpeg**][ffmpeg] is the only prerequisites you need to configure/install manually. **You could easily do it by referring [**FFmpeg Installation doc**](../installation/ffmpeg_install/)**.
-
-
-
-## Installation
-
-### A. Installation using pip (Recommended)
-
-
-> _Best option for easily getting stable DeFFcode installed._
-
-
-**Installation is as simple as:**
-
??? warning ":fontawesome-brands-windows: Windows Installation"
If you are using Windows, some of the commands given below, may not work out-of-the-box.
@@ -165,7 +165,54 @@ pip install deffcode-0.2.0-py3-none-any.whl
-### B. Installation from Source
+### B. Installation using Poetry
+
+> _Best option for managing DeFFcode as a dependency in a [Poetry](https://python-poetry.org/)-managed project._
+
+DeFFcode's [`pyproject.toml`](https://github.com/abhiTronix/deffcode/blob/master/pyproject.toml) is PEP 517/621 compliant, so it can be consumed directly by [Poetry](https://python-poetry.org/docs/#installation).
+
+??? info "Don't have Poetry installed?"
+
+ Follow the [official Poetry installation guide](https://python-poetry.org/docs/#installation) before proceeding. You can verify your install with:
+
+ ```sh
+ poetry --version
+ ```
+
+**Add DeFFcode to an existing Poetry project:**
+
+```sh
+# Add latest stable release as a project dependency
+poetry add deffcode
+```
+
+**Or, install directly from source in a Poetry-managed environment:**
+
+```sh
+# clone the repository and get inside
+git clone https://github.com/abhiTronix/deffcode.git && cd deffcode
+
+# Install it into Poetry's virtualenv
+poetry install
+```
+
+??? tip "Running commands inside Poetry's virtualenv"
+
+ Use `poetry run` to execute DeFFcode-powered scripts without activating the shell:
+
+ ```sh
+ poetry run python your_script.py
+ ```
+
+ Or spawn a shell inside the virtualenv:
+
+ ```sh
+ poetry shell
+ ```
+
+
+
+### C. Installation from Source
> Best option for trying latest patches(maybe experimental), forking for Pull Requests, or automatically installing all prerequisites(with a few exceptions).
@@ -220,7 +267,7 @@ git clone https://github.com/abhiTronix/deffcode.git && cd deffcode
pip install -U .
```
-
+
[^1]: :warning: The `ensurepip` module is missing/disabled on Ubuntu. Use `pip` method only.
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index 910935b6..71f45d25 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -18,12 +18,15 @@
{% endblock %}
{% block announce %}
-{% set announcement_link = config.site_url ~
-'/recipes/basic/decode-camera-devices/#decoding-camera-devices-using-indexes' %}
+{% set announcement_link_1 = config.site_url ~
+'/recipes/advanced/multi_input/' %}
+{% set announcement_link_2 = config.site_url ~
+'recipes/advanced/extract-frame-metadata/' %}
+{% set announcement_link_3 = config.site_url ~
+'/recipes/basic/decode-video-files/#__tabbed_3_2' %}
-Hey, Index based Camera Device Capture support has been added in v0.2.4. Checkout these new recipes {% include ".icons/material/pot-steam-outline.svg"
- %} ➶
+
+ {% include ".icons/material/rocket-launch.svg" %} v0.2.7 is out! Decode multiple input streams in parallel ➶ , grab per-frame metadata async ➶ , and fly through YUV with -extract_luma ➶
{% endblock %}
{% block outdated %}
You're not viewing the latest version.
diff --git a/docs/recipes/advanced/decode-live-virtual-sources.md b/docs/recipes/advanced/decode-live-virtual-sources.md
index cce37c3d..b2e5d250 100644
--- a/docs/recipes/advanced/decode-live-virtual-sources.md
+++ b/docs/recipes/advanced/decode-live-virtual-sources.md
@@ -22,7 +22,7 @@ limitations under the License.
> Instead of using prerecorded video files as streams, DeFFcode's FFdecoder API with the help of powerful [`lavfi`](http://underpop.online.fr/f/ffmpeg/help/lavfi.htm.gz) _(**Libavfilter** input virtual device)_ source that reads data from the open output pads of a libavfilter filtergraph, is also capable of creating virtual video frames out of thin air in real-time, which you might want to use as input for testing, compositing, and merging with other streams to obtain desired output on-the-fly.
-We'll discuss the recipies for generating Live Fake Sources briefly below:
+We'll discuss the recipes for generating Live Fake Sources briefly below:
@@ -65,7 +65,7 @@ We'll discuss the recipies for generating Live Fake Sources briefly below:
> The [`sierpinski`](https://ffmpeg.org/ffmpeg-filters.html#toc-sierpinski) graph generates a Sierpinski carpet/triangle fractal, and randomly pan around by a single pixel each frame.
- { width="500" }
+ { width="500" }
Sierpinski carpet fractal
@@ -122,7 +122,7 @@ decoder.terminate()
> The [`testsrc`](https://ffmpeg.org/ffmpeg-filters.html#toc-allrgb_002c-allyuv_002c-color_002c-colorchart_002c-colorspectrum_002c-haldclutsrc_002c-nullsrc_002c-pal75bars_002c-pal100bars_002c-rgbtestsrc_002c-smptebars_002c-smptehdbars_002c-testsrc_002c-testsrc2_002c-yuvtestsrc) graph generates a test video pattern showing a color pattern, a scrolling gradient, and a timestamp. This is useful for testing purposes.
- { width="500" }
+ { width="500" }
Test Source pattern
@@ -181,7 +181,7 @@ decoder.terminate()
> The [`gradients`](https://ffmpeg.org/ffmpeg-filters.html#toc-gradients) graph (as name suggests) generates several random gradients.
- { width="500" }
+ { width="500" }
Gradients pattern with real-time text output
@@ -249,7 +249,7 @@ decoder.terminate()
> The [`mandelbrot`](https://ffmpeg.org/ffmpeg-filters.html#toc-mandelbrot) graph generate a [**Mandelbrot set fractal**](https://en.wikipedia.org/wiki/Mandelbrot_set), that progressively zoom towards a specfic point.
- { width="500" }
+ { width="500" }
Mandelbrot pattern with a Vectorscope & two Waveforms
@@ -316,7 +316,7 @@ decoder.terminate()
> The [`life`](https://ffmpeg.org/ffmpeg-filters.html#toc-life) graph generates a life pattern based on a generalization of John Conway’s life game. The sourced input represents a life grid, each pixel represents a cell which can be in one of two possible states, alive or dead. Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each interaction the grid evolves according to the adopted rule, which specifies the number of neighbor alive cells which will make a cell stay alive or born.
- { width="500" }
+ { width="500" }
Game of Life Visualization
diff --git a/docs/recipes/advanced/extract-frame-metadata.md b/docs/recipes/advanced/extract-frame-metadata.md
new file mode 100644
index 00000000..ddbbcb08
--- /dev/null
+++ b/docs/recipes/advanced/extract-frame-metadata.md
@@ -0,0 +1,141 @@
+
+
+# :material-timer-sync: Per-Frame Metadata Extraction
+
+> Each raw numpy frame handed to you by FFdecoder normally loses its temporal context — it's just a matrix of pixels with no notion of _when_ it should appear (PTS) or _how_ it was encoded (Keyframe vs. Predictive frame). The [`-extract_metadata`](../../reference/ffdecoder/params/#exclusive-parameters) exclusive parameter closes that gap: when enabled, [`generateFrame()`](../../reference/ffdecoder/#deffcode.ffdecoder.FFdecoder.generateFrame) yields `(frame, meta)` tuples, where `meta` is a python dict parsed from FFmpeg's [`showinfo`](https://ffmpeg.org/ffmpeg-filters.html#showinfo) filter — emitted on stderr and consumed asynchronously by a background daemon thread so the main `stdout` frame pipe is never throttled.
+
+The metadata dict contains the following keys:
+
+- **`frame_num`** _(int)_: monotonic frame index as emitted by FFmpeg.
+- **`pts_time`** _(float)_: presentation timestamp in seconds.
+- **`is_keyframe`** _(bool)_: `True` if the frame is a keyframe _(I-frame)_.
+- **`frame_type`** _(str)_: one of `"I"` _(keyframe)_, `"P"` _(predictive)_, `"B"` _(bi-predictive)_, `"?"` _(unknown)_.
+
+We'll walk through two flagship optimizations this unlocks in the recipes below.
+
+
+
+!!! warning "DeFFcode APIs requires FFmpeg executable"
+
+ ==DeFFcode APIs **MUST** requires valid FFmpeg executable for all of its core functionality==, and any failure in detection will raise `RuntimeError` immediately. Follow dedicated [FFmpeg Installation doc ➶](../../../installation/ffmpeg_install/) for its installation.
+
+!!! warning "Incompatible with `-filter_complex`"
+
+ `-extract_metadata` cannot be combined with the `-filter_complex` attribute (graph-label routing is ambiguous). If both are supplied, a warning is logged and metadata extraction is silently disabled. A pre-existing `-vf` is fine — `showinfo` is automatically comma-chained onto it.
+
+??? danger "Never name your python script `deffcode.py`"
+
+ When trying out these recipes, never name your python script `deffcode.py` otherwise it will result in `ModuleNotFound` error.
+
+
+
+## Smart Keyframe-only decoding for heavy AI inference
+
+> Many Computer Vision workflows — perceptual hashing, scene-change detection, video summarisation, heavyweight AI-model inference _(YOLO, ResNet, etc.)_ — only really care about **Keyframes (I-frames)**. On a 60 FPS source with a typical GOP size, that's ~1-2 frames per second worth looking at. Without `-extract_metadata` you'd still decode and run your model on every single P/B frame and waste 98%+ of your compute on nearly-identical predictive frames.
+
+With `meta["is_keyframe"]` in hand, you can skip those frames entirely:
+
+```python
+# import the necessary packages
+from deffcode import FFdecoder
+
+# instantiate the decoder with per-frame metadata extraction enabled
+decoder = FFdecoder(
+ "foo.mp4",
+ frame_format="bgr24",
+ **{"-extract_metadata": True},
+).formulate()
+
+# grab (frame, meta) pairs from the generator
+for frame, meta in decoder.generateFrame():
+
+ # check if frame is None
+ if frame is None:
+ break
+
+ # OPTIMIZATION: skip processing entirely if it is not a keyframe
+ if not meta["is_keyframe"]:
+ continue
+
+ # now run your heavy AI model on ~1-2 frames per second only
+ results = heavy_ai_model.predict(frame)
+
+# terminate the decoder
+decoder.terminate()
+```
+
+!!! success "Depending on the source's GOP (Group-of-Pictures) size, this pattern reduces downstream processing time by 10–50× without skipping any scene-boundary information."
+
+
+
+## Variable-Frame-Rate (VFR) synchronization via `pts_time`
+
+> Most modern video sources — smartphones, screen recordings, webcams, browser captures — are **Variable-Frame-Rate**. The gap between frame 1 and 2 might be 16 ms while the gap between frame 2 and 3 is 40 ms. If you are measuring motion for sports analytics, computing velocity vectors, or keeping OpenCV bounding boxes synchronised with an audio track, _assuming a constant frame rate will drift out of sync very quickly_.
+
+With `meta["pts_time"]` you know the **exact presentation timestamp** of every frame:
+
+```python
+# import the necessary packages
+from deffcode import FFdecoder
+
+# instantiate decoder for a VFR source
+decoder = FFdecoder(
+ "screen_recording.mp4",
+ frame_format="bgr24",
+ **{"-extract_metadata": True},
+).formulate()
+
+prev_pts = None
+for frame, meta in decoder.generateFrame():
+ if frame is None:
+ break
+
+ # exact presentation timestamp in seconds
+ pts = meta["pts_time"]
+
+ # compute real inter-frame delta (not the nominal 1/fps value)
+ delta_ms = None if prev_pts is None else (pts - prev_pts) * 1000.0
+ prev_pts = pts
+
+ # use real delta for per-frame motion/velocity calculations
+ # e.g. velocity = displacement_px / delta_ms
+
+# terminate the decoder
+decoder.terminate()
+```
+
+!!! tip "The same `pts_time` stream is what you need to keep processed frames locked to an audio track when re-muxing downstream."
+
+
+
+## Implementation notes
+
+- The `showinfo` filter is appended _(not overwritten)_ to any user-supplied `-vf` filter via comma-concatenation, so your existing filter graph is preserved.
+- FFmpeg's stderr is captured with `subprocess.PIPE` regardless of the `verbose` flag — otherwise a verbose pipeline would let stderr leak to the parent tty and starve the metadata reader.
+- The background reader thread is a **daemon**; on [`terminate()`](../../reference/ffdecoder/#deffcode.ffdecoder.FFdecoder.terminate) the stderr pipe is closed, a stop-event is signalled, and the thread is joined with a 2-second timeout so no pipeline ever outlives the decoder object.
+- `metadata_queue.get()` uses a bounded 10-second timeout. If `showinfo` ever stops emitting lines (e.g. an exotic filter chain drops frames), the consumer logs a warning and yields the frame with `meta=None` rather than deadlocking.
+
+
+
+
+[ffmpeg]:https://www.ffmpeg.org/
diff --git a/docs/recipes/advanced/index.md b/docs/recipes/advanced/index.md
index bafccc7e..8608269a 100644
--- a/docs/recipes/advanced/index.md
+++ b/docs/recipes/advanced/index.md
@@ -74,6 +74,12 @@ The following challenging recipes will take your skills to the next level and wi
- [CUDA-accelerated Video Transcoding with OpenCV's VideoWriter API](../advanced/transcode-hw-acceleration/#cuda-accelerated-video-transcoding-with-opencvs-videowriter-api)
- [CUDA-NVENC-accelerated Video Transcoding with WriteGear API](../advanced/transcode-hw-acceleration/#cuda-nvenc-accelerated-video-transcoding-with-writegear-api)
- [CUDA-NVENC-accelerated End-to-end Lossless Video Transcoding with WriteGear API](../advanced/transcode-hw-acceleration/#cuda-nvenc-accelerated-end-to-end-lossless-video-transcoding-with-writegear-api)
+- [x] **[:material-play-box-multiple: Multi-Input Source Configurations](../advanced/multi_input/#multi-input-source-configurations)**
+ - [Decoding multiple inputs as side-by-side composite](../advanced/multi_input/#decoding-multiple-inputs-as-side-by-side-composite)
+ - [Decoding multiple RTSP streams in parallel](../advanced/multi_input/#decoding-multiple-rtsp-streams-in-parallel)
+ - [Decoding Picture-in-Picture overlay with per-input configuration](../advanced/multi_input/#decoding-picture-in-picture-overlay-with-per-input-configuration)
+ - [Decoding mixed sources with different demuxers](../advanced/multi_input/#decoding-mixed-sources-with-different-demuxers)
+ - [Probing multiple inputs with Sourcer API](../advanced/multi_input/#probing-multiple-inputs-with-sourcer-api)
@@ -82,6 +88,9 @@ The following challenging recipes will take your skills to the next level and wi
- [x] **[:material-cog-refresh: Updating Video Metadata](../advanced/update-metadata/#updating-video-metadata)**
- [Added new attributes to metadata in FFdecoder API](../advanced/update-metadata/#added-new-attributes-to-metadata-in-ffdecoder-api)
- [Overriding source video metadata in FFdecoder API](../advanced/update-metadata/#overriding-source-video-metadata-in-ffdecoder-api)
+- [x] **[:material-timer-sync: Per-Frame Metadata Extraction](../advanced/extract-frame-metadata/#per-frame-metadata-extraction)**
+ - [Smart Keyframe-only decoding for heavy AI inference](../advanced/extract-frame-metadata/#smart-keyframe-only-decoding-for-heavy-ai-inference)
+ - [Variable-Frame-Rate (VFR) synchronization via `pts_time`](../advanced/extract-frame-metadata/#variable-frame-rate-vfr-synchronization-via-pts_time)
diff --git a/docs/recipes/advanced/multi_input.md b/docs/recipes/advanced/multi_input.md
new file mode 100644
index 00000000..9cad4f39
--- /dev/null
+++ b/docs/recipes/advanced/multi_input.md
@@ -0,0 +1,330 @@
+
+
+# :material-play-box-multiple: Multi-Input Source Configurations
+
+> DeFFcode's [Sourcer](../../reference/sourcer/) and [FFdecoder](../../reference/ffdecoder/) APIs accept their `source` and `source_demuxer` parameters as Python lists, ingesting multiple media streams simultaneously inside a single FFmpeg instance. This unlocks side-by-side composites, Picture-in-Picture (PiP) overlays, multi-camera comparisons, and custom video mixing — all driven natively by FFmpeg's filter graph, with no inter-process glue on your side.
+
+We'll walk through Multi-Input Source Configurations in the recipes below:
+
+
+
+!!! warning "DeFFcode APIs requires FFmpeg executable"
+
+ ==DeFFcode APIs **MUST** requires valid FFmpeg executable for all of its core functionality==, and any failure in detection will raise `RuntimeError` immediately. Follow dedicated [FFmpeg Installation doc ➶](../../../installation/ffmpeg_install/) for its installation.
+
+??? info "Additional Python Dependencies for following recipes"
+
+ Following recipes requires additional python dependencies which can be installed easily as below:
+
+ - [x] **OpenCV:** OpenCV is required for previewing video frames. You can easily install it directly via [`pip`](https://pypi.org/project/opencv-python/):
+
+ ??? tip "OpenCV installation from source"
+
+ You can also follow online tutorials for building & installing OpenCV on [Windows](https://www.learnopencv.com/install-opencv3-on-windows/), [Linux](https://www.pyimagesearch.com/2018/05/28/ubuntu-18-04-how-to-install-opencv/), [MacOS](https://www.pyimagesearch.com/2018/08/17/install-opencv-4-on-macos/) and [Raspberry Pi](https://www.pyimagesearch.com/2018/09/26/install-opencv-4-on-your-raspberry-pi/) machines manually from its source.
+
+ :warning: Make sure not to install both *pip* and *source* version together. Otherwise installation will fail to work!
+
+ ??? info "Other OpenCV binaries"
+
+ OpenCV maintainers also provide additional binaries via pip that contains both main modules and contrib/extra modules [`opencv-contrib-python`](https://pypi.org/project/opencv-contrib-python/), and for server (headless) environments like [`opencv-python-headless`](https://pypi.org/project/opencv-python-headless/) and [`opencv-contrib-python-headless`](https://pypi.org/project/opencv-contrib-python-headless/). You can also install ==any one of them== in similar manner. More information can be found [here](https://github.com/opencv/opencv-python#installation-and-usage).
+
+
+ ```sh
+ pip install opencv-python
+ ```
+
+!!! warning "FFdecoder requires explicit stream routing in multi-input mode"
+
+ With multiple `-i` inputs FFmpeg auto-selects only the "best" video stream when no routing is given, which is rarely what you want. To prevent ambiguous decoding, FFdecoder API **requires** you to pass either `-map` or `-filter_complex` whenever `source` is a list. If neither is present, [`formulate()`](../../reference/ffdecoder/#deffcode.ffdecoder.FFdecoder.formulate) raises a **`ValueError`** at initialization time.
+
+!!! danger "Multi-input pipeline limitations"
+
+ 1. **`-vcodec` is input-scoped to source 0.** A single `-vcodec` parameter only applies to the first `-i` input (FFmpeg's positional-options rule). To pin a decoder per input in a multi-decoder pipeline, route it explicitly via `-filter_complex` or use FFmpeg's per-input options inside `-ffprefixes`.
+ 2. **`-extract_metadata` is incompatible with `-filter_complex`.** The [`showinfo`](https://ffmpeg.org/ffmpeg-filters.html#showinfo) filter that backs per-frame metadata cannot share the graph with `-filter_complex`, so FFdecoder will warn and disable `-extract_metadata` in any multi-input pipeline that uses one.
+ 3. **Per-input lists must match `source` length.** If you pass `-ffprefixes` or `source_demuxer` as a list, its length must equal the `source` list length — otherwise DeFFcode raises `ValueError` immediately. Use an empty inner list (`[]`) or `None` for any input that needs no value.
+
+!!! tip "To learn about exclusive `-ffprefixes` parameter and its multi-input list-of-lists shape, see [Exclusive Parameters ➶](../../reference/ffdecoder/params/#b-exclusive-parameters)."
+
+!!! note "Always use FFdecoder API's [`terminate()`](../../reference/ffdecoder/#deffcode.ffdecoder.FFdecoder.terminate) method at the end to avoid undesired behavior."
+
+??? danger "Never name your python script `deffcode.py`"
+
+ When trying out these recipes, never name your python script `deffcode.py` otherwise it will result in `ModuleNotFound` error.
+
+
+
+## Decoding multiple inputs as side-by-side composite
+
+> The simplest multi-input workflow is binding two media streams together horizontally with FFmpeg's [`hstack`](https://ffmpeg.org/ffmpeg-filters.html#hstack) filter — useful for A/B comparisons, before/after diffs, or multi-camera views.
+
+In this example we will decode two video files _(say `video_stream_1.mp4` and `video_stream_2.mp4`)_ as a single side-by-side BGR24 frame stream by passing them as a list to FFdecoder API and routing both inputs through `hstack` via the `-filter_complex` parameter, and preview the composited frames using OpenCV Library's `cv2.imshow()` method in real-time.
+
+!!! alert "Both inputs must share the same height for `hstack` to succeed. Use a `scale` clause inside `-filter_complex` if your sources differ in resolution."
+
+```python
+# import the necessary packages
+from deffcode import FFdecoder
+import cv2
+
+# define our two media paths to stack side-by-side
+source = [
+ "video_stream_1.mp4", # first input (-i #0)
+ "video_stream_2.mp4", # second input (-i #1)
+]
+
+# `-filter_complex` is mandatory in multi-input mode;
+# `hstack=inputs=2` concatenates both streams horizontally
+ffparams = {"-filter_complex": "hstack=inputs=2"}
+
+# initialize and formulate the decoder for BGR24 output
+decoder = FFdecoder(source, frame_format="bgr24", **ffparams).formulate()
+
+# grab the BGR24 frame from the decoder
+for frame in decoder.generateFrame():
+
+ # check if frame is None
+ if frame is None:
+ break
+
+ # {do something with the frame here}
+
+ # Show output window
+ cv2.imshow("Output", frame)
+
+ # check for 'q' key if pressed
+ key = cv2.waitKey(1) & 0xFF
+ if key == ord("q"):
+ break
+
+# close output window
+cv2.destroyAllWindows()
+
+# terminate the decoder
+decoder.terminate()
+```
+
+
+
+## Decoding multiple RTSP streams in parallel
+
+> When ingesting multiple live network streams _(such as IP cameras over RTSP)_, you typically need transport-level options that differ per camera _(e.g. forcing TCP transport to reduce packet corruption)_. The `-ffprefixes` exclusive parameter accepts a **list of per-input lists** in source order so each `-i` group gets its own pre-input options.
+
+In this example we will decode two live RTSP camera feeds, force TCP transport on both inputs through per-input `-ffprefixes`, route them side-by-side with `hstack`, and preview the multiplexed BGR24 frames using OpenCV Library's `cv2.imshow()` method in real-time.
+
+!!! alert "Remember to replace the placeholder RTSP URLs with the credentials and addresses of your own cameras before using this recipe."
+
+```python
+# import the necessary packages
+from deffcode import FFdecoder
+import cv2
+
+# define multiple RTSP camera streams as our source list
+source = [
+ "rtsp://admin:pass@192.168.1.10:554/stream1",
+ "rtsp://admin:pass@192.168.1.11:554/stream2",
+]
+
+# define per-input prefixes: one inner list per source, in source order
+ffparams = {
+ "-ffprefixes": [
+ ["-rtsp_transport", "tcp"], # applies to source 0 only
+ ["-rtsp_transport", "tcp"], # applies to source 1 only
+ ],
+ # route both inputs side-by-side
+ "-filter_complex": "hstack=inputs=2",
+}
+
+# initialize and formulate the decoder for BGR24 output
+decoder = FFdecoder(source, frame_format="bgr24", **ffparams).formulate()
+
+# grab the BGR24 frame from the decoder
+for frame in decoder.generateFrame():
+
+ # check if frame is None
+ if frame is None:
+ break
+
+ # {do something with the frame here}
+
+ # Show output window
+ cv2.imshow("Output", frame)
+
+ # check for 'q' key if pressed
+ key = cv2.waitKey(1) & 0xFF
+ if key == ord("q"):
+ break
+
+# close output window
+cv2.destroyAllWindows()
+
+# terminate the decoder
+decoder.terminate()
+```
+
+
+
+## Decoding Picture-in-Picture overlay with per-input configuration
+
+> Real-world multi-input pipelines almost always need _different_ per-input options — for instance, real-time pacing _(`-re`)_ on a live stream paired with infinite looping _(`-stream_loop -1`)_ on a local asset. With `-ffprefixes` shaped as a list-of-lists, every input is configured independently while still sharing a single FFmpeg pipeline.
+
+In this example we will overlay a looping local video file _(say `local_file.mp4`)_ in the top-right corner of a paced live HLS stream _(say `network_stream_1.m3u8`)_ via FFmpeg's [`overlay`](https://ffmpeg.org/ffmpeg-filters.html#toc-overlay-1) filter inside `-filter_complex`, supply per-input prefixes for each, and preview the resulting Picture-in-Picture BGR24 frames using OpenCV Library's `cv2.imshow()` method in real-time.
+
+!!! info "You can use FFdecoder's [`metadata`](../../reference/ffdecoder/#deffcode.ffdecoder.FFdecoder.metadata) property to inspect the per-source metadata under the `sources` key once the pipeline is formulated."
+
+```python
+# import the necessary packages
+from deffcode import FFdecoder
+import cv2
+
+# define our multi-input sources
+source = [
+ "network_stream_1.m3u8", # live HLS stream as the base layer
+ "local_file.mp4", # local asset overlaid in the top-right corner
+]
+
+# define per-input prefixes and the overlay filter graph
+ffparams = {
+ "-ffprefixes": [
+ ["-re"], # pace input 0 at native frame rate
+ ["-stream_loop", "-1"], # loop input 1 infinitely
+ ],
+ # PiP overlay: input 1 anchored 10px from the top-right of input 0
+ "-filter_complex": "[0:v][1:v]overlay=main_w-overlay_w-10:10",
+}
+
+# initialize and formulate the decoder for BGR24 output
+decoder = FFdecoder(source, frame_format="bgr24", **ffparams).formulate()
+
+# grab the BGR24 frame from the decoder
+for frame in decoder.generateFrame():
+
+ # check if frame is None
+ if frame is None:
+ break
+
+ # {do something with the frame here}
+
+ # Show output window
+ cv2.imshow("Output", frame)
+
+ # check for 'q' key if pressed
+ key = cv2.waitKey(1) & 0xFF
+ if key == ord("q"):
+ break
+
+# close output window
+cv2.destroyAllWindows()
+
+# terminate the decoder
+decoder.terminate()
+```
+
+
+
+## Decoding mixed sources with different demuxers
+
+> Inputs in a multi-input pipeline can also originate from completely different device classes — for instance, a Linux webcam captured via `v4l2` paired with a synthetically generated [`lavfi`](http://underpop.online.fr/f/ffmpeg/help/lavfi.htm.gz) source. The `source_demuxer` parameter accepts a list whose entries align positionally with `source`, so each input gets its own `-f` directive.
+
+In this example we will combine a live webcam feed _(captured via `v4l2` on Linux)_ with a generated Mandelbrot pattern _(via `lavfi`)_, stack them side-by-side with `hstack`, and preview the composite BGR24 frames using OpenCV Library's `cv2.imshow()` method in real-time.
+
+!!! alert "This recipe requires Linux for `v4l2`. On other operating systems substitute `dshow` (Windows) or `avfoundation` (MacOS) along with the platform-appropriate device path."
+
+!!! tip "Use `None` for any inner entry of `source_demuxer` whose corresponding source does not need an explicit `-f` directive — DeFFcode will simply omit it for that input."
+
+```python
+# import the necessary packages
+from deffcode import FFdecoder
+import cv2
+
+# webcam + virtual mandelbrot source
+source = [
+ "/dev/video0", # v4l2 camera (Linux)
+ "mandelbrot=size=1280x720:rate=30", # libavfilter virtual source
+]
+
+# per-input demuxers, aligned positionally with source
+source_demuxer = [
+ "v4l2", # for /dev/video0
+ "lavfi", # for the mandelbrot filtergraph
+]
+
+# stack the camera feed next to the generated mandelbrot
+ffparams = {"-filter_complex": "hstack=inputs=2"}
+
+# initialize and formulate the decoder for BGR24 output
+decoder = FFdecoder(
+ source, source_demuxer=source_demuxer, frame_format="bgr24", **ffparams
+).formulate()
+
+# grab the BGR24 frame from the decoder
+for frame in decoder.generateFrame():
+
+ # check if frame is None
+ if frame is None:
+ break
+
+ # {do something with the frame here}
+
+ # Show output window
+ cv2.imshow("Output", frame)
+
+ # check for 'q' key if pressed
+ key = cv2.waitKey(1) & 0xFF
+ if key == ord("q"):
+ break
+
+# close output window
+cv2.destroyAllWindows()
+
+# terminate the decoder
+decoder.terminate()
+```
+
+
+
+## Probing multiple inputs with Sourcer API
+
+> The [Sourcer API](../../reference/sourcer/) probes each source independently — no `-map` or `-filter_complex` is required because nothing is being decoded into a single stream. The primary input's flat metadata fields _(`source_video_resolution`, `source_video_framerate`, etc.)_ come from `source[0]` and remain in the same shape as a single-source probe, while a new `sources` key is appended carrying the per-input metadata dict for every input in order.
+
+In this example we will probe two video files _(say `video1.mp4` and `video2.mp4`)_ as a single Sourcer call and pretty-print the per-source metadata list extracted from the `sources` key.
+
+!!! info "The flat top-level fields _(e.g. `source_video_resolution`)_ always describe `source[0]` so existing single-source consumers keep working unchanged."
+
+```python
+# import the necessary packages
+from deffcode import Sourcer
+import json
+
+# define our multi-input sources
+source = ["video1.mp4", "video2.mp4"]
+
+# initialize the sourcer and probe each source sequentially
+sourcer = Sourcer(source).probe_stream()
+
+# the returned metadata mirrors the single-input shape for source[0]
+# and exposes per-source dicts under the `sources` key
+metadata = sourcer.retrieve_metadata()
+
+# pretty-print the per-source metadata list
+print(json.dumps(metadata["sources"], indent=4))
+```
+
+
diff --git a/docs/recipes/advanced/transcode-art-filtergraphs.md b/docs/recipes/advanced/transcode-art-filtergraphs.md
index d7232eae..047e2b6d 100644
--- a/docs/recipes/advanced/transcode-art-filtergraphs.md
+++ b/docs/recipes/advanced/transcode-art-filtergraphs.md
@@ -28,7 +28,7 @@ limitations under the License.
They can be processed by simply inserting an additional step between decoding and encoding of video frames:
- { loading=lazy }
+ { loading=lazy }
Simple filtergraphs are configured with the per-stream `-filter` option _(with `-vf` for video)_.
@@ -84,7 +84,7 @@ We'll discuss the Transcoding Video Art with Filtergraphs in the following recip
> Based on the QCTools bitplane visualization, this video art has numerical values ranging between `-1`(no change) and `10`(noisiest) for the `Y` _(luminance)_, `U` and `V` _(chroma or color difference)_ planes, yielding cool and different results for different values.
- 
+ 
YUV Bitplane Visualization
@@ -155,7 +155,7 @@ writer.close()
> This video art uses FFmpeg's [`pseudocolor`](https://ffmpeg.org/ffmpeg-filters.html#toc-pseudocolor) filter to create a **Jetcolor effect** which is high contrast, high brightness, and high saturation colormap that ranges from blue to red, and passes through the colors cyan, yellow, and orange. The jet colormap is associated with an astrophysical fluid jet simulation from the National Center for Supercomputer Applications.
- 
+ 
Jetcolor effect
@@ -232,7 +232,7 @@ writer.close()
> This video art using FFmpeg’s [`lagfun`](https://ffmpeg.org/ffmpeg-filters.html#toc-lagfun) filter to create a video echo/ghost/trailing effect.
- 
+ 
Ghosting effect
@@ -300,7 +300,7 @@ writer.close()
> This video art uses FFmpeg’s `overlay`, `smartblur` and stacks of `dilation` filters to intentionally Pixelate your video in artistically cool looking ways such that each pixel become visible to the naked eye.
- 
+ 
Pixelation effect
diff --git a/docs/recipes/advanced/transcode-hw-acceleration.md b/docs/recipes/advanced/transcode-hw-acceleration.md
index 2b42edbb..f5f69da0 100644
--- a/docs/recipes/advanced/transcode-hw-acceleration.md
+++ b/docs/recipes/advanced/transcode-hw-acceleration.md
@@ -31,25 +31,41 @@ limitations under the License.
> DeFFcode's FFdecoder API in conjunction with VidGear's WriteGear API is able to exploit almost any FFmpeg parameter for achieving anything imaginable with multimedia video data all while **allowing us to process real-time video frames** with immense flexibility. Both these APIs are capable of utilizing the potential of GPU backed fully-accelerated **Hardware based video Decoding(FFdecoder API with hardware decoder) and Encoding (WriteGear API with hardware encoder)**, thus dramatically improving the transcoding performance. At same time, FFdecoder API Hardware-decoded frames are **fully compatible with OpenCV's VideoWriter API** for producing high-quality output video in real-time.
-??? danger "Limitation: Bottleneck in Hardware-Accelerated Video Transcoding performance with Real-time Frame processing"
+??? danger "Limitation: Performance Bottleneck in Hardware-Accelerated Video Transcoding with Real-Time Frame Processing"
- As we know, using the `–hwaccel cuda -hwaccel_output_format cuda` flags in FFmpeg pipeline will keep video frames in GPU memory, and this ensures that the memory transfers (system memory to video memory and vice versa) are eliminated, and that transcoding is performed with the highest possible performance on the available GPU hardware.
+ When using FFmpeg with `-hwaccel cuda -hwaccel_output_format cuda`, decoded frames remain in GPU memory. This avoids costly memory transfers between system (CPU) memory and GPU memory, enabling near-optimal transcoding performance on supported hardware.
- { width="350" }
- General Memory Flow with Hardware Acceleration
+ { width="350" }
+ Memory Flow with Hardware Acceleration
-
- But unfortunately, for processing real-time frames in our python script with FFdecoder and WriteGear APIs, we're bound to sacrifice this performance gain by explicitly copying raw decoded frames between System and GPU memory _(via the PCIe bus)_, thereby creating self-made latency in transfer time and increasing PCIe bandwidth occupancy due to overheads in communication over the bus. Moreover, given PCIe bandwidth limits, copying uncompressed image data would quickly saturate the PCIe bus.
+
+ However, when integrating real-time frame processing in Python using FFdecoder and WriteGear APIs, this advantage is partially lost. To operate on individual frames within Python, decoded frames must be transferred from GPU memory back to system memory.
+
+ This introduces a critical bottleneck:
+
+ - **Explicit GPU ↔ CPU memory transfers** over the PCIe bus
+ - **Increased latency** due to data movement overhead
+ - **Higher PCIe bandwidth utilization**, especially with uncompressed frame data
+ - **Potential bus saturation**, limiting overall throughput
+
+ As a result, the pipeline incurs additional overhead that directly impacts real-time performance.
- { width="350" }
- Memory Flow with Hardware Acceleration and Real-time Processing
+ { width="350" }
+ Memory Flow with Hardware Acceleration and Real-Time Processing
- On the bright side, however, GPU enabled Hardware based encoding/decoding is inherently faster and more efficient _(do not use much CPU resources when frames in GPU)_ thus freeing up the CPU for other tasks, as compared to Software based encoding/decoding that is known to be completely CPU intensive. Plus scaling, de-interlacing, filtering, etc. tasks will be way faster and efficient than usual using these Hardware based decoders/encoders as oppose to Software ones.
+ That said, hardware-accelerated encoding and decoding still provide significant advantages:
+
+ - **Lower CPU utilization**, as most processing remains on the GPU
+ - **Faster execution** compared to CPU-based (software) pipelines
+ - **Efficient video operations**, including scaling, deinterlacing, and filtering
+ - **Better overall system resource distribution**, freeing CPU for parallel workloads
+
+ In contrast, software-based transcoding is entirely CPU-bound and typically less efficient for high-throughput or real-time scenarios.
- !!! summary "As you can see the pros definitely outweigh the cons and you're getting to process video frames in the real-time with immense speed and flexibility, which is impossible to do otherwise."
+ !!! summary "While GPU–CPU memory transfers introduce unavoidable overhead in real-time processing pipelines, hardware acceleration still delivers substantial performance and efficiency gains—making it the preferred approach for most modern video workflows."
We'll discuss its Hardware-Accelerated Video Transcoding capabilities using these APIs briefly in the following recipes:
diff --git a/docs/recipes/advanced/transcode-live-frames-complexgraphs.md b/docs/recipes/advanced/transcode-live-frames-complexgraphs.md
index c09b79a9..bef1085d 100644
--- a/docs/recipes/advanced/transcode-live-frames-complexgraphs.md
+++ b/docs/recipes/advanced/transcode-live-frames-complexgraphs.md
@@ -80,7 +80,7 @@ We'll discuss the transcoding of live complex filtergraphs in the following reci
## Transcoding video with Live Custom watermark image overlay
- 
+ 
Big Buck Bunny with custom watermark
@@ -153,7 +153,7 @@ writer.close()
## Transcoding video from sequence of Images with additional filtering
- 
+ 
Mandelbrot pattern blend with Fish school video
diff --git a/docs/recipes/basic/decode-video-files.md b/docs/recipes/basic/decode-video-files.md
index fbd67cbd..f360c4b2 100644
--- a/docs/recipes/basic/decode-video-files.md
+++ b/docs/recipes/basic/decode-video-files.md
@@ -273,10 +273,61 @@ In this example we will decode live **Grayscale** and **YUV** video frames from
decoder.terminate()
```
+=== "Decode Grayscale via YUV (fastest)"
+
+ !!! success ":zap: Fastest RAW-to-Grayscale via `-extract_luma`"
+
+ Every YUV/NV bytestream stores the **Luma (Y) plane** uncompressed at the top of each frame. The exclusive [`-extract_luma`](../../reference/ffdecoder/params/#b-exclusive-parameters) boolean attribute makes FFdecoder slice that Y-plane directly and hand back a 2D `(H, W)` grayscale ndarray — **no colorspace conversion in FFmpeg, no `cv2.cvtColor` in Python**. This is strictly faster than `frame_format="gray"`, which still asks FFmpeg to do a `yuv→gray` conversion on every frame.
+
+ Combined with the reduced pipe-bytes of YUV 4:2:0 ingest, this is the fastest grayscale pipeline the API can produce.
+
+ ```python
+ # import the necessary packages
+ from deffcode import FFdecoder
+ import cv2
+
+ # enable direct Luma (Y-plane) extraction
+ ffparams = {"-extract_luma": True}
+
+ # initialize the decoder with a YUV pixel-format
+ decoder = FFdecoder(
+ "input_foo.mp4", frame_format="yuv420p", verbose=True, **ffparams
+ ).formulate()
+
+ # grab the 2D (H, W) grayscale frames from the decoder
+ for gray in decoder.generateFrame():
+
+ # check if frame is None
+ if gray is None:
+ break
+
+ # {do something with the gray frame here}
+
+ # Show output window
+ cv2.imshow("Gray Output", gray)
+
+ # check for 'q' key if pressed
+ key = cv2.waitKey(1) & 0xFF
+ if key == ord("q"):
+ break
+
+ # close output window
+ cv2.destroyAllWindows()
+
+ # terminate the decoder
+ decoder.terminate()
+ ```
+
=== "Decode YUV frames"
!!! quote "With FFdecoder API, frames extracted with YUV pixel formats _(`yuv420p`, `yuv444p`, `nv12`, `nv21` etc.)_ are generally incompatible with OpenCV APIs. But you can make them easily compatible by using exclusive [`-enforce_cv_patch`](../../reference/ffdecoder/params/#b-exclusive-parameters) boolean attribute of its `ffparam` dictionary parameter."
+ !!! success "Performance Mode — :zap: Faster Decoding via YUV420p"
+
+ Ingesting frames as 12-bit **YUV 4:2:0** instead of 24-bit **RGB/BGR** halves the bytes moving through the FFmpeg pipe, so the subprocess pipeline spends less time blocked on I/O. In community benchmarks on 1080p MP4 _(see [issue #15](https://github.com/abhiTronix/deffcode/issues/15))_, RAW ingest jumped from **~96 FPS (RGB24)** to **~213 FPS (YUV420p)**, and **~155 FPS** when converted to BGR inside Python via OpenCV — a **25–33% gain** over the RGB path for the majority of common video sources _(which are already YUV420 on disk)_.
+
+ Use this mode when you're throughput-bound on decoding and can afford a single `cv2.cvtColor` call per frame. Skip it for scientific workloads where the implicit chroma subsampling of YUV 4:2:0 is unacceptable.
+
Let's try decoding YUV420p pixel-format frames in following python code:
!!! info "You can also use other YUV pixel formats such `yuv422p`(4:2:2 subsampling) or `yuv444p`(4:4:4 subsampling) etc. instead for more higher dynamic range in the similar manner."
diff --git a/docs/recipes/basic/transcode-live-frames-simplegraphs.md b/docs/recipes/basic/transcode-live-frames-simplegraphs.md
index 1f3885bb..231e9fba 100644
--- a/docs/recipes/basic/transcode-live-frames-simplegraphs.md
+++ b/docs/recipes/basic/transcode-live-frames-simplegraphs.md
@@ -28,7 +28,7 @@ limitations under the License.
They can be processed by simply inserting an additional step between decoding and encoding of video frames:
- { loading=lazy }
+ { loading=lazy }
Simple filtergraphs are configured with the per-stream `-filter` option _(with `-vf` for video)_.
@@ -74,7 +74,7 @@ We'll discuss the transcoding of live simple filtergraphs in the following recip
## Transcoding Trimmed and Reversed video
- 
+ 
Big Buck Bunny Reversed
@@ -140,7 +140,7 @@ writer.release()
## Transcoding Cropped video
- 
+ 
Big Buck Bunny Cropped
@@ -206,7 +206,7 @@ writer.release()
!!! quote "FFmpeg features **Rotate** Filter that is used to rotate videos by an arbitrary angle (expressed in radians)."
- 
+ 
Big Buck Bunny Rotated (with rotate filter)
@@ -270,7 +270,7 @@ writer.release()
!!! quote "FFmpeg also features **Transpose** Filter that is used to rotate videos by 90 degrees clockwise and counter-clockwise direction as well as flip them vertically and horizontally."
- 
+ 
Big Buck Bunny Rotated (with transpose filter)
@@ -331,7 +331,7 @@ writer.release()
## Transcoding Horizontally flipped and Scaled video
- 
+ 
Big Buck Bunny Horizontally flipped and Scaled
diff --git a/docs/recipes/basic/transcode-live-frames.md b/docs/recipes/basic/transcode-live-frames.md
index 14d61f22..f57c5320 100644
--- a/docs/recipes/basic/transcode-live-frames.md
+++ b/docs/recipes/basic/transcode-live-frames.md
@@ -79,11 +79,11 @@ We'll discuss transcoding using both these libraries briefly in the following re
-## Transcoding video using OpenCV VideoWriter API
+## Transcoding Video using OpenCV VideoWriter API
-!!! quote "OpenCV's' [`VideoWriter()`](https://docs.opencv.org/3.4/dd/d9e/classcv_1_1VideoWriter.html#ad59c61d8881ba2b2da22cff5487465b5) class can be used directly with DeFFcode's FFdecoder API to encode video frames into a multimedia video file but it lacks the ability to control output quality, bitrate, compression, and other important features which are only available with VidGear's WriteGear API."
+OpenCV's [`VideoWriter()`](https://docs.opencv.org/3.4/dd/d9e/classcv_1_1VideoWriter.html#ad59c61d8881ba2b2da22cff5487465b5) class can be used directly with DeFFcode's FFdecoder API to encode video frames into a multimedia file. However, it lacks fine-grained control over output quality, bitrate, compression, and other advanced parameters—features that are readily available with VidGear's WriteGear API.
-In this example we will decode different pixel formats video frames from a given Video file _(say `foo.mp4`)_ in FFdecoder API, and encode them using OpenCV Library's `VideoWriter()` method in real-time.
+In this example, we will decode video frames with different pixel formats from a given video file *(e.g., `foo.mp4`)* using the FFdecoder API, and then encode them in real time using OpenCV's `VideoWriter()` method..
!!! info "OpenCV's `VideoWriter()` class requires a valid Output filename _(e.g. output_foo.avi)_, [FourCC](https://www.fourcc.org/fourcc.php) code, framerate, and resolution as input."
@@ -288,15 +288,41 @@ In this example we will decode different pixel formats video frames from a given
## Transcoding lossless video using WriteGear API
-!!! danger "==WriteGear's Compression Mode support for FFdecoder API is currently in beta so you can expect much higher than usual CPU utilization!=="
+!!! danger "High CPU Usage when chaining FFdecoder with WriteGear"
-???+ quote "Lossless transcoding with FFdecoder and WriteGear API"
+ When chaining FFdecoder with WriteGear, both FFmpeg processes _(decoding + encoding)_ run **as fast as your hardware allows** with no artificial pacing between them. This causes the pipeline to max out your CPU to process the video in the shortest time possible, which may be undesirable.
+
+ You can mitigate this in two ways depending on your use case:
+
+ === "Throttle to Real-Time Speed"
+
+ Pass the `-re` flag via FFdecoder's `-ffprefixes` parameter to force FFmpeg to read the input at its native framerate. This naturally paces the pipeline to real-time speed and **drastically reduces CPU usage**:
+
+ ```python
+ # force input to be read at native framerate
+ decoder = FFdecoder("foo.mp4", frame_format="bgr24", **{"-ffprefixes": ["-re"]}).formulate()
+ ```
+
+ === "Limit FFmpeg Threads"
+
+ Pass `-threads` to both FFdecoder and WriteGear to cap the number of CPU threads each FFmpeg process may use. This leaves headroom for other system tasks:
+
+ ```python
+ # limit decoder to 2 threads
+ decoder = FFdecoder("foo.mp4", frame_format="bgr24", **{"-threads": 2}).formulate()
+
+ # limit encoder to 2 threads
+ writer = WriteGear(output="output_foo.mp4", **{"-input_framerate": fps, "-threads": 2})
+ ```
+
+ !!! tip "Hardware Acceleration"
+ If your machine has a dedicated GPU, you can offload encoding to the GPU entirely — for example by passing `"-vcodec": "h264_nvenc"` to WriteGear _(NVIDIA)_ — shifting the heavy lifting off the CPU.
- VidGear's [**WriteGear API**](https://abhitronix.github.io/vidgear/latest/gears/writegear/introduction/) implements a complete, flexible, and robust wrapper around FFmpeg in [compression mode](https://abhitronix.github.io/vidgear/latest/gears/writegear/compression/overview/) for encoding real-time video frames to a lossless compressed multimedia output file(s)/stream(s).
+**VidGear's [WriteGear API](https://abhitronix.github.io/vidgear/latest/gears/writegear/introduction/)** provides a flexible and robust wrapper over FFmpeg (compression mode) for encoding real-time video frames into lossless multimedia files or streams.
- DeFFcode's FFdecoder API in conjunction with WriteGear API creates a high-level **High-performance Lossless FFmpeg Transcoding _(Decoding + Encoding)_ Pipeline :fire:** that is able to exploit almost any FFmpeg parameter for achieving anything imaginable with multimedia video data all while allow us to manipulate the real-time video frames with immense flexibility.
+Combined with **DeFFcode's FFdecoder API**, it enables a high-level **lossless FFmpeg transcoding pipeline (decoding + encoding)** with full control over FFmpeg parameters and real-time frame manipulation.
-In this example we will decode different pixel formats video frames from a given Video file _(say `foo.mp4`)_ in FFdecoder API, and encode them into lossless video file with controlled framerate using WriteGear API in real-time.
+In this example, we will decode video frames with different pixel formats from a given video file *(e.g., `foo.mp4`)* using the FFdecoder API, and then encode them into a lossless video file with a controlled framerate using the WriteGear API in real time.
!!! info "Additional Parameters in WriteGear API"
@@ -325,7 +351,7 @@ In this example we will decode different pixel formats video frames from a given
# Define writer with default parameters and suitable
# output filename for e.g. `output_foo.mp4`
- writer = WriteGear(output_filename="output_foo.mp4", **output_params)
+ writer = WriteGear(output="output_foo.mp4", **output_params)
# grab the BGR24 frame from the decoder
for frame in decoder.generateFrame():
@@ -367,7 +393,7 @@ In this example we will decode different pixel formats video frames from a given
# Define writer with default parameters and suitable
# output filename for e.g. `output_foo.mp4`
- writer = WriteGear(output_filename="output_foo.mp4", **output_params)
+ writer = WriteGear(output="output_foo.mp4", **output_params)
# grab the BGR24 frame from the decoder
for frame in decoder.generateFrame():
@@ -409,7 +435,7 @@ In this example we will decode different pixel formats video frames from a given
# Define writer with default parameters and suitable
# output filename for e.g. `output_foo_gray.mp4`
- writer = WriteGear(output_filename="output_foo_gray.mp4", **output_params)
+ writer = WriteGear(output="output_foo_gray.mp4", **output_params)
# grab the GRAYSCALE frame from the decoder
for frame in decoder.generateFrame():
@@ -457,7 +483,7 @@ In this example we will decode different pixel formats video frames from a given
# Define writer with default parameters and suitable
# output filename for e.g. `output_foo_yuv.mp4`
- writer = WriteGear(output_filename="output_foo_yuv.mp4", logging=True, **output_params)
+ writer = WriteGear(output="output_foo_yuv.mp4", logging=True, **output_params)
# grab the YUV420 frame from the decoder
for frame in decoder.generateFrame():
diff --git a/docs/reference/ffdecoder/params.md b/docs/reference/ffdecoder/params.md
index d525a2eb..44d28f19 100644
--- a/docs/reference/ffdecoder/params.md
+++ b/docs/reference/ffdecoder/params.md
@@ -29,9 +29,9 @@ This parameter defines the input source (`-i`) for decoding real-time frames.
!!! danger "FFdecoder API checks for _`video bitrate`_ or _`frame-size` and `framerate`_ in video's metadata to ensure given input `source` has usable video stream available. Thereby, it will throw `ValueError` if it fails to find those parameters."
-!!! info "Multiple video inputs are not yet supported!"
+!!! info "Multiple video inputs are fully supported! Pass a Python list of source strings to natively process multiple media streams simultaneously. A `-filter_complex` or `-map` parameter is required."
-**Data-Type:** String.
+**Data-Type:** String or List of Strings.
Its valid input can be one of the following:
@@ -471,7 +471,7 @@ This parameter specifies the demuxer(`-f`) for the input source _(such as `dshow
!!! example "Related usage recipes :material-pot-steam: can found [here ➶](../../../recipes/basic/decode-camera-devices)"
-**Data-Type:** String
+**Data-Type:** String or List of Strings (if `source` is a list, you can pass a list of identical length mapping demuxers to corresponding sources).
**Default Value:** Its default value is `None`.
@@ -527,7 +527,7 @@ This parameter can be used to manually assigns the system _file-path/directory_
??? question "How to change FFmpeg Static Binaries download directory?"
- You can use `-ffmpeg_download_path` _(via. [`-custom_sourcer_params`](#exclusive-parameters))_ exclusive parameter in FFdecoder API to set the custom directory for downloading FFmpeg Static Binaries during the [Auto-Installation](../../../installation/ffmpeg_install/#a-auto-installation) step on Windows Machines. If this parameter is not altered, then these binaries will auto-save to the default temporary directory (for e.g. `C:/User/temp`) on your windows machine. It can be used as follows in FFdecoder API:
+ You can use `-ffmpeg_download_path` _(via. [`-custom_sourcer_params`](#b-exclusive-parameters))_ exclusive parameter in FFdecoder API to set the custom directory for downloading FFmpeg Static Binaries during the [Auto-Installation](../../../installation/ffmpeg_install/#a-auto-installation) step on Windows Machines. If this parameter is not altered, then these binaries will auto-save to the default temporary directory (for e.g. `C:/User/temp`) on your windows machine. It can be used as follows in FFdecoder API:
```python
# # define suitable parameter to download at "C:/User/foo/foo1"
@@ -683,6 +683,19 @@ These parameters are discussed below:
ffparams = {"-ffprefixes": ['-re']} # executes as `ffmpeg -re `
```
+ !!! info "Multi-input mode: per-source list-of-lists"
+ When [`source`](#source) is a list, `-ffprefixes` must be a **list of per-input lists** with one entry per source (in the same order). Flat lists are rejected as ambiguous, and a length mismatch raises `ValueError`.
+
+ ```python
+ # source[0] gets `-re`; source[1] gets `-stream_loop -1`
+ ffparams = {
+ "-ffprefixes": [["-re"], ["-stream_loop", "-1"]],
+ "-filter_complex": "hstack=inputs=2", # required for multi-input
+ }
+ ```
+
+ Use an empty inner list (`[]`) for any input that needs no prefix. See the [Multi-Input Source Configurations recipe ➶](../../../recipes/advanced/multi_input/#multi-input-source-configurations) for full examples.
+
* **`-clones`** _(list)_: This attribute sets the special FFmpeg parameters after that are repeated more than once or occurs in a specific order _(that cannot be altered)_ in the FFmpeg command. Its value can be of datatype **`list`** only and its usage is as follows:
@@ -698,7 +711,7 @@ These parameters are discussed below:
-* **`-custom_sourcer_params`** _(dict)_ : This attribute assigns all [**Exclusive Parameter**](../../sourcer/params/#exclusive-parameters) meant for Sourcer API's `sourcer_params` dictionary parameter directly through FFdecoder API. Its usage is as follows:
+* **`-custom_sourcer_params`** _(dict)_ : This attribute assigns all [**Exclusive Parameter**](../../sourcer/params/#b-exclusive-parameters) meant for Sourcer API's `sourcer_params` dictionary parameter directly through FFdecoder API. Its usage is as follows:
```python
# define suitable parameter meant for `sourcer_params`
@@ -729,6 +742,50 @@ These parameters are discussed below:
+* **`-extract_luma`** _(bool)_ : This attribute can be enabled(`True`) to directly extract the **Luma (Y) plane** as a 2D grayscale `(H, W)` ndarray from YUV/NV pixel-format streams _(such as `yuv420p`, `yuv422p`, `yuv444p`, `nv12`, `nv21` etc.)_. This is the **fastest path to grayscale** available in FFdecoder — the Y plane sits uncompressed at the top of every YUV/NV bytestream, so no colorspace conversion runs either in FFmpeg or in Python; the decoder just slices it out. It can be used as follows:
+
+ !!! warning "As of now, this flag is only applied when `frame_format` resolves to a pixel-format starting with `yuv` or `nv`. For other pixel-formats, the flag is ignored and the default reshape path is used."
+
+ !!! tip "Pair with [Performance Mode ➶](../../../recipes/basic/decode-video-files/#playing-with-any-other-ffmpeg-pixel-formats) via `frame_format=\"yuv420p\"` for the fastest RAW-to-grayscale pipeline. Takes precedence over `-enforce_cv_patch` when both are enabled."
+
+ ```python
+ # define suitable parameter
+ ffparams = {"-extract_luma": True} # direct Y-plane (grayscale) extraction
+ ```
+
+
+
+* **`-extract_metadata`** _(bool)_: This attribute can be enabled(`True`) to activate **asynchronous per-frame metadata extraction** via FFmpeg's [`showinfo`](https://ffmpeg.org/ffmpeg-filters.html#showinfo) filter. When enabled, the [`generateFrame()`](../../../reference/ffdecoder/#deffcode.ffdecoder.FFdecoder.generateFrame) generator yields `(frame, metadata)` tuples instead of plain ndarrays, where `metadata` is a dict with the following keys:
+
+ - **`frame_num`** _(int)_: monotonic frame index as emitted by FFmpeg.
+ - **`pts_time`** _(float)_: presentation timestamp in seconds — the exact millisecond the frame is meant to appear, crucial for VFR (Variable-Frame-Rate) sources.
+ - **`is_keyframe`** _(bool)_: `True` if the frame is a keyframe (I-frame).
+ - **`frame_type`** _(str)_: one of `"I"` _(keyframe)_, `"P"` _(predictive)_, `"B"` _(bi-predictive)_, or `"?"` _(unknown)_.
+
+ A background daemon thread parses `showinfo` lines off FFmpeg's stderr and feeds them into a thread-safe queue, so the main `stdout` frame pipe is never throttled. It can be used as follows:
+
+ !!! warning "This flag is **incompatible with `-filter_complex`** (graph-label routing is ambiguous). If both are supplied, a warning is logged and `-extract_metadata` is disabled for that pipeline. A pre-existing `-vf` filter **is preserved** — `showinfo` is comma-chained onto it automatically."
+
+ !!! tip "Enables **Smart Keyframe Extraction**: for workflows like perceptual hashing, scene-change detection, or heavy AI-model inference (YOLO, ResNet, etc.) that only need I-frames, you can skip P/B frames entirely and reduce downstream compute by 10–50×, depending on the source's GOP size."
+
+ ```python
+ # define suitable parameter
+ ffparams = {"-extract_metadata": True} # yields (frame, meta) tuples
+ ```
+
+ Example: skip every non-keyframe for heavy AI inference.
+
+ ```python
+ decoder = FFdecoder("input.mp4", **{"-extract_metadata": True}).formulate()
+
+ for frame, meta in decoder.generateFrame():
+ if not meta["is_keyframe"]:
+ continue
+ results = heavy_ai_model.predict(frame) # runs on ~1-2 frames per second
+ ```
+
+
+
* **`-disable_ffmpeg_window`** _(bool)_: This attribute can be used to prevent the FFmpeg command line window from appearing when using the FFdecoder API on Windows. This is especially useful when creating an `.exe` file for your Python script with logging disabled(`verbose=False`), as it stops the FFmpeg window from popping up even in windowed or no-console mode. Its usage is as follows:
!!! warning "The `-disable_ffmpeg_window` flag is only available on :fontawesome-brands-windows: Windows OS with logging disabled."
diff --git a/docs/reference/ffhelper.md b/docs/reference/ffhelper.md
index 07e2ecc0..987db760 100644
--- a/docs/reference/ffhelper.md
+++ b/docs/reference/ffhelper.md
@@ -28,10 +28,6 @@ limitations under the License.
-::: deffcode.ffhelper.get_valid_ffmpeg_path
-
-
-
::: deffcode.ffhelper.download_ffmpeg_binaries
diff --git a/docs/reference/sourcer/params.md b/docs/reference/sourcer/params.md
index 5fe78f4f..d431e52c 100644
--- a/docs/reference/sourcer/params.md
+++ b/docs/reference/sourcer/params.md
@@ -28,9 +28,9 @@ This parameter defines the input source (`-i`) for probing.
!!! danger "Sourcer API checks for _`video bitrate`_ or _`frame-size` and `framerate`_ in video's metadata to ensure given input `source` has usable video stream available. Thereby, it will throw `ValueError` if it fails to find those parameters."
-!!! info "Multiple video inputs are not yet supported!"
+!!! info "Multiple video inputs are fully supported! Pass a Python list of source strings to probe multiple media streams simultaneously. The probed dictionaries will be appended to the `sources` metadata list."
-**Data-Type:** String.
+**Data-Type:** String or List of Strings.
Its valid input can be one of the following:
@@ -362,7 +362,7 @@ This parameter specifies the demuxer(`-f`) for the input source _(such as `dshow
sourcer = Sourcer("0", source_demuxer="auto).probe_stream()
```
-**Data-Type:** String
+**Data-Type:** String or List of Strings (if `source` is a list, you can pass a list of identical length mapping demuxers to corresponding sources).
**Default Value:** Its default value is `None`.
@@ -461,6 +461,18 @@ These parameters are discussed below:
sourcer_params = {"-ffprefixes": ['-re']} # executes as `ffmpeg -re `
```
+ !!! info "Multi-input mode: per-source list-of-lists"
+ When [`source`](#source) is a list, `-ffprefixes` must also be a **list of per-input lists** with one entry per source (in the same order). Flat lists are rejected as ambiguous, and a length mismatch raises `ValueError`.
+
+ ```python
+ # source[0] gets `-re`; source[1] gets `-stream_loop -1`
+ sourcer_params = {
+ "-ffprefixes": [["-re"], ["-stream_loop", "-1"]],
+ }
+ ```
+
+ Use an empty inner list (`[]`) for any input that needs no prefix.
+
* **`-ffmpeg_download_path`** _(string)_: sets the custom directory for downloading FFmpeg Static Binaries in Compression Mode, during the [Auto-Installation](../ffmpeg_install/#a-auto-installation) on Windows Machines Only. If this parameter is not altered, then these binaries will auto-save to the default temporary directory (for e.g. `C:/User/temp`) on your windows machine. It can be used as follows:
diff --git a/docs/reference/utils.md b/docs/reference/utils.md
index 25d46704..4c920624 100644
--- a/docs/reference/utils.md
+++ b/docs/reference/utils.md
@@ -35,4 +35,8 @@ limitations under the License.
::: deffcode.utils.delete_file_safe
+
+
+::: deffcode.utils.validate_device_index
+
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index 92f1bf21..c9c556dc 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -36,6 +36,7 @@ theme:
language: en
features:
- announce.dismiss
+ - navigation.prune
- navigation.tabs
- navigation.tabs.sticky
- navigation.indexes
@@ -83,20 +84,24 @@ theme:
# Plugins
plugins:
- search
- - git-revision-date-localized
+ - git-revision-date-localized:
+ enable_creation_date: true
+ fallback_to_build_date: true
- minify:
minify_html: true
- mkdocstrings:
handlers:
python:
options:
- show_root_heading: false
- show_root_toc_entry: false
- show_source: true
- heading_level: 3
- - exclude:
- glob:
- - overrides/assets/README.md
+ filters:
+ - "!^_"
+ - "^__init__$"
+ - "^__call__$"
+ extra:
+ show_root_heading: false
+ show_root_toc_entry: false
+ show_source: true
+ heading_level: 3
# Customization
extra:
@@ -139,7 +144,8 @@ markdown_extensions:
permalink_title: Anchor link to this section for reference
- codehilite:
guess_lang: false
- - pymdownx.arithmatex
+ - pymdownx.arithmatex:
+ generic: true
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
@@ -163,7 +169,11 @@ markdown_extensions:
- pymdownx.smartsymbols
- pymdownx.snippets:
check_paths: true
- - pymdownx.superfences
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
@@ -171,7 +181,9 @@ markdown_extensions:
- pymdownx.tilde
- pymdownx.striphtml:
strip_comments: true
- - pymdownx.magiclink
+
+exclude_docs: |
+ overrides/assets/README.md
# Page tree
nav:
@@ -187,7 +199,7 @@ nav:
- Pull Request(PR) Guidelines: contribution/PR.md
- Changelog: changelog.md
- License: license.md
- - Recipies:
+ - Recipes:
- Basic Recipes:
- Overview: recipes/basic/index.md
- Decoding Video Files: recipes/basic/decode-video-files.md
@@ -198,7 +210,7 @@ nav:
- Transcoding Live Simple Filtergraphs: recipes/basic/transcode-live-frames-simplegraphs.md
- Saving Key-frames as Image: recipes/basic/save-keyframe-image.md
- Extracting video metadata: recipes/basic/extract-video-metadata.md
- - Advanced Recipies:
+ - Advanced Recipes:
- Overview: recipes/advanced/index.md
- Decoding Live Virtual Sources: recipes/advanced/decode-live-virtual-sources.md
- Decoding Live Feed Devices: recipes/advanced/decode-live-feed-devices.md
@@ -206,7 +218,9 @@ nav:
- Transcoding Live Complex Filtergraphs: recipes/advanced/transcode-live-frames-complexgraphs.md
- Transcoding Video Art with Filtergraphs: recipes/advanced/transcode-art-filtergraphs.md
- Hardware-Accelerated Video Transcoding: recipes/advanced/transcode-hw-acceleration.md
+ - Multi-Input Source Configurations: recipes/advanced/multi_input.md
- Updating Video Metadata: recipes/advanced/update-metadata.md
+ - Per-Frame Metadata Extraction: recipes/advanced/extract-frame-metadata.md
- API References:
- deffcode.FFdecoder:
- API: reference/ffdecoder/index.md
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..4b7ccc5f
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,104 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "deffcode"
+description = "A cross-platform High-performance & Flexible Real-time Video Frames Decoder in Python."
+authors = [{name = "Abhishek Thakur", email = "abhi.una12@gmail.com"}]
+license = {text = "Apache License 2.0"}
+requires-python = ">=3.10"
+keywords = [
+ "FFmpeg",
+ "Decoder",
+ "Realtime",
+ "Framework",
+ "Cross-platform",
+ "Video Processing",
+ "Computer Vision",
+ "Video Decoding",
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Operating System :: POSIX",
+ "Operating System :: MacOS :: MacOS X",
+ "Operating System :: Microsoft :: Windows",
+ "Topic :: Multimedia :: Video",
+ "Topic :: Multimedia :: Video :: Conversion",
+ "Topic :: Scientific/Engineering",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "Intended Audience :: Education",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+ "cython",
+ "numpy",
+ "requests",
+ "colorlog",
+ "tqdm",
+]
+dynamic = ["version", "readme"]
+
+[project.urls]
+Homepage = "https://abhitronix.github.io/deffcode"
+"Bug Reports" = "https://github.com/abhiTronix/deffcode/issues"
+Funding = "https://ko-fi.com/W7W8WTYO"
+Source = "https://github.com/abhiTronix/deffcode"
+Documentation = "https://abhitronix.github.io/deffcode"
+Changelog = "https://abhitronix.github.io/deffcode/latest/changelog/"
+
+[tool.setuptools]
+packages = ["deffcode"]
+
+[tool.setuptools.dynamic]
+version = {attr = "deffcode.version.__version__"}
+
+[tool.ruff]
+line-length = 100
+target-version = "py310"
+extend-exclude = [
+ "build",
+ "dist",
+ ".venv",
+ "venv",
+ "docs",
+ "*.egg-info",
+]
+
+[tool.ruff.lint]
+select = [
+ "E", # pycodestyle errors
+ "W", # pycodestyle warnings
+ "F", # Pyflakes
+ "I", # isort
+ "B", # flake8-bugbear
+ "UP", # pyupgrade
+ "C4", # flake8-comprehensions
+ "SIM", # flake8-simplify
+ "PIE", # flake8-pie
+ "RUF", # Ruff-specific rules
+]
+ignore = [
+ "E501", # line-too-long (handled by formatter)
+ "B008", # function calls in argument defaults
+ "B904", # raise ... from ...
+ "SIM105", # contextlib.suppress (pattern clarity)
+ "SIM108", # ternary-in-place
+ "UP032", # f-strings (keep .format() calls for readability in logs)
+ "RUF012", # mutable class attrs with ClassVar
+ "E722", # bare except (kept intentional in a few places)
+]
+
+[tool.ruff.lint.per-file-ignores]
+"tests/*" = ["B011", "E402", "F401"]
+"deffcode/__init__.py" = ["F401"]
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+line-ending = "auto"
diff --git a/setup.py b/setup.py
index 13580449..1980233d 100644
--- a/setup.py
+++ b/setup.py
@@ -18,19 +18,11 @@
===============================================
"""
-# import the necessary packages
from setuptools import setup
-from distutils.util import convert_path
-
-# parse PKG version
-pkg_version = {}
-ver_path = convert_path("deffcode/version.py")
-with open(ver_path) as ver_file:
- exec(ver_file.read(), pkg_version)
# apply various patches to README text and prepare
# valid long_description
-with open("README.md", "r", encoding="utf-8") as fh:
+with open("README.md", encoding="utf-8") as fh:
long_description = fh.read()
# patch to remove github README specific text
long_description = (
@@ -45,63 +37,9 @@
# patch for unicodes
long_description = long_description.replace("➶", ">>").replace("©", "(c)")
# patch internal hyperlinks
- long_description = long_description.replace(
- "(#", "(https://github.com/abhiTronix/deffcode#"
- )
-
+ long_description = long_description.replace("(#", "(https://github.com/abhiTronix/deffcode#")
setup(
- name="deffcode",
- packages=["deffcode"],
- version=pkg_version["__version__"],
- description="A cross-platform High-performance & Flexible Real-time Video Frames Decoder in Python.",
- license="Apache License 2.0",
- author="Abhishek Thakur",
- install_requires=[
- "cython", # (not really a dependency) just helper for numpy install
- "numpy",
- "requests",
- "colorlog",
- "tqdm",
- ],
long_description=long_description,
long_description_content_type="text/markdown",
- author_email="abhi.una12@gmail.com",
- url="https://abhitronix.github.io/deffcode",
- keywords=[
- "FFmpeg",
- "Decoder",
- "Realtime",
- "Framework",
- "Cross-platform",
- "Video Processing",
- "Computer Vision",
- "Video Decoding",
- ],
- classifiers=[
- "Development Status :: 5 - Production/Stable",
- "Operating System :: POSIX",
- "Operating System :: MacOS :: MacOS X",
- "Operating System :: Microsoft :: Windows",
- "Topic :: Multimedia :: Video",
- "Topic :: Multimedia :: Video :: Conversion",
- "Topic :: Scientific/Engineering",
- "Intended Audience :: Developers",
- "Intended Audience :: Science/Research",
- "Intended Audience :: Education",
- "License :: OSI Approved :: Apache Software License",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- ],
- python_requires=">=3.8",
- scripts=[],
- project_urls={
- "Bug Reports": "https://github.com/abhiTronix/deffcode/issues",
- "Funding": "https://ko-fi.com/W7W8WTYO",
- "Source": "https://github.com/abhiTronix/deffcode",
- "Documentation": "https://abhitronix.github.io/deffcode",
- "Changelog": "https://abhitronix.github.io/deffcode/latest/changelog/",
- },
)
diff --git a/tests/__init__.py b/tests/__init__.py
index 3473f681..d4ca7958 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1 +1 @@
-__author__ = "Abhishek Thakur (@abhiTronix) "
\ No newline at end of file
+__author__ = "Abhishek Thakur (@abhiTronix) "
diff --git a/tests/essentials.py b/tests/essentials.py
index 28cba432..19ad382e 100644
--- a/tests/essentials.py
+++ b/tests/essentials.py
@@ -19,12 +19,17 @@
"""
# import the necessary packages
+from __future__ import annotations
-import os, cv2
-import tempfile
import logging
+import os
import platform
+import tempfile
+from typing import Any
+
+import cv2
from vidgear.gears import WriteGear
+
from deffcode.utils import logger_handler
# define test logger
@@ -34,30 +39,24 @@
logger.setLevel(logging.DEBUG)
# define machine os
-is_windows = True if os.name == "nt" else False
+is_windows: bool = os.name == "nt"
-def return_static_ffmpeg():
+def return_static_ffmpeg() -> str:
"""
returns system specific FFmpeg static path
"""
path = ""
if platform.system() == "Windows":
- path += os.path.join(
- tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/bin/ffmpeg.exe"
- )
+ path += os.path.join(tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/bin/ffmpeg.exe")
elif platform.system() == "Darwin":
- path += os.path.join(
- tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/bin/ffmpeg"
- )
+ path += os.path.join(tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/bin/ffmpeg")
else:
- path += os.path.join(
- tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/ffmpeg"
- )
+ path += os.path.join(tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/ffmpeg")
return os.path.abspath(path)
-def remove_file_safe(path):
+def remove_file_safe(path: str) -> None:
"""
Remove file safely
"""
@@ -68,7 +67,7 @@ def remove_file_safe(path):
logger.exception(e)
-def return_testvideo_path(fmt="av"):
+def return_testvideo_path(fmt: str = "av") -> str:
"""
returns Test video path
"""
@@ -78,13 +77,11 @@ def return_testvideo_path(fmt="av"):
"ao": "BigBuckBunny_4sec_AO.mp4",
}
req_fmt = fmt if (fmt in supported_fmts) else "av"
- path = "{}/Downloads/Test_videos/{}".format(
- tempfile.gettempdir(), supported_fmts[req_fmt]
- )
+ path = "{}/Downloads/Test_videos/{}".format(tempfile.gettempdir(), supported_fmts[req_fmt])
return os.path.abspath(path)
-def return_generated_frames_path(path):
+def return_generated_frames_path(path: str) -> str:
"""
returns Test video path
"""
@@ -107,13 +104,13 @@ def return_generated_frames_path(path):
return frames_path
-def actual_frame_count_n_frame_size(path):
+def actual_frame_count_n_frame_size(path: str) -> tuple[int, Any]:
"""
simply counts the total frames in a given video
"""
stream = cv2.VideoCapture(path)
- num_cv = 0
- shape = None
+ num_cv: int = 0
+ shape: Any = None
while True:
(grabbed, frame) = stream.read()
if not grabbed:
diff --git a/tests/test_ffdecoder.py b/tests/test_ffdecoder.py
index 27164bb7..5422baef 100644
--- a/tests/test_ffdecoder.py
+++ b/tests/test_ffdecoder.py
@@ -19,25 +19,30 @@
"""
# import the necessary packages
+from __future__ import annotations
-import os
-import cv2
import json
-import pytest
-import tempfile
+import logging
+import os
import platform
+import tempfile
+from typing import Any
+
+import cv2
import numpy as np
-import logging
+import pytest
+from PIL import Image
+
+from deffcode import FFdecoder
+from deffcode.utils import logger_handler
+
from .essentials import (
- return_static_ffmpeg,
- return_testvideo_path,
- return_generated_frames_path,
actual_frame_count_n_frame_size,
remove_file_safe,
+ return_generated_frames_path,
+ return_static_ffmpeg,
+ return_testvideo_path,
)
-from PIL import Image
-from deffcode import FFdecoder
-from deffcode.utils import logger_handler
# define test logger
logger = logging.getLogger("Test_FFdecoder")
@@ -49,9 +54,9 @@
@pytest.mark.parametrize(
"source, custom_ffmpeg, output",
[
- (return_testvideo_path(fmt="av"), return_static_ffmpeg(), True),
+ (return_testvideo_path(fmt="av"), "", True),
(
- "https://gitlab.com/abhiTronix/Imbakup/-/raw/master/Images/starship.mkv",
+ "https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1MB.mp4",
"",
True,
),
@@ -64,7 +69,7 @@
),
],
)
-def test_source_playback(source, custom_ffmpeg, output):
+def test_source_playback(source: str, custom_ffmpeg: str, output: bool) -> None:
"""
Paths Source Playback - Test playback of various source paths/urls supported by FFdecoder API
"""
@@ -95,16 +100,26 @@ def test_source_playback(source, custom_ffmpeg, output):
# gather data
actual_frame_num, actual_frame_shape = actual_frame_count_n_frame_size(source)
+ logger.info(
+ f"Actual Frames Number: {actual_frame_num} and Actual Frame Shape: {actual_frame_shape}"
+ )
+
+ # Update output if the actual_frame_count_n_frame_size fails to decode stream
+ output = output and (actual_frame_shape is not None)
# grab RGB24(default) 3D frames from decoder
for frame in decoder.generateFrame():
# check shape
if frame.shape != actual_frame_shape:
- raise RuntimeError("Test failed")
+ raise RuntimeError(
+ f"Test failed - Frame Shape: {frame.shape} vs Actual Frame Shape: {actual_frame_shape}"
+ )
# increment number of frames
frame_num += 1
- assert frame_num >= actual_frame_num, "Test failed"
+ assert frame_num >= actual_frame_num, (
+ f"Test failed - Total Frames: {frame_num} vs Actual Frames: {actual_frame_num}"
+ )
except Exception as e:
if not output:
logger.exception(str(e))
@@ -113,20 +128,19 @@ def test_source_playback(source, custom_ffmpeg, output):
pytest.fail(str(e))
finally:
# terminate the decoder
- not (decoder is None) and decoder.terminate()
+ decoder is not None and decoder.terminate()
@pytest.mark.parametrize(
"pixfmts", ["bgr24", "gray", "rgba", "invalid", "invalid2", "yuv420p", "bgr48be"]
)
-def test_frame_format(pixfmts):
+def test_frame_format(pixfmts: str) -> None:
"""
Testing `frame_format` with different pixel formats.
"""
decoder = None
- frame_num = 0
source = return_testvideo_path(fmt="vo")
- actual_frame_num, actual_frame_shape = actual_frame_count_n_frame_size(source)
+ _actual_frame_num, _actual_frame_shape = actual_frame_count_n_frame_size(source)
ffparams = {"-pix_fmt": "bgr24"}
try:
# formulate the decoder with suitable source(for e.g. foo.mp4)
@@ -153,7 +167,7 @@ def test_frame_format(pixfmts):
**ffparams,
)
# assign manually pix-format via `metadata` property object {special case}
- decoder.metadata = dict(output_frames_pixfmt="yuvj422p")
+ decoder.metadata = {"output_frames_pixfmt": "yuvj422p"}
# formulate decoder
decoder.formulate()
@@ -169,7 +183,271 @@ def test_frame_format(pixfmts):
pytest.fail(str(e))
finally:
# terminate the decoder
- not (decoder is None) and decoder.terminate()
+ decoder is not None and decoder.terminate()
+
+
+@pytest.mark.parametrize(
+ "pixfmt, cv_color_code",
+ [
+ ("yuv420p", cv2.COLOR_YUV2BGR_I420),
+ ("nv12", cv2.COLOR_YUV2BGR_NV12),
+ ("nv21", cv2.COLOR_YUV2BGR_NV21),
+ ],
+)
+def test_yuv_family_ingest(pixfmt: str, cv_color_code: int) -> None:
+ """
+ Validates the YUV/NV ingest path from Issue #15: FFdecoder must deliver a
+ compact 3:2 planar buffer for `yuv`/`nv` pixel-formats under
+ `-enforce_cv_patch`, and that buffer must round-trip to BGR via OpenCV.
+ """
+ decoder = None
+ source = return_testvideo_path(fmt="vo")
+ _, actual_shape = actual_frame_count_n_frame_size(source)
+ try:
+ decoder = FFdecoder(
+ source,
+ frame_format=pixfmt,
+ custom_ffmpeg=return_static_ffmpeg(),
+ verbose=True,
+ **{"-enforce_cv_patch": True},
+ ).formulate()
+
+ # pixel-format may fall back to rgb24 if the local FFmpeg build lacks it
+ metadata = json.loads(decoder.metadata)
+ if metadata.get("output_frames_pixfmt") != pixfmt:
+ pytest.skip(f"FFmpeg build does not advertise `{pixfmt}` pixel-format")
+
+ frame = next(decoder.generateFrame(), None)
+ assert frame is not None, "Test failed - no frame retrieved"
+
+ h, w = actual_shape[0], actual_shape[1]
+ # YUV/NV ingest with cv_patch yields a 2D buffer with height = h*3/2
+ assert frame.shape == (h * 3 // 2, w), (
+ f"Test failed - unexpected YUV buffer shape {frame.shape}, expected {(h * 3 // 2, w)}"
+ )
+
+ # round-trip via OpenCV to confirm planar layout is valid
+ bgr = cv2.cvtColor(frame, cv_color_code)
+ assert bgr.shape == (h, w, 3), (
+ f"Test failed - unexpected BGR shape after conversion {bgr.shape}"
+ )
+ except Exception as e:
+ pytest.fail(str(e))
+ finally:
+ decoder is not None and decoder.terminate()
+
+
+@pytest.mark.parametrize(
+ "pixfmt",
+ ["yuv420p", "nv12", "nv21"],
+)
+def test_extract_luma(pixfmt: str) -> None:
+ """
+ Validates the `-extract_luma` fast-path: for YUV/NV pixel-formats the
+ decoder must slice the pure Y-plane out of the bytestream and hand back a
+ 2D grayscale (H, W) ndarray, without requiring `-enforce_cv_patch`.
+ """
+ decoder = None
+ source = return_testvideo_path(fmt="vo")
+ _, actual_shape = actual_frame_count_n_frame_size(source)
+ try:
+ decoder = FFdecoder(
+ source,
+ frame_format=pixfmt,
+ custom_ffmpeg=return_static_ffmpeg(),
+ verbose=True,
+ **{"-extract_luma": True},
+ ).formulate()
+
+ # skip if FFmpeg build does not advertise the requested pixel-format
+ metadata = json.loads(decoder.metadata)
+ if metadata.get("output_frames_pixfmt") != pixfmt:
+ pytest.skip(f"FFmpeg build does not advertise `{pixfmt}` pixel-format")
+
+ h, w = actual_shape[0], actual_shape[1]
+ frames_checked = 0
+ # iterate a few frames to confirm pipe stays aligned across reads
+ for frame in decoder.generateFrame():
+ assert frame is not None, "Test failed - no frame retrieved"
+ # luma-only output must be a 2D (H, W) uint8 ndarray
+ assert frame.shape == (h, w), (
+ f"Test failed - unexpected luma shape {frame.shape}, expected {(h, w)}"
+ )
+ assert frame.dtype == np.uint8, f"Test failed - unexpected luma dtype {frame.dtype}"
+ frames_checked += 1
+ if frames_checked >= 3:
+ break
+ assert frames_checked > 0, "Test failed - generator yielded no frames"
+ except Exception as e:
+ pytest.fail(str(e))
+ finally:
+ decoder is not None and decoder.terminate()
+
+
+def test_extract_metadata_basic() -> None:
+ """
+ Validates the `-extract_metadata` asynchronous showinfo parser: when
+ enabled, `generateFrame()` must yield `(frame, meta)` tuples with the
+ documented metadata keys and sensible values for a CFR source.
+ """
+ decoder = None
+ source = return_testvideo_path(fmt="vo")
+ _, actual_shape = actual_frame_count_n_frame_size(source)
+ try:
+ decoder = FFdecoder(
+ source,
+ frame_format="bgr24",
+ custom_ffmpeg=return_static_ffmpeg(),
+ verbose=True,
+ **{"-extract_metadata": True},
+ ).formulate()
+
+ expected_keys = {"frame_num", "pts_time", "is_keyframe", "frame_type"}
+ prev_frame_num = -1
+ frames_checked = 0
+ for pair in decoder.generateFrame():
+ assert isinstance(pair, tuple) and len(pair) == 2, (
+ "Test failed - expected (frame, meta) tuple when `-extract_metadata` is enabled"
+ )
+ frame, meta = pair
+ assert frame is not None and frame.shape == actual_shape, (
+ f"Test failed - frame shape {None if frame is None else frame.shape}, "
+ f"expected {actual_shape}"
+ )
+ assert isinstance(meta, dict), "Test failed - metadata must be a dict"
+ assert expected_keys.issubset(meta.keys()), (
+ f"Test failed - missing metadata keys, got {list(meta.keys())}"
+ )
+ assert meta["frame_num"] == prev_frame_num + 1, (
+ f"Test failed - non-monotonic frame_num {meta['frame_num']} after {prev_frame_num}"
+ )
+ assert meta["pts_time"] >= 0.0, "Test failed - negative pts_time"
+ assert meta["frame_type"] in {"I", "P", "B", "?"}, (
+ f"Test failed - unexpected frame_type `{meta['frame_type']}`"
+ )
+ prev_frame_num = meta["frame_num"]
+ frames_checked += 1
+ if frames_checked >= 5:
+ break
+ assert frames_checked > 0, "Test failed - generator yielded no frames"
+ assert prev_frame_num == 0 or any(True for _ in [0]), "sanity: loop must have executed"
+ except Exception as e:
+ pytest.fail(str(e))
+ finally:
+ decoder is not None and decoder.terminate()
+
+
+def test_extract_metadata_preserves_user_vf() -> None:
+ """
+ A user-supplied `-vf` filter must be preserved by comma-chaining
+ `showinfo` onto the filter graph rather than overwriting it.
+ """
+ decoder = None
+ source = return_testvideo_path(fmt="vo")
+ try:
+ decoder = FFdecoder(
+ source,
+ frame_format="bgr24",
+ custom_ffmpeg=return_static_ffmpeg(),
+ **{"-extract_metadata": True, "-vf": "scale=160:120"},
+ ).formulate()
+
+ frame, meta = next(decoder.generateFrame(), (None, None))
+ assert frame is not None, "Test failed - no frame retrieved"
+ # scale filter must have survived alongside showinfo
+ assert frame.shape == (120, 160, 3), (
+ f"Test failed - user `-vf scale=160:120` was not preserved, shape={frame.shape}"
+ )
+ assert isinstance(meta, dict) and "frame_num" in meta, (
+ "Test failed - metadata not produced when chaining with user -vf"
+ )
+ except Exception as e:
+ pytest.fail(str(e))
+ finally:
+ decoder is not None and decoder.terminate()
+
+
+def test_extract_metadata_invalid_type() -> None:
+ """
+ Non-bool `-extract_metadata` values must be discarded silently and the
+ decoder should fall back to yielding plain ndarray frames (no tuple).
+ """
+ decoder = None
+ source = return_testvideo_path(fmt="vo")
+ _, actual_shape = actual_frame_count_n_frame_size(source)
+ try:
+ decoder = FFdecoder(
+ source,
+ frame_format="bgr24",
+ custom_ffmpeg=return_static_ffmpeg(),
+ **{"-extract_metadata": "yes"}, # invalid, must be coerced to False
+ ).formulate()
+ frame = next(decoder.generateFrame(), None)
+ assert frame is not None, "Test failed - no frame retrieved"
+ assert not isinstance(frame, tuple), (
+ "Test failed - invalid `-extract_metadata` value should not enable tuple output"
+ )
+ assert frame.shape == actual_shape
+ except Exception as e:
+ pytest.fail(str(e))
+ finally:
+ decoder is not None and decoder.terminate()
+
+
+def test_extract_metadata_filter_complex_disables() -> None:
+ """
+ `-extract_metadata` cannot coexist with `-filter_complex` (graph-label
+ routing is ambiguous). The decoder must warn and fall back to plain
+ ndarray frames rather than emitting tuples.
+ """
+ decoder = None
+ source = return_testvideo_path(fmt="vo")
+ try:
+ decoder = FFdecoder(
+ source,
+ frame_format="bgr24",
+ custom_ffmpeg=return_static_ffmpeg(),
+ **{
+ "-extract_metadata": True,
+ "-filter_complex": "[0:v]scale=160:120[out]",
+ },
+ ).formulate()
+ frame = next(decoder.generateFrame(), None)
+ # decoder should fall back to plain ndarray output (not tuple)
+ assert frame is None or not isinstance(frame, tuple), (
+ "Test failed - `-extract_metadata` should be disabled when `-filter_complex` is set"
+ )
+ except Exception as e:
+ # some FFmpeg builds may reject the exact filter_complex above; that's
+ # fine — the only contract under test is "no tuple output"
+ logger.info(f"filter_complex path errored as expected: {e}")
+ finally:
+ decoder is not None and decoder.terminate()
+
+
+def test_extract_luma_invalid_type() -> None:
+ """
+ Non-bool `-extract_luma` values must be discarded silently and the decoder
+ should fall back to the default reshape path.
+ """
+ decoder = None
+ source = return_testvideo_path(fmt="vo")
+ _, actual_shape = actual_frame_count_n_frame_size(source)
+ try:
+ decoder = FFdecoder(
+ source,
+ frame_format="bgr24",
+ custom_ffmpeg=return_static_ffmpeg(),
+ **{"-extract_luma": "yes"}, # invalid, must be coerced to False
+ ).formulate()
+ frame = next(decoder.generateFrame(), None)
+ assert frame is not None and frame.shape == actual_shape, (
+ f"Test failed - got {None if frame is None else frame.shape}, expected {actual_shape}"
+ )
+ except Exception as e:
+ pytest.fail(str(e))
+ finally:
+ decoder is not None and decoder.terminate()
@pytest.mark.parametrize(
@@ -195,21 +473,21 @@ def test_frame_format(pixfmts):
),
(["invalid"], False),
(
- dict(
- mystring="abcd", # string data
- myint=1234, # integers data
- mylist=[1, "Rohan", ["inner_list"]], # list data
- mydict={"anotherstring": "hello"}, # dictionary data
- myjson=json.loads(
+ {
+ "mystring": "abcd", # string data
+ "myint": 1234, # integers data
+ "mylist": [1, "Rohan", ["inner_list"]], # list data
+ "mydict": {"anotherstring": "hello"}, # dictionary data
+ "myjson": json.loads(
'{"name": "John", "age": 30, "city": "New York"}'
), # json data
- source_video_resolution=[640, 480],
- ),
+ "source_video_resolution": [640, 480],
+ },
True,
),
],
)
-def test_metadata(custom_params, checks):
+def test_metadata(custom_params: Any, checks: bool) -> None:
"""
Testing `metadata` print and updation
"""
@@ -240,8 +518,7 @@ def test_metadata(custom_params, checks):
if checks:
assert all(
- json.loads(decoder.metadata)[x] == custom_params[x]
- for x in custom_params
+ json.loads(decoder.metadata)[x] == custom_params[x] for x in custom_params
), "Test failed"
except Exception as e:
if not checks:
@@ -250,7 +527,7 @@ def test_metadata(custom_params, checks):
pytest.fail(str(e))
finally:
# terminate the decoder
- not (decoder is None) and decoder.terminate()
+ decoder is not None and decoder.terminate()
@pytest.mark.parametrize(
@@ -275,14 +552,13 @@ def test_metadata(custom_params, checks):
"-framerate": "invalid",
"-ffprefixes": "invalid",
"-clones": "invalid",
- "-framerate": "invalid",
"-vcodec": None,
},
"gray",
),
],
)
-def test_seek_n_save(ffparams, pixfmts):
+def test_seek_n_save(ffparams: dict[str, Any], pixfmts: str) -> None:
"""
Testing `frame_format` with different colorspaces.
"""
@@ -302,22 +578,18 @@ def test_seek_n_save(ffparams, pixfmts):
frame = next(decoder.generateFrame(), None)
# check if frame is None
- if not (frame is None) and pixfmts == "rgba":
+ if frame is not None and pixfmts == "rgba":
# Convert and save our output
filename = os.path.abspath(
- os.path.join(
- *[tempfile.gettempdir(), "temp_write", "filename_rgba.jpeg"]
- )
+ os.path.join(*[tempfile.gettempdir(), "temp_write", "filename_rgba.jpeg"])
)
im = Image.fromarray(frame)
im = im.convert("RGB")
im.save(filename)
- elif not (frame is None) and pixfmts == "gray":
+ elif frame is not None and pixfmts == "gray":
# Convert and save our output
filename = os.path.abspath(
- os.path.join(
- *[tempfile.gettempdir(), "temp_write", "filename_gray.png"]
- )
+ os.path.join(*[tempfile.gettempdir(), "temp_write", "filename_gray.png"])
)
cv2.imwrite(filename, frame)
else:
@@ -328,7 +600,7 @@ def test_seek_n_save(ffparams, pixfmts):
pytest.fail(str(e))
finally:
# terminate the decoder
- not (decoder is None) and decoder.terminate()
+ decoder is not None and decoder.terminate()
filename and remove_file_safe(filename)
@@ -356,11 +628,23 @@ def test_seek_n_save(ffparams, pixfmts):
},
True,
),
+ (
+ [return_testvideo_path(), return_testvideo_path()],
+ {"-filter_complex": "hstack=inputs=2"},
+ True,
+ ),
+ (
+ [return_testvideo_path(), return_testvideo_path()],
+ {
+ "-extract_metadata": True
+ }, # Should fail with ValueError because of missing map/filter_complex
+ False,
+ ),
]
@pytest.mark.parametrize("source, ffparams, result", test_data)
-def test_FFdecoder_params(source, ffparams, result):
+def test_FFdecoder_params(source: str | list[str], ffparams: dict[str, Any], result: bool) -> None:
"""
Testing FFdecoder API with different parameters and save output
"""
@@ -373,13 +657,10 @@ def test_FFdecoder_params(source, ffparams, result):
source,
frame_format="bgr24",
source_demuxer=(
- "lavfi"
- if (isinstance(source, str) and source.startswith("testsrc"))
- else None
+ "lavfi" if (isinstance(source, str) and source.startswith("testsrc")) else None
),
**ffparams,
) as decoder:
-
# retrieve JSON Metadata and convert it to dict
metadata_dict = json.loads(decoder.metadata)
@@ -393,7 +674,6 @@ def test_FFdecoder_params(source, ffparams, result):
# grab the BGR24 frame from the decoder
for frame in decoder.generateFrame():
-
# check if frame is None
if frame is None:
break
@@ -407,7 +687,7 @@ def test_FFdecoder_params(source, ffparams, result):
pytest.xfail(str(e))
finally:
# terminate the decoder
- if not (writer is None):
+ if writer is not None:
writer.release()
remove_file_safe(f_name)
@@ -416,17 +696,17 @@ def test_FFdecoder_params(source, ffparams, result):
(
"/dev/video0",
"v4l2",
- True if platform.system() == "Linux" else False,
+ platform.system() == "Linux",
), # manual source and demuxer
(
0,
None,
- True if platform.system() == "Linux" else False,
+ platform.system() == "Linux",
), # +ve index and no demuxer
(
"-1",
"auto",
- True if platform.system() == "Linux" else False,
+ platform.system() == "Linux",
), # -ve index and "auto" demuxer
("5", "auto", False), # out-of-range index and "auto" demuxer
("invalid", "auto", False), # invalid source and "auto" demuxer
@@ -435,7 +715,7 @@ def test_FFdecoder_params(source, ffparams, result):
@pytest.mark.parametrize("source, source_demuxer, result", test_data)
-def test_camera_capture(source, source_demuxer, result):
+def test_camera_capture(source: str | int, source_demuxer: str | None, result: bool) -> None:
"""
Tests FFdecoder's realtime Webcam and Virtual playback capabilities
as well as Index based Camera Device Capturing
@@ -450,7 +730,7 @@ def test_camera_capture(source, source_demuxer, result):
verbose=True,
).formulate()
# capture 5 camera frames
- for i in range(5):
+ for _i in range(5):
# grab the bgr24 frame from the decoder
frame_recv = next(decoder.generateFrame(), None)
# check if frame is None
@@ -464,7 +744,7 @@ def test_camera_capture(source, source_demuxer, result):
pytest.xfail(str(e))
finally:
# terminate
- not (decoder is None) and decoder.terminate()
+ decoder is not None and decoder.terminate()
test_data = [
@@ -522,7 +802,7 @@ def test_camera_capture(source, source_demuxer, result):
@pytest.mark.parametrize("frame_format, ffparams, result", test_data)
-def test_discard_n_filter_params(frame_format, ffparams, result):
+def test_discard_n_filter_params(frame_format: str, ffparams: dict[str, Any], result: bool) -> None:
"""
Tests FFdecoder's discarding FFmpeg parameters and using FFmpeg Filter
capabilities
@@ -530,7 +810,7 @@ def test_discard_n_filter_params(frame_format, ffparams, result):
decoder = None
try:
# initialize and formulate the decode with suitable source
- if not frame_format in ["invalid2", "invalid3"]:
+ if frame_format not in ["invalid2", "invalid3"]:
decoder = FFdecoder(
return_testvideo_path(),
frame_format=frame_format,
@@ -553,7 +833,7 @@ def test_discard_n_filter_params(frame_format, ffparams, result):
# formulate decoder
decoder.formulate()
# capture 2 camera frames
- for i in range(2):
+ for _i in range(2):
# grab the bgr24 frame from the decoder
frame_recv = next(decoder.generateFrame(), None)
# check if frame is None
@@ -567,4 +847,4 @@ def test_discard_n_filter_params(frame_format, ffparams, result):
pytest.xfail(str(e))
finally:
# terminate
- not (decoder is None) and decoder.terminate()
+ decoder is not None and decoder.terminate()
diff --git a/tests/test_ffhelper.py b/tests/test_ffhelper.py
index 78229258..b5727277 100644
--- a/tests/test_ffhelper.py
+++ b/tests/test_ffhelper.py
@@ -17,30 +17,37 @@
limitations under the License.
===============================================
"""
+
# import the necessary packages
+from __future__ import annotations
+import logging
import os
-import pytest
import shutil
-import logging
-import requests
import tempfile
-from .essentials import (
- is_windows,
- return_static_ffmpeg,
- return_testvideo_path,
- return_generated_frames_path,
-)
-from deffcode.utils import logger_handler
+
+import pytest
+import requests
+
+from deffcode import ffhelper
from deffcode.ffhelper import (
- get_valid_ffmpeg_path,
+ check_sp_output,
download_ffmpeg_binaries,
- validate_ffmpeg,
- validate_imgseqdir,
+ extract_device_n_demuxer,
+ get_supported_demuxers,
+ get_valid_ffmpeg_path,
is_valid_image_seq,
is_valid_url,
- check_sp_output,
- extract_device_n_demuxer,
+ validate_ffmpeg,
+ validate_imgseqdir,
+)
+from deffcode.utils import logger_handler
+
+from .essentials import (
+ is_windows,
+ return_generated_frames_path,
+ return_static_ffmpeg,
+ return_testvideo_path,
)
# define test logger
@@ -64,15 +71,13 @@
@pytest.mark.parametrize("paths, os_bit", test_data)
-def test_ffmpeg_binaries_download(paths, os_bit):
+def test_ffmpeg_binaries_download(paths: str, os_bit: str) -> None:
"""
Testing Static FFmpeg auto-download on Windows OS
"""
file_path = ""
try:
- file_path = download_ffmpeg_binaries(
- path=paths, os_windows=is_windows, os_bit=os_bit
- )
+ file_path = download_ffmpeg_binaries(path=paths, os_windows=is_windows, os_bit=os_bit)
if file_path:
logger.debug("FFmpeg Binary path: {}".format(file_path))
assert os.path.isfile(file_path), "FFmpeg download failed!"
@@ -85,7 +90,7 @@ def test_ffmpeg_binaries_download(paths, os_bit):
@pytest.mark.parametrize("paths", ["wrong_test_path", return_static_ffmpeg()])
-def test_validate_ffmpeg(paths):
+def test_validate_ffmpeg(paths: str) -> None:
"""
Testing downloaded FFmpeg Static binaries validation on Windows OS
"""
@@ -111,7 +116,7 @@ def test_validate_ffmpeg(paths):
@pytest.mark.parametrize("paths, ffmpeg_download_paths, results", test_data)
-def test_get_valid_ffmpeg_path(paths, ffmpeg_download_paths, results):
+def test_get_valid_ffmpeg_path(paths: str, ffmpeg_download_paths: str, results: bool) -> None:
"""
Testing FFmpeg excutables validation and correction:
"""
@@ -122,13 +127,11 @@ def test_get_valid_ffmpeg_path(paths, ffmpeg_download_paths, results):
ffmpeg_download_path=ffmpeg_download_paths,
verbose=True,
)
- if not (
- paths == "wrong_test_path" or ffmpeg_download_paths == "wrong_test_path"
- ):
- assert (
- bool(output) == results
- ), "FFmpeg excutables validation and correction Test failed at path: {} and FFmpeg ffmpeg_download_paths: {}".format(
- paths, ffmpeg_download_paths
+ if not (paths == "wrong_test_path" or ffmpeg_download_paths == "wrong_test_path"):
+ assert bool(output) == results, (
+ "FFmpeg excutables validation and correction Test failed at path: {} and FFmpeg ffmpeg_download_paths: {}".format(
+ paths, ffmpeg_download_paths
+ )
)
except Exception as e:
if paths == "wrong_test_path" or ffmpeg_download_paths == "wrong_test_path":
@@ -140,7 +143,7 @@ def test_get_valid_ffmpeg_path(paths, ffmpeg_download_paths, results):
@pytest.mark.xfail(raises=Exception)
-def test_check_sp_output():
+def test_check_sp_output() -> None:
"""
Testing check_sp_output method
"""
@@ -155,7 +158,7 @@ def test_check_sp_output():
("unknown://invalid.com/", False),
],
)
-def test_is_valid_url(URL, result):
+def test_is_valid_url(URL: str | None, result: bool) -> None:
"""
Testing is_valid_url method
"""
@@ -178,14 +181,12 @@ def test_is_valid_url(URL, result):
),
],
)
-def test_is_valid_image_seq(source, result):
+def test_is_valid_image_seq(source: str | None, result: bool) -> None:
"""
Testing test_is_valid_image_seq method
"""
try:
- result_url = is_valid_image_seq(
- return_static_ffmpeg(), source=source, verbose=True
- )
+ result_url = is_valid_image_seq(return_static_ffmpeg(), source=source, verbose=True)
assert result_url == result, "Image sequence validity test Failed!"
except Exception as e:
result and pytest.fail(str(e))
@@ -198,7 +199,7 @@ def test_is_valid_image_seq(source, result):
("unknown://invalid.com/", False),
],
)
-def test_validate_imgseqdir(path, result):
+def test_validate_imgseqdir(path: str, result: bool) -> None:
"""
Testing validate_imgseqdir method
"""
@@ -210,8 +211,32 @@ def test_validate_imgseqdir(path, result):
@pytest.mark.xfail(raises=ValueError)
-def test_extract_device_n_demuxer():
+def test_extract_device_n_demuxer() -> None:
"""
Testing extract_device_n_demuxer method
"""
- extract_device_n_demuxer(return_static_ffmpeg(), machine_OS="invalid", verbose=True)
\ No newline at end of file
+ extract_device_n_demuxer(return_static_ffmpeg(), machine_OS="invalid", verbose=True)
+
+
+def test_get_supported_demuxers_valid() -> None:
+ """
+ Testing get_supported_demuxers returns a non-empty list when the FFmpeg
+ `-demuxers` output contains the expected `--` separator.
+ """
+ demuxers = get_supported_demuxers(return_static_ffmpeg())
+ assert isinstance(demuxers, list) and len(demuxers) > 0, (
+ "Expected a non-empty list of supported demuxers from a valid FFmpeg binary."
+ )
+
+
+def test_get_supported_demuxers_missing_separator(monkeypatch: pytest.MonkeyPatch) -> None:
+ """
+ Testing get_supported_demuxers safely returns an empty list (instead of
+ raising StopIteration) when the FFmpeg `-demuxers` output is missing
+ the `--` separator.
+ """
+ # simulate malformed FFmpeg output with no `--` separator line
+ malformed_output = b"File formats:\n D. = Demuxing supported\n garbage line\n"
+ monkeypatch.setattr(ffhelper, "check_sp_output", lambda *args, **kwargs: malformed_output)
+ result = get_supported_demuxers("fake_ffmpeg")
+ assert result == [], "Expected empty list when `--` separator is missing from demuxers output."
diff --git a/tests/test_sourcer.py b/tests/test_sourcer.py
index b3e76700..a30fa330 100644
--- a/tests/test_sourcer.py
+++ b/tests/test_sourcer.py
@@ -17,18 +17,24 @@
limitations under the License.
===============================================
"""
+
# import the necessary packages
+from __future__ import annotations
-import pytest
import logging
+from typing import Any
+
+import pytest
+
+from deffcode import Sourcer
+from deffcode.utils import logger_handler
+
from .essentials import (
+ actual_frame_count_n_frame_size,
+ return_generated_frames_path,
return_static_ffmpeg,
return_testvideo_path,
- return_generated_frames_path,
- actual_frame_count_n_frame_size,
)
-from deffcode.utils import logger_handler
-from deffcode import Sourcer
# define test logger
logger = logging.getLogger("Test_Sourcer")
@@ -71,9 +77,25 @@
{},
"invalid_ffmpeg", # invalid FFmpeg
),
+ (
+ [return_testvideo_path(), return_testvideo_path()],
+ {
+ "-ffprefixes": [["-re"], ["-stream_loop", "-1"]],
+ },
+ return_static_ffmpeg(),
+ ),
+ (
+ [return_testvideo_path(), return_testvideo_path()],
+ {
+ "-ffprefixes": "invalid" # list of lists mismatch
+ },
+ return_static_ffmpeg(),
+ ),
],
)
-def test_source(source, sourcer_params, custom_ffmpeg):
+def test_source(
+ source: str | list[str], sourcer_params: dict[str, Any], custom_ffmpeg: str
+) -> None:
"""
Paths Source - Test various source paths/urls supported by Sourcer.
"""
@@ -107,20 +129,32 @@ def test_source(source, sourcer_params, custom_ffmpeg):
(0, 0),
["source_has_image_sequence"],
),
+ (
+ [return_testvideo_path(), "mandelbrot=size=1280x720:rate=30"],
+ (0, 0),
+ ["source_has_video", "sources"], # tests sources list
+ ),
],
)
-def test_probe_stream_n_retrieve_metadata(source, default_stream_indexes, params):
+def test_probe_stream_n_retrieve_metadata(
+ source: str | list[str],
+ default_stream_indexes: tuple[int, ...] | list[int],
+ params: list[str],
+) -> None:
"""
Test `probe_stream` and `retrieve_metadata` function.
"""
try:
- source_demuxer = (
- "lavfi" if source == "mandelbrot=size=1280x720:rate=30" else None
- )
+ source_demuxer = None
+ if isinstance(source, list):
+ source_demuxer = [
+ "lavfi" if s == "mandelbrot=size=1280x720:rate=30" else None for s in source
+ ]
+ elif source == "mandelbrot=size=1280x720:rate=30":
+ source_demuxer = "lavfi"
+
if source == "invalid":
- sourcer = Sourcer(
- source, custom_ffmpeg=return_static_ffmpeg(), verbose=True
- )
+ sourcer = Sourcer(source, custom_ffmpeg=return_static_ffmpeg(), verbose=True)
else:
sourcer = Sourcer(
source,
@@ -130,22 +164,34 @@ def test_probe_stream_n_retrieve_metadata(source, default_stream_indexes, params
).probe_stream(default_stream_indexes=default_stream_indexes)
metadata = sourcer.retrieve_metadata()
logger.debug("Found Metadata: `{}`".format(metadata))
- assert all(metadata[x] == True for x in params), "Test Failed!"
- if (
+
+ # Test sources exists and is valid
+ if "sources" in params:
+ assert "sources" in metadata and len(metadata["sources"]) == len(source), (
+ "Multi-input Test Failed!"
+ )
+
+ assert all(
+ metadata.get(x, metadata["sources"] if x == "sources" else False) for x in params
+ ), "Test Failed!"
+
+ is_skipped = False
+ if isinstance(source, list) or (
source.startswith("http")
or source.endswith("png")
or source == "mandelbrot=size=1280x720:rate=30"
):
+ is_skipped = True
+
+ if is_skipped:
logger.debug("Skipped check!")
else:
- assert (
- metadata["approx_video_nframes"]
- >= actual_frame_count_n_frame_size(source)[0]
- ), "Test Failed for frames count!"
+ assert metadata["approx_video_nframes"] >= actual_frame_count_n_frame_size(source)[0], (
+ "Test Failed for frames count!"
+ )
except Exception as e:
if isinstance(e, ValueError) or (
- source in ["invalid", "unknown://invalid.com/"]
- and isinstance(e, AssertionError)
+ source in ["invalid", "unknown://invalid.com/"] and isinstance(e, AssertionError)
):
pytest.xfail("Test Still Passed!")
else:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4a282866..a5f880e8 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -17,14 +17,19 @@
limitations under the License.
===============================================
"""
+
# import the necessary packages
+from __future__ import annotations
-import pytest
import logging
import os
import tempfile
from os.path import expanduser
-from deffcode.utils import dict2Args, logger_handler, delete_file_safe
+from typing import Any
+
+import pytest
+
+from deffcode.utils import delete_file_safe, dict2Args, logger_handler
# define test logger
logger = logging.getLogger("Test_Utilities")
@@ -40,14 +45,14 @@
@pytest.mark.parametrize("log_filepath, handler_type", test_data)
-def test_loggerhandler(log_filepath, handler_type):
+def test_loggerhandler(log_filepath: Any, handler_type: logging.Handler) -> None:
"""
Testing dict2Args utils function.
"""
if log_filepath:
os.environ["DEFFCODE_LOGFILE"] = log_filepath
try:
- assert type(logger_handler()) == type(handler_type), "Test failed"
+ assert type(logger_handler()) is type(handler_type), "Test failed"
except AssertionError:
pytest.fail("Logger handler test failed!")
finally:
@@ -75,7 +80,7 @@ def test_loggerhandler(log_filepath, handler_type):
@pytest.mark.parametrize("dictionary", test_data)
-def test_dict2Args(dictionary):
+def test_dict2Args(dictionary: dict[str, Any]) -> None:
"""
Testing dict2Args utils function.
"""
@@ -93,7 +98,7 @@ def test_dict2Args(dictionary):
@pytest.mark.parametrize("file_path, result", test_data)
-def test_delete_file_safe(file_path, result):
+def test_delete_file_safe(file_path: str, result: bool) -> None:
"""
Testing delete_file_safe method
"""