diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64709f2..059f8c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,30 +68,7 @@ jobs: libegl1 libopengl0 libxkbcommon-x11-0 \ libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 \ - ffmpeg - - - name: Detect FFmpeg version (Linux) - if: matrix.os == 'ubuntu-latest' - id: ffmpeg-version-linux - shell: bash - run: | - version="$(ffmpeg -version | head -n1 | awk '{print $3}')" - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Cache bundled FFmpeg (Linux) - if: matrix.os == 'ubuntu-latest' - id: cache-ffmpeg-linux - uses: actions/cache@v4 - with: - path: vendor/ffmpeg/linux - key: ffmpeg-${{ matrix.os }}-${{ runner.image_version }}-${{ steps.ffmpeg-version-linux.outputs.version }}-bundle - - - name: Bundle FFmpeg (Linux) - if: matrix.os == 'ubuntu-latest' && steps.cache-ffmpeg-linux.outputs.cache-hit != 'true' - run: | - mkdir -p vendor/ffmpeg/linux - cp "$(command -v ffmpeg)" vendor/ffmpeg/linux/ffmpeg - chmod +x vendor/ffmpeg/linux/ffmpeg + zip - name: Cache bundled FFmpeg (Windows) if: matrix.os == 'windows-latest' @@ -128,46 +105,9 @@ jobs: cd "$repo_root" ./scripts/build_ffmpeg_windows_msys2.sh - - name: Detect FFmpeg version (macOS) - if: matrix.os == 'macos-latest' - id: ffmpeg-version-macos - shell: bash - run: | - export HOMEBREW_NO_AUTO_UPDATE=1 - export HOMEBREW_NO_INSTALL_CLEANUP=1 - export HOMEBREW_NO_ENV_HINTS=1 - brew_info="$(brew info --json=v2 ffmpeg 2>/dev/null || true)" - version="$(BREW_INFO="$brew_info" python - <<'PY' - import json - import os - data = os.environ.get("BREW_INFO", "").strip() - if not data: - print("unknown") - else: - try: - parsed = json.loads(data) - print(parsed["formulae"][0]["versions"]["stable"]) - except Exception: - print("unknown") - PY - )" - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Cache bundled FFmpeg (macOS) - if: matrix.os == 'macos-latest' - id: cache-ffmpeg-macos - uses: actions/cache@v4 - with: - path: vendor/ffmpeg/macos - key: ffmpeg-${{ matrix.os }}-${{ runner.image_version }}-${{ steps.ffmpeg-version-macos.outputs.version }}-bundle - - - name: Bundle FFmpeg (macOS) - if: matrix.os == 'macos-latest' && steps.cache-ffmpeg-macos.outputs.cache-hit != 'true' - run: | - brew install ffmpeg - mkdir -p vendor/ffmpeg/macos - cp "$(brew --prefix)/bin/ffmpeg" vendor/ffmpeg/macos/ffmpeg - chmod +x vendor/ffmpeg/macos/ffmpeg + - name: Stage portable FFmpeg (Linux/macOS) + if: matrix.os != 'windows-latest' + run: uv --native-tls run --extra packaging python scripts/stage_portable_ffmpeg.py - name: Install dependencies run: uv pip install --system -e . pyinstaller @@ -180,9 +120,48 @@ jobs: shell: bash run: | out="dist/RenderKit-${{ steps.meta.outputs.os_label }}${{ steps.meta.outputs.artifact_suffix }}.zip" - python -m zipfile -c "$out" dist/RenderKit + if [[ "${{ matrix.os }}" == "windows-latest" ]]; then + python -m zipfile -c "$out" dist/RenderKit + else + zip -qry "$out" dist/RenderKit + fi echo "package_path=$out" >> "$GITHUB_OUTPUT" + - name: Smoke test bundled FFmpeg + shell: pwsh + run: | + $zipPath = "${{ steps.package.outputs.package_path }}" + $extractRoot = Join-Path $env:RUNNER_TEMP "renderkit-pyinstaller-smoke" + if (Test-Path $extractRoot) { + Remove-Item $extractRoot -Recurse -Force + } + New-Item -ItemType Directory -Path $extractRoot -Force | Out-Null + Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force + + $ffmpegName = if ($IsWindows) { "ffmpeg.exe" } else { "ffmpeg" } + $escapedFfmpegName = [regex]::Escape($ffmpegName) + $ffmpeg = Get-ChildItem -Path $extractRoot -Recurse -File -Filter $ffmpegName | + Where-Object { $_.FullName -match "[/\\]ffmpeg[/\\]$escapedFfmpegName$" } | + Select-Object -First 1 + if ($null -eq $ffmpeg) { + throw "Bundled FFmpeg was not found in $zipPath" + } + + & $ffmpeg.FullName -version + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + $encoders = & $ffmpeg.FullName -hide_banner -encoders + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + foreach ($encoder in @("libx264", "libx265", "libaom-av1")) { + if (-not ($encoders -match $encoder)) { + throw "Bundled FFmpeg is missing required encoder: $encoder" + } + } + - name: Upload build artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/nuitka-standalone-zip.yml b/.github/workflows/nuitka-standalone-zip.yml index 8a83879..67342d4 100644 --- a/.github/workflows/nuitka-standalone-zip.yml +++ b/.github/workflows/nuitka-standalone-zip.yml @@ -1,15 +1,32 @@ name: Nuitka Standalone Zip on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] workflow_dispatch: permissions: contents: read +env: + FFMPEG_VERSION: "8.0.1" + jobs: - build-windows: - name: Build Nuitka Standalone Zip (Windows) - runs-on: windows-latest + build: + name: Build Nuitka Standalone Zip (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + platform: windows + - os: ubuntu-latest + platform: linux + - os: macos-latest + platform: macos steps: - uses: actions/checkout@v4 @@ -24,15 +41,31 @@ jobs: enable-cache: true cache-dependency-glob: "uv.lock" - - name: Cache bundled FFmpeg + - name: Install Linux system dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y \ + libopenexr-dev libilmbase-dev \ + libegl1 libopengl0 libxkbcommon-x11-0 \ + libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ + libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 \ + patchelf zip + + - name: Stage portable FFmpeg (Linux/macOS) + if: matrix.os != 'windows-latest' + run: uv --native-tls run --extra packaging python scripts/stage_portable_ffmpeg.py + + - name: Cache bundled FFmpeg (Windows) + if: matrix.os == 'windows-latest' id: cache-ffmpeg uses: actions/cache@v4 with: path: vendor/ffmpeg/windows - key: ffmpeg-windows-8.0.1-${{ hashFiles('scripts/build_ffmpeg_windows_msys2.sh', 'scripts/build_ffmpeg_windows_msys2_bootstrap.sh') }} + key: ffmpeg-nuitka-${{ matrix.os }}-${{ env.FFMPEG_VERSION }}-${{ hashFiles('scripts/build_ffmpeg_windows_msys2.sh', 'scripts/build_ffmpeg_windows_msys2_bootstrap.sh') }} - - name: Set up MSYS2 - if: steps.cache-ffmpeg.outputs.cache-hit != 'true' + - name: Set up MSYS2 (Windows) + if: matrix.os == 'windows-latest' && steps.cache-ffmpeg.outputs.cache-hit != 'true' uses: msys2/setup-msys2@v2 with: msystem: UCRT64 @@ -47,9 +80,11 @@ jobs: mingw-w64-ucrt-x86_64-x265 mingw-w64-ucrt-x86_64-aom - - name: Build FFmpeg - if: steps.cache-ffmpeg.outputs.cache-hit != 'true' + - name: Build FFmpeg (Windows) + if: matrix.os == 'windows-latest' && steps.cache-ffmpeg.outputs.cache-hit != 'true' shell: msys2 {0} + env: + FFMPEG_VERSION: ${{ env.FFMPEG_VERSION }} run: | set -euo pipefail repo_root="$(cygpath -u "$GITHUB_WORKSPACE")" @@ -58,10 +93,54 @@ jobs: - name: Build Nuitka artifact shell: pwsh - run: .\scripts\build_nuitka.ps1 + run: ./scripts/build_nuitka.ps1 + + - name: Verify Nuitka artifact + shell: pwsh + run: | + $zipPath = "dist-nuitka/RenderKit-nuitka-${{ matrix.platform }}-standalone.zip" + if (-not (Test-Path $zipPath)) { + throw "Expected Nuitka artifact was not created: $zipPath" + } + Get-Item $zipPath + + - name: Smoke test bundled FFmpeg + shell: pwsh + run: | + $zipPath = "dist-nuitka/RenderKit-nuitka-${{ matrix.platform }}-standalone.zip" + $extractRoot = Join-Path $env:RUNNER_TEMP "renderkit-nuitka-smoke" + if (Test-Path $extractRoot) { + Remove-Item $extractRoot -Recurse -Force + } + New-Item -ItemType Directory -Path $extractRoot -Force | Out-Null + Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force + + $ffmpegName = if ($IsWindows) { "ffmpeg.exe" } else { "ffmpeg" } + $escapedFfmpegName = [regex]::Escape($ffmpegName) + $ffmpeg = Get-ChildItem -Path $extractRoot -Recurse -File -Filter $ffmpegName | + Where-Object { $_.FullName -match "[/\\]ffmpeg[/\\]$escapedFfmpegName$" } | + Select-Object -First 1 + if ($null -eq $ffmpeg) { + throw "Bundled FFmpeg was not found in $zipPath" + } + + & $ffmpeg.FullName -version + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + $encoders = & $ffmpeg.FullName -hide_banner -encoders + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + foreach ($encoder in @("libx264", "libx265", "libaom-av1")) { + if (-not ($encoders -match $encoder)) { + throw "Bundled FFmpeg is missing required encoder: $encoder" + } + } - name: Upload Nuitka artifact uses: actions/upload-artifact@v4 with: - name: RenderKit-nuitka-windows-standalone - path: dist-nuitka/RenderKit-nuitka-windows-standalone.zip + name: RenderKit-nuitka-${{ matrix.platform }}-standalone + path: dist-nuitka/RenderKit-nuitka-${{ matrix.platform }}-standalone.zip diff --git a/docs/development.md b/docs/development.md index 2c8f40f..602efa8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -82,10 +82,23 @@ release-style zip is written to `dist-nuitka/RenderKit-nuitka--standal Before using the Nuitka zip for a release, test it by extracting the zip on the target platform and running a real conversion. -## Bundled FFmpeg (Windows, Hybrid) +## Bundled FFmpeg -The repo does not commit `vendor/ffmpeg/`. You can build and stage a minimal -GPL FFmpeg locally (x265 + AV1 only), and CI will also generate it for releases. +The repo does not commit `vendor/ffmpeg/`. Packaging workflows stage FFmpeg +into `vendor/ffmpeg//` before building artifacts, and the packaged +app uses that binary ahead of any system `ffmpeg` on `PATH`. + +Linux and macOS use the packaging extra's portable `imageio-ffmpeg` binary: + +```bash +uv --native-tls run --extra packaging python scripts/stage_portable_ffmpeg.py +``` + +The staging helper verifies the binary starts and includes the required +`libx264`, `libx265`, and `libaom-av1` encoders. + +Windows uses a minimal GPL FFmpeg build from MSYS2 so the required DLLs can be +copied alongside `ffmpeg.exe`. ### Prerequisites (MSYS2 UCRT64) @@ -106,9 +119,9 @@ pacman -S --needed \ ./scripts/build_ffmpeg_windows_msys2.sh ``` -This script writes `ffmpeg.exe` and required DLLs to `vendor/ffmpeg/`, which the -PyInstaller spec bundles automatically. To build a different version, set -`FFMPEG_VERSION` (default: 8.0.1): +This script writes `ffmpeg.exe` and required DLLs to `vendor/ffmpeg/windows/`, +which the packaging workflows bundle automatically. To build a different +version, set `FFMPEG_VERSION` (default: 8.0.1): ```bash FFMPEG_VERSION=8.0.1 ./scripts/build_ffmpeg_windows_msys2.sh diff --git a/pyproject.toml b/pyproject.toml index 8c193bd..8a4f4db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ docs = [ ] packaging = [ + "imageio-ffmpeg>=0.6.0,<0.7", "nuitka>=4.1.0", "ordered-set>=4.1.0", "pyinstaller>=6.0.0", diff --git a/scripts/build_nuitka.ps1 b/scripts/build_nuitka.ps1 index 2d29140..e64d669 100644 --- a/scripts/build_nuitka.ps1 +++ b/scripts/build_nuitka.ps1 @@ -1,15 +1,67 @@ $ErrorActionPreference = "Stop" -$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") -$outputRoot = Join-Path $repoRoot "dist-nuitka" -$entryScript = Join-Path $repoRoot "src\renderkit\ui\main_window.py" -$initContent = Get-Content (Join-Path $repoRoot "src\renderkit\__init__.py") -Raw +function Join-NativePath { + param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]] $Parts + ) + + [System.IO.Path]::Combine($Parts) +} + +function Compress-NuitkaOutput { + param( + [Parameter(Mandatory = $true)] + [System.IO.DirectoryInfo] $PackageRoot, + + [Parameter(Mandatory = $true)] + [string] $ZipPath + ) + + if ((-not $IsWindows) -and (Get-Command zip -ErrorAction SilentlyContinue)) { + $zipWorkingDir = $PackageRoot.FullName + $zipTarget = "." + if ($PackageRoot.Extension -eq ".app") { + $zipWorkingDir = $PackageRoot.Parent.FullName + $zipTarget = $PackageRoot.Name + } + + Push-Location $zipWorkingDir + try { + & zip -qry $ZipPath $zipTarget + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + } finally { + Pop-Location + } + return + } + + $zipSource = if ($PackageRoot.Extension -eq ".app") { + $PackageRoot.FullName + } else { + Join-Path $PackageRoot.FullName "*" + } + Compress-Archive -Path $zipSource -DestinationPath $ZipPath -Force +} + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$outputRoot = Join-NativePath $repoRoot "dist-nuitka" +$entryScript = Join-NativePath $repoRoot "src" "renderkit" "ui" "main_window.py" +$initContent = Get-Content (Join-NativePath $repoRoot "src" "renderkit" "__init__.py") -Raw $versionMatch = [regex]::Match($initContent, "__version__\s*=\s*['""]([^'""]+)['""]") $appVersion = if ($versionMatch.Success) { $versionMatch.Groups[1].Value } else { "0.0.0" } +$iconsDir = Join-NativePath $repoRoot "src" "renderkit" "ui" "icons" +$stylesheetsDir = Join-NativePath $repoRoot "src" "renderkit" "ui" "stylesheets" +$ocioDir = Join-NativePath $repoRoot "src" "renderkit" "data" "ocio" +$nuitkaMode = "standalone" +$outputFilename = if ($IsMacOS) { "RenderKitApp" } else { "RenderKit" } + $nuitkaArgs = @( "--remove-output", - "--mode=standalone", + "--mode=$nuitkaMode", "--assume-yes-for-downloads", "--enable-plugin=pyside6", "--include-module=PySide6", @@ -18,21 +70,20 @@ $nuitkaArgs = @( "--include-module=PySide6.QtWidgets", "--include-qt-plugins=sensible", "--include-package=renderkit", - "--include-data-dir=$repoRoot\src\renderkit\ui\icons=renderkit/ui/icons", - "--include-data-dir=$repoRoot\src\renderkit\ui\stylesheets=renderkit/ui/stylesheets", - "--include-data-dir=$repoRoot\src\renderkit\data\ocio=renderkit/data/ocio", + "--include-data-dir=$iconsDir=renderkit/ui/icons", + "--include-data-dir=$stylesheetsDir=renderkit/ui/stylesheets", + "--include-data-dir=$ocioDir=renderkit/data/ocio", "--output-dir=$outputRoot", - "--output-filename=RenderKit", + "--output-filename=$outputFilename", "--product-name=RenderKit", "--product-version=$appVersion", "--file-version=$appVersion", "--file-description=RenderKit desktop app", - "--copyright=Ahmed Hindy", - "--windows-console-mode=disable" + "--copyright=Ahmed Hindy" ) if ($IsWindows) { - $nuitkaArgs = @("--zig") + $nuitkaArgs + $nuitkaArgs = @("--zig", "--windows-console-mode=disable") + $nuitkaArgs } & uv --native-tls run --extra packaging python -m nuitka @nuitkaArgs $entryScript @@ -47,10 +98,11 @@ $platformDir = switch ($true) { default { $null } } +$vendorFfmpegDir = $null if ($null -eq $platformDir) { Write-Host "Skipping FFmpeg copy: unsupported platform." } else { - $vendorFfmpegDir = Join-Path $repoRoot "vendor\ffmpeg\$platformDir" + $vendorFfmpegDir = Join-NativePath $repoRoot "vendor" "ffmpeg" $platformDir if (-not (Test-Path $vendorFfmpegDir)) { Write-Host "Skipping FFmpeg copy: $vendorFfmpegDir does not exist." } @@ -60,12 +112,22 @@ $standaloneDir = Get-ChildItem -Path $outputRoot -Directory -Filter "*.dist" -Er Sort-Object LastWriteTime | Select-Object -Last 1 -if ($null -eq $standaloneDir) { - throw "Could not find Nuitka standalone output." +$appBundleDir = Get-ChildItem -Path $outputRoot -Directory -Filter "*.app" -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime | + Select-Object -Last 1 + +$packageRoot = if ($IsMacOS -and $null -ne $appBundleDir) { $appBundleDir } else { $standaloneDir } + +if ($null -eq $packageRoot) { + throw "Could not find Nuitka output." } if ($null -ne $platformDir -and (Test-Path $vendorFfmpegDir)) { - $ffmpegTarget = Join-Path $standaloneDir.FullName "ffmpeg" + $ffmpegTarget = if ($IsMacOS -and $packageRoot.Extension -eq ".app") { + Join-NativePath $packageRoot.FullName "Contents" "MacOS" "ffmpeg" + } else { + Join-Path $packageRoot.FullName "ffmpeg" + } New-Item -ItemType Directory -Path $ffmpegTarget -Force | Out-Null Copy-Item -Path (Join-Path $vendorFfmpegDir "*") -Destination $ffmpegTarget -Recurse -Force Write-Host "Copied bundled FFmpeg to $ffmpegTarget" @@ -77,5 +139,5 @@ if (Test-Path $zipPath) { Remove-Item $zipPath -Force } -Compress-Archive -Path (Join-Path $standaloneDir.FullName "*") -DestinationPath $zipPath -Force +Compress-NuitkaOutput -PackageRoot $packageRoot -ZipPath $zipPath Write-Host "Packaged Nuitka artifact: $zipPath" diff --git a/scripts/stage_portable_ffmpeg.py b/scripts/stage_portable_ffmpeg.py new file mode 100644 index 0000000..bb8a8e8 --- /dev/null +++ b/scripts/stage_portable_ffmpeg.py @@ -0,0 +1,85 @@ +"""Stage a portable FFmpeg binary for packaging workflows.""" + +from __future__ import annotations + +import os +import shutil +import stat +import subprocess +import sys +from pathlib import Path + +import imageio_ffmpeg + +PLATFORM_DIRS = { + "win32": "windows", + "linux": "linux", + "darwin": "macos", +} +REQUIRED_ENCODERS = ("libx264", "libx265", "libaom-av1") + + +def _target_name() -> str: + return "ffmpeg.exe" if sys.platform == "win32" else "ffmpeg" + + +def _make_executable(path: Path) -> None: + if sys.platform == "win32": + return + + current_mode = path.stat().st_mode + path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def _verify_ffmpeg(path: Path) -> None: + version_result = subprocess.run( + [str(path), "-version"], + check=True, + capture_output=True, + text=True, + ) + first_line = ( + version_result.stdout.splitlines()[0] if version_result.stdout else "ffmpeg version unknown" + ) + print(first_line) + + encoders_result = subprocess.run( + [str(path), "-hide_banner", "-encoders"], + check=True, + capture_output=True, + text=True, + ) + encoders = encoders_result.stdout + missing = [encoder for encoder in REQUIRED_ENCODERS if encoder not in encoders] + if missing: + raise RuntimeError(f"FFmpeg is missing required encoders: {', '.join(missing)}") + + +def main() -> int: + platform_dir = PLATFORM_DIRS.get(sys.platform) + if platform_dir is None: + raise RuntimeError(f"Unsupported platform for FFmpeg staging: {sys.platform}") + + repo_root = Path(__file__).resolve().parent.parent + source = Path(imageio_ffmpeg.get_ffmpeg_exe()) + if not source.is_file(): + raise RuntimeError(f"imageio-ffmpeg did not provide a usable binary: {source}") + + target_dir = repo_root / "vendor" / "ffmpeg" / platform_dir + target_dir.mkdir(parents=True, exist_ok=True) + target = target_dir / _target_name() + + if target.exists(): + target.unlink() + + shutil.copy2(source, target) + _make_executable(target) + + os.environ["IMAGEIO_FFMPEG_EXE"] = str(target) + _verify_ffmpeg(target) + print(f"Staged portable FFmpeg: {target}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/uv.lock b/uv.lock index 93c5ceb..c493ba9 100644 --- a/uv.lock +++ b/uv.lock @@ -192,6 +192,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "imageio-ffmpeg" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, + { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -829,6 +843,7 @@ docs = [ { name = "mkdocstrings", extra = ["python"] }, ] packaging = [ + { name = "imageio-ffmpeg" }, { name = "nuitka" }, { name = "ordered-set" }, { name = "pyinstaller" }, @@ -838,6 +853,7 @@ packaging = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.0" }, + { name = "imageio-ffmpeg", marker = "extra == 'packaging'", specifier = ">=0.6.0,<0.7" }, { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5.0" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.24.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" },