Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ff62b087c | ||
|
|
bbbcf321e5 | ||
|
|
3608de25ed | ||
|
|
75dfab1d35 | ||
|
|
138a1f1b2f | ||
|
|
39cfb7234c | ||
|
|
c3cdee9bf5 | ||
|
|
dee97e893c | ||
|
|
93b8528d09 | ||
|
|
b5205fef52 | ||
|
|
515a3c2912 | ||
|
|
8c994965d4 | ||
|
|
b27135308f | ||
|
|
64c92da356 | ||
|
|
81d257c7fa | ||
|
|
071358cbf7 | ||
|
|
5a1ec6198e | ||
|
|
0430d907bb | ||
|
|
5c05246098 | ||
|
|
01294e3a53 | ||
|
|
38554c1c6e | ||
|
|
d5e23146c0 | ||
|
|
a20cd44ed4 | ||
|
|
ef40890c80 | ||
|
|
185332a866 | ||
|
|
cdd1f3efce | ||
|
|
1062aa780f | ||
|
|
50b3fd1012 | ||
|
|
2366c4fe31 | ||
|
|
f34f311c69 | ||
|
|
3b283dc26e | ||
|
|
e6bcac8770 | ||
|
|
9c7409ea40 | ||
|
|
b0ec759fad | ||
|
|
96fc3eca10 | ||
|
|
185a69c806 | ||
|
|
9a1d8be4cc | ||
|
|
a404830456 | ||
|
|
88f29a9411 | ||
|
|
50fa228bab | ||
|
|
70b81ac3d7 | ||
|
|
f33e442b43 | ||
|
|
cab3aa1726 | ||
|
|
4b57d11dd8 | ||
|
|
eecd04247f | ||
|
|
b7cd023643 | ||
|
|
5d1556132e | ||
|
|
83cfabda89 | ||
|
|
7f83ec6c2f | ||
|
|
a6e51215fd | ||
|
|
61b96c3837 | ||
|
|
6c379f8fef | ||
|
|
7a31f1089e | ||
|
|
509843f0d4 | ||
|
|
c91a482b4e | ||
|
|
d3e000768d | ||
|
|
1573a9876b | ||
|
|
3041d9bb43 | ||
|
|
5d0b269e58 | ||
|
|
db15c2615d | ||
|
|
5c6b09a5c5 | ||
|
|
9c10b97e31 | ||
|
|
31087697a9 | ||
|
|
2a57ee36f8 | ||
|
|
e4d8573475 | ||
|
|
6d8f3b45b5 | ||
|
|
c985438db1 | ||
|
|
55bc664eca | ||
|
|
a34f4051fc | ||
|
|
370b0ab494 | ||
|
|
cf5db2d485 | ||
|
|
dc0766b2f3 | ||
|
|
e6d6498a49 | ||
|
|
ca92dab6fd | ||
|
|
6331ae7fd9 | ||
|
|
d0659d2b61 | ||
|
|
540c0d3027 | ||
|
|
d24f7fc6ac | ||
|
|
0a359525a7 | ||
|
|
896bb47235 | ||
|
|
b5e54aea6e | ||
|
|
119ec0dd83 |
@@ -252,6 +252,11 @@ jobs:
|
|||||||
run: bun run build
|
run: bun run build
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: bun run lint
|
run: bun run lint
|
||||||
|
# Scoped to server/: the console's browser code has no test runner, but the gate that keeps a
|
||||||
|
# plugin's origin apart from the console's does — and its failure mode is a well-formed header
|
||||||
|
# that only a browser rejects, which nothing else here would catch.
|
||||||
|
- name: Test
|
||||||
|
run: bun run test
|
||||||
|
|
||||||
docs-site:
|
docs-site:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
|||||||
@@ -119,10 +119,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
# python3 is used by scripts/ci/gitea-release.sh for the stable-tag release attach.
|
# python3 is used by scripts/ci/gitea-release.sh for the stable-tag release attach.
|
||||||
# libvulkan-dev: /usr/include/vulkan/vulkan.h for the client's pf-ffvk bindgen
|
# No libvulkan-dev: nothing here compiles or links against Vulkan (ash dlopens
|
||||||
# (FFmpeg's hwcontext_vulkan.h includes it).
|
# libvulkan and pf-vkdecode binds nothing at build time), so neither the compile nor
|
||||||
|
# dpkg-shlibdeps — which resolves DT_NEEDED sonames only — ever asks for it. The
|
||||||
|
# client's `Depends: libvulkan1` is added by hand in packaging/debian/build-client-deb.sh
|
||||||
|
# precisely because a dlopen is invisible to shlibdeps.
|
||||||
|
# No libav*-dev: the client links no FFmpeg since M10 (§6 of
|
||||||
|
# design/client-native-decode.md).
|
||||||
apt-get install -y --no-install-recommends dpkg-dev python3 \
|
apt-get install -y --no-install-recommends dpkg-dev python3 \
|
||||||
libgtk-4-dev libadwaita-1-dev libsdl3-dev libvulkan-dev
|
libgtk-4-dev libadwaita-1-dev libsdl3-dev
|
||||||
|
|
||||||
# Share ci.yml's cache keys so the release build reuses its registry + target artifacts.
|
# Share ci.yml's cache keys so the release build reuses its registry + target artifacts.
|
||||||
- name: Cache keys
|
- name: Cache keys
|
||||||
|
|||||||
@@ -148,7 +148,12 @@ jobs:
|
|||||||
-t "$CI_REGISTRY_PUSH/${{ matrix.image }}:latest" \
|
-t "$CI_REGISTRY_PUSH/${{ matrix.image }}:latest" \
|
||||||
ci
|
ci
|
||||||
|
|
||||||
|
# Gated like Build/Push: only the docker CLI needs this login (Reconcile and Tag-for-release
|
||||||
|
# authenticate via curl -u), so a cache-hit job with nothing to push must not be able to fail
|
||||||
|
# on a login it never uses — proven on run 16013, where a host with a misconfigured daemon
|
||||||
|
# failed exactly here on a hit=true leg.
|
||||||
- name: Log in to the LAN registry
|
- name: Log in to the LAN registry
|
||||||
|
if: steps.exists.outputs.hit == 'false'
|
||||||
run: |
|
run: |
|
||||||
echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY_PUSH" -u ci --password-stdin
|
echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY_PUSH" -u ci --password-stdin
|
||||||
env:
|
env:
|
||||||
@@ -236,7 +241,9 @@ jobs:
|
|||||||
-t "$CI_REGISTRY_PUSH/$IMAGE:latest" \
|
-t "$CI_REGISTRY_PUSH/$IMAGE:latest" \
|
||||||
.
|
.
|
||||||
|
|
||||||
|
# Same gate as the builders job above: the login only serves Push.
|
||||||
- name: Log in to the LAN registry
|
- name: Log in to the LAN registry
|
||||||
|
if: steps.exists.outputs.hit == 'false'
|
||||||
run: |
|
run: |
|
||||||
echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY_PUSH" -u ci --password-stdin
|
echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY_PUSH" -u ci --password-stdin
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ on:
|
|||||||
# The flatpak is the CLIENT — only rebuild when the client/core/manifest change, not on every
|
# The flatpak is the CLIENT — only rebuild when the client/core/manifest change, not on every
|
||||||
# design/host push (this is a heavy flatpak-builder run). Tags (v*, the client release) build too.
|
# design/host push (this is a heavy flatpak-builder run). Tags (v*, the client release) build too.
|
||||||
# The bundle ships BOTH client binaries (shell + Vulkan session), so every crate in either
|
# The bundle ships BOTH client binaries (shell + Vulkan session), so every crate in either
|
||||||
# binary's dependency closure must be listed here.
|
# binary's dependency closure must be listed here — including the native decode rungs, or a
|
||||||
|
# commit that only touches the decoder never rebuilds the bundle and the Deck canary quietly
|
||||||
|
# stops tracking it. pf-dxvadec is absent on purpose: it is `cfg(windows)` in pf-client-core
|
||||||
|
# and never enters the Linux closure (windows.yml / windows-msix.yml carry it instead).
|
||||||
paths:
|
paths:
|
||||||
- 'clients/linux/**'
|
- 'clients/linux/**'
|
||||||
- 'clients/session/**'
|
- 'clients/session/**'
|
||||||
@@ -42,6 +45,9 @@ on:
|
|||||||
- 'crates/pf-client-core/**'
|
- 'crates/pf-client-core/**'
|
||||||
- 'crates/pf-presenter/**'
|
- 'crates/pf-presenter/**'
|
||||||
- 'crates/pf-console-ui/**'
|
- 'crates/pf-console-ui/**'
|
||||||
|
- 'crates/pf-bitstream/**'
|
||||||
|
- 'crates/pf-vkdecode/**'
|
||||||
|
- 'crates/pf-vaadec/**'
|
||||||
- 'packaging/flatpak/**'
|
- 'packaging/flatpak/**'
|
||||||
- 'Cargo.lock'
|
- 'Cargo.lock'
|
||||||
- '.gitea/workflows/flatpak.yml'
|
- '.gitea/workflows/flatpak.yml'
|
||||||
@@ -128,7 +134,7 @@ jobs:
|
|||||||
# authselect trigger fires — so this line alone was never the fix for the failures
|
# authselect trigger fires — so this line alone was never the fix for the failures
|
||||||
# below. See the retry.sh bump for the real cause.
|
# below. See the retry.sh bump for the real cause.
|
||||||
sed -i 's/resolve \[!UNAVAIL=return\] //' /etc/nsswitch.conf
|
sed -i 's/resolve \[!UNAVAIL=return\] //' /etc/nsswitch.conf
|
||||||
# Flathub provides the GNOME runtime/SDK + the rust-stable + ffmpeg-full extensions.
|
# Flathub provides the GNOME runtime/SDK + the rust-stable and llvm20 extensions.
|
||||||
#
|
#
|
||||||
# ROOT CAUSE (confirmed 2026-07-11 by watching a live run on home-runner-1): this is
|
# ROOT CAUSE (confirmed 2026-07-11 by watching a live run on home-runner-1): this is
|
||||||
# NOT a deterministic nsswitch/DNS-config bug. gitea-runner-fleet on home-runner-1 is
|
# NOT a deterministic nsswitch/DNS-config bug. gitea-runner-fleet on home-runner-1 is
|
||||||
@@ -147,7 +153,7 @@ jobs:
|
|||||||
git config --global --add safe.directory "$PWD"
|
git config --global --add safe.directory "$PWD"
|
||||||
|
|
||||||
# This job was the fleet's single heaviest network consumer: every run re-downloaded
|
# This job was the fleet's single heaviest network consumer: every run re-downloaded
|
||||||
# the GNOME runtime + SDK + llvm/rust/ffmpeg extensions (multi-GB from Flathub) and
|
# the GNOME runtime + SDK + llvm/rust extensions (multi-GB from Flathub) and
|
||||||
# every crate source. Both live in well-defined directories, both are idempotently
|
# every crate source. Both live in well-defined directories, both are idempotently
|
||||||
# verified/extended by the steps below, and the central cache server restores them
|
# verified/extended by the steps below, and the central cache server restores them
|
||||||
# at LAN speed — so cache them. Keyed on what actually pins them: the manifest tree
|
# at LAN speed — so cache them. Keyed on what actually pins them: the manifest tree
|
||||||
@@ -251,7 +257,8 @@ jobs:
|
|||||||
# or TCP dial costs a backoff-retry instead of the whole (long) compile:
|
# or TCP dial costs a backoff-retry instead of the whole (long) compile:
|
||||||
# 1) --install-deps-only pulls everything the manifest declares from Flathub: the
|
# 1) --install-deps-only pulls everything the manifest declares from Flathub: the
|
||||||
# GNOME 50 runtime/SDK + the rust-stable (//25.08, rustc 1.96) and llvm20 SDK
|
# GNOME 50 runtime/SDK + the rust-stable (//25.08, rustc 1.96) and llvm20 SDK
|
||||||
# extensions, plus the runtime's auto codecs-extra (HEVC libavcodec).
|
# extensions. (No codec extension: the client links no FFmpeg — see the
|
||||||
|
# manifest header.)
|
||||||
# 2) --download-only fetches every source (all crates in cargo-sources.json) into
|
# 2) --download-only fetches every source (all crates in cargo-sources.json) into
|
||||||
# the .flatpak-builder state dir. Both are resumable/idempotent, so re-running
|
# the .flatpak-builder state dir. Both are resumable/idempotent, so re-running
|
||||||
# after a partial failure is safe and cheap.
|
# after a partial failure is safe and cheap.
|
||||||
|
|||||||
@@ -96,9 +96,12 @@ jobs:
|
|||||||
- name: Prep
|
- name: Prep
|
||||||
run: |
|
run: |
|
||||||
git config --global --add safe.directory "$PWD"
|
git config --global --add safe.directory "$PWD"
|
||||||
# vulkan-headers: the client's pf-ffvk crate runs bindgen over FFmpeg's
|
# No vulkan-headers: nothing in the workspace compiles against the system Vulkan headers.
|
||||||
# libavutil/hwcontext_vulkan.h (#include <vulkan/vulkan.h>).
|
# The host's Vulkan encode hand-rolls its structs, pyrowave-sys bindgens its own vendored
|
||||||
dnf -y install gtk4-devel libadwaita-devel SDL3-devel vulkan-headers
|
# copy, and both host and client reach Vulkan through ash, which dlopens the loader. (The
|
||||||
|
# HDR gamescope leg further down does need them, and pulls them itself via `dnf builddep
|
||||||
|
# gamescope`.) Matches packaging/rpm/punktfunk.spec, which dropped its BuildRequires too.
|
||||||
|
dnf -y install gtk4-devel libadwaita-devel SDL3-devel
|
||||||
# sysext build (packaging/bazzite/build-sysext.sh): squashfs + SELinux labeling.
|
# sysext build (packaging/bazzite/build-sysext.sh): squashfs + SELinux labeling.
|
||||||
dnf -y install squashfs-tools cpio libselinux-utils selinux-policy-targeted
|
dnf -y install squashfs-tools cpio libselinux-utils selinux-policy-targeted
|
||||||
# Fedora's own gamescope, for its RUNTIME libraries only — never shipped, never run. The
|
# Fedora's own gamescope, for its RUNTIME libraries only — never shipped, never run. The
|
||||||
|
|||||||
@@ -141,8 +141,10 @@ jobs:
|
|||||||
# observed on a clean build on this very runner (2026-07-17). No-op for compliant
|
# observed on a clean build on this very runner (2026-07-17). No-op for compliant
|
||||||
# projects (libvpl-sys pins 3.13+).
|
# projects (libvpl-sys pins 3.13+).
|
||||||
"CMAKE_POLICY_VERSION_MINIMUM=3.5" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
"CMAKE_POLICY_VERSION_MINIMUM=3.5" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
# FFMPEG_DIR: the same BtbN lgpl-shared x64 tree the Windows CLIENT links against (provisioned
|
# FFMPEG_DIR: the BtbN lgpl-shared x64 tree, provisioned by
|
||||||
# by scripts/ci/provision-windows-punktfunk-extras.ps1). The host's AMD/Intel AMF/QSV encode backend
|
# scripts/ci/provision-windows-punktfunk-extras.ps1. The CLIENT used to link it too; since M10
|
||||||
|
# it links no libav* at all (windows.yml sets no FFMPEG_DIR), so this tree is the HOST's alone
|
||||||
|
# and the provisioning step keeps fetching it for that reason. The host's AMD/Intel AMF/QSV encode backend
|
||||||
# (--features amf-qsv) link-imports avcodec/avutil/swscale from it; pack-host-installer.ps1
|
# (--features amf-qsv) link-imports avcodec/avutil/swscale from it; pack-host-installer.ps1
|
||||||
# then bundles its bin\*.dll into the installer. LIBCLANG_PATH is in the runner daemon env.
|
# then bundles its bin\*.dll into the installer. LIBCLANG_PATH is in the runner daemon env.
|
||||||
if (-not $env:FFMPEG_DIR) {
|
if (-not $env:FFMPEG_DIR) {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
# Build the punktfunk Windows client as signed MSIX packages (x64 + ARM64) and publish them to
|
# Build the punktfunk Windows client as signed MSIX packages (x64 + ARM64) and publish them to
|
||||||
# Gitea's generic package registry, so Windows boxes can download + install a real package (Start
|
# Gitea's generic package registry, so Windows boxes can download + install a real package (Start
|
||||||
# tile, clean install/uninstall) instead of a loose exe. Runs on a self-hosted windows-amd64
|
# tile, clean install/uninstall) instead of a loose exe. Runs on a self-hosted windows-amd64
|
||||||
# runner (host mode; the MSVC/WinUI toolchain comes from unom/infra's windows-runner/, FFmpeg
|
# runner (host mode; the MSVC/WinUI toolchain comes from unom/infra's windows-runner/, the rest
|
||||||
# self-provisions via the "Ensure Windows toolchain" step below, same as windows.yml) — the
|
# self-provisions via the "Ensure Windows toolchain" step below, same as windows.yml) — the
|
||||||
# Windows SDK's makeappx/signtool are baked into the runner's daemon env.
|
# Windows SDK's makeappx/signtool are baked into the runner's daemon env.
|
||||||
#
|
#
|
||||||
# Both arches come off the ONE x64 runner: x86_64 natively, aarch64 cross-compiled (the x64 MSVC
|
# Both arches come off the ONE x64 runner: x86_64 natively, aarch64 cross-compiled (the x64 MSVC
|
||||||
# toolset has the ARM64 cross compiler; the matrix points FFMPEG_DIR at the ARM64 FFmpeg tree). See
|
# toolset has the ARM64 cross compiler). See windows.yml for the cross-build rationale + the
|
||||||
# windows.yml for the cross-build rationale + the BOM/MAX_PATH runner gotchas.
|
# BOM/MAX_PATH runner gotchas.
|
||||||
|
#
|
||||||
|
# NO FFmpeg since M10 (design/client-native-decode.md §6): the client decodes natively, so the
|
||||||
|
# package carries no libav* DLLs and this workflow sets no FFMPEG_DIR. The host installer
|
||||||
|
# (windows-host.yml) is unchanged.
|
||||||
#
|
#
|
||||||
# Registry (public, unom org): https://git.unom.io/unom/-/packages (generic group)
|
# Registry (public, unom org): https://git.unom.io/unom/-/packages (generic group)
|
||||||
# Packaging internals: clients/windows/packaging/README.md.
|
# Packaging internals: clients/windows/packaging/README.md.
|
||||||
@@ -49,7 +53,9 @@ on:
|
|||||||
- 'crates/pf-client-core/**'
|
- 'crates/pf-client-core/**'
|
||||||
- 'crates/pf-presenter/**'
|
- 'crates/pf-presenter/**'
|
||||||
- 'crates/pf-console-ui/**'
|
- 'crates/pf-console-ui/**'
|
||||||
- 'crates/pf-ffvk/**'
|
- 'crates/pf-bitstream/**'
|
||||||
|
- 'crates/pf-vkdecode/**'
|
||||||
|
- 'crates/pf-dxvadec/**'
|
||||||
- 'Cargo.lock'
|
- 'Cargo.lock'
|
||||||
- 'Cargo.toml'
|
- 'Cargo.toml'
|
||||||
- '.gitea/workflows/windows-msix.yml'
|
- '.gitea/workflows/windows-msix.yml'
|
||||||
@@ -80,12 +86,10 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- arch: x64
|
- arch: x64
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
ffmpeg: C:\Users\Public\ffmpeg
|
|
||||||
td: C:\t
|
td: C:\t
|
||||||
session_flags: ''
|
session_flags: ''
|
||||||
- arch: arm64
|
- arch: arm64
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
ffmpeg: C:\Users\Public\ffmpeg-arm64
|
|
||||||
td: C:\t-a64
|
td: C:\t-a64
|
||||||
# No skia-binaries prebuilt for aarch64-pc-windows-msvc: the session ships
|
# No skia-binaries prebuilt for aarch64-pc-windows-msvc: the session ships
|
||||||
# without the Skia console UI on ARM64 (streaming unaffected) — flip when
|
# without the Skia console UI on ARM64 (streaming unaffected) — flip when
|
||||||
@@ -94,7 +98,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Ensure Windows toolchain (WDK, FFmpeg, Inno Setup, ARM64 target)
|
- name: Ensure Windows toolchain (WDK, Inno Setup, ARM64 target)
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: ./scripts/ci/ensure-windows-toolchain.ps1
|
run: ./scripts/ci/ensure-windows-toolchain.ps1
|
||||||
|
|
||||||
@@ -102,12 +106,9 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
# CARGO_TARGET_DIR (per-arch, short) dodges the MAX_PATH wall in the CMake-from-source
|
# CARGO_TARGET_DIR (per-arch, short) dodges the MAX_PATH wall in the CMake-from-source
|
||||||
# crates (see windows.yml). FFMPEG_DIR selects the arch's import libs + is read by
|
# crates (see windows.yml). No FFMPEG_DIR: nothing in this package links libav* (M10),
|
||||||
# pack-msix.ps1 for the runtime DLLs. All via GITHUB_ENV.
|
# and pack-msix.ps1 no longer copies runtime DLLs from one.
|
||||||
"CARGO_TARGET_DIR=${{ matrix.td }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
"CARGO_TARGET_DIR=${{ matrix.td }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
"FFMPEG_DIR=${{ matrix.ffmpeg }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
|
||||||
# pf-ffvk's bindgen needs Vulkan headers (arch-independent; provisioned alongside FFmpeg).
|
|
||||||
"PF_FFVK_VULKAN_INCLUDE=C:\Users\Public\vulkan-headers\include" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
|
||||||
rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }}
|
||||||
$pf = & "$env:GITHUB_WORKSPACE/scripts/ci/pf-version.ps1" # single source of truth: base is one minor ahead of the latest stable tag
|
$pf = & "$env:GITHUB_WORKSPACE/scripts/ci/pf-version.ps1" # single source of truth: base is one minor ahead of the latest stable tag
|
||||||
$parts = if ($env:GITHUB_REF -like 'refs/tags/v*') {
|
$parts = if ($env:GITHUB_REF -like 'refs/tags/v*') {
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
# Windows client CI — runs on a self-hosted windows-amd64 runner (host mode; the generic runner +
|
# Windows client CI — runs on a self-hosted windows-amd64 runner (host mode; the generic runner +
|
||||||
# toolchain come from unom/infra's windows-runner/; punktfunk's own extras - FFmpeg,
|
# toolchain come from unom/infra's windows-runner/; punktfunk's own extras - WDK, Inno Setup,
|
||||||
# Vulkan-Headers, WDK, Inno Setup, the ARM64 rustup target - self-provision via the "Ensure
|
# the ARM64 rustup target - self-provision via the "Ensure Windows toolchain" step below, a fast
|
||||||
# Windows toolchain" step below, a fast no-op once already present, so any runner with that label
|
# no-op once already present, so any runner with that label works with no manual dispatch step
|
||||||
# works with no manual dispatch step first). Build + clippy + fmt + test BOTH client binaries:
|
# first). Build + clippy + fmt + test BOTH client binaries: the WinUI 3 shell
|
||||||
# the WinUI 3 shell (windows-reactor + WASAPI + SDL3) and the punktfunk-session Vulkan client
|
# (windows-reactor + WASAPI + SDL3) and the punktfunk-session Vulkan client
|
||||||
# (pf-presenter/pf-client-core/pf-console-ui/pf-ffvk — every stream runs in it, spawned by the
|
# (pf-presenter/pf-client-core/pf-console-ui — every stream runs in it, spawned by the
|
||||||
# shell). ARM64 note: rust-skia publishes no aarch64-pc-windows-msvc prebuilt binaries, so the
|
# shell). ARM64 note: rust-skia publishes no aarch64-pc-windows-msvc prebuilt binaries, so the
|
||||||
# session builds --no-default-features there (no Skia console UI; streaming is unaffected) —
|
# session builds --no-default-features there (no Skia console UI; streaming is unaffected) —
|
||||||
# flip when skia-binaries adds the target.
|
# flip when skia-binaries adds the target.
|
||||||
#
|
#
|
||||||
|
# NO FFmpeg here since M10 (design/client-native-decode.md §6): the client decodes with
|
||||||
|
# pf-vkdecode / pf-dxvadec / openh264+rav1d and links no libav* at all, so this workflow sets
|
||||||
|
# no FFMPEG_DIR, no PF_FFVK_VULKAN_INCLUDE and prepends nothing to PATH. The provisioning
|
||||||
|
# script still fetches the FFmpeg trees because the HOST keeps FFmpeg — windows-host.yml's
|
||||||
|
# `amf-qsv` leg link-imports them.
|
||||||
|
#
|
||||||
# Two architectures from ONE x64 runner: x86_64-pc-windows-msvc natively and
|
# Two architectures from ONE x64 runner: x86_64-pc-windows-msvc natively and
|
||||||
# aarch64-pc-windows-msvc by cross-compiling. The x64 MSVC toolset ships an ARM64 cross compiler
|
# aarch64-pc-windows-msvc by cross-compiling. The x64 MSVC toolset ships an ARM64 cross compiler
|
||||||
# (VC\Tools\MSVC\<ver>\bin\Hostx64\arm64\cl.exe) and aarch64-pc-windows-msvc is a tier-2 Rust
|
# (VC\Tools\MSVC\<ver>\bin\Hostx64\arm64\cl.exe) and aarch64-pc-windows-msvc is a tier-2 Rust
|
||||||
# target with host tools, so no ARM64 runner is needed — the cc/cmake crates pick the ARM64
|
# target with host tools, so no ARM64 runner is needed — the cc/cmake crates pick the ARM64
|
||||||
# compiler from the target triple (SDL3 + libopus build-from-source cross-compile fine). The one
|
# compiler from the target triple (SDL3 + libopus build-from-source cross-compile fine). The one
|
||||||
# arch-specific external dep is FFmpeg's import libs: the runner keeps an x64 tree at
|
# thing the aarch64 build can't do is *run* on the x64 host, so fmt + test run only for x64.
|
||||||
# C:\Users\Public\ffmpeg and an ARM64 tree at C:\Users\Public\ffmpeg-arm64 (both FFmpeg 7.x /
|
|
||||||
# avcodec-61); the matrix points FFMPEG_DIR at the right one. aarch64 can't *run* on the x64 host,
|
|
||||||
# so fmt + test run only for x64.
|
|
||||||
#
|
#
|
||||||
# The MSVC/WinUI/FFmpeg toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, the x64 FFmpeg,
|
# The MSVC/WinUI toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, CARGO_HOME,
|
||||||
# CARGO_HOME, CMAKE_POLICY_VERSION_MINIMUM, …) is baked into the runner's daemon env. Per-checkout
|
# CMAKE_POLICY_VERSION_MINIMUM, …) is baked into the runner's daemon env. Per-checkout
|
||||||
# / per-arch vars are set in a step:
|
# / per-arch vars are set in a step:
|
||||||
# - CARGO_TARGET_DIR=C:\t… the runner's host workdir is buried deep under
|
# - CARGO_TARGET_DIR=C:\t… the runner's host workdir is buried deep under
|
||||||
# C:\Windows\System32\config\systemprofile\.cache\act\<hash>\hostexecutor\,
|
# C:\Windows\System32\config\systemprofile\.cache\act\<hash>\hostexecutor\,
|
||||||
@@ -29,7 +32,6 @@
|
|||||||
# can't create its .tlog (DirectoryNotFoundException -> MSB6003). A short
|
# can't create its .tlog (DirectoryNotFoundException -> MSB6003). A short
|
||||||
# root keeps every nested path well under the limit (per-arch so the two
|
# root keeps every nested path well under the limit (per-arch so the two
|
||||||
# matrix legs don't share a target dir).
|
# matrix legs don't share a target dir).
|
||||||
# - FFMPEG_DIR per-arch FFmpeg import libs (x64 vs arm64 tree).
|
|
||||||
#
|
#
|
||||||
# Steps use `shell: pwsh` (PowerShell 7) deliberately: Windows PowerShell 5.1's
|
# Steps use `shell: pwsh` (PowerShell 7) deliberately: Windows PowerShell 5.1's
|
||||||
# `Out-File -Encoding utf8` prepends a UTF-8 BOM that corrupts the first GITHUB_ENV line (that
|
# `Out-File -Encoding utf8` prepends a UTF-8 BOM that corrupts the first GITHUB_ENV line (that
|
||||||
@@ -55,7 +57,9 @@ on:
|
|||||||
- 'crates/pf-client-core/**'
|
- 'crates/pf-client-core/**'
|
||||||
- 'crates/pf-presenter/**'
|
- 'crates/pf-presenter/**'
|
||||||
- 'crates/pf-console-ui/**'
|
- 'crates/pf-console-ui/**'
|
||||||
- 'crates/pf-ffvk/**'
|
- 'crates/pf-bitstream/**'
|
||||||
|
- 'crates/pf-vkdecode/**'
|
||||||
|
- 'crates/pf-dxvadec/**'
|
||||||
- 'Cargo.lock'
|
- 'Cargo.lock'
|
||||||
- 'Cargo.toml'
|
- 'Cargo.toml'
|
||||||
- '.gitea/workflows/windows.yml'
|
- '.gitea/workflows/windows.yml'
|
||||||
@@ -67,7 +71,9 @@ on:
|
|||||||
- 'crates/pf-client-core/**'
|
- 'crates/pf-client-core/**'
|
||||||
- 'crates/pf-presenter/**'
|
- 'crates/pf-presenter/**'
|
||||||
- 'crates/pf-console-ui/**'
|
- 'crates/pf-console-ui/**'
|
||||||
- 'crates/pf-ffvk/**'
|
- 'crates/pf-bitstream/**'
|
||||||
|
- 'crates/pf-vkdecode/**'
|
||||||
|
- 'crates/pf-dxvadec/**'
|
||||||
- 'Cargo.lock'
|
- 'Cargo.lock'
|
||||||
- 'Cargo.toml'
|
- 'Cargo.toml'
|
||||||
- '.gitea/workflows/windows.yml'
|
- '.gitea/workflows/windows.yml'
|
||||||
@@ -110,7 +116,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Ensure Windows toolchain (WDK, FFmpeg, Inno Setup, ARM64 target)
|
- name: Ensure Windows toolchain (WDK, Inno Setup, ARM64 target)
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: ./scripts/ci/ensure-windows-toolchain.ps1
|
run: ./scripts/ci/ensure-windows-toolchain.ps1
|
||||||
|
|
||||||
@@ -120,21 +126,13 @@ jobs:
|
|||||||
# Per-arch short target root (dodges MAX_PATH; keeps the two legs from sharing target\).
|
# Per-arch short target root (dodges MAX_PATH; keeps the two legs from sharing target\).
|
||||||
$td = if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { 'C:\t-a64' } else { 'C:\t' }
|
$td = if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { 'C:\t-a64' } else { 'C:\t' }
|
||||||
"CARGO_TARGET_DIR=$td" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
"CARGO_TARGET_DIR=$td" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
# Per-arch FFmpeg import libs (provision-windows-punktfunk-extras.ps1 fetches both).
|
# No FFMPEG_DIR / PF_FFVK_VULKAN_INCLUDE / PATH prepend: the client links no libav*
|
||||||
$ff = if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { 'C:\Users\Public\ffmpeg-arm64' } else { 'C:\Users\Public\ffmpeg' }
|
# since M10 (see this file's header), so nothing here needs import libs or runtime DLLs.
|
||||||
"FFMPEG_DIR=$ff" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
# The HOST still does — windows-host.yml sets them for its amf-qsv leg.
|
||||||
# pf-ffvk's bindgen needs Vulkan headers (arch-independent; provisioned alongside FFmpeg).
|
|
||||||
"PF_FFVK_VULKAN_INCLUDE=C:\Users\Public\vulkan-headers\include" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
|
||||||
# $ff\bin on PATH too (not just FFMPEG_DIR, which only satisfies the linker): the test
|
|
||||||
# binary needs the actual DLLs to load at runtime. Set here rather than relying on the
|
|
||||||
# daemon's own env (project-env.ps1) - on a freshly cloned/registered runner the daemon
|
|
||||||
# starts before this job's "Ensure Windows toolchain" step ever writes that file, so its
|
|
||||||
# PATH doesn't include this yet on a first run (confirmed live: STATUS_DLL_NOT_FOUND).
|
|
||||||
"$ff\bin" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
|
|
||||||
rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }}
|
||||||
rustc --version
|
rustc --version
|
||||||
cargo --version
|
cargo --version
|
||||||
Write-Output "target ${{ matrix.target }} target-dir $td ffmpeg $ff"
|
Write-Output "target ${{ matrix.target }} target-dir $td"
|
||||||
|
|
||||||
# Both client binaries. ARM64: no skia-binaries prebuilt for the target, so the session
|
# Both client binaries. ARM64: no skia-binaries prebuilt for the target, so the session
|
||||||
# drops its `ui` feature there (pf-console-ui excluded; --no-default-features is a no-op
|
# drops its `ui` feature there (pf-console-ui excluded; --no-default-features is a no-op
|
||||||
@@ -152,7 +150,10 @@ jobs:
|
|||||||
- name: Clippy (-D warnings)
|
- name: Clippy (-D warnings)
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$pkgs = @('-p','punktfunk-client-windows','-p','punktfunk-client-session','-p','punktfunk-cli','-p','pf-client-core','-p','pf-presenter','-p','pf-ffvk')
|
# Every crate in the `paths:` trigger above is named here: `cargo clippy -p X` BUILDS a
|
||||||
|
# dependency but only LINTS the packages it is given, so a decode crate that starts the
|
||||||
|
# run but is missing from this list would be gated by nothing.
|
||||||
|
$pkgs = @('-p','punktfunk-client-windows','-p','punktfunk-client-session','-p','punktfunk-cli','-p','pf-client-core','-p','pf-presenter','-p','pf-bitstream','-p','pf-vkdecode','-p','pf-dxvadec')
|
||||||
$sf = @()
|
$sf = @()
|
||||||
if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { $sf = @('--no-default-features') } else { $pkgs += @('-p','pf-console-ui') }
|
if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { $sf = @('--no-default-features') } else { $pkgs += @('-p','pf-console-ui') }
|
||||||
cargo clippy @pkgs --all-targets @sf --target ${{ matrix.target }} -- -D warnings
|
cargo clippy @pkgs --all-targets @sf --target ${{ matrix.target }} -- -D warnings
|
||||||
@@ -160,9 +161,9 @@ jobs:
|
|||||||
- name: Rustfmt check
|
- name: Rustfmt check
|
||||||
if: matrix.target == 'x86_64-pc-windows-msvc'
|
if: matrix.target == 'x86_64-pc-windows-msvc'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: cargo fmt -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-ffvk -- --check
|
run: cargo fmt -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-dxvadec -- --check
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
if: matrix.target == 'x86_64-pc-windows-msvc'
|
if: matrix.target == 'x86_64-pc-windows-msvc'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: cargo test -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-ffvk --target ${{ matrix.target }}
|
run: cargo test -p punktfunk-client-windows -p punktfunk-client-session -p punktfunk-cli -p pf-client-core -p pf-presenter -p pf-console-ui -p pf-dxvadec --target ${{ matrix.target }}
|
||||||
|
|||||||
+5
-3
@@ -46,9 +46,11 @@ sudo apt install build-essential clang libclang-dev pkg-config cmake \
|
|||||||
libvulkan-dev
|
libvulkan-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
(The last two groups are the Linux client shell and `pf-ffvk`; skip them only if you never build
|
(The last two groups are the Linux client shell and the Vulkan session presenter; skip them only
|
||||||
those crates. `scripts/bootstrap-ubuntu.sh` sets up an Ubuntu **capture-test host** — NVIDIA, Sway,
|
if you never build those crates. `libvulkan-dev` is for the LOADER's pkg-config/soname — ash
|
||||||
PipeWire — and is not a substitute for the list above.)
|
dlopens it, and the client links no FFmpeg at all, so no libav*-dev appears here.
|
||||||
|
`scripts/bootstrap-ubuntu.sh` sets up an Ubuntu **capture-test host** — NVIDIA, Sway, PipeWire —
|
||||||
|
and is not a substitute for the list above.)
|
||||||
|
|
||||||
## Before you push
|
## Before you push
|
||||||
|
|
||||||
|
|||||||
Generated
+307
-41
@@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826"
|
checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_log-sys",
|
"android_log-sys",
|
||||||
"env_filter",
|
"env_filter 0.1.4",
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -204,6 +204,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert_matches"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -341,6 +347,26 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomig"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd0f41f4bb89f5c6450325e283fb78c4a3d042181b54f3855ee2f872919f9863"
|
||||||
|
dependencies = [
|
||||||
|
"atomig-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomig-macro"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49c98dba06b920588de7d63f6acc23f1e6a9fade5fd6198e564506334fb5a4f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "audiopus_sys"
|
name = "audiopus_sys"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -446,7 +472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"annotate-snippets",
|
"annotate-snippets",
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
@@ -475,6 +501,12 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -538,6 +570,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder-lite"
|
name = "byteorder-lite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -556,7 +594,7 @@ version = "0.22.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
|
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
"glib",
|
"glib",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -885,6 +923,15 @@ dependencies = [
|
|||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cros-codecs"
|
||||||
|
version = "0.0.5"
|
||||||
|
dependencies = [
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -991,6 +1038,37 @@ version = "2.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"defmt-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt-macros"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||||
|
dependencies = [
|
||||||
|
"defmt-parser",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt-parser"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -1101,6 +1179,29 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.11.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"env_filter 2.0.0",
|
||||||
|
"jiff",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -1193,7 +1294,7 @@ version = "8.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d"
|
checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"ffmpeg-sys-next",
|
"ffmpeg-sys-next",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -1584,7 +1685,7 @@ version = "0.22.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a"
|
checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
@@ -1777,7 +1878,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"crunchy",
|
"crunchy",
|
||||||
"zerocopy",
|
"zerocopy 0.8.52",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2127,6 +2228,42 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
|
||||||
|
dependencies = [
|
||||||
|
"defmt",
|
||||||
|
"jiff-core",
|
||||||
|
"jiff-static",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
|
||||||
|
dependencies = [
|
||||||
|
"defmt",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-static"
|
||||||
|
version = "0.2.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-core",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -2291,7 +2428,7 @@ version = "0.9.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4"
|
checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"cc",
|
"cc",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"cookie-factory",
|
"cookie-factory",
|
||||||
@@ -2486,6 +2623,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149"
|
checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2493,7 +2631,7 @@ dependencies = [
|
|||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"jni-sys 0.3.1",
|
"jni-sys 0.3.1",
|
||||||
"log",
|
"log",
|
||||||
"ndk-sys",
|
"ndk-sys",
|
||||||
@@ -2517,7 +2655,7 @@ version = "0.29.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2530,7 +2668,7 @@ version = "0.30.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2848,6 +2986,14 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-bitstream"
|
||||||
|
version = "0.24.0"
|
||||||
|
dependencies = [
|
||||||
|
"cros-codecs",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-capture"
|
name = "pf-capture"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -2876,19 +3022,26 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"ash",
|
"ash",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"ffmpeg-next",
|
"libc",
|
||||||
|
"libloading",
|
||||||
"mdns-sd",
|
"mdns-sd",
|
||||||
|
"openh264",
|
||||||
"opus",
|
"opus",
|
||||||
"pf-ffvk",
|
"pf-bitstream",
|
||||||
|
"pf-dxvadec",
|
||||||
"pf-update-check",
|
"pf-update-check",
|
||||||
|
"pf-vaadec",
|
||||||
|
"pf-vkdecode",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
"punktfunk-core",
|
"punktfunk-core",
|
||||||
"pyrowave-sys",
|
"pyrowave-sys",
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
|
"rav1d",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sdl3",
|
"sdl3",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"ureq",
|
"ureq",
|
||||||
"wasapi",
|
"wasapi",
|
||||||
@@ -2935,6 +3088,16 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-dxvadec"
|
||||||
|
version = "0.24.0"
|
||||||
|
dependencies = [
|
||||||
|
"cros-codecs",
|
||||||
|
"pf-bitstream",
|
||||||
|
"pf-vkdecode",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-encode"
|
name = "pf-encode"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -2959,15 +3122,6 @@ dependencies = [
|
|||||||
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pf-ffvk"
|
|
||||||
version = "0.24.0"
|
|
||||||
dependencies = [
|
|
||||||
"ash",
|
|
||||||
"bindgen",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-frame"
|
name = "pf-frame"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -3042,7 +3196,6 @@ dependencies = [
|
|||||||
"ash",
|
"ash",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"pf-client-core",
|
"pf-client-core",
|
||||||
"pf-ffvk",
|
|
||||||
"punktfunk-core",
|
"punktfunk-core",
|
||||||
"sdl3",
|
"sdl3",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -3069,13 +3222,22 @@ dependencies = [
|
|||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-vaadec"
|
||||||
|
version = "0.24.0"
|
||||||
|
dependencies = [
|
||||||
|
"cros-codecs",
|
||||||
|
"pf-bitstream",
|
||||||
|
"pf-vkdecode",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-vdisplay"
|
name = "pf-vdisplay"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -3102,6 +3264,17 @@ dependencies = [
|
|||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-vkdecode"
|
||||||
|
version = "0.24.0"
|
||||||
|
dependencies = [
|
||||||
|
"ash",
|
||||||
|
"cros-codecs",
|
||||||
|
"pf-bitstream",
|
||||||
|
"sha2",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-win-display"
|
name = "pf-win-display"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -3153,7 +3326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44"
|
checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"libc",
|
"libc",
|
||||||
"libspa",
|
"libspa",
|
||||||
"libspa-sys",
|
"libspa-sys",
|
||||||
@@ -3207,7 +3380,7 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"fdeflate",
|
"fdeflate",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -3251,6 +3424,21 @@ dependencies = [
|
|||||||
"universal-hash",
|
"universal-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -3272,7 +3460,7 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy",
|
"zerocopy 0.8.52",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3311,7 +3499,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
@@ -3388,7 +3576,6 @@ name = "punktfunk-client-windows"
|
|||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"ffmpeg-next",
|
|
||||||
"mdns-sd",
|
"mdns-sd",
|
||||||
"pf-client-core",
|
"pf-client-core",
|
||||||
"punktfunk-core",
|
"punktfunk-core",
|
||||||
@@ -3431,7 +3618,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"zerocopy",
|
"zerocopy 0.8.52",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3726,6 +3913,36 @@ dependencies = [
|
|||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rav1d"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1932f060d5e7bd49dc9f8b272c1dc5e9ce0ffe141c28be900265d3989b36c9ed"
|
||||||
|
dependencies = [
|
||||||
|
"assert_matches",
|
||||||
|
"atomig",
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"nasm-rs",
|
||||||
|
"parking_lot",
|
||||||
|
"paste",
|
||||||
|
"raw-cpuid",
|
||||||
|
"strum",
|
||||||
|
"to_method",
|
||||||
|
"zerocopy 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "raw-cpuid"
|
||||||
|
version = "11.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -3777,7 +3994,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3934,7 +4151,7 @@ version = "0.40.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323"
|
checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
@@ -3973,7 +4190,7 @@ version = "1.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -4127,7 +4344,7 @@ version = "0.18.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25bd22eb1bbc9137e914022b4994ed35591eea0884e9e3e98e6d9895cad6e1d2"
|
checksum = "25bd22eb1bbc9137e914022b4994ed35591eea0884e9e3e98e6d9895cad6e1d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"libc",
|
"libc",
|
||||||
"sdl3-image-sys",
|
"sdl3-image-sys",
|
||||||
"sdl3-mixer-sys",
|
"sdl3-mixer-sys",
|
||||||
@@ -4222,7 +4439,7 @@ version = "3.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -4428,7 +4645,7 @@ version = "0.87.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0f7d94f3e7537c71ad4cf132eb26e3be8c8a886ed3649c4525c089041fc312b2"
|
checksum = "0f7d94f3e7537c71ad4cf132eb26e3be8c8a886ed3649c4525c089041fc312b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"skia-bindings",
|
"skia-bindings",
|
||||||
]
|
]
|
||||||
@@ -4521,6 +4738,28 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -4724,6 +4963,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "to_method"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.3"
|
version = "1.52.3"
|
||||||
@@ -5301,7 +5546,7 @@ version = "0.31.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
|
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"rustix",
|
"rustix",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
@@ -5313,7 +5558,7 @@ version = "0.32.13"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
|
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
@@ -5325,7 +5570,7 @@ version = "0.3.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
|
checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols",
|
"wayland-protocols",
|
||||||
@@ -5338,7 +5583,7 @@ version = "0.3.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
|
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols",
|
"wayland-protocols",
|
||||||
@@ -5690,7 +5935,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a"
|
checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.13.0",
|
||||||
"widestring",
|
"widestring",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
@@ -6182,13 +6427,34 @@ dependencies = [
|
|||||||
"zvariant",
|
"zvariant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"zerocopy-derive 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.52"
|
version = "0.8.52"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
|
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive 0.8.52",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+5
-1
@@ -5,11 +5,12 @@ members = [
|
|||||||
"crates/punktfunk-host",
|
"crates/punktfunk-host",
|
||||||
"crates/punktfunk-host/vendor/usbip-sim",
|
"crates/punktfunk-host/vendor/usbip-sim",
|
||||||
"crates/punktfunk-tray",
|
"crates/punktfunk-tray",
|
||||||
|
"crates/pf-bitstream",
|
||||||
|
"crates/pf-bitstream/vendor/cros-codecs",
|
||||||
"crates/pf-client-core",
|
"crates/pf-client-core",
|
||||||
"crates/pf-clipboard",
|
"crates/pf-clipboard",
|
||||||
"crates/pf-presenter",
|
"crates/pf-presenter",
|
||||||
"crates/pf-console-ui",
|
"crates/pf-console-ui",
|
||||||
"crates/pf-ffvk",
|
|
||||||
"crates/pf-driver-proto",
|
"crates/pf-driver-proto",
|
||||||
"crates/pf-paths",
|
"crates/pf-paths",
|
||||||
"crates/pf-update",
|
"crates/pf-update",
|
||||||
@@ -23,6 +24,9 @@ members = [
|
|||||||
"crates/pf-capture",
|
"crates/pf-capture",
|
||||||
"crates/pf-inject",
|
"crates/pf-inject",
|
||||||
"crates/pf-vdisplay",
|
"crates/pf-vdisplay",
|
||||||
|
"crates/pf-vkdecode",
|
||||||
|
"crates/pf-dxvadec",
|
||||||
|
"crates/pf-vaadec",
|
||||||
"crates/pyrowave-sys",
|
"crates/pyrowave-sys",
|
||||||
"crates/libvpl-sys",
|
"crates/libvpl-sys",
|
||||||
"clients/probe",
|
"clients/probe",
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2026 unom
|
Copyright 2026 unom - Enrico Bühler
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 unom
|
Copyright (c) 2026 unom - Enrico Bühler
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ mid-stream mode renegotiation and a wall-clock skew handshake so latency stays v
|
|||||||
Both run from **one process**: bare `punktfunk-host serve` is the **secure native-only default**
|
Both run from **one process**: bare `punktfunk-host serve` is the **secure native-only default**
|
||||||
(`punktfunk/1` + the management API/web console), and `serve --gamestream` additionally enables the
|
(`punktfunk/1` + the management API/web console), and `serve --gamestream` additionally enables the
|
||||||
GameStream/Moonlight-compat planes (opt-in, trusted-LAN only — GameStream has inherent on-path
|
GameStream/Moonlight-compat planes (opt-in, trusted-LAN only — GameStream has inherent on-path
|
||||||
weaknesses). The host is managed through a REST API and web console. Builds against FFmpeg 7 or 8.
|
weaknesses). The host is managed through a REST API and web console. The **host** builds against
|
||||||
|
FFmpeg 7 or 8; the **clients** link no FFmpeg at all — they decode natively (Vulkan Video, DXVA,
|
||||||
|
VAAPI, VideoToolbox, MediaCodec, openh264 + rav1d).
|
||||||
|
|
||||||
What works where: **[the support matrix](https://docs.punktfunk.unom.io/docs/support-matrix)** ·
|
What works where: **[the support matrix](https://docs.punktfunk.unom.io/docs/support-matrix)** ·
|
||||||
where it's heading: **[the roadmap](https://docs.punktfunk.unom.io/docs/roadmap)**.
|
where it's heading: **[the roadmap](https://docs.punktfunk.unom.io/docs/roadmap)**.
|
||||||
@@ -187,10 +189,13 @@ and the [docs site](https://docs.punktfunk.unom.io).
|
|||||||
crates/
|
crates/
|
||||||
punktfunk-core/ protocol · FEC · pacing · crypto · QUIC control plane — the C ABI (lib + cdylib + staticlib)
|
punktfunk-core/ protocol · FEC · pacing · crypto · QUIC control plane — the C ABI (lib + cdylib + staticlib)
|
||||||
punktfunk-host/ the host (Linux + Windows): virtual displays · capture · encode · input · GameStream · punktfunk/1 · mgmt
|
punktfunk-host/ the host (Linux + Windows): virtual displays · capture · encode · input · GameStream · punktfunk/1 · mgmt
|
||||||
pf-client-core/ shared client plumbing (Linux + Windows): session pump · FFmpeg decode · audio · SDL3 gamepads · trust · discovery
|
pf-client-core/ shared client plumbing (Linux + Windows): session pump · native decode ladder · audio · SDL3 gamepads · trust · discovery
|
||||||
pf-presenter/ Vulkan session presenter: SDL3 window · ash swapchain · frame present · input capture
|
pf-presenter/ Vulkan session presenter: SDL3 window · ash swapchain · frame present · input capture
|
||||||
pf-console-ui/ Skia console UI for the session client: gamepad shell · stats OSD · pairing · on-screen keyboard
|
pf-console-ui/ Skia console UI for the session client: gamepad shell · stats OSD · pairing · on-screen keyboard
|
||||||
pf-ffvk/ FFmpeg Vulkan hwcontext bindings (AVVkFrame) for Vulkan Video decode on the presenter's device
|
pf-bitstream/ H.264 / H.265 / AV1 bitstream parsing + per-AU decode plans — the one parser every native rung submits from
|
||||||
|
pf-vkdecode/ native Vulkan Video decode (H.264 / H.265 / AV1) on the presenter's own device
|
||||||
|
pf-dxvadec/ native DXVA buffer layouts + AuPlan → picparams conversion (the Windows D3D11VA rung)
|
||||||
|
pf-vaadec/ native libva buffer layouts + AuPlan → picparams conversion (the Linux VAAPI rung)
|
||||||
pf-driver-proto/ host ↔ pf-vdisplay driver contract: control IOCTLs + IDD-push frame transport (no_std)
|
pf-driver-proto/ host ↔ pf-vdisplay driver contract: control IOCTLs + IDD-push frame transport (no_std)
|
||||||
punktfunk-tray/ host tray icon (Windows notification area / Linux StatusNotifierItem)
|
punktfunk-tray/ host tray icon (Windows notification area / Linux StatusNotifierItem)
|
||||||
clients/
|
clients/
|
||||||
@@ -245,9 +250,10 @@ additional terms or conditions. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|||||||
|
|
||||||
Punktfunk's own source is MIT/Apache-2.0. Shipped binaries additionally link third-party components
|
Punktfunk's own source is MIT/Apache-2.0. Shipped binaries additionally link third-party components
|
||||||
under their own (permissive) licenses — see [`THIRD-PARTY-NOTICES.txt`](THIRD-PARTY-NOTICES.txt)
|
under their own (permissive) licenses — see [`THIRD-PARTY-NOTICES.txt`](THIRD-PARTY-NOTICES.txt)
|
||||||
(regenerate with `scripts/gen-third-party-notices.sh`). The Windows host and client builds also
|
(regenerate with `scripts/gen-third-party-notices.sh`). The Windows **host** build also
|
||||||
bundle FFmpeg under the **LGPL v2.1+** (dynamically linked, replaceable DLLs; the license text and
|
bundles FFmpeg under the **LGPL v2.1+** (dynamically linked, replaceable DLLs; the license text and
|
||||||
notice ship in the installed `licenses/` folder).
|
notice ship in the installed `licenses/` folder). The **clients** bundle no FFmpeg — they link
|
||||||
|
none.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
|
|||||||
+566
-223
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
THIRD-PARTY SOFTWARE NOTICES
|
THIRD-PARTY SOFTWARE NOTICES
|
||||||
============================================================================
|
============================================================================
|
||||||
|
|
||||||
punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0.
|
Punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0.
|
||||||
The binaries it ships statically/dynamically link the third-party Rust crates below.
|
The binaries it ships statically/dynamically link the third-party Rust crates below.
|
||||||
Each is distributed under its own permissive license; full texts follow.
|
Each is distributed under its own permissive license; full texts follow.
|
||||||
Generated by `cargo about generate about.hbs` (see about.toml) — do not edit by hand.
|
Generated by `cargo about generate about.hbs` (see about.toml) — do not edit by hand.
|
||||||
|
|||||||
+14
-2
@@ -4,10 +4,22 @@
|
|||||||
# cargo about generate about.hbs > THIRD-PARTY-NOTICES.txt # (or use scripts/gen-third-party-notices.sh)
|
# cargo about generate about.hbs > THIRD-PARTY-NOTICES.txt # (or use scripts/gen-third-party-notices.sh)
|
||||||
#
|
#
|
||||||
# `accepted` is the allow-list of SPDX licenses permitted in the dependency tree. CI fails if a crate
|
# `accepted` is the allow-list of SPDX licenses permitted in the dependency tree. CI fails if a crate
|
||||||
# carries anything not listed here — which is exactly the regression guard we want against a copyleft
|
# carries anything not listed here — the regression guard against a copyleft dependency silently
|
||||||
# dependency silently entering the linked set. All entries
|
# entering the linked set. All entries
|
||||||
# below are permissive / attribution-only; deliberately NO GPL/LGPL/AGPL/MPL-link/SSPL/EPL.
|
# below are permissive / attribution-only; deliberately NO GPL/LGPL/AGPL/MPL-link/SSPL/EPL.
|
||||||
#
|
#
|
||||||
|
# ⚠ KNOW THE LIMIT OF THIS GATE. cargo-about walks the CARGO graph, so it sees CRATES. A native
|
||||||
|
# library linked through a permissively-licensed `-sys` crate is INVISIBLE to it, licence and all.
|
||||||
|
# FFmpeg is precisely that shape: `ffmpeg-sys-next` is WTFPL and passes cleanly, while the LGPL
|
||||||
|
# libavcodec/libavutil/swscale it link-imports — and which the Windows host installer bundles as
|
||||||
|
# DLLs — never appear in the harvest at all. This gate did not catch FFmpeg entering the tree and
|
||||||
|
# would not catch the next such library. Copyleft arriving as C behind a -sys crate is a REVIEW
|
||||||
|
# question, not a CI one; the LGPL obligations we do carry are discharged by hand (the notice files
|
||||||
|
# and the replaceable-DLL linkage, see packaging/windows/punktfunk-host.iss).
|
||||||
|
#
|
||||||
|
# Since M10 this is a HOST-only concern: the client links no FFmpeg, so for every client artifact
|
||||||
|
# the crate graph and the linked set finally coincide and the gate means what it appears to mean.
|
||||||
|
#
|
||||||
# The dependency-free fallback is scripts/gen-third-party-notices.py (reads the cargo registry cache),
|
# The dependency-free fallback is scripts/gen-third-party-notices.py (reads the cargo registry cache),
|
||||||
# which is what produced the committed baseline when cargo-about is unavailable offline.
|
# which is what produced the committed baseline when cargo-about is unavailable offline.
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ FROM docker.io/library/archlinux:base-devel
|
|||||||
# One transaction: the main build/runtime deps (first list) + the gamescope companion's
|
# One transaction: the main build/runtime deps (first list) + the gamescope companion's
|
||||||
# deps (second list) — both copied verbatim from what arch.yml installed in-job, where
|
# deps (second list) — both copied verbatim from what arch.yml installed in-job, where
|
||||||
# they now no-op as `--needed` guards.
|
# they now no-op as `--needed` guards.
|
||||||
|
# vulkan-headers rides the first list only because arch.yml's copy does; the package it actually
|
||||||
|
# serves is the gamescope companion (packaging/gamescope/PKGBUILD makedepends). punktfunk itself
|
||||||
|
# needs no system Vulkan headers — pyrowave-sys bindgens its own vendored copy and ash dlopens the
|
||||||
|
# loader — but arch.yml builds gamescope with `makepkg -d`, so an absent makedepend would not be
|
||||||
|
# reported as a missing dependency, only as a compile failure. Keep it.
|
||||||
RUN pacman -Syu --noconfirm --needed \
|
RUN pacman -Syu --noconfirm --needed \
|
||||||
git nodejs rust clang cmake ninja nasm pkgconf python vulkan-headers \
|
git nodejs rust clang cmake ninja nasm pkgconf python vulkan-headers \
|
||||||
gtk4 libadwaita sdl3 ffmpeg pipewire wayland libxkbcommon opus libei \
|
gtk4 libadwaita sdl3 ffmpeg pipewire wayland libxkbcommon opus libei \
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ RUN dnf -y install \
|
|||||||
mesa-libGL-devel mesa-libgbm-devel \
|
mesa-libGL-devel mesa-libgbm-devel \
|
||||||
# punktfunk-client link deps (GTK4 shell + SDL3 gamepads)
|
# punktfunk-client link deps (GTK4 shell + SDL3 gamepads)
|
||||||
gtk4-devel libadwaita-devel SDL3-devel \
|
gtk4-devel libadwaita-devel SDL3-devel \
|
||||||
# pf-ffvk bindgen over libavutil/hwcontext_vulkan.h needs <vulkan/vulkan.h>
|
# No vulkan-headers: nothing in the workspace compiles against the system Vulkan headers
|
||||||
vulkan-headers \
|
# (pyrowave-sys bindgens its own vendored copy; host and client both reach Vulkan through
|
||||||
|
# ash, which dlopens the loader), and packaging/rpm/punktfunk.spec BuildRequires none.
|
||||||
|
# rpm.yml's HDR gamescope leg needs them and pulls them with `dnf builddep gamescope`.
|
||||||
&& dnf clean all
|
&& dnf clean all
|
||||||
|
|
||||||
# bun — both the BUILD tool and the RUNTIME for the punktfunk-web console (`bun run build` -> the
|
# bun — both the BUILD tool and the RUNTIME for the punktfunk-web console (`bun run build` -> the
|
||||||
|
|||||||
@@ -29,15 +29,16 @@ RUN sed -i 's|^Types: deb$|Types: deb\nArchitectures: amd64|' /etc/apt/sources.l
|
|||||||
&& dpkg --add-architecture arm64
|
&& dpkg --add-architecture arm64
|
||||||
|
|
||||||
# 2. The cross toolchain + every arm64 dev lib the client links. Mirrors the client half of
|
# 2. The cross toolchain + every arm64 dev lib the client links. Mirrors the client half of
|
||||||
# rust-ci.Dockerfile's list (FFmpeg, PipeWire, Opus, SDL3, GTK4/libadwaita, xkbcommon,
|
# rust-ci.Dockerfile's list (FFmpeg, PipeWire, Opus, SDL3, GTK4/libadwaita, xkbcommon). No
|
||||||
# Vulkan headers for pf-ffvk's bindgen over hwcontext_vulkan.h).
|
# Vulkan dev package: nothing compiles or links against Vulkan — ash dlopens the loader, and
|
||||||
|
# pyrowave-sys bindgens its own vendored headers.
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
crossbuild-essential-arm64 \
|
crossbuild-essential-arm64 \
|
||||||
libavcodec-dev:arm64 libavformat-dev:arm64 libavutil-dev:arm64 libswscale-dev:arm64 \
|
libavcodec-dev:arm64 libavformat-dev:arm64 libavutil-dev:arm64 libswscale-dev:arm64 \
|
||||||
libavfilter-dev:arm64 libavdevice-dev:arm64 \
|
libavfilter-dev:arm64 libavdevice-dev:arm64 \
|
||||||
libpipewire-0.3-dev:arm64 libopus-dev:arm64 \
|
libpipewire-0.3-dev:arm64 libopus-dev:arm64 \
|
||||||
libsdl3-dev:arm64 libgtk-4-dev:arm64 libadwaita-1-dev:arm64 \
|
libsdl3-dev:arm64 libgtk-4-dev:arm64 libadwaita-1-dev:arm64 \
|
||||||
libwayland-dev:arm64 libxkbcommon-dev:arm64 libvulkan-dev:arm64 \
|
libwayland-dev:arm64 libxkbcommon-dev:arm64 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 3. The Rust target — installed against the toolchain the WORKSPACE pins, not the image's
|
# 3. The Rust target — installed against the toolchain the WORKSPACE pins, not the image's
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libgl-dev libegl-dev libgbm-dev \
|
libgl-dev libegl-dev libgbm-dev \
|
||||||
# punktfunk-client-linux (GTK4/libadwaita shell, SDL3 gamepads)
|
# punktfunk-client-linux (GTK4/libadwaita shell, SDL3 gamepads)
|
||||||
libgtk-4-dev libadwaita-1-dev libsdl3-dev \
|
libgtk-4-dev libadwaita-1-dev libsdl3-dev \
|
||||||
# pf-ffvk (bindgen over libavutil/hwcontext_vulkan.h needs <vulkan/vulkan.h>)
|
# No libvulkan-dev: nothing in the workspace compiles or links against Vulkan (pyrowave-sys
|
||||||
libvulkan-dev \
|
# bindgens its own vendored headers, and both host and client reach Vulkan through ash, which
|
||||||
|
# dlopens the loader), so neither the build nor deb.yml's dpkg-shlibdeps ever asks for it.
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# bun — builds the punktfunk-web console in deb.yml (which runs the web build in THIS image).
|
# bun — builds the punktfunk-web console in deb.yml (which runs the web build in THIS image).
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -168,9 +168,9 @@ internal fun LocalNetworkDialog(onAllow: () -> Unit, onSettings: () -> Unit, onD
|
|||||||
title = { Text("Allow local network access") },
|
title = { Text("Allow local network access") },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
"Android blocks punktfunk from talking to devices on your network, so it can't " +
|
"Android blocks Punktfunk from talking to devices on your network, so it can't " +
|
||||||
"find or reach any host until you allow it. If no prompt appears when you tap " +
|
"find or reach any host until you allow it. If no prompt appears when you tap " +
|
||||||
"Allow, enable “Nearby devices” for punktfunk in system settings.",
|
"Allow, enable “Nearby devices” for Punktfunk in system settings.",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package io.unom.punktfunk
|
package io.unom.punktfunk
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -622,6 +625,32 @@ fun ConnectScreen(
|
|||||||
savedHosts = knownHostStore.all()
|
savedHosts = knownHostStore.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Copy link" — the self-emitted form every other client already hands out
|
||||||
|
// (design/client-deep-links.md §4): the host's STABLE id first, with `host=` and `fp=` alongside,
|
||||||
|
// so a link written today still lands on the right box after the host changes address or this
|
||||||
|
// client is reinstalled. A PINNED card copies its own profile with it, because that combination
|
||||||
|
// is the thing being copied; a host card copies no profile at all and so keeps honouring the
|
||||||
|
// host's binding, exactly like a tap on it does.
|
||||||
|
fun copyLink(kh: KnownHost, pin: StreamProfile?) {
|
||||||
|
val url = DeepLinks.forHost(kh, profile = pin?.id).toUrl()
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||||
|
val copied = clipboard != null && runCatching {
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("Punktfunk link", url))
|
||||||
|
}.isSuccess
|
||||||
|
// Android 13 draws its own clipboard confirmation, and stacking a second one on top of it is
|
||||||
|
// the platform's own documented anti-pattern. Below it nothing visible happens at all unless
|
||||||
|
// we say so — a silent menu item reads as a broken one.
|
||||||
|
if (copied && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return
|
||||||
|
val message = if (copied) "Link copied." else "Couldn't copy the link to the clipboard."
|
||||||
|
// The console home renders neither the notice nor the status banner, so there it has to be a
|
||||||
|
// toast; the touch grid has both, and a success dressed as an error banner is a small lie.
|
||||||
|
when {
|
||||||
|
gamepadUi -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||||
|
copied -> notice = message
|
||||||
|
else -> status = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The profile rows a card's overflow menu grows. With no profiles at all it stays empty — a
|
// The profile rows a card's overflow menu grows. With no profiles at all it stays empty — a
|
||||||
// user who never wants this feature sees no new clutter anywhere but the settings scope chips.
|
// user who never wants this feature sees no new clutter anywhere but the settings scope chips.
|
||||||
// "Connect with" is a ONE-OFF on every card: it never rebinds the host, which is why rebinding
|
// "Connect with" is a ONE-OFF on every card: it never rebinds the host, which is why rebinding
|
||||||
@@ -630,6 +659,7 @@ fun ConnectScreen(
|
|||||||
if (pin == null) {
|
if (pin == null) {
|
||||||
add(HostMenuItem("Network speed test") { startSpeedTest(HostCardEntry(kh, null)) })
|
add(HostMenuItem("Network speed test") { startSpeedTest(HostCardEntry(kh, null)) })
|
||||||
}
|
}
|
||||||
|
add(HostMenuItem("Copy link") { copyLink(kh, pin) })
|
||||||
if (profiles.isEmpty()) return@buildList
|
if (profiles.isEmpty()) return@buildList
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
add(HostMenuItem("Unpin card", startsSection = true) { togglePin(kh, pin) })
|
add(HostMenuItem("Unpin card", startsSection = true) { togglePin(kh, pin) })
|
||||||
@@ -911,7 +941,7 @@ fun ConnectScreen(
|
|||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Android blocks punktfunk from finding or reaching hosts until you allow it.",
|
"Android blocks Punktfunk from finding or reaching hosts until you allow it.",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
@@ -1162,6 +1192,7 @@ fun ConnectScreen(
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
|
onCopyLink = { optionsTarget = null; copyLink(kh, pin) },
|
||||||
onEdit = { optionsTarget = null; editTarget = kh },
|
onEdit = { optionsTarget = null; editTarget = kh },
|
||||||
onForget = {
|
onForget = {
|
||||||
knownHostStore.remove(kh)
|
knownHostStore.remove(kh)
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ fun ControllersScreen(gamepadSetting: Int, onBack: () -> Unit) {
|
|||||||
dsUsb?.let { DsRow(it) }
|
dsUsb?.let { DsRow(it) }
|
||||||
if (pads.isEmpty() && !sc2Present) {
|
if (pads.isEmpty() && !sc2Present) {
|
||||||
Text(
|
Text(
|
||||||
"No controller detected. punktfunk can only forward devices Android " +
|
"No controller detected. Punktfunk can only forward devices Android " +
|
||||||
"classifies as a gamepad or joystick — a pad connected through an adapter " +
|
"classifies as a gamepad or joystick — a pad connected through an adapter " +
|
||||||
"or hub may show up under \"Other input devices\" below with the adapter's " +
|
"or hub may show up under \"Other input devices\" below with the adapter's " +
|
||||||
"identity, or not at all.",
|
"identity, or not at all.",
|
||||||
|
|||||||
@@ -205,9 +205,9 @@ private fun DialogText(text: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Console host options for a saved tile — Wake (offered only when offline + a MAC is known), Edit,
|
* Console host options for a saved tile — Wake (offered only when offline + a MAC is known), Copy
|
||||||
* Forget. Reached by pressing Up on a focused saved host in the carousel; the console counterpart of
|
* link, Edit, Forget. Reached by pressing Up on a focused saved host in the carousel; the console
|
||||||
* the touch host card's overflow menu.
|
* counterpart of the touch host card's overflow menu.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun GamepadHostOptionsDialog(
|
fun GamepadHostOptionsDialog(
|
||||||
@@ -217,6 +217,12 @@ fun GamepadHostOptionsDialog(
|
|||||||
onLibrary: (() -> Unit)?, // non-null when the game library is enabled → reachable without Y
|
onLibrary: (() -> Unit)?, // non-null when the game library is enabled → reachable without Y
|
||||||
onEdit: () -> Unit,
|
onEdit: () -> Unit,
|
||||||
onForget: () -> Unit,
|
onForget: () -> Unit,
|
||||||
|
/**
|
||||||
|
* Copy this tile's `punktfunk://` link. Offered on a pinned tile too — unlike the host's other
|
||||||
|
* actions it says nothing about the host, it hands out the shortcut this very tile already is
|
||||||
|
* (profile included), which is exactly what a pin is for.
|
||||||
|
*/
|
||||||
|
onCopyLink: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSpeedTest: (() -> Unit)? = null,
|
onSpeedTest: (() -> Unit)? = null,
|
||||||
/**
|
/**
|
||||||
@@ -233,12 +239,14 @@ fun GamepadHostOptionsDialog(
|
|||||||
actions = buildList {
|
actions = buildList {
|
||||||
if (onUnpin != null) {
|
if (onUnpin != null) {
|
||||||
add(DialogAction("Unpin card", primary = true, onClick = onUnpin))
|
add(DialogAction("Unpin card", primary = true, onClick = onUnpin))
|
||||||
|
add(DialogAction("Copy link", onClick = onCopyLink))
|
||||||
add(DialogAction("Cancel", onClick = onDismiss))
|
add(DialogAction("Cancel", onClick = onDismiss))
|
||||||
return@buildList
|
return@buildList
|
||||||
}
|
}
|
||||||
if (onLibrary != null) add(DialogAction("Library", primary = true, onClick = onLibrary))
|
if (onLibrary != null) add(DialogAction("Library", primary = true, onClick = onLibrary))
|
||||||
if (canWake) add(DialogAction("Wake host", onClick = onWake))
|
if (canWake) add(DialogAction("Wake host", onClick = onWake))
|
||||||
if (onSpeedTest != null) add(DialogAction("Network speed test", onClick = onSpeedTest))
|
if (onSpeedTest != null) add(DialogAction("Network speed test", onClick = onSpeedTest))
|
||||||
|
add(DialogAction("Copy link", onClick = onCopyLink))
|
||||||
add(DialogAction("Edit…", primary = onLibrary == null, onClick = onEdit))
|
add(DialogAction("Edit…", primary = onLibrary == null, onClick = onEdit))
|
||||||
add(DialogAction("Forget", onClick = onForget))
|
add(DialogAction("Forget", onClick = onForget))
|
||||||
add(DialogAction("Cancel", onClick = onDismiss))
|
add(DialogAction("Cancel", onClick = onDismiss))
|
||||||
@@ -455,11 +463,11 @@ fun GamepadLocalNetworkDialog(onAllow: () -> Unit, onSettings: () -> Unit, onDis
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
DialogText(
|
DialogText(
|
||||||
"Android blocks punktfunk from talking to devices on your network, so it can't find " +
|
"Android blocks Punktfunk from talking to devices on your network, so it can't find " +
|
||||||
"or reach any host until you allow it.",
|
"or reach any host until you allow it.",
|
||||||
)
|
)
|
||||||
DialogText(
|
DialogText(
|
||||||
"If no prompt appears after Allow, enable “Nearby devices” for punktfunk in " +
|
"If no prompt appears after Allow, enable “Nearby devices” for Punktfunk in " +
|
||||||
"system settings.",
|
"system settings.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ struct HomeView: View {
|
|||||||
ContentUnavailableView {
|
ContentUnavailableView {
|
||||||
Label("No Hosts", systemImage: "rectangle.connected.to.line.below")
|
Label("No Hosts", systemImage: "rectangle.connected.to.line.below")
|
||||||
} description: {
|
} description: {
|
||||||
Text("Add your punktfunk host with the + button, or scan the network again.")
|
Text("Add your Punktfunk host with the + button, or scan the network again.")
|
||||||
} actions: {
|
} actions: {
|
||||||
Button("Add Host") { showAddHost = true }
|
Button("Add Host") { showAddHost = true }
|
||||||
.glassProminentButtonStyle()
|
.glassProminentButtonStyle()
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ struct AcknowledgementsView: View {
|
|||||||
.font(.geist(Self.headlineFont, .semibold, relativeTo: .headline))
|
.font(.geist(Self.headlineFont, .semibold, relativeTo: .headline))
|
||||||
Text(
|
Text(
|
||||||
"Punktfunk uses the open-source components below, each under its own license. "
|
"Punktfunk uses the open-source components below, each under its own license. "
|
||||||
+ "On some platforms FFmpeg is additionally bundled under the LGPL v2.1+ "
|
+ "Video decoding uses the system's own VideoToolbox framework, so nothing "
|
||||||
+ "(dynamically linked, replaceable)."
|
+ "is bundled for it — and no Punktfunk client bundles FFmpeg on any platform."
|
||||||
)
|
)
|
||||||
.font(.geist(Self.captionFont, relativeTo: .caption))
|
.font(.geist(Self.captionFont, relativeTo: .caption))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|||||||
@@ -15,10 +15,16 @@ import os
|
|||||||
/// audible blip". It is now the same two-stage scheme the Rust clients share
|
/// audible blip". It is now the same two-stage scheme the Rust clients share
|
||||||
/// (`punktfunk_core::audio::JitterPolicy`): a slow depth average that sits above target for a
|
/// (`punktfunk_core::audio::JitterPolicy`): a slow depth average that sits above target for a
|
||||||
/// sustained window sheds ONE 5 ms frame with a crossfade, and the hard cap is only a backstop.
|
/// sustained window sheds ONE 5 ms frame with a crossfade, and the hard cap is only a backstop.
|
||||||
/// Keep the constants here in step with `JitterTuning.COREAUDIO`.
|
///
|
||||||
|
/// **Adaptive depth.** The target is a floor, not a constant: repeated genuine underruns grow it
|
||||||
|
/// a step at a time (`noteRead`, mirroring `JitterPolicy::note_read`) up to `maxTargetMS`, and a
|
||||||
|
/// long quiet spell relaxes it back toward the base — so a session on Wi-Fi that bunches arrivals
|
||||||
|
/// deepens until it stops crackling, while a clean LAN keeps the tight base latency. Keep the
|
||||||
|
/// constants here in step with `JitterTuning.COREAUDIO`.
|
||||||
final class AudioRing: @unchecked Sendable {
|
final class AudioRing: @unchecked Sendable {
|
||||||
/// Mirrors `JitterTuning::COREAUDIO` — see that type for the rationale.
|
/// Mirrors `JitterTuning::COREAUDIO` — see that type for the rationale.
|
||||||
private static let targetMS = 20
|
private static let targetMS = 20
|
||||||
|
private static let maxTargetMS = 70
|
||||||
private static let headroomMS = 30
|
private static let headroomMS = 30
|
||||||
private static let hardCapMS = 90
|
private static let hardCapMS = 90
|
||||||
private static let deprimeAfter = 4
|
private static let deprimeAfter = 4
|
||||||
@@ -33,6 +39,15 @@ final class AudioRing: @unchecked Sendable {
|
|||||||
private static let crossfadeMS = 2
|
private static let crossfadeMS = 2
|
||||||
/// Time constant of the depth average.
|
/// Time constant of the depth average.
|
||||||
private static let ewmaTauMS = 1_000
|
private static let ewmaTauMS = 1_000
|
||||||
|
/// Adaptive target floor, mirroring `JitterPolicy::note_read`: this many genuine underruns
|
||||||
|
/// inside one window grow the live target a step (up to `maxTargetMS`), and a long quiet
|
||||||
|
/// spell relaxes it a step back toward the base — so only the sessions that actually starve
|
||||||
|
/// (Wi-Fi power-save bunching is the classic) pay for extra depth, and only while they need
|
||||||
|
/// it. All spans are measured in consumed samples, like the Rust policy.
|
||||||
|
private static let growUnderruns = 3
|
||||||
|
private static let growWindowMS = 5_000
|
||||||
|
private static let growStepMS = 10
|
||||||
|
private static let shrinkQuietMS = 30_000
|
||||||
|
|
||||||
private var buf: [Float]
|
private var buf: [Float]
|
||||||
private var readIdx = 0
|
private var readIdx = 0
|
||||||
@@ -42,6 +57,14 @@ final class AudioRing: @unchecked Sendable {
|
|||||||
private var emptyReads = 0
|
private var emptyReads = 0
|
||||||
private var depthAvg: Double = 0
|
private var depthAvg: Double = 0
|
||||||
private var overRun = 0
|
private var overRun = 0
|
||||||
|
/// The live target in interleaved samples — `targetMS` grown by underrun pressure
|
||||||
|
/// (`noteRead`), never below the base. Set in `init` (needs `perMS`).
|
||||||
|
private var targetLive = 0
|
||||||
|
/// Underruns seen in the current growth window, and the window's consumed-sample count.
|
||||||
|
private var underrunsInWindow = 0
|
||||||
|
private var windowRun = 0
|
||||||
|
/// Consumed samples since the last underrun (drives the relax-back-down step).
|
||||||
|
private var quietRun = 0
|
||||||
/// Reported, not acted on: short reads that actually starved the callback, and smooth drift
|
/// Reported, not acted on: short reads that actually starved the callback, and smooth drift
|
||||||
/// corrections. A rising underrun count means the ring is being starved (network or CPU),
|
/// corrections. A rising underrun count means the ring is being starved (network or CPU),
|
||||||
/// which is a different problem from the depth being wrong.
|
/// which is a different problem from the depth being wrong.
|
||||||
@@ -57,12 +80,14 @@ final class AudioRing: @unchecked Sendable {
|
|||||||
buf = [Float](repeating: 0, count: capacity)
|
buf = [Float](repeating: 0, count: capacity)
|
||||||
self.channels = channels
|
self.channels = channels
|
||||||
perMS = 48 * channels
|
perMS = 48 * channels
|
||||||
|
targetLive = Self.targetMS * perMS
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Live target depth in interleaved samples, lifted so it can always serve one device quantum
|
/// Effective target depth in interleaved samples: the (adaptively grown) live target, lifted
|
||||||
/// plus a packet (a large-buffer device cannot sustain a target below its own quantum).
|
/// so it can always serve one device quantum plus a packet (a large-buffer device cannot
|
||||||
|
/// sustain a target below its own quantum).
|
||||||
private var target: Int {
|
private var target: Int {
|
||||||
max(Self.targetMS * perMS, renderQuantum + Self.frameMS * perMS)
|
max(targetLive, renderQuantum + Self.frameMS * perMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func write(_ samples: UnsafePointer<Float>, count: Int) {
|
func write(_ samples: UnsafePointer<Float>, count: Int) {
|
||||||
@@ -80,8 +105,13 @@ final class AudioRing: @unchecked Sendable {
|
|||||||
buf[(writeIdx + i) % capacity] = samples[i]
|
buf[(writeIdx + i) % capacity] = samples[i]
|
||||||
}
|
}
|
||||||
writeIdx += count
|
writeIdx += count
|
||||||
// Backstop only: the smooth shed in `read` is what normally holds the depth down.
|
// Backstop only: the smooth shed in `read` is what normally holds the depth down. The
|
||||||
let cap = min(target + Self.headroomMS * perMS, Self.hardCapMS * perMS)
|
// hard cap must always leave room for one device quantum past the target (mirrors the
|
||||||
|
// Rust policy's `.max(target + want)`) or a large-quantum device would trim itself into
|
||||||
|
// a permanent underrun.
|
||||||
|
let cap = max(
|
||||||
|
min(target + Self.headroomMS * perMS, Self.hardCapMS * perMS),
|
||||||
|
target + renderQuantum)
|
||||||
if writeIdx - readIdx > cap {
|
if writeIdx - readIdx > cap {
|
||||||
readIdx = writeIdx - cap
|
readIdx = writeIdx - cap
|
||||||
depthAvg = Double(cap)
|
depthAvg = Double(cap)
|
||||||
@@ -133,13 +163,43 @@ final class AudioRing: @unchecked Sendable {
|
|||||||
readIdx += n
|
readIdx += n
|
||||||
if n < count {
|
if n < count {
|
||||||
for i in n..<count { out[i] = 0 }
|
for i in n..<count { out[i] = 0 }
|
||||||
// De-prime only after a RUN of short reads: a single transient drain must not
|
}
|
||||||
// manufacture a whole target's worth of fresh silence.
|
noteRead(ranShort: n < count, count: count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The outcome accounting of one primed read — the Swift mirror of
|
||||||
|
/// `JitterPolicy::note_read`. A short read drives both the de-prime hysteresis (a single
|
||||||
|
/// transient drain must not manufacture a whole target's worth of fresh silence) and the
|
||||||
|
/// adaptive target floor: a device that genuinely keeps starving gets more slack, one step
|
||||||
|
/// per window, capped — and gives it back after a long quiet spell, so one bad minute
|
||||||
|
/// doesn't cost latency for the rest of the session. Caller holds the lock.
|
||||||
|
private func noteRead(ranShort: Bool, count: Int) {
|
||||||
|
windowRun += count
|
||||||
|
if windowRun >= Self.growWindowMS * perMS {
|
||||||
|
windowRun = 0
|
||||||
|
underrunsInWindow = 0
|
||||||
|
}
|
||||||
|
if ranShort {
|
||||||
|
quietRun = 0
|
||||||
emptyReads += 1
|
emptyReads += 1
|
||||||
underrunCount += 1
|
underrunCount += 1
|
||||||
if emptyReads >= Self.deprimeAfter { primed = false }
|
if emptyReads >= Self.deprimeAfter {
|
||||||
|
primed = false
|
||||||
|
emptyReads = 0
|
||||||
|
}
|
||||||
|
underrunsInWindow += 1
|
||||||
|
if underrunsInWindow >= Self.growUnderruns {
|
||||||
|
underrunsInWindow = 0
|
||||||
|
windowRun = 0
|
||||||
|
targetLive = min(targetLive + Self.growStepMS * perMS, Self.maxTargetMS * perMS)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyReads = 0
|
emptyReads = 0
|
||||||
|
quietRun += count
|
||||||
|
if quietRun >= Self.shrinkQuietMS * perMS {
|
||||||
|
quietRun = 0
|
||||||
|
targetLive = max(targetLive - Self.growStepMS * perMS, Self.targetMS * perMS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ public final class HostDiscovery: ObservableObject {
|
|||||||
|
|
||||||
private static func instanceName(_ endpoint: NWEndpoint) -> String {
|
private static func instanceName(_ endpoint: NWEndpoint) -> String {
|
||||||
if case let .service(name, _, _, _) = endpoint { return name }
|
if case let .service(name, _, _, _) = endpoint { return name }
|
||||||
return "punktfunk host"
|
return "Punktfunk host"
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func entry(_ txt: NWTXTRecord, _ field: String) -> String? {
|
private static func entry(_ txt: NWTXTRecord, _ field: String) -> String? {
|
||||||
|
|||||||
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2026 unom
|
Copyright 2026 unom - Enrico Bühler
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 unom
|
Copyright (c) 2026 unom - Enrico Bühler
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -91,5 +91,112 @@ final class AudioRingDriftTests: XCTestCase {
|
|||||||
scratch.contains { $0 != 0 },
|
scratch.contains { $0 != 0 },
|
||||||
"a single short read must not force a full re-prime")
|
"a single short read must not force a full re-prime")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mirror of the Rust `target_grows_on_underruns_and_relaxes_when_quiet`: clustered genuine
|
||||||
|
/// underruns raise the target floor (that session needs the slack), a long quiet spell gives
|
||||||
|
/// it back — and the floor never dips below the base.
|
||||||
|
func testTargetGrowsOnUnderrunsAndRelaxesWhenQuiet() {
|
||||||
|
let ring = AudioRing(capacity: 48_000 * channels, channels: channels)
|
||||||
|
let want = 5 * perMS
|
||||||
|
var scratch = [Float](repeating: 0, count: want)
|
||||||
|
let feed = [Float](repeating: 0.5, count: 25 * perMS)
|
||||||
|
func write(ms: Int) {
|
||||||
|
feed.withUnsafeBufferPointer { ring.write($0.baseAddress!, count: ms * perMS) }
|
||||||
|
}
|
||||||
|
func read() {
|
||||||
|
scratch.withUnsafeMutableBufferPointer { ring.read(into: $0.baseAddress!, count: want) }
|
||||||
|
}
|
||||||
|
XCTAssertEqual(ring.stats.targetMS, 20, "base target must match JitterTuning.COREAUDIO")
|
||||||
|
|
||||||
|
// Prime, drain dry, then alternate starve/refill: each dry read is a genuine underrun,
|
||||||
|
// each full read in between keeps the de-prime hysteresis from tripping.
|
||||||
|
write(ms: 25)
|
||||||
|
for _ in 0..<5 { read() } // drains to zero
|
||||||
|
read() // short — underrun 1
|
||||||
|
write(ms: 5); read() // full — hysteresis reset
|
||||||
|
read() // short — underrun 2
|
||||||
|
write(ms: 5); read() // full
|
||||||
|
read() // short — underrun 3 → the floor grows one step
|
||||||
|
XCTAssertEqual(ring.stats.targetMS, 30, "3 clustered underruns must grow the target 10 ms")
|
||||||
|
XCTAssertEqual(ring.stats.underruns, 3)
|
||||||
|
|
||||||
|
// A long clean run (30 s of consumed audio) relaxes the growth back to the base…
|
||||||
|
for _ in 0..<(30_000 / 5 + 10) {
|
||||||
|
write(ms: 5)
|
||||||
|
read()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(ring.stats.targetMS, 20, "a quiet spell must give the growth back")
|
||||||
|
// …and stays there: quiet forever never dips below the base.
|
||||||
|
for _ in 0..<(30_000 / 5 + 10) {
|
||||||
|
write(ms: 5)
|
||||||
|
read()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(ring.stats.targetMS, 20, "the floor must never go below the base target")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Growth is capped at `maxTargetMS`, exactly like `JitterPolicy` respects
|
||||||
|
/// `JitterTuning.max_target_ms`.
|
||||||
|
func testTargetGrowthRespectsTheCap() {
|
||||||
|
let ring = AudioRing(capacity: 48_000 * channels, channels: channels)
|
||||||
|
let want = 5 * perMS
|
||||||
|
var scratch = [Float](repeating: 0, count: want)
|
||||||
|
let feed = [Float](repeating: 0.5, count: 25 * perMS)
|
||||||
|
feed.withUnsafeBufferPointer { ring.write($0.baseAddress!, count: 25 * perMS) }
|
||||||
|
// Starve it far past what six growth steps (20 → 70) would need.
|
||||||
|
for _ in 0..<40 {
|
||||||
|
for _ in 0..<5 {
|
||||||
|
scratch.withUnsafeMutableBufferPointer {
|
||||||
|
ring.read(into: $0.baseAddress!, count: want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feed.withUnsafeBufferPointer { ring.write($0.baseAddress!, count: 25 * perMS) }
|
||||||
|
}
|
||||||
|
XCTAssertLessThanOrEqual(ring.stats.targetMS, 70, "growth must respect maxTargetMS")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// THE field scenario: Wi-Fi power-save bunches arrivals — audio is produced steadily but
|
||||||
|
/// delivered in bursts, some of them late. A fixed 20 ms target crackles on every late burst
|
||||||
|
/// forever; the adaptive floor must deepen until the bunching rides through, and the tail of
|
||||||
|
/// the session must be silence-free.
|
||||||
|
func testWifiBunchingConvergesToSilenceFree() {
|
||||||
|
let ring = AudioRing(capacity: 48_000 * channels, channels: channels)
|
||||||
|
let want = 5 * perMS
|
||||||
|
var scratch = [Float](repeating: 0, count: want)
|
||||||
|
var pending = 0 // ms produced by the host but still "in flight"
|
||||||
|
var burst = 0
|
||||||
|
var silentTail = 0
|
||||||
|
let steps = 4000 // 20 s in 5 ms callbacks
|
||||||
|
let feed = [Float](repeating: 0.5, count: 200 * perMS)
|
||||||
|
for step in 0..<steps {
|
||||||
|
pending += 5 // the host encodes 5 ms per 5 ms of wall clock, stall or not
|
||||||
|
// Delivery bunches into ~60 ms bursts; every 4th burst arrives a further 30 ms late.
|
||||||
|
if step % 12 == 11 {
|
||||||
|
if burst % 4 == 3 {
|
||||||
|
// Hold this burst 30 ms: it is flushed 6 callbacks later instead.
|
||||||
|
burst += 1
|
||||||
|
} else {
|
||||||
|
feed.withUnsafeBufferPointer {
|
||||||
|
ring.write($0.baseAddress!, count: pending * perMS)
|
||||||
|
}
|
||||||
|
pending = 0
|
||||||
|
burst += 1
|
||||||
|
}
|
||||||
|
} else if step % 12 == 5, pending >= 60 {
|
||||||
|
// The held burst lands, together with everything produced since.
|
||||||
|
feed.withUnsafeBufferPointer {
|
||||||
|
ring.write($0.baseAddress!, count: pending * perMS)
|
||||||
|
}
|
||||||
|
pending = 0
|
||||||
|
}
|
||||||
|
scratch.withUnsafeMutableBufferPointer { ring.read(into: $0.baseAddress!, count: want) }
|
||||||
|
if step >= steps - 600, scratch.allSatisfy({ $0 == 0 }) { silentTail += 1 }
|
||||||
|
}
|
||||||
|
XCTAssertGreaterThanOrEqual(
|
||||||
|
ring.stats.targetMS, 30,
|
||||||
|
"bunched delivery must have grown the target floor")
|
||||||
|
XCTAssertEqual(
|
||||||
|
silentTail, 0,
|
||||||
|
"after adapting, the last 3 s must play through the bunching without a dropout")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 unom
|
Copyright (c) 2026 unom - Enrico Bühler
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "punktfunk-client-linux"
|
name = "punktfunk-client-linux"
|
||||||
description = "Native Linux punktfunk/1 client — GTK4/libadwaita shell, FFmpeg decode, PipeWire audio, SDL3 gamepads"
|
description = "Native Linux punktfunk/1 client — GTK4/libadwaita shell, PipeWire audio, SDL3 gamepads; streaming runs in the spawned punktfunk-session binary"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|||||||
+13
-8
@@ -12,9 +12,11 @@ Built in Rust end to end (no C ABI): the shell shares its plumbing with the sess
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Zero-copy hardware decode** — the session presenter decodes via **Vulkan Video** on every GPU
|
- **Zero-copy hardware decode, and it's ours** — the session presenter decodes with Punktfunk's own
|
||||||
vendor (including NVIDIA), falling back to FFmpeg VAAPI → DRM-PRIME dmabuf and then software when
|
decoders; no FFmpeg is linked or bundled. **Vulkan Video** (`pf-vkdecode`, decoding onto the
|
||||||
Vulkan Video is unavailable.
|
presenter's own device) leads on NVIDIA and AMD, **VAAPI** (`pf-vaadec` driving a dlopen'd libva,
|
||||||
|
exporting DRM-PRIME dmabufs) leads on Intel, whichever isn't first is the fallback, and an
|
||||||
|
OpenH264/rav1d CPU rung is last.
|
||||||
- **Your display's native mode** — the host builds a virtual output at exactly your WxH@Hz; no
|
- **Your display's native mode** — the host builds a virtual output at exactly your WxH@Hz; no
|
||||||
scaling, no letterboxing. Steady 60 fps at 1080p60, ~6 ms capture→decoded on the LAN.
|
scaling, no letterboxing. Steady 60 fps at 1080p60, ~6 ms capture→decoded on the LAN.
|
||||||
- **Audio both ways** — PipeWire playback with a jitter ring, plus mic uplink to the host.
|
- **Audio both ways** — PipeWire playback with a jitter ring, plus mic uplink to the host.
|
||||||
@@ -50,8 +52,11 @@ Per-device install steps and pairing walkthrough:
|
|||||||
|
|
||||||
## Build & run from source
|
## Build & run from source
|
||||||
|
|
||||||
Requires GTK ≥ 4.16, libadwaita ≥ 1.5, FFmpeg 7 or 8 (with VAAPI for hardware decode), PipeWire,
|
Requires GTK ≥ 4.16, libadwaita ≥ 1.5, PipeWire, and SDL3 (with hidapi) development packages,
|
||||||
and SDL3 (with hidapi) development packages.
|
plus a C compiler (the CPU rung builds OpenH264 from source). No *decoder* development package
|
||||||
|
is needed: libva and the Vulkan loader are both opened at runtime rather than linked, so
|
||||||
|
hardware decode is a fact about the box you **run** on — a Vulkan loader and your GPU's driver,
|
||||||
|
and libva for the VAAPI rung — not about the one you build on.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# from the repo root
|
# from the repo root
|
||||||
@@ -85,9 +90,9 @@ src/
|
|||||||
tools/screenshots.sh store screenshot capture (app self-capture; Xvfb fallback)
|
tools/screenshots.sh store screenshot capture (app self-capture; Xvfb fallback)
|
||||||
```
|
```
|
||||||
|
|
||||||
The UI-agnostic plumbing — session pump, FFmpeg decode, PipeWire audio, SDL3 gamepads +
|
The UI-agnostic plumbing — session pump, the native decode ladder (Vulkan Video · VAAPI ·
|
||||||
keymap, trust store, mDNS discovery, library client, Wake-on-LAN — lives in
|
OpenH264/rav1d), PipeWire audio, SDL3 gamepads + keymap, trust store, mDNS discovery, library
|
||||||
`crates/pf-client-core`, shared with the Vulkan session binary.
|
client, Wake-on-LAN — lives in `crates/pf-client-core`, shared with the Vulkan session binary.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -728,7 +728,11 @@ const CODEC_LABELS: &[&str] = &[
|
|||||||
"AV1",
|
"AV1",
|
||||||
"PyroWave (wired LAN)",
|
"PyroWave (wired LAN)",
|
||||||
];
|
];
|
||||||
const DECODERS: &[&str] = &["auto", "vulkan", "vaapi", "software"];
|
// Stored decoder-preference values. `native-*` since M10 — the bare "vulkan"/"vaapi"
|
||||||
|
// named libavcodec's rungs, which are deleted; a store still holding them is migrated on
|
||||||
|
// read (`pf_client_core::video::migrate_decoder_pref`) and simply matches no entry here
|
||||||
|
// until the user re-picks. The labels below are unchanged and still true.
|
||||||
|
const DECODERS: &[&str] = &["auto", "native-vulkan", "native-vaapi", "software"];
|
||||||
/// Touch-input model values (persisted) paired with their display labels below — the
|
/// Touch-input model values (persisted) paired with their display labels below — the
|
||||||
/// cross-client set (Android/Apple). Only meaningful on a touchscreen (Deck/tablet).
|
/// cross-client set (Android/Apple). Only meaningful on a touchscreen (Deck/tablet).
|
||||||
const TOUCH_MODES: &[&str] = &["trackpad", "pointer", "touch"];
|
const TOUCH_MODES: &[&str] = &["trackpad", "pointer", "touch"];
|
||||||
@@ -773,16 +777,26 @@ const APP_LICENSE: &str = concat!(
|
|||||||
"\n\n=============================== Apache-2.0 ===============================\n\n",
|
"\n\n=============================== Apache-2.0 ===============================\n\n",
|
||||||
include_str!("../../../LICENSE-APACHE"),
|
include_str!("../../../LICENSE-APACHE"),
|
||||||
);
|
);
|
||||||
/// Third-party software notices for the linked Rust crates (generated by
|
/// Third-party software notices for the Rust crates THIS CLIENT links — the shell, the
|
||||||
/// scripts/gen-third-party-notices.sh; shown as a Legal section in the About dialog).
|
/// session streamer, the headless CLI and the update helper (generated by
|
||||||
const THIRD_PARTY_NOTICES: &str = include_str!("../../../THIRD-PARTY-NOTICES.txt");
|
/// scripts/gen-third-party-notices.sh; shown as a Legal section in the About dialog, and
|
||||||
|
/// shipped as /usr/share/doc/punktfunk-client/THIRD-PARTY-NOTICES.txt by the packages).
|
||||||
|
///
|
||||||
|
/// Deliberately the client-scoped file and not the workspace-wide one at the repo root:
|
||||||
|
/// that root file is the HOST's, it still carries `ffmpeg-next` and the full FFmpeg licence
|
||||||
|
/// text — and after M10 this app links no FFmpeg at all, which is exactly what the section
|
||||||
|
/// below it claims.
|
||||||
|
const THIRD_PARTY_NOTICES: &str = include_str!("../THIRD-PARTY-NOTICES.txt");
|
||||||
|
|
||||||
/// The dynamically linked system libraries — not in the crate notices, since they aren't
|
/// The dynamically linked system libraries — not in the crate notices, since they aren't
|
||||||
/// crates. Their full texts ship with each project rather than being vendored here.
|
/// crates. Their full texts ship with each project rather than being vendored here.
|
||||||
const SYSTEM_LIBRARY_NOTICES: &str =
|
const SYSTEM_LIBRARY_NOTICES: &str =
|
||||||
"This application dynamically links system libraries under their own licenses, including \
|
"This application dynamically links system libraries under their own licenses, including \
|
||||||
FFmpeg (LGPL v2.1+), GTK 4 and libadwaita (LGPL v2.1+), PipeWire (MIT), and SDL 3 (Zlib). \
|
GTK 4 and libadwaita (LGPL v2.1+), PipeWire (MIT), and SDL 3 (Zlib). \
|
||||||
Their full license texts are available from each project.";
|
Their full license texts are available from each project. Video decoding uses the \
|
||||||
|
system's own Vulkan Video and VAAPI drivers (loaded at runtime, never linked), with \
|
||||||
|
OpenH264 and rav1d — both BSD-2-Clause, and both in the Rust crate notices — as the \
|
||||||
|
CPU fallback; no FFmpeg is linked or bundled.";
|
||||||
|
|
||||||
/// Show the About dialog (app license + the third-party-software Legal section) — reached
|
/// Show the About dialog (app license + the third-party-software Legal section) — reached
|
||||||
/// from the primary menu (app.rs `win.about`).
|
/// from the primary menu (app.rs `win.about`).
|
||||||
@@ -806,7 +820,7 @@ pub fn show_about(parent: &impl IsA<gtk::Widget>) {
|
|||||||
.license_type(gtk::License::Custom)
|
.license_type(gtk::License::Custom)
|
||||||
.license(license.as_str())
|
.license(license.as_str())
|
||||||
.build();
|
.build();
|
||||||
// The native (FFmpeg/GTK/PipeWire/SDL3) components are dynamically linked under their own
|
// The native (GTK/PipeWire/SDL3) components are dynamically linked under their own
|
||||||
// (LGPL/Zlib/MIT) licenses; the Rust crate notices are the substantive attribution set.
|
// (LGPL/Zlib/MIT) licenses; the Rust crate notices are the substantive attribution set.
|
||||||
about.add_legal_section(
|
about.add_legal_section(
|
||||||
"Third-party software (Rust crates)",
|
"Third-party software (Rust crates)",
|
||||||
@@ -1628,7 +1642,11 @@ pub fn show_scoped(
|
|||||||
mouse_row.set_selected(mouse_i);
|
mouse_row.set_selected(mouse_i);
|
||||||
set_row_subtitle(mouse_row.widget(), MOUSE_MODE_CAPTIONS[mouse_i as usize]);
|
set_row_subtitle(mouse_row.widget(), MOUSE_MODE_CAPTIONS[mouse_i as usize]);
|
||||||
compositor_row.set_selected(index::compositor(s));
|
compositor_row.set_selected(index::compositor(s));
|
||||||
let dec_i = DECODERS.iter().position(|&d| d == s.decoder).unwrap_or(0);
|
// Migrated for the LOOKUP only (the store is left alone): a pre-M10 settings file
|
||||||
|
// holds `vulkan`/`vaapi`, which match no entry — the combo would show Automatic and
|
||||||
|
// a save would silently rewrite the user's hardware preference to `auto`.
|
||||||
|
let dec_stored = pf_client_core::video::migrate_decoder_pref(&s.decoder);
|
||||||
|
let dec_i = DECODERS.iter().position(|&d| d == dec_stored).unwrap_or(0);
|
||||||
decoder_row.set_selected(dec_i as u32);
|
decoder_row.set_selected(dec_i as u32);
|
||||||
stats_row.set_selected(index::stats(s));
|
stats_row.set_selected(index::stats(s));
|
||||||
fullscreen_row.set_active(s.fullscreen_on_stream);
|
fullscreen_row.set_active(s.fullscreen_on_stream);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ pyrowave = ["pf-client-core/pyrowave", "pf-presenter/pyrowave"]
|
|||||||
# (`--no-default-features`) is the ~15 MB-smaller power-user build: same streaming,
|
# (`--no-default-features`) is the ~15 MB-smaller power-user build: same streaming,
|
||||||
# stats on stdout only.
|
# stats on stdout only.
|
||||||
ui = ["dep:pf-console-ui", "dep:serde_json"]
|
ui = ["dep:pf-console-ui", "dep:serde_json"]
|
||||||
|
|
||||||
# Same Linux+Windows gating as the rest of the client stack; elsewhere this is a stub
|
# Same Linux+Windows gating as the rest of the client stack; elsewhere this is a stub
|
||||||
# binary.
|
# binary.
|
||||||
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||||
|
|||||||
@@ -49,19 +49,67 @@ path + per-stage latency equation); any tier but Off also emits the stdout mirro
|
|||||||
`--no-default-features` is the ~5 MB power-user build — same streaming, stats on stdout
|
`--no-default-features` is the ~5 MB power-user build — same streaming, stats on stdout
|
||||||
only, no Skia anywhere in the dependency tree.
|
only, no Skia anywhere in the dependency tree.
|
||||||
|
|
||||||
Decode follows the Settings preference (auto: Vulkan Video → VAAPI → software on Linux,
|
Decode follows the Settings preference (auto is vendor-ordered: Vulkan Video → VAAPI →
|
||||||
Vulkan Video → D3D11VA → software on Windows): FFmpeg's Vulkan Video decoder runs on the
|
software on Linux, Vulkan Video → D3D11VA → software on Windows, with VAAPI/D3D11VA first
|
||||||
presenter's own device where the stack supports it (every vendor, zero copy); VAAPI
|
on Intel — every rung native since M10; see "Decode rungs" below): the Vulkan decoder runs
|
||||||
dmabufs import per-plane elsewhere (D3D11VA textures on Windows); software is the
|
on the presenter's own device where the stack supports it (every vendor, zero copy); VAAPI
|
||||||
universal fallback. 10-bit Main10 and HDR10 are advertised
|
dmabufs import per-plane elsewhere (D3D11VA textures on Windows); software is the universal
|
||||||
(`VIDEO_CAP_10BIT|HDR`): P010 decodes through all three paths, and PQ streams present
|
fallback. 10-bit Main10 and HDR10 are advertised (`VIDEO_CAP_10BIT|HDR`): P010 decodes
|
||||||
|
through the Vulkan and VAAPI/D3D11VA paths (the CPU rung is 8-bit by contract and refuses
|
||||||
|
10-bit rather than mis-scaling it), and PQ streams present
|
||||||
on an HDR10/ST.2084 swapchain when the desktop offers one (KDE HDR, gamescope) or
|
on an HDR10/ST.2084 swapchain when the desktop offers one (KDE HDR, gamescope) or
|
||||||
tone-map in-shader to SDR when it doesn't (`PUNKTFUNK_TONEMAP_PEAK` tunes the rolloff,
|
tone-map in-shader to SDR when it doesn't (`PUNKTFUNK_TONEMAP_PEAK` tunes the rolloff,
|
||||||
default ≈1000 nits). The host still gates the upgrade behind its `PUNKTFUNK_10BIT`
|
default ≈1000 nits). The host still gates the upgrade behind its `PUNKTFUNK_10BIT`
|
||||||
policy.
|
policy.
|
||||||
|
|
||||||
Debug/bisect knobs: `PUNKTFUNK_DECODER=vulkan|vaapi|d3d11va|software`, `PUNKTFUNK_PRESENT_MODE=
|
## Decode rungs (M10: native only)
|
||||||
|
|
||||||
|
**This binary contains no FFmpeg.** `auto` walks native rungs — pf-vkdecode over Vulkan
|
||||||
|
Video, then the platform's own (pf-dxvadec on Windows, pf-vaadec on Linux), then the CPU
|
||||||
|
rung (openh264/rav1d). The libavcodec rungs that used to sit under each of them are
|
||||||
|
deleted, along with `pf-ffvk` and the `ffmpeg-next` dependency.
|
||||||
|
|
||||||
|
Two of the native rungs have never decoded a frame on real hardware (native VAAPI at all;
|
||||||
|
native D3D11VA's AV1 leg). They run anyway — with the libavcodec twins gone, the only
|
||||||
|
thing below them is the CPU, so barring them would cost the session hardware decode
|
||||||
|
outright rather than move it one rung down. What replaces the safety net is the log: every
|
||||||
|
session names the rung it landed on with its evidence state,
|
||||||
|
|
||||||
|
decode rung active rung=native-vulkan codec=HEVC hardware_verified=true evidence=...
|
||||||
|
|
||||||
|
…and that line is a **WARNING** when nothing has ever decoded a frame through the
|
||||||
|
rung/codec pair the session chose. `pf-client-core`'s `video.rs` module docs carry the full
|
||||||
|
table; read any field report about M10 against it.
|
||||||
|
|
||||||
|
Debug/bisect knobs: `PUNKTFUNK_DECODER=native-vulkan|native-vaapi|native-d3d11va|software`
|
||||||
|
(a pin skips the vendor order, which is how a lab run reaches a rung `auto` will not pick
|
||||||
|
on this device; a pinned rung that cannot open still falls through to the standard ladder,
|
||||||
|
loudly; `native-vaapi` also takes `PUNKTFUNK_VAAPI_DEVICE=/dev/dri/renderDNNN` to choose
|
||||||
|
the GPU). The pre-M10 spellings `vulkan`/`vaapi`/`d3d11va` named the libavcodec rungs
|
||||||
|
specifically; they are MIGRATED onto the native rung for the same hardware family, with a
|
||||||
|
`warn` line saying so — every desktop Settings UI offered those values, so refusing them
|
||||||
|
would end a session over a dropdown someone picked long ago.
|
||||||
|
`PUNKTFUNK_PRESENT_MODE=
|
||||||
mailbox|fifo|immediate|fifo_relaxed` (default MAILBOX, FIFO where the surface offers no
|
mailbox|fifo|immediate|fifo_relaxed` (default MAILBOX, FIFO where the surface offers no
|
||||||
MAILBOX — AMD on Windows), `PUNKTFUNK_VK_DEVICE=<index>` (multi-GPU), and
|
MAILBOX — AMD on Windows), `PUNKTFUNK_VK_DEVICE=<index>` (multi-GPU), and
|
||||||
`PUNKTFUNK_HW_FAULT=import` (fault every VAAPI dmabuf import — proves the three-strike
|
`PUNKTFUNK_HW_FAULT=import` (fault every VAAPI dmabuf import — proves the three-strike
|
||||||
demotion to software on healthy hardware).
|
demotion to software on healthy hardware).
|
||||||
|
|
||||||
|
`PUNKTFUNK_AU_FAULT=drop|truncate|flip[:period]` deliberately corrupts decoder input on the
|
||||||
|
native Vulkan lane (default period 60 — one AU a second at 60 fps; inert everywhere else, and
|
||||||
|
inert entirely if the value doesn't parse). `drop` swallows the AU, so the next one references a
|
||||||
|
picture that was never decoded — the bitstream planner catches it immediately. `truncate` delivers
|
||||||
|
a picture whose slice data stops mid-frame and `flip` alters one byte deep in the payload: both
|
||||||
|
parse perfectly, so only the driver's per-frame decode-status query can see them, and neither is
|
||||||
|
visible at all on a driver without `queryResultStatusSupport`. Watch the
|
||||||
|
result on the Detailed stats line's `integrity:` term (`damaged` = concealment the planner caught,
|
||||||
|
`refused` = AUs the decoder rejected outright, `driver-failed` = the hardware's own verdict, `run`
|
||||||
|
= consecutive frames with no picture, `worst run` = the longest such stretch of the session — the
|
||||||
|
once-a-second `run` sample misses the bad moment almost every time — and `no driver status` = this
|
||||||
|
device cannot answer the driver question at all). A session that lands on any other lane says so
|
||||||
|
in the log rather than faulting silently.
|
||||||
|
|
||||||
|
Note that `PUNKTFUNK_AU_DUMP` records the AU as it arrived from the HOST, while the fault injector
|
||||||
|
runs later, at the native decoder's own entry. On a faulted run the dump is therefore the clean
|
||||||
|
bitstream — reconstruct the damaged bytes from the spec if you need them (the injector is pure and
|
||||||
|
deterministic).
|
||||||
|
|||||||
@@ -346,9 +346,13 @@ mod session_main {
|
|||||||
bitrate_kbps: settings.bitrate_kbps,
|
bitrate_kbps: settings.bitrate_kbps,
|
||||||
audio_channels: settings.audio_channels,
|
audio_channels: settings.audio_channels,
|
||||||
preferred_codec: settings.preferred_codec(),
|
preferred_codec: settings.preferred_codec(),
|
||||||
|
// Nothing excluded on a fresh dial. Only the run loop's codec-fallback retry
|
||||||
|
// sets this, and it does so on a CLONE of these params — a Settings-level
|
||||||
|
// "never use HEVC" would be `preferred_codec`, not this.
|
||||||
|
exclude_codecs: 0,
|
||||||
// HDR off = don't advertise 10-bit/HDR at all; the host then never upgrades.
|
// HDR off = don't advertise 10-bit/HDR at all; the host then never upgrades.
|
||||||
// MULTI_SLICE is decoder truth for THIS embedder: every desktop decode stack
|
// MULTI_SLICE is decoder truth for THIS embedder: every desktop decode stack
|
||||||
// (FFmpeg software, VAAPI, D3D11VA, Vulkan Video) handles AUs carrying several
|
// (Vulkan Video, D3D11VA, VAAPI, openh264/rav1d) handles AUs carrying several
|
||||||
// slice NALs, so the host may keep its multi-slice low-latency default (§7 LN1).
|
// slice NALs, so the host may keep its multi-slice low-latency default (§7 LN1).
|
||||||
// The mobile/TV embedders must NOT copy this blindly — Amlogic MediaCodec wedges
|
// The mobile/TV embedders must NOT copy this blindly — Amlogic MediaCodec wedges
|
||||||
// on multi-slice AUs (see `VIDEO_CAP_MULTI_SLICE`), so they advertise per-decoder.
|
// on multi-slice AUs (see `VIDEO_CAP_MULTI_SLICE`), so they advertise per-decoder.
|
||||||
@@ -357,12 +361,14 @@ mod session_main {
|
|||||||
// HEVC, and a real GPU 4:4:4 encode probe, and answers the resolved chroma in the
|
// HEVC, and a real GPU 4:4:4 encode probe, and answers the resolved chroma in the
|
||||||
// Welcome BEFORE we build a decoder. Advertised whenever the user asks because
|
// Welcome BEFORE we build a decoder. Advertised whenever the user asks because
|
||||||
// every path can DISPLAY it: the Vulkan presenter samples the 2-plane 4:4:4 pool
|
// every path can DISPLAY it: the Vulkan presenter samples the 2-plane 4:4:4 pool
|
||||||
// formats (hardware RExt decode where the driver offers it — NVIDIA today) and
|
// formats (hardware RExt decode where the driver offers it — NVIDIA today),
|
||||||
// swscale converts anything else for the software rung, with the decoder ladder
|
// with the decoder ladder demoting on its own. No capability probe gates the
|
||||||
// demoting on its own. No capability probe gates the bit — software decode is the
|
// bit — but note (M8) that the software rung below it is 4:2:0 8-bit ONLY and
|
||||||
// guaranteed floor — but the cost is VISIBLE, not silent: the Detailed stats
|
// refuses anything else rather than mis-scaling it, so on a box whose hardware
|
||||||
// overlay prints the resolved chroma ("4:4:4→4:2:0" when the host declined) and
|
// 4:4:4 decode fails the floor is a codec fallback, not a converted picture.
|
||||||
// the decode path frames actually took.
|
// The cost stays VISIBLE, not silent: the Detailed stats overlay prints the
|
||||||
|
// resolved chroma ("4:4:4→4:2:0" when the host declined) and the decode path
|
||||||
|
// frames actually took.
|
||||||
video_caps: punktfunk_core::quic::VIDEO_CAP_MULTI_SLICE
|
video_caps: punktfunk_core::quic::VIDEO_CAP_MULTI_SLICE
|
||||||
| if settings.hdr_enabled {
|
| if settings.hdr_enabled {
|
||||||
punktfunk_core::quic::VIDEO_CAP_10BIT | punktfunk_core::quic::VIDEO_CAP_HDR
|
punktfunk_core::quic::VIDEO_CAP_10BIT | punktfunk_core::quic::VIDEO_CAP_HDR
|
||||||
@@ -488,7 +494,8 @@ mod session_main {
|
|||||||
///
|
///
|
||||||
/// RADV-only knob: ANV/NVIDIA/other drivers ignore `RADV_PERFTEST`, and a box where video
|
/// RADV-only knob: ANV/NVIDIA/other drivers ignore `RADV_PERFTEST`, and a box where video
|
||||||
/// decode is already the default just no-ops. Append rather than clobber so a user's own
|
/// decode is already the default just no-ops. Append rather than clobber so a user's own
|
||||||
/// `RADV_PERFTEST` survives; `PUNKTFUNK_DECODER=vaapi` still overrides the decoder choice.
|
/// `RADV_PERFTEST` survives; `PUNKTFUNK_DECODER=native-vaapi` still overrides the decoder
|
||||||
|
/// choice (the pre-M10 `vaapi` spelling reaches the same rung — it migrates, loudly).
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn enable_radv_video_decode() {
|
fn enable_radv_video_decode() {
|
||||||
const TOKEN: &str = "video_decode";
|
const TOKEN: &str = "video_decode";
|
||||||
@@ -530,6 +537,80 @@ mod session_main {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `--probe-decode`: per-adapter Vulkan Video decode capability, then exit. Human
|
||||||
|
// output on purpose — this is a triage tool, not a picker source, which is also
|
||||||
|
// why it is a separate flag: `--list-adapters` is parsed line-by-line by the
|
||||||
|
// desktop shells' GPU picker and must keep printing bare names.
|
||||||
|
if arg_flag("--probe-decode") {
|
||||||
|
return match pf_presenter::vk::probe_decode() {
|
||||||
|
Ok(adapters) => {
|
||||||
|
if adapters.is_empty() {
|
||||||
|
println!("no Vulkan physical devices");
|
||||||
|
}
|
||||||
|
for (i, a) in adapters.iter().enumerate() {
|
||||||
|
// The index IS the PUNKTFUNK_VK_DEVICE value, and entry 0 is what
|
||||||
|
// a default run presents on — the decoder shares that device, so
|
||||||
|
// on a hybrid box this line is usually the answer.
|
||||||
|
let kind = if a.discrete { "discrete" } else { "integrated" };
|
||||||
|
println!(
|
||||||
|
"[{i}] {} ({kind}){}",
|
||||||
|
a.name,
|
||||||
|
if i == 0 { " <- default presenter" } else { "" }
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" vulkan video decode: {}",
|
||||||
|
if a.usable { "YES" } else { "no" }
|
||||||
|
);
|
||||||
|
let codecs: Vec<&str> = [(0x1u32, "H.264"), (0x2, "H.265"), (0x4, "AV1")]
|
||||||
|
.iter()
|
||||||
|
.filter(|(bit, _)| a.codec_ops & bit != 0)
|
||||||
|
.map(|(_, n)| *n)
|
||||||
|
.collect();
|
||||||
|
println!(
|
||||||
|
" driver decode ops: {}",
|
||||||
|
if codecs.is_empty() {
|
||||||
|
format!("none (0x{:X})", a.codec_ops)
|
||||||
|
} else {
|
||||||
|
format!("{} (0x{:X})", codecs.join(", "), a.codec_ops)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if !a.usable {
|
||||||
|
// Say which conjunct failed. "no" with no reason is the thing
|
||||||
|
// this whole flag exists to stop.
|
||||||
|
let mut why: Vec<String> = Vec::new();
|
||||||
|
if !a.api_1_3 {
|
||||||
|
why.push("device is not Vulkan 1.3".into());
|
||||||
|
}
|
||||||
|
if !a.features_ok {
|
||||||
|
why.push(
|
||||||
|
"missing samplerYcbcrConversion / timelineSemaphore / \
|
||||||
|
synchronization2"
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if a.decode_family.is_none() {
|
||||||
|
why.push("no queue family advertises VIDEO_DECODE".into());
|
||||||
|
}
|
||||||
|
if !a.base_missing.is_empty() {
|
||||||
|
why.push(format!("missing {}", a.base_missing.join(", ")));
|
||||||
|
}
|
||||||
|
if a.codec_exts.is_empty() {
|
||||||
|
why.push("no VK_KHR_video_decode_{h264,h265,av1} extension".into());
|
||||||
|
}
|
||||||
|
println!(" why not: {}", why.join("; "));
|
||||||
|
} else {
|
||||||
|
println!(" extensions: {}", a.codec_exts.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("probe-decode: {e:#}");
|
||||||
|
EXIT_PRESENTER_FAILED
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// `--list-audio`: the PipeWire endpoints the settings pickers offer, as
|
// `--list-audio`: the PipeWire endpoints the settings pickers offer, as
|
||||||
// `sink|source<TAB>node.name<TAB>description` lines — a debug window into the
|
// `sink|source<TAB>node.name<TAB>description` lines — a debug window into the
|
||||||
// same enumeration the GTK shell probes.
|
// same enumeration the GTK shell probes.
|
||||||
|
|||||||
@@ -78,11 +78,6 @@ windows = { git = "https://github.com/microsoft/windows-rs", rev = "acb5a1a74410
|
|||||||
"winuser",
|
"winuser",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# FFmpeg — used only to enumerate which codecs this client can decode (probe::decodable_codecs),
|
|
||||||
# advertised to the host on the speed-test connect. Same pin as the host/Linux client. (Real
|
|
||||||
# decode + present live in the spawned punktfunk-session binary.)
|
|
||||||
ffmpeg-next = "8"
|
|
||||||
|
|
||||||
# Gamepad enumeration + pin persistence for Settings runs on pf-client-core's shared SDL service
|
# Gamepad enumeration + pin persistence for Settings runs on pf-client-core's shared SDL service
|
||||||
# (see the `gamepad` field in app/); the spawned punktfunk-session does the actual forwarding. SDL3
|
# (see the `gamepad` field in app/); the spawned punktfunk-session does the actual forwarding. SDL3
|
||||||
# itself (built from source via the bundled CMake on Windows) is pulled transitively by
|
# itself (built from source via the bundled CMake on Windows) is pulled transitively by
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
The native **Windows** app for streaming a punktfunk host to your PC. A modern WinUI 3 app that
|
The native **Windows** app for streaming a punktfunk host to your PC. A modern WinUI 3 app that
|
||||||
discovers hosts on your network, pairs with a PIN, and streams at your display's own resolution and
|
discovers hosts on your network, pairs with a PIN, and streams at your display's own resolution and
|
||||||
refresh rate — with a hardware-accelerated D3D11 video path and HDR.
|
refresh rate — with hardware-accelerated video decode and HDR.
|
||||||
|
|
||||||
It's **pure Rust**: the UI is WinUI 3 driven through [windows-reactor](https://github.com/microsoft/windows-rs)
|
It's **pure Rust**: the UI is WinUI 3 driven through [windows-reactor](https://github.com/microsoft/windows-rs)
|
||||||
(a declarative, React-like framework), and it links the shared **`punktfunk-core`** directly to speak
|
(a declarative, React-like framework), and it links the shared **`punktfunk-core`** directly to speak
|
||||||
@@ -10,9 +10,11 @@ the fast **`punktfunk/1`** protocol.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Hardware decode, GPU present** — FFmpeg HEVC with a **D3D11VA zero-copy path** (decoder and
|
- **Hardware decode, GPU present** — Punktfunk's own decoders, no FFmpeg anywhere in the client:
|
||||||
presenter share one D3D11 device; NV12/P010 textures sampled straight into a `SwapChainPanel`
|
**Vulkan Video** (`pf-vkdecode`) leads on NVIDIA and AMD, **D3D11VA** (`pf-dxvadec` driving
|
||||||
composition swapchain), with a robust software-decode fallback.
|
`ID3D11VideoDecoder`) leads on Intel, whichever isn't first is the fallback, and an
|
||||||
|
OpenH264/rav1d CPU rung is last. Either hardware rung hands its surface to the Vulkan presenter
|
||||||
|
without a CPU copy.
|
||||||
- **HDR10** — advertise 10-bit/HDR, detect PQ in-band, and flip the swapchain to `R10G10B10A2` +
|
- **HDR10** — advertise 10-bit/HDR, detect PQ in-band, and flip the swapchain to `R10G10B10A2` +
|
||||||
ST.2084 with HDR10 metadata.
|
ST.2084 with HDR10 metadata.
|
||||||
- **Your display's native mode** — the host builds a virtual display at exactly your WxH@Hz.
|
- **Your display's native mode** — the host builds a virtual display at exactly your WxH@Hz.
|
||||||
@@ -42,9 +44,9 @@ A stock [Moonlight](https://moonlight-stream.org/) client also works over GameSt
|
|||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
Windows-only (the crate builds as a stub on other platforms so the workspace stays green). You need
|
Windows-only (the crate builds as a stub on other platforms so the workspace stays green). You need
|
||||||
the MSVC toolchain, an `FFMPEG_DIR` FFmpeg tree, and CMake (SDL3 builds from source). The Windows
|
the MSVC toolchain and CMake (SDL3 builds from source) — nothing else: decode is native since M10,
|
||||||
App SDK runtime bootstrap is staged next to the exe by `windows-reactor-setup` from this crate's
|
so there is no `FFMPEG_DIR` to point anywhere, and the Windows App SDK runtime bootstrap is staged
|
||||||
own `build.rs` — no extra environment needed.
|
next to the exe by `windows-reactor-setup` from this crate's own `build.rs`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo build -p punktfunk-client-windows --target x86_64-pc-windows-msvc
|
cargo build -p punktfunk-client-windows --target x86_64-pc-windows-msvc
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,9 @@ touches the client (canary) and on `vX.Y.Z` release tags (stable) — see
|
|||||||
|
|
||||||
**Two architectures, one x64 runner.** Both `x64` and `arm64` packages are produced off the single
|
**Two architectures, one x64 runner.** Both `x64` and `arm64` packages are produced off the single
|
||||||
x64 Windows runner — `x86_64-pc-windows-msvc` builds natively, `aarch64-pc-windows-msvc` is
|
x64 Windows runner — `x86_64-pc-windows-msvc` builds natively, `aarch64-pc-windows-msvc` is
|
||||||
cross-compiled (the x64 MSVC toolset ships the ARM64 cross compiler; the matrix points `FFMPEG_DIR`
|
cross-compiled (the x64 MSVC toolset ships the ARM64 cross compiler; since M10 nothing in the
|
||||||
at the runner's ARM64 FFmpeg tree, `C:\Users\Public\ffmpeg-arm64`). Artifacts are arch-suffixed
|
package links FFmpeg, so neither arch needs a per-arch `FFMPEG_DIR` tree staged on the runner —
|
||||||
|
one less thing the ARM64 leg can be missing). Artifacts are arch-suffixed
|
||||||
(`..._x64.msix` / `..._arm64.msix`, each with its matching `.cer`); `pack-msix.ps1 -Arch x64|arm64`
|
(`..._x64.msix` / `..._arm64.msix`, each with its matching `.cer`); `pack-msix.ps1 -Arch x64|arm64`
|
||||||
stamps the manifest `ProcessorArchitecture` and names the output. See
|
stamps the manifest `ProcessorArchitecture` and names the output. See
|
||||||
[`windows.yml`](../../../.gitea/workflows/windows.yml) for the cross-build rationale.
|
[`windows.yml`](../../../.gitea/workflows/windows.yml) for the cross-build rationale.
|
||||||
@@ -25,10 +26,17 @@ stamps the manifest `ProcessorArchitecture` and names the output. See
|
|||||||
| `punktfunk-session.exe` | the release build — the Vulkan session client the shell spawns for every stream (sibling resolution, `src/spawn.rs`). Skia links statically; `vulkan-1.dll` is a GPU-driver component, never bundled. ARM64 builds it `--no-default-features` (no Skia console UI) until rust-skia ships aarch64-pc-windows-msvc prebuilts |
|
| `punktfunk-session.exe` | the release build — the Vulkan session client the shell spawns for every stream (sibling resolution, `src/spawn.rs`). Skia links statically; `vulkan-1.dll` is a GPU-driver component, never bundled. ARM64 builds it `--no-default-features` (no Skia console UI) until rust-skia ships aarch64-pc-windows-msvc prebuilts |
|
||||||
| `Microsoft.WindowsAppRuntime.Bootstrap.dll`, `resources.pri` | staged by the client's `build.rs` via `windows-reactor-setup::as_framework_dependent()` |
|
| `Microsoft.WindowsAppRuntime.Bootstrap.dll`, `resources.pri` | staged by the client's `build.rs` via `windows-reactor-setup::as_framework_dependent()` |
|
||||||
| `SDL3.dll` | auto-staged by the `sdl3` crate |
|
| `SDL3.dll` | auto-staged by the `sdl3` crate |
|
||||||
| `avcodec/avformat/avutil/swscale/swresample/...-*.dll` | `FFMPEG_DIR\bin` |
|
| `licenses\*` | the project's MIT/Apache texts + the generated `THIRD-PARTY-NOTICES.txt` (MSIX has no installer EULA page, so attribution ships as files) |
|
||||||
| `Assets\*.png` | checked-in tile/store logos (rasterized from `packaging/flatpak/io.unom.Punktfunk.svg`) |
|
| `Assets\*.png` | checked-in tile/store logos (rasterized from `packaging/flatpak/io.unom.Punktfunk.svg`) |
|
||||||
| `AppxManifest.xml` | the template here, with `{VERSION}`/`{PUBLISHER}` substituted |
|
| `AppxManifest.xml` | the template here, with `{VERSION}`/`{PUBLISHER}` substituted |
|
||||||
|
|
||||||
|
**No FFmpeg DLLs.** The client decodes natively since M10 (`pf-vkdecode` / `pf-dxvadec` /
|
||||||
|
OpenH264+rav1d — punktfunk-planning `design/client-native-decode.md` §6), so nothing here
|
||||||
|
link-imports `libav*` and the wildcard `avcodec/avformat/avutil/swscale/swresample-*.dll` copy is
|
||||||
|
gone, along with the FFmpeg LGPL notice that accompanied it — shipping that notice now would claim
|
||||||
|
a dependency the package doesn't have. The **host** installer is unchanged:
|
||||||
|
`packaging/windows/pack-host-installer.ps1` still ships those DLLs for its AMF/QSV encode path.
|
||||||
|
|
||||||
### Why an "unpackaged" WinUI app packages cleanly
|
### Why an "unpackaged" WinUI app packages cleanly
|
||||||
|
|
||||||
`main` calls `windows_reactor::bootstrap()`, which runs `MddBootstrapInitialize2` with
|
`main` calls `windows_reactor::bootstrap()`, which runs `MddBootstrapInitialize2` with
|
||||||
@@ -87,8 +95,7 @@ cargo build --release -p punktfunk-client-windows --target x86_64-pc-windows-msv
|
|||||||
pwsh -File clients/windows/packaging/pack-msix.ps1 `
|
pwsh -File clients/windows/packaging/pack-msix.ps1 `
|
||||||
-Version 0.2.0.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix
|
-Version 0.2.0.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix
|
||||||
|
|
||||||
# arm64 (cross-compiled; point FFMPEG_DIR at the ARM64 tree)
|
# arm64 (cross-compiled; no extra environment — the client links no FFmpeg)
|
||||||
$env:FFMPEG_DIR = 'C:\Users\Public\ffmpeg-arm64'
|
|
||||||
cargo build --release -p punktfunk-client-windows --target aarch64-pc-windows-msvc
|
cargo build --release -p punktfunk-client-windows --target aarch64-pc-windows-msvc
|
||||||
pwsh -File clients/windows/packaging/pack-msix.ps1 `
|
pwsh -File clients/windows/packaging/pack-msix.ps1 `
|
||||||
-Version 0.2.0.0 -Arch arm64 -TargetDir C:\t\aarch64-pc-windows-msvc\release -OutDir C:\t\msix
|
-Version 0.2.0.0 -Arch arm64 -TargetDir C:\t\aarch64-pc-windows-msvc\release -OutDir C:\t\msix
|
||||||
|
|||||||
@@ -4,9 +4,14 @@
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Builds a packaging layout from a release `cargo build` output (exe + the reactor/SDL3 auto-staged
|
Builds a packaging layout from a release `cargo build` output (exe + the reactor/SDL3 auto-staged
|
||||||
DLLs + resources.pri + FFmpeg DLLs + the checked-in Assets + the manifest), runs makeappx, and
|
DLLs + resources.pri + the checked-in Assets + the manifest), runs makeappx, and
|
||||||
signs with signtool. Idempotent; safe to re-run.
|
signs with signtool. Idempotent; safe to re-run.
|
||||||
|
|
||||||
|
NO FFmpeg DLLs since M10 (design/client-native-decode.md §6): the client decodes natively
|
||||||
|
(pf-vkdecode / pf-dxvadec / openh264+rav1d) and link-imports no libav* at all, so the
|
||||||
|
wildcard copy and its LGPL notice are gone with it. The HOST installer is unchanged —
|
||||||
|
packaging/windows/pack-host-installer.ps1 still ships them for its amf-qsv encode path.
|
||||||
|
|
||||||
Signing cert precedence:
|
Signing cert precedence:
|
||||||
1. -PfxBase64 / -PfxPassword (a real or shared code-signing cert, e.g. from CI secrets) — the
|
1. -PfxBase64 / -PfxPassword (a real or shared code-signing cert, e.g. from CI secrets) — the
|
||||||
cert's subject DN MUST match -Publisher (which is stamped into the manifest Identity).
|
cert's subject DN MUST match -Publisher (which is stamped into the manifest Identity).
|
||||||
@@ -22,8 +27,7 @@
|
|||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
# x64 (default arch):
|
# x64 (default arch):
|
||||||
pwsh -File pack-msix.ps1 -Version 0.2.137.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix
|
pwsh -File pack-msix.ps1 -Version 0.2.137.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix
|
||||||
# arm64 (point -TargetDir + FFMPEG_DIR at the ARM64 build/tree):
|
# arm64 (point -TargetDir at the ARM64 build):
|
||||||
$env:FFMPEG_DIR='C:\Users\Public\ffmpeg-arm64'
|
|
||||||
pwsh -File pack-msix.ps1 -Version 0.2.137.0 -Arch arm64 -TargetDir C:\t-a64\aarch64-pc-windows-msvc\release -OutDir C:\t-a64\msix
|
pwsh -File pack-msix.ps1 -Version 0.2.137.0 -Arch arm64 -TargetDir C:\t-a64\aarch64-pc-windows-msvc\release -OutDir C:\t-a64\msix
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
@@ -31,7 +35,6 @@ param(
|
|||||||
[Parameter(Mandatory = $true)][string]$Version, # 4-part numeric, e.g. 0.2.137.0
|
[Parameter(Mandatory = $true)][string]$Version, # 4-part numeric, e.g. 0.2.137.0
|
||||||
[Parameter(Mandatory = $true)][string]$TargetDir, # cargo --release output dir (has the exe)
|
[Parameter(Mandatory = $true)][string]$TargetDir, # cargo --release output dir (has the exe)
|
||||||
[ValidateSet('x64', 'arm64')][string]$Arch = 'x64', # package ProcessorArchitecture + artifact suffix
|
[ValidateSet('x64', 'arm64')][string]$Arch = 'x64', # package ProcessorArchitecture + artifact suffix
|
||||||
[string]$FfmpegBin = $(if ($env:FFMPEG_DIR) { Join-Path $env:FFMPEG_DIR 'bin' } else { 'C:\Users\Public\ffmpeg\bin' }),
|
|
||||||
[string]$OutDir = (Join-Path $TargetDir 'msix'),
|
[string]$OutDir = (Join-Path $TargetDir 'msix'),
|
||||||
[string]$Publisher = 'CN=unom', # MUST equal the signing cert subject DN
|
[string]$Publisher = 'CN=unom', # MUST equal the signing cert subject DN
|
||||||
[string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # optional: base64 of a code-signing .pfx
|
[string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # optional: base64 of a code-signing .pfx
|
||||||
@@ -83,28 +86,32 @@ foreach ($f in $required) {
|
|||||||
Copy-Item $src (Join-Path $layout $f) -Force
|
Copy-Item $src (Join-Path $layout $f) -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
# FFmpeg runtime DLLs (the exe link-imports the decode set; copy them all — small and correct).
|
# license/attribution payload (MSIX has no installer EULA page, so ship them as files): the
|
||||||
# These are unmodified BtbN *lgpl-shared* builds, linked dynamically (replaceable DLLs) — FFmpeg is
|
# project's own MIT/Apache texts plus the generated third-party notices, which is where every
|
||||||
# used under the LGPL v2.1+; the license text + notice ship in licenses\ below.
|
# vendored/statically-linked dependency's attribution lives (openh264 BSD-2, rav1d BSD-2, …).
|
||||||
$ff = Get-ChildItem -Path $FfmpegBin -Filter *.dll -ErrorAction SilentlyContinue
|
#
|
||||||
if (-not $ff) { throw "no FFmpeg DLLs in $FfmpegBin" }
|
# The FFmpeg LGPL notice + license texts that used to be copied here went with the DLLs at M10:
|
||||||
$ff | ForEach-Object { Copy-Item $_.FullName (Join-Path $layout $_.Name) -Force }
|
# nothing in this package links libav* any more, so shipping an LGPL notice would be claiming a
|
||||||
|
# dependency that is not there.
|
||||||
# license/attribution payload (MSIX has no installer EULA page, so ship them as files): FFmpeg's LGPL
|
#
|
||||||
# notice + license text, the project's own MIT/Apache texts, and the generated third-party notices.
|
# For the same reason the notices come from clients/windows/ and NOT from the repo root: the root
|
||||||
|
# file is workspace-wide, it is what the HOST ships out of, and it still lists ffmpeg-next plus the
|
||||||
|
# full FFmpeg licence text. The client-scoped file (same generator, `--packages
|
||||||
|
# punktfunk-client-windows,punktfunk-client-session,punktfunk-cli`) is the one that describes what
|
||||||
|
# is actually inside this .msix — and it is the same file the app's Licenses page shows.
|
||||||
$licDir = Join-Path $layout 'licenses'
|
$licDir = Join-Path $layout 'licenses'
|
||||||
New-Item -ItemType Directory -Force -Path $licDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $licDir | Out-Null
|
||||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
|
||||||
Copy-Item (Join-Path $repoRoot 'packaging\windows\licenses\FFmpeg-LGPL-NOTICE.txt') $licDir -Force -ErrorAction SilentlyContinue
|
$clientRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
foreach ($n in @('THIRD-PARTY-NOTICES.txt', 'LICENSE-MIT', 'LICENSE-APACHE')) {
|
foreach ($n in @('LICENSE-MIT', 'LICENSE-APACHE')) {
|
||||||
$p = Join-Path $repoRoot $n
|
$p = Join-Path $repoRoot $n
|
||||||
if (Test-Path $p) { Copy-Item $p $licDir -Force }
|
if (Test-Path $p) { Copy-Item $p $licDir -Force }
|
||||||
}
|
}
|
||||||
$ffRoot = Split-Path $FfmpegBin -Parent
|
$notices = Join-Path $clientRoot 'THIRD-PARTY-NOTICES.txt'
|
||||||
foreach ($lic in @('LICENSE.txt', 'LICENSE', 'COPYING.LGPLv2.1', 'COPYING.LGPLv3', 'COPYING.txt')) {
|
if (-not (Test-Path $notices)) {
|
||||||
$p = Join-Path $ffRoot $lic
|
throw "missing $notices — run scripts/gen-third-party-notices.sh (it generates the per-client copies)"
|
||||||
if (Test-Path $p) { Copy-Item $p $licDir -Force }
|
|
||||||
}
|
}
|
||||||
|
Copy-Item $notices $licDir -Force
|
||||||
|
|
||||||
# tile/store assets
|
# tile/store assets
|
||||||
Copy-Item (Join-Path $assets '*') (Join-Path $layout 'Assets') -Force
|
Copy-Item (Join-Path $assets '*') (Join-Path $layout 'Assets') -Force
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ const APP_LICENSE: &str = concat!(
|
|||||||
"\n\n================================ Apache-2.0 ================================\n\n",
|
"\n\n================================ Apache-2.0 ================================\n\n",
|
||||||
include_str!("../../../../LICENSE-APACHE"),
|
include_str!("../../../../LICENSE-APACHE"),
|
||||||
);
|
);
|
||||||
/// Third-party software notices for the linked Rust crates (generated by
|
/// Third-party software notices for the Rust crates THIS CLIENT links — the shell, the
|
||||||
/// scripts/gen-third-party-notices.sh; the MSIX also ships this under licenses/).
|
/// session streamer and the headless CLI (generated by scripts/gen-third-party-notices.sh;
|
||||||
const THIRD_PARTY_NOTICES: &str = include_str!("../../../../THIRD-PARTY-NOTICES.txt");
|
/// the MSIX ships the same file under licenses/).
|
||||||
|
///
|
||||||
|
/// Deliberately the client-scoped file and not the workspace-wide one at the repo root:
|
||||||
|
/// that root file is the HOST's, it still carries `ffmpeg-next` and the full FFmpeg licence
|
||||||
|
/// text — and after M10 this app bundles no FFmpeg at all, so printing that attribution
|
||||||
|
/// three lines under a card saying so would be a false statement to the user's face.
|
||||||
|
const THIRD_PARTY_NOTICES: &str = include_str!("../../THIRD-PARTY-NOTICES.txt");
|
||||||
|
|
||||||
pub(crate) fn licenses_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen>) -> Element {
|
pub(crate) fn licenses_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen>) -> Element {
|
||||||
let back_btn = button("Back").accent().icon(Symbol::Back).on_click({
|
let back_btn = button("Back").accent().icon(Symbol::Back).on_click({
|
||||||
@@ -46,9 +52,10 @@ pub(crate) fn licenses_page(ctx: &Arc<AppCtx>, set_screen: &AsyncSetState<Screen
|
|||||||
vstack((
|
vstack((
|
||||||
text_block("Bundled components").font_size(15.0).semibold(),
|
text_block("Bundled components").font_size(15.0).semibold(),
|
||||||
text_block(
|
text_block(
|
||||||
"FFmpeg is bundled under the LGPL v2.1+ (dynamically linked, replaceable DLLs); its \
|
"SDL 3 (Zlib) and the Windows App SDK (Microsoft) are linked; their notices ship \
|
||||||
license and notice ship in the installed licenses\\ folder. SDL 3 (Zlib) and the \
|
in the installed licenses\\ folder. Video decoding uses Windows' own DXVA and \
|
||||||
Windows App SDK (Microsoft) are also linked.",
|
Vulkan Video, with OpenH264 and rav1d (both BSD-2-Clause) as the CPU fallback — \
|
||||||
|
no FFmpeg is bundled.",
|
||||||
)
|
)
|
||||||
.font_size(12.0)
|
.font_size(12.0)
|
||||||
.wrap()
|
.wrap()
|
||||||
|
|||||||
@@ -45,12 +45,14 @@ fn render_scale_label(scale: f64) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Decode backend presets: `(stored value, display label)`.
|
/// Decode backend presets: `(stored value, display label)`.
|
||||||
// A stored legacy "hardware" (the D3D11VA era) matches no preset, so the combo shows
|
// A stored legacy value that matches no preset (the D3D11VA-era "hardware", and since M10
|
||||||
// Automatic — which is exactly how the session's decoder chain reads that value.
|
// the bare "vulkan"/"d3d11va" that named libavcodec's rungs) shows as Automatic — which is
|
||||||
|
// how the session's ladder reads "hardware", and near enough for the other two, which
|
||||||
|
// `pf_client_core::video::migrate_decoder_pref` maps onto the entries below anyway.
|
||||||
const DECODERS: &[(&str, &str)] = &[
|
const DECODERS: &[(&str, &str)] = &[
|
||||||
("auto", "Automatic (GPU, fall back to CPU)"),
|
("auto", "Automatic (GPU, fall back to CPU)"),
|
||||||
("vulkan", "Hardware (Vulkan Video)"),
|
("native-vulkan", "Hardware (Vulkan Video)"),
|
||||||
("d3d11va", "Hardware (Direct3D 11 / DXVA)"),
|
("native-d3d11va", "Hardware (Direct3D 11 / DXVA)"),
|
||||||
("software", "Software (CPU)"),
|
("software", "Software (CPU)"),
|
||||||
];
|
];
|
||||||
/// Audio channel presets: `(channel count, display label)`. The host clamps to what it can
|
/// Audio channel presets: `(channel count, display label)`. The host clamps to what it can
|
||||||
@@ -862,7 +864,11 @@ pub(crate) fn settings_page(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- Video -----------------------------------------------------------------------------
|
// --- Video -----------------------------------------------------------------------------
|
||||||
let (dec_names, dec_i) = presets(DECODERS, |v| *v == s.decoder);
|
// Migrated for the LOOKUP only (the store is left alone): a pre-M10 settings file
|
||||||
|
// holds `vulkan`/`d3d11va`, which match no preset — the combo would show Automatic and
|
||||||
|
// a save would silently rewrite the user's hardware preference to `auto`.
|
||||||
|
let stored_decoder = pf_client_core::video::migrate_decoder_pref(&s.decoder);
|
||||||
|
let (dec_names, dec_i) = presets(DECODERS, |v| *v == stored_decoder);
|
||||||
let decoder_combo = setting_combo(ctx, scope, (rev, set_rev), dec_names, dec_i, |s, i| {
|
let decoder_combo = setting_combo(ctx, scope, (rev, set_rev), dec_names, dec_i, |s, i| {
|
||||||
s.decoder = DECODERS[i].0.to_string();
|
s.decoder = DECODERS[i].0.to_string();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,8 +81,13 @@ pub(crate) fn session_page(ctx: &Arc<super::AppCtx>, hud: &HudSample) -> Element
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|c| !c.is_empty())
|
.filter(|c| !c.is_empty())
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
|
// The `stats:` decode-path tags (see pf-client-core's session
|
||||||
|
// pump). M10 removed the `vulkan`/`vaapi`/`d3d11va` tags with their
|
||||||
|
// rungs; a hardware rung is now always a `native-*` one.
|
||||||
let kind = match c {
|
let kind = match c {
|
||||||
"vulkan" | "vaapi" => Pill::Good,
|
"native-vulkan" | "native-vaapi" | "native-d3d11va" | "pyrowave" => {
|
||||||
|
Pill::Good
|
||||||
|
}
|
||||||
"software" => Pill::Info,
|
"software" => Pill::Info,
|
||||||
_ => Pill::Neutral,
|
_ => Pill::Neutral,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ fn run_headless_cli(args: &[String], identity: (String, String)) {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn discover_and_print() {
|
fn discover_and_print() {
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
println!("Browsing the LAN for punktfunk hosts (~5 s)…");
|
println!("Browsing the LAN for Punktfunk hosts (~5 s)…");
|
||||||
let (rx, _rescan) = discovery::browse();
|
let (rx, _rescan) = discovery::browse();
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
|||||||
@@ -6,26 +6,27 @@
|
|||||||
//! over the real data plane, so it stays here. [`decodable_codecs`] rode along for the same
|
//! over the real data plane, so it stays here. [`decodable_codecs`] rode along for the same
|
||||||
//! reason — the probe connect still advertises which codecs this client can decode.
|
//! reason — the probe connect still advertises which codecs this client can decode.
|
||||||
|
|
||||||
use ffmpeg_next as ffmpeg;
|
|
||||||
use punktfunk_core::client::NativeClient;
|
use punktfunk_core::client::NativeClient;
|
||||||
use punktfunk_core::config::{CompositorPref, GamepadPref, Mode};
|
use punktfunk_core::config::{CompositorPref, GamepadPref, Mode};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
/// The `quic` codec bitfield this client can decode — whatever FFmpeg has a decoder for (HEVC/H.264
|
/// The `quic` codec bitfield this client can decode. Advertised to the host so it never emits
|
||||||
/// always; AV1 when built in). Advertised to the host so it never emits a codec we can't decode.
|
/// a codec we can't decode.
|
||||||
|
///
|
||||||
|
/// It is pf-client-core's [`decodable_codecs`](pf_client_core::video::decodable_codecs) —
|
||||||
|
/// the codecs the SESSION BINARY's rungs speak, which is the process that actually decodes.
|
||||||
|
/// This shell used to walk libavcodec's registry (`ffmpeg::decoder::find` per id) for the
|
||||||
|
/// same answer; that was wrong in two ways even before M10 deleted the dependency. It
|
||||||
|
/// described the decoders in THIS process, which decodes nothing, and it answered "a
|
||||||
|
/// decoder exists" where the question is "a rung can keep up" — the AV1-on-CPU promise
|
||||||
|
/// `decodable_codecs_for` exists to refuse.
|
||||||
|
///
|
||||||
|
/// ⚠ Deliberately the DEVICE-FREE answer ([`decodable_codecs`], not
|
||||||
|
/// `decodable_codecs_for`): this connect creates no presenter and has no `VulkanDecodeDevice`
|
||||||
|
/// to gate AV1 on, and it decodes nothing — the codec it advertises is never exercised. A
|
||||||
|
/// real session's Hello is built in the session binary, with the device in hand.
|
||||||
pub fn decodable_codecs() -> u8 {
|
pub fn decodable_codecs() -> u8 {
|
||||||
let _ = ffmpeg::init();
|
pf_client_core::video::decodable_codecs()
|
||||||
let mut bits = 0u8;
|
|
||||||
for (id, bit) in [
|
|
||||||
(ffmpeg::codec::Id::HEVC, punktfunk_core::quic::CODEC_HEVC),
|
|
||||||
(ffmpeg::codec::Id::H264, punktfunk_core::quic::CODEC_H264),
|
|
||||||
(ffmpeg::codec::Id::AV1, punktfunk_core::quic::CODEC_AV1),
|
|
||||||
] {
|
|
||||||
if ffmpeg::decoder::find(id).is_some() {
|
|
||||||
bits |= bit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bits
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blocking speed-test probe (the GUI's per-host "Test" and the `--headless --speed-test` CLI):
|
/// Blocking speed-test probe (the GUI's per-host "Test" and the `--headless --speed-test` CLI):
|
||||||
|
|||||||
@@ -46,8 +46,8 @@
|
|||||||
{
|
{
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"name": "FFmpeg",
|
"name": "FFmpeg",
|
||||||
"version": "7.x/8.x (system-provided on Linux; replaceable DLLs bundled with the Windows packages)",
|
"version": "7.x/8.x (HOST only \u2014 system-provided on Linux; replaceable DLLs bundled with the Windows host installer)",
|
||||||
"description": "Dynamically linked libav* decode/encode; LGPL notice at packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt",
|
"description": "Dynamically linked libav* ENCODE for the host (pf-encode: NVENC-libav, VAAPI, AMF/QSV); LGPL notice at packaging/windows/licenses/FFmpeg-LGPL-NOTICE.txt. No punktfunk CLIENT links FFmpeg since M10 \u2014 client decode is Vulkan Video / DXVA / VAAPI / VideoToolbox / MediaCodec with openh264 + rav1d as the CPU floor.",
|
||||||
"licenses": [{ "license": { "id": "LGPL-2.1-or-later" } }],
|
"licenses": [{ "license": { "id": "LGPL-2.1-or-later" } }],
|
||||||
"externalReferences": [{ "type": "website", "url": "https://ffmpeg.org" }]
|
"externalReferences": [{ "type": "website", "url": "https://ffmpeg.org" }]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "pf-bitstream"
|
||||||
|
description = "Client-side bitstream layer for native decode: AU parsing, POC/DPB/reference derivation and per-AU DecodePlans (H.264/HEVC/AV1) on the vendored cros-codecs parsers — the layer libavcodec used to be (design/client-native-decode.md §3.1)"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cros-codecs = { path = "vendor/cros-codecs" }
|
||||||
|
tracing = "0.1"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
|||||||
|
//! The client's bitstream layer for native decode (design/client-native-decode.md §3.1):
|
||||||
|
//! everything a stateless hardware decoder needs to know about an AU before submission —
|
||||||
|
//! parsed headers, POC, DPB state, reference lists (including MMCO/LTR, which the hosts'
|
||||||
|
//! RFI recovery actively uses), recovery-point SEI — derived once here and consumed by
|
||||||
|
//! every backend (Vulkan `StdVideo*`, DXVA picparams, libva buffers).
|
||||||
|
//!
|
||||||
|
//! Parsing primitives come from the vendored cros-codecs parser layer
|
||||||
|
//! (`vendor/cros-codecs`, see its PROVENANCE.md); this crate owns what upstream keeps in
|
||||||
|
//! its Linux-only `decoder::stateless` half — the per-AU orchestration — plus the pieces
|
||||||
|
//! upstream lacks (SEI payload parsing: their parsers classify SEI NALUs but never read
|
||||||
|
//! them).
|
||||||
|
//!
|
||||||
|
//! Scope discipline: punktfunk clients decode punktfunk hosts — zero-reorder, no
|
||||||
|
//! B-frames, progressive, parameter sets from encoders we control. Implement to spec
|
||||||
|
//! where cheap; reject-with-log outside that envelope rather than half-decode.
|
||||||
|
//!
|
||||||
|
//! Nothing in this crate may touch a GPU API, an OS handle, or the network: CPU-only by
|
||||||
|
//! construction, so its tests run on every CI leg including macOS. And no `unsafe`,
|
||||||
|
//! compiler-enforced — this layer exists to replace C parsers; it does not get to
|
||||||
|
//! reintroduce their failure mode.
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub mod av1;
|
||||||
|
pub mod h264;
|
||||||
|
pub mod h265;
|
||||||
|
pub mod sei;
|
||||||
|
|
||||||
|
// The vendor-pinning smoke tests below assert against byte counts and golden values from
|
||||||
|
// the vendored snapshot's own test vectors; a cros-codecs re-sync that shifts parser
|
||||||
|
// behavior must trip HERE, in our tree, not in a decode session.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod vendor_smoke {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use cros_codecs::bitstream_utils::IvfIterator;
|
||||||
|
use cros_codecs::codec::av1::parser::ObuAction;
|
||||||
|
use cros_codecs::codec::av1::parser::ParsedObu;
|
||||||
|
use cros_codecs::codec::h264::parser::Nalu as H264Nalu;
|
||||||
|
use cros_codecs::codec::h264::parser::Parser as H264Parser;
|
||||||
|
use cros_codecs::codec::h265::parser::Nalu as H265Nalu;
|
||||||
|
use cros_codecs::codec::h265::parser::Parser as H265Parser;
|
||||||
|
|
||||||
|
const H264_25FPS: &[u8] =
|
||||||
|
include_bytes!("../vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264");
|
||||||
|
const H265_25FPS: &[u8] =
|
||||||
|
include_bytes!("../vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265");
|
||||||
|
const AV1_25FPS: &[u8] =
|
||||||
|
include_bytes!("../vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1");
|
||||||
|
const VP9_25FPS: &[u8] =
|
||||||
|
include_bytes!("../vendor/cros-codecs/src/codec/vp9/test_data/test-25fps.vp9");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn h264_parses_the_vendored_vector_to_its_goldens() {
|
||||||
|
let mut cursor = Cursor::new(H264_25FPS);
|
||||||
|
let mut parser = H264Parser::default();
|
||||||
|
let (mut nalus, mut sps, mut slices) = (0u32, 0u32, 0u32);
|
||||||
|
let mut coded = (0u32, 0u32);
|
||||||
|
while let Ok(nalu) = H264Nalu::next(&mut cursor) {
|
||||||
|
nalus += 1;
|
||||||
|
if let Ok(s) = parser.parse_sps(&nalu) {
|
||||||
|
sps += 1;
|
||||||
|
coded = (
|
||||||
|
(s.pic_width_in_mbs_minus1 as u32 + 1) * 16,
|
||||||
|
(s.pic_height_in_map_units_minus1 as u32 + 1) * 16,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if parser.parse_pps(&nalu).is_ok() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if parser.parse_slice_header(nalu).is_ok() {
|
||||||
|
slices += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 759 is upstream's own golden for this stream (chromium h264_parser_unittest lineage).
|
||||||
|
assert_eq!(nalus, 759);
|
||||||
|
assert_eq!(sps, 4);
|
||||||
|
assert_eq!(slices, 500);
|
||||||
|
assert_eq!(coded, (320, 240));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn h265_parses_the_vendored_vector() {
|
||||||
|
let mut cursor = Cursor::new(H265_25FPS);
|
||||||
|
let mut parser = H265Parser::default();
|
||||||
|
let (mut nalus, mut sps, mut slices) = (0u32, 0u32, 0u32);
|
||||||
|
while let Ok(nalu) = H265Nalu::next(&mut cursor) {
|
||||||
|
nalus += 1;
|
||||||
|
if parser.parse_sps(&nalu).is_ok() {
|
||||||
|
sps += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if parser.parse_pps(&nalu).is_ok() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if parser.parse_slice_header(nalu).is_ok() {
|
||||||
|
slices += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(nalus, 254);
|
||||||
|
assert_eq!(sps, 1);
|
||||||
|
assert_eq!(slices, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn av1_walks_obus_and_maintains_ref_slots_across_the_stream() {
|
||||||
|
let mut parser = cros_codecs::codec::av1::parser::Parser::default();
|
||||||
|
let (mut obus, mut frames) = (0u32, 0u32);
|
||||||
|
for packet in IvfIterator::new(AV1_25FPS) {
|
||||||
|
let mut consumed = 0;
|
||||||
|
while let Ok(action) = parser.read_obu(&packet[consumed..]) {
|
||||||
|
let obu = match action {
|
||||||
|
ObuAction::Process(obu) => obu,
|
||||||
|
ObuAction::Drop(n) => {
|
||||||
|
consumed += n as usize;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
consumed += obu.bytes_used;
|
||||||
|
obus += 1;
|
||||||
|
// `ref_frame_update` is the parser's ref-slot bookkeeping; without it,
|
||||||
|
// inter frames fail with "Reference is invalid" — the parser validates
|
||||||
|
// reference integrity rather than trusting the stream.
|
||||||
|
match parser.parse_obu(obu).expect("parse_obu") {
|
||||||
|
ParsedObu::FrameHeader(fh) => {
|
||||||
|
frames += 1;
|
||||||
|
parser.ref_frame_update(&fh).expect("ref slot update");
|
||||||
|
}
|
||||||
|
ParsedObu::Frame(f) => {
|
||||||
|
frames += 1;
|
||||||
|
parser.ref_frame_update(&f.header).expect("ref slot update");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 525 is upstream's own golden (cross-checked against GStreamer's OBU walk).
|
||||||
|
assert_eq!(obus, 525);
|
||||||
|
assert_eq!(frames, 274);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vp9_splits_superframes_and_parses_headers() {
|
||||||
|
let mut parser = cros_codecs::codec::vp9::parser::Parser::default();
|
||||||
|
let (mut chunks, mut frames) = (0u32, 0u32);
|
||||||
|
for packet in IvfIterator::new(VP9_25FPS) {
|
||||||
|
chunks += 1;
|
||||||
|
frames += parser
|
||||||
|
.parse_chunk(packet.as_ref())
|
||||||
|
.expect("vp9 chunk")
|
||||||
|
.len() as u32;
|
||||||
|
}
|
||||||
|
assert_eq!(chunks, 250);
|
||||||
|
// > chunks proves superframe splitting engaged.
|
||||||
|
assert_eq!(frames, 269);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
//! SEI payload parsing — the piece the vendored parser layer lacks: upstream classifies
|
||||||
|
//! SEI NALUs but never reads a payload. punktfunk needs exactly one payload type per
|
||||||
|
//! codec: the recovery point SEI, which hosts emit on RFI recovery so the client knows
|
||||||
|
//! where a decode-from-here point lands. Every other payload type is skipped by its
|
||||||
|
//! declared size.
|
||||||
|
//!
|
||||||
|
//! Both codecs put the recovery point at payload type 6 with the same D.1 message
|
||||||
|
//! framing, but the payload syntax differs: H.264 (D.1.8/D.2.8) counts recovery in
|
||||||
|
//! `frame_num` increments (`recovery_frame_cnt`, ue(v)) and carries a slice-group bit
|
||||||
|
//! pair; H.265 (D.2.8/D.3.8) counts in picture order (`recovery_poc_cnt`, se(v) — it
|
||||||
|
//! can be negative) and has no slice-group field. Hence two parsers over one shared
|
||||||
|
//! message walk.
|
||||||
|
|
||||||
|
/// Recovery point SEI (D.2.8).
|
||||||
|
///
|
||||||
|
/// `recovery_frame_cnt` counts in `frame_num` increments from the AU carrying the SEI to
|
||||||
|
/// the picture at which output is exact (`exact_match`) or approximate. `broken_link` set
|
||||||
|
/// means pictures before the recovery point may be visually broken and must not be shown.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct RecoveryPoint {
|
||||||
|
pub recovery_frame_cnt: u32,
|
||||||
|
pub exact_match: bool,
|
||||||
|
pub broken_link: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recovery point SEI, H.265 flavour (D.3.8).
|
||||||
|
///
|
||||||
|
/// `recovery_poc_cnt` is the POC delta from the picture carrying the SEI to the
|
||||||
|
/// recovery-point picture — se(v)-coded, so unlike H.264's `recovery_frame_cnt` it can
|
||||||
|
/// be NEGATIVE (a recovery point among leading pictures). `exact_match`/`broken_link`
|
||||||
|
/// keep their H.264 semantics.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct RecoveryPointHevc {
|
||||||
|
pub recovery_poc_cnt: i32,
|
||||||
|
pub exact_match: bool,
|
||||||
|
pub broken_link: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the first recovery point SEI message out of an H.264 SEI NALU.
|
||||||
|
///
|
||||||
|
/// `sei_payload` are the bytes of the NALU after its one-byte NAL header, emulation
|
||||||
|
/// prevention bytes still in place (they are removed here — 7.4.1 RBSP extraction).
|
||||||
|
/// `Ok(None)` means the NALU parsed cleanly but carries no recovery point.
|
||||||
|
pub fn parse_recovery_point(sei_payload: &[u8]) -> Result<Option<RecoveryPoint>, String> {
|
||||||
|
let rbsp = strip_emulation_prevention(sei_payload);
|
||||||
|
let Some(payload) = first_recovery_point_payload(&rbsp)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut r = BitCursor::new(payload);
|
||||||
|
let recovery_frame_cnt = r.read_ue()?;
|
||||||
|
let exact_match = r.read_bit()? != 0;
|
||||||
|
let broken_link = r.read_bit()? != 0;
|
||||||
|
// changing_slice_group_idc u(2): parsed to keep the reader honest, unused —
|
||||||
|
// slice groups are outside every profile punktfunk hosts emit.
|
||||||
|
let _changing_slice_group_idc = r.read_bits(2)?;
|
||||||
|
Ok(Some(RecoveryPoint {
|
||||||
|
recovery_frame_cnt,
|
||||||
|
exact_match,
|
||||||
|
broken_link,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the first recovery point SEI message out of an H.265 prefix SEI NALU.
|
||||||
|
///
|
||||||
|
/// `sei_payload` are the bytes of the NALU after its TWO-byte NAL header (H.265 NALU
|
||||||
|
/// headers are 16 bits), emulation prevention still in place. Only prefix SEI NALUs
|
||||||
|
/// (type 39) can carry a recovery point — D.2.1 lists it as prefix-only, so suffix SEI
|
||||||
|
/// NALUs (type 40) need never reach here.
|
||||||
|
pub fn parse_recovery_point_hevc(sei_payload: &[u8]) -> Result<Option<RecoveryPointHevc>, String> {
|
||||||
|
let rbsp = strip_emulation_prevention(sei_payload);
|
||||||
|
let Some(payload) = first_recovery_point_payload(&rbsp)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut r = BitCursor::new(payload);
|
||||||
|
let recovery_poc_cnt = r.read_se()?;
|
||||||
|
let exact_match = r.read_bit()? != 0;
|
||||||
|
let broken_link = r.read_bit()? != 0;
|
||||||
|
Ok(Some(RecoveryPointHevc {
|
||||||
|
recovery_poc_cnt,
|
||||||
|
exact_match,
|
||||||
|
broken_link,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the D.1 SEI message framing (shared verbatim between H.264 and H.265) and
|
||||||
|
/// return the payload bytes of the first recovery point message (payload type 6 in
|
||||||
|
/// both codecs), if any. `rbsp` is already emulation-prevention-stripped.
|
||||||
|
fn first_recovery_point_payload(rbsp: &[u8]) -> Result<Option<&[u8]>, String> {
|
||||||
|
let mut i = 0usize;
|
||||||
|
while i < rbsp.len() && !is_rbsp_trailing(rbsp, i) {
|
||||||
|
// D.1: payload type and size are ff-coded — 0xFF bytes each add 255 until a
|
||||||
|
// non-0xFF byte terminates the value. The run length is unbounded, so the type
|
||||||
|
// accumulates saturating: an adversarial ~16M-byte 0xFF run must not overflow
|
||||||
|
// (a saturated type simply never matches 6). The size accumulator is a usize
|
||||||
|
// whose use is bounds-checked below.
|
||||||
|
let mut payload_type = 0u32;
|
||||||
|
while i < rbsp.len() && rbsp[i] == 0xFF {
|
||||||
|
payload_type = payload_type.saturating_add(255);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i >= rbsp.len() {
|
||||||
|
return Err("truncated SEI payload type".into());
|
||||||
|
}
|
||||||
|
payload_type = payload_type.saturating_add(u32::from(rbsp[i]));
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
let mut payload_size = 0usize;
|
||||||
|
while i < rbsp.len() && rbsp[i] == 0xFF {
|
||||||
|
payload_size += 255;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i >= rbsp.len() {
|
||||||
|
return Err("truncated SEI payload size".into());
|
||||||
|
}
|
||||||
|
payload_size += usize::from(rbsp[i]);
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
let end = i
|
||||||
|
.checked_add(payload_size)
|
||||||
|
.filter(|&end| end <= rbsp.len())
|
||||||
|
.ok_or_else(|| "SEI payload overruns the NALU".to_string())?;
|
||||||
|
|
||||||
|
if payload_type == 6 {
|
||||||
|
return Ok(Some(&rbsp[i..end]));
|
||||||
|
}
|
||||||
|
|
||||||
|
i = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 7.4.1: within the RBSP, `00 00 03` encodes two zero bytes; the `03` is the emulation
|
||||||
|
/// prevention byte and is dropped.
|
||||||
|
fn strip_emulation_prevention(data: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(data.len());
|
||||||
|
let mut zeros = 0usize;
|
||||||
|
for &byte in data {
|
||||||
|
if zeros >= 2 && byte == 0x03 {
|
||||||
|
zeros = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
zeros = if byte == 0 { zeros + 1 } else { 0 };
|
||||||
|
out.push(byte);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `more_rbsp_data()` at a byte-aligned message boundary: the remainder is trailing bits
|
||||||
|
/// iff it is the stop bit (0x80) followed by nothing but zero bytes.
|
||||||
|
fn is_rbsp_trailing(rbsp: &[u8], i: usize) -> bool {
|
||||||
|
rbsp[i] == 0x80 && rbsp[i + 1..].iter().all(|&b| b == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal MSB-first bit reader over an already-unescaped RBSP slice. The vendored
|
||||||
|
/// `BitReader` is `pub(crate)` to the vendored crate, so this crate carries its own.
|
||||||
|
struct BitCursor<'a> {
|
||||||
|
data: &'a [u8],
|
||||||
|
/// Position in bits from the start of `data`.
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BitCursor<'a> {
|
||||||
|
fn new(data: &'a [u8]) -> Self {
|
||||||
|
Self { data, pos: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bit(&mut self) -> Result<u32, String> {
|
||||||
|
let byte = *self
|
||||||
|
.data
|
||||||
|
.get(self.pos / 8)
|
||||||
|
.ok_or("SEI payload out of bits")?;
|
||||||
|
let bit = (byte >> (7 - self.pos % 8)) & 1;
|
||||||
|
self.pos += 1;
|
||||||
|
Ok(u32::from(bit))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bits(&mut self, count: usize) -> Result<u32, String> {
|
||||||
|
debug_assert!(count <= 31);
|
||||||
|
let mut out = 0u32;
|
||||||
|
for _ in 0..count {
|
||||||
|
out = (out << 1) | self.read_bit()?;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ue(v), spec 9.1.
|
||||||
|
fn read_ue(&mut self) -> Result<u32, String> {
|
||||||
|
let mut leading_zeros = 0usize;
|
||||||
|
while self.read_bit()? == 0 {
|
||||||
|
leading_zeros += 1;
|
||||||
|
if leading_zeros > 31 {
|
||||||
|
return Err("invalid exp-Golomb code in SEI payload".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let suffix = self.read_bits(leading_zeros)?;
|
||||||
|
((1u32 << leading_zeros) - 1)
|
||||||
|
.checked_add(suffix)
|
||||||
|
.ok_or_else(|| "exp-Golomb value overflows u32".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// se(v), spec 9.1.1: the ue(v) code point k maps to (−1)^(k+1) · ⌈k/2⌉.
|
||||||
|
fn read_se(&mut self) -> Result<i32, String> {
|
||||||
|
let k = self.read_ue()?;
|
||||||
|
let magnitude = k.div_ceil(2);
|
||||||
|
let magnitude =
|
||||||
|
i32::try_from(magnitude).map_err(|_| "exp-Golomb value overflows i32".to_string())?;
|
||||||
|
Ok(if k % 2 == 1 { magnitude } else { -magnitude })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_minimal_recovery_point_message_parses_to_its_field_values() {
|
||||||
|
// Message: type 6, size 1. Payload bits: ue(0)='1', exact=0, broken=0, csg=00,
|
||||||
|
// then payload alignment '1' + zeros -> 0b1000_0100. NALU trailing 0x80.
|
||||||
|
let sei = [0x06, 0x01, 0x84, 0x80];
|
||||||
|
assert_eq!(
|
||||||
|
parse_recovery_point(&sei).unwrap(),
|
||||||
|
Some(RecoveryPoint {
|
||||||
|
recovery_frame_cnt: 0,
|
||||||
|
exact_match: false,
|
||||||
|
broken_link: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recovery_frame_cnt_and_both_flags_round_trip_through_the_bit_reader() {
|
||||||
|
// ue(5)='00110', exact=1, broken=1, csg=00, alignment -> 0b0011_0110 0b0100_0000.
|
||||||
|
let sei = [0x06, 0x02, 0x36, 0x40, 0x80];
|
||||||
|
assert_eq!(
|
||||||
|
parse_recovery_point(&sei).unwrap(),
|
||||||
|
Some(RecoveryPoint {
|
||||||
|
recovery_frame_cnt: 5,
|
||||||
|
exact_match: true,
|
||||||
|
broken_link: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn earlier_messages_and_ff_coded_types_are_skipped_to_reach_the_recovery_point() {
|
||||||
|
// First message: ff-coded payload type 255 (0xFF 0x00), size 1, payload 0x55.
|
||||||
|
// Second message: type 5 (user data), size 3. Third: the recovery point.
|
||||||
|
let sei = [
|
||||||
|
0xFF, 0x00, 0x01, 0x55, // type 255
|
||||||
|
0x05, 0x03, 0xAA, 0xBB, 0xCC, // type 5
|
||||||
|
0x06, 0x01, 0x84, // recovery point
|
||||||
|
0x80,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
parse_recovery_point(&sei).unwrap(),
|
||||||
|
Some(RecoveryPoint {
|
||||||
|
recovery_frame_cnt: 0,
|
||||||
|
exact_match: false,
|
||||||
|
broken_link: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emulation_prevention_bytes_inside_the_payload_are_removed_before_reading() {
|
||||||
|
// Unescaped payload (7 bytes): ue with a 22-zero prefix => recovery_frame_cnt
|
||||||
|
// 2^22-1 = 4194303, exact=1, broken=0, csg=00, alignment. Its first bytes are
|
||||||
|
// 00 00 02, which the escaper must have written as 00 00 03 02 on the wire.
|
||||||
|
let sei = [
|
||||||
|
0x06, 0x07, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x04, 0x40, 0x80,
|
||||||
|
];
|
||||||
|
assert!(sei.windows(3).any(|w| w == [0x00, 0x00, 0x03]));
|
||||||
|
assert_eq!(
|
||||||
|
parse_recovery_point(&sei).unwrap(),
|
||||||
|
Some(RecoveryPoint {
|
||||||
|
recovery_frame_cnt: 4194303,
|
||||||
|
exact_match: true,
|
||||||
|
broken_link: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_sei_nalu_without_a_recovery_point_yields_none_not_an_error() {
|
||||||
|
let sei = [0x05, 0x01, 0x00, 0x80];
|
||||||
|
assert_eq!(parse_recovery_point(&sei).unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_payload_size_overrunning_the_nalu_is_a_parse_error() {
|
||||||
|
let sei = [0x06, 0x0A, 0x00];
|
||||||
|
assert!(parse_recovery_point(&sei).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_hevc_recovery_point_parses_its_se_coded_poc_count() {
|
||||||
|
// recovery_poc_cnt se(0) = '1', exact = 0, broken = 0, payload alignment:
|
||||||
|
// 0b1001_0000.
|
||||||
|
let sei = [0x06, 0x01, 0x90, 0x80];
|
||||||
|
assert_eq!(
|
||||||
|
parse_recovery_point_hevc(&sei).unwrap(),
|
||||||
|
Some(RecoveryPointHevc {
|
||||||
|
recovery_poc_cnt: 0,
|
||||||
|
exact_match: false,
|
||||||
|
broken_link: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// se(-1) = '011' (ue code point 2), exact = 1, broken = 0, alignment:
|
||||||
|
// 0b0111_0100 — the negative range H.264's ue(v) syntax cannot express.
|
||||||
|
let sei = [0x06, 0x01, 0x74, 0x80];
|
||||||
|
assert_eq!(
|
||||||
|
parse_recovery_point_hevc(&sei).unwrap(),
|
||||||
|
Some(RecoveryPointHevc {
|
||||||
|
recovery_poc_cnt: -1,
|
||||||
|
exact_match: true,
|
||||||
|
broken_link: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_hevc_parser_skips_earlier_messages_and_reports_absence_as_none() {
|
||||||
|
// User-data message first, then the recovery point (poc_cnt se(3): ue code
|
||||||
|
// point 5 = '00110', exact = 1, broken = 1, alignment: 0b0011_0111).
|
||||||
|
let sei = [
|
||||||
|
0x05, 0x02, 0xAA, 0xBB, // type 5
|
||||||
|
0x06, 0x01, 0x37, // recovery point
|
||||||
|
0x80,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
parse_recovery_point_hevc(&sei).unwrap(),
|
||||||
|
Some(RecoveryPointHevc {
|
||||||
|
recovery_poc_cnt: 3,
|
||||||
|
exact_match: true,
|
||||||
|
broken_link: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let sei = [0x05, 0x01, 0x00, 0x80];
|
||||||
|
assert_eq!(parse_recovery_point_hevc(&sei).unwrap(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
//! Corpus replay: walk a captured real-host stream through the planners.
|
||||||
|
//!
|
||||||
|
//! The M0 capture hook (`PUNKTFUNK_DUMP_VIDEO=<dir>` on any desktop client) writes
|
||||||
|
//! the exact decoder input of a live session — `au-<stamp>.<codec>` plus an `.idx`
|
||||||
|
//! sidecar carrying `offset len flags complete` per AU. This harness feeds those AUs
|
||||||
|
//! back through [`pf_bitstream::h264::H264Planner`] / [`pf_bitstream::h265::H265Planner`]
|
||||||
|
//! and asserts the planner survives a REAL host stream: every AU plans (bar the
|
||||||
|
//! deliberate skips), no panic, and the warnings are only the ones a clean capture may
|
||||||
|
//! legitimately produce.
|
||||||
|
//!
|
||||||
|
//! Why this exists separately from the vendored conformance vectors: those prove we
|
||||||
|
//! match the spec's own test streams, and the on-glass sessions prove the whole pipe —
|
||||||
|
//! but between the two sits "does the planner handle what OUR five host encoder
|
||||||
|
//! families actually emit", which is the question the corpus was captured to answer.
|
||||||
|
//! For HEVC this is the ONLY pre-wiring validation against real host output (the
|
||||||
|
//! client's HEVC rung is still being built), so it runs long before M3 finishes.
|
||||||
|
//!
|
||||||
|
//! Ignored by default: captures are hundreds of megabytes and live outside the repo.
|
||||||
|
//! Run one explicitly —
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! PF_CORPUS=/path/to/au-1785970273.h265 \
|
||||||
|
//! cargo test -p pf-bitstream --test corpus_replay -- --ignored --nocapture
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The `.idx` sidecar is found next to the data file (`<data>.idx`); the codec comes
|
||||||
|
//! from the extension, matching the capture hook's own naming convention.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// One captured access unit: its byte range in the data file, plus the wire bits the
|
||||||
|
/// byte stream itself cannot carry.
|
||||||
|
struct CapturedAu {
|
||||||
|
offset: usize,
|
||||||
|
len: usize,
|
||||||
|
/// The wire `flags` byte (`USER_FLAG_*`) — kept for the RFI/intra-refresh legs,
|
||||||
|
/// which discriminate on it.
|
||||||
|
_flags: u32,
|
||||||
|
complete: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the `.idx` sidecar: one `offset len flags complete` line per AU, `#` comments
|
||||||
|
/// and blank lines skipped (the hook writes none today, but a hand-trimmed corpus file
|
||||||
|
/// is a thing a human will produce).
|
||||||
|
///
|
||||||
|
/// A malformed FINAL line is dropped with a note instead of failing: ending a capture
|
||||||
|
/// means killing the client, so the last buffered line is routinely half-written (the
|
||||||
|
/// hook's own docs call a truncated last AU acceptable). Anywhere else a malformed line
|
||||||
|
/// means the sidecar is corrupt and the run must not quietly replay a subset.
|
||||||
|
fn read_index(path: &Path) -> Vec<CapturedAu> {
|
||||||
|
let text = std::fs::read_to_string(path)
|
||||||
|
.unwrap_or_else(|e| panic!("cannot read the index sidecar {}: {e}", path.display()));
|
||||||
|
let lines: Vec<&str> = text
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim().is_empty() && !line.trim_start().starts_with('#'))
|
||||||
|
.collect();
|
||||||
|
let last = lines.len().saturating_sub(1);
|
||||||
|
let mut out = Vec::with_capacity(lines.len());
|
||||||
|
for (n, line) in lines.iter().enumerate() {
|
||||||
|
match parse_index_line(line) {
|
||||||
|
Some(au) => out.push(au),
|
||||||
|
None if n == last => {
|
||||||
|
println!("note: dropping a truncated final index line ({line:?})");
|
||||||
|
}
|
||||||
|
None => panic!("index line {n} is malformed: {line:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `offset len flags complete` line, or `None` when it is not four parsable fields.
|
||||||
|
fn parse_index_line(line: &str) -> Option<CapturedAu> {
|
||||||
|
let mut it = line.split_whitespace();
|
||||||
|
let num = |raw: &str| -> Option<u64> {
|
||||||
|
match raw.strip_prefix("0x") {
|
||||||
|
Some(hex) => u64::from_str_radix(hex, 16).ok(),
|
||||||
|
None => raw.parse().ok(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let offset = num(it.next()?)?;
|
||||||
|
let len = num(it.next()?)?;
|
||||||
|
let flags = num(it.next()?)?;
|
||||||
|
let complete = num(it.next()?)?;
|
||||||
|
Some(CapturedAu {
|
||||||
|
offset: offset as usize,
|
||||||
|
len: len as usize,
|
||||||
|
_flags: flags as u32,
|
||||||
|
complete: complete != 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The capture named by `PF_CORPUS`, or `None` when the variable is unset.
|
||||||
|
fn corpus_from_env() -> Option<(PathBuf, Vec<u8>, Vec<CapturedAu>)> {
|
||||||
|
let path = PathBuf::from(std::env::var_os("PF_CORPUS")?);
|
||||||
|
let data = std::fs::read(&path)
|
||||||
|
.unwrap_or_else(|e| panic!("cannot read the capture {}: {e}", path.display()));
|
||||||
|
let mut idx = path.clone().into_os_string();
|
||||||
|
idx.push(".idx");
|
||||||
|
let mut index = read_index(Path::new(&idx));
|
||||||
|
// Same truncation story on the data side: the final AU's bytes may not all have
|
||||||
|
// reached the file before the client died. Drop AUs the data cannot cover — but
|
||||||
|
// only from the tail, so a short file can never silently hide a middle gap.
|
||||||
|
let covered = index
|
||||||
|
.iter()
|
||||||
|
.take_while(|au| au.offset.saturating_add(au.len) <= data.len())
|
||||||
|
.count();
|
||||||
|
if covered < index.len() {
|
||||||
|
println!(
|
||||||
|
"note: dropping {} index entr{} past the end of the data file (truncated capture)",
|
||||||
|
index.len() - covered,
|
||||||
|
if index.len() - covered == 1 {
|
||||||
|
"y"
|
||||||
|
} else {
|
||||||
|
"ies"
|
||||||
|
},
|
||||||
|
);
|
||||||
|
index.truncate(covered);
|
||||||
|
}
|
||||||
|
assert!(!index.is_empty(), "the capture's index is empty");
|
||||||
|
Some((path, data, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-AU outcome tally — what the run reports and asserts on.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Tally {
|
||||||
|
planned: usize,
|
||||||
|
skipped: usize,
|
||||||
|
errors: Vec<String>,
|
||||||
|
warnings: Vec<String>,
|
||||||
|
partial: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tally {
|
||||||
|
/// A clean capture of a healthy session must plan every complete AU. Errors are
|
||||||
|
/// hard failures; warnings are printed and capped — `MissingReference` on a stream
|
||||||
|
/// that never lost a packet would mean the planner invented a gap.
|
||||||
|
fn assert_clean(&self, total: usize) {
|
||||||
|
println!(
|
||||||
|
"planned {} / skipped {} / partial-AUs-ignored {} / errors {} / warnings {} \
|
||||||
|
(of {total} captured AUs)",
|
||||||
|
self.planned,
|
||||||
|
self.skipped,
|
||||||
|
self.partial,
|
||||||
|
self.errors.len(),
|
||||||
|
self.warnings.len(),
|
||||||
|
);
|
||||||
|
for w in self.warnings.iter().take(20) {
|
||||||
|
println!(" warning: {w}");
|
||||||
|
}
|
||||||
|
for e in self.errors.iter().take(20) {
|
||||||
|
println!(" ERROR: {e}");
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
self.errors.is_empty(),
|
||||||
|
"{} AUs failed to plan — first: {}",
|
||||||
|
self.errors.len(),
|
||||||
|
self.errors[0],
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
self.warnings.is_empty(),
|
||||||
|
"{} planner warnings on a clean capture — first: {}",
|
||||||
|
self.warnings.len(),
|
||||||
|
self.warnings[0],
|
||||||
|
);
|
||||||
|
assert!(self.planned > 0, "no AU planned at all");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "needs a capture: PF_CORPUS=<au-file> (see the module docs)"]
|
||||||
|
fn a_captured_host_stream_replays_through_the_planner() {
|
||||||
|
let Some((path, data, index)) = corpus_from_env() else {
|
||||||
|
panic!("PF_CORPUS is unset — see the module docs for the invocation");
|
||||||
|
};
|
||||||
|
let ext = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned();
|
||||||
|
println!(
|
||||||
|
"replaying {} ({} bytes, {} AUs, codec {ext})",
|
||||||
|
path.display(),
|
||||||
|
data.len(),
|
||||||
|
index.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut tally = Tally::default();
|
||||||
|
// The planners take one COMPLETE AU. A partial AU (the wire's shard split) is the
|
||||||
|
// pump's business, not the planner's — count and skip rather than feed a fragment.
|
||||||
|
let complete: Vec<&CapturedAu> = index.iter().filter(|au| au.complete).collect();
|
||||||
|
tally.partial = index.len() - complete.len();
|
||||||
|
|
||||||
|
match ext.as_str() {
|
||||||
|
"h265" => {
|
||||||
|
let mut planner = pf_bitstream::h265::H265Planner::new();
|
||||||
|
for (i, au) in complete.iter().enumerate() {
|
||||||
|
let bytes = &data[au.offset..au.offset + au.len];
|
||||||
|
match planner.plan_au(bytes) {
|
||||||
|
Ok(plan) => {
|
||||||
|
tally.planned += 1;
|
||||||
|
for w in &plan.warnings {
|
||||||
|
tally.warnings.push(format!("AU {i}: {w:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The spec's own skip (8.1.3): decode nothing, show nothing, the
|
||||||
|
// stream is healthy — never an error (the WP-2 contract note).
|
||||||
|
Err(pf_bitstream::h265::PlanError::RaslSkipped { .. }) => tally.skipped += 1,
|
||||||
|
Err(e) => tally.errors.push(format!("AU {i}: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"h264" => {
|
||||||
|
let mut planner = pf_bitstream::h264::H264Planner::new();
|
||||||
|
for (i, au) in complete.iter().enumerate() {
|
||||||
|
let bytes = &data[au.offset..au.offset + au.len];
|
||||||
|
match planner.plan_au(bytes) {
|
||||||
|
Ok(plan) => {
|
||||||
|
tally.planned += 1;
|
||||||
|
for w in &plan.warnings {
|
||||||
|
tally.warnings.push(format!("AU {i}: {w:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => tally.errors.push(format!("AU {i}: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("no planner for a .{other} capture (h264/h265 only today)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
tally.assert_clean(index.len());
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Vendored snapshot — see PROVENANCE.md. Deliberately NOT opted into workspace lints
|
||||||
|
# or workspace package inheritance: upstream code stays as close to pristine as the
|
||||||
|
# trim allows, so re-syncing against the AOSP tree stays a diff, not an archaeology dig.
|
||||||
|
[package]
|
||||||
|
name = "cros-codecs"
|
||||||
|
version = "0.0.5"
|
||||||
|
license = "BSD-3-Clause"
|
||||||
|
description = "Vendored cros-codecs parser layer (codec module only) for pf-bitstream"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
# Upstream's in-tree unit tests (kept — they are the conformance goldens) want these.
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.11"
|
||||||
|
serde_json = "1"
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
Copyright 2022 The ChromiumOS Authors
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Vendored: cros-codecs (parser layer only)
|
||||||
|
|
||||||
|
- **Upstream:** <https://android.googlesource.com/platform/system/cros-codecs/> (the
|
||||||
|
authoritative AOSP tree). Snapshot taken from the read-only GitHub mirror
|
||||||
|
<https://github.com/chromeos/cros-codecs>, branch `main`,
|
||||||
|
commit **`5ff6d693ffae0b36935b8fc13092c733b4c2646f`**, fetched 2026-08-05.
|
||||||
|
- **License:** BSD-3-Clause (`LICENSE`, copied verbatim). Attribution headers retained
|
||||||
|
in every source file.
|
||||||
|
- **Why vendored, not a crates.io dependency:** the GitHub repo is a read-only mirror
|
||||||
|
and the crates.io release lags it; a pinned, reviewed snapshot is the supply-chain
|
||||||
|
posture punktfunk already uses elsewhere (`clients/android/native/vendor/ndk`,
|
||||||
|
`punktfunk-host/vendor/usbip-sim`). Decision of record:
|
||||||
|
punktfunk-planning `design/client-native-decode.md` §8.1.
|
||||||
|
|
||||||
|
## What was taken
|
||||||
|
|
||||||
|
`src/codec/{h264,h265,av1,vp9}` (parsers, DPBs, picture types, NALU/OBU machinery,
|
||||||
|
their `test_data` vectors — they double as punktfunk's conformance corpus),
|
||||||
|
`src/bitstream_utils.rs`, `LICENSE`. Upstream designed the `codec` module for exactly
|
||||||
|
this extraction — its module doc: "There shall be no dependencies from other modules of
|
||||||
|
this crate to this module, so that it can be turned into a crate of its own if needed
|
||||||
|
in the future."
|
||||||
|
|
||||||
|
## What was left behind
|
||||||
|
|
||||||
|
- `decoder/`, `encoder/`, `backend/`, `c2_wrapper/`, `video_frame`, `image_processing`,
|
||||||
|
`utils` — the Linux-only halves (libva/v4l2/gbm/nix). punktfunk's `pf-bitstream` +
|
||||||
|
`pf-vkdecode` occupy that layer.
|
||||||
|
- `codec/vp8` — VP9 has no dependency on it (verified) and no punktfunk host will ever
|
||||||
|
emit VP8.
|
||||||
|
|
||||||
|
## Deviations from pristine upstream
|
||||||
|
|
||||||
|
1. `src/lib.rs` — rewritten: keeps only the module decls and `Resolution` /
|
||||||
|
`ResolutionRoundMode` (the sole root items `codec` references), both copied verbatim;
|
||||||
|
adds crate-level `#![allow(clippy::all, mismatched_lifetime_syntaxes)]` — vendored
|
||||||
|
code is not held to the workspace lint bar (CI's `-D warnings` legs would fail on
|
||||||
|
upstream style otherwise).
|
||||||
|
2. `src/codec.rs` — one line removed (`pub mod vp8;`).
|
||||||
|
3. `Cargo.toml` — rewritten: `log` is the only dependency the vendored subset needs,
|
||||||
|
plus `env_logger`/`serde_json` dev-dependencies for upstream's in-tree tests.
|
||||||
|
4. `cargo fmt` normalization under the workspace's rustfmt config (mechanical only).
|
||||||
|
5. **Zero-unsafe, enforced**: `#![forbid(unsafe_code)]` added to lib.rs. Upstream's codec
|
||||||
|
module had exactly one production `unsafe` (h264/dpb.rs `build_ref_pic_lists`: ref→index
|
||||||
|
via pointer `offset_from`) — replaced with a safe `position(ptr::eq)` over the ≤16-entry
|
||||||
|
DPB — and three test-only `mem::zeroed()` asserts, replaced with `Default::default()`
|
||||||
|
(`PredWeightTable` derives `Default`; all-integer struct, identical value). The layer
|
||||||
|
facing untrusted bytes is now compiler-verified free of unsafe — the property that
|
||||||
|
motivates replacing libavcodec's C parsers in the first place.
|
||||||
|
6. `src/codec/h264/picture.rs` — `PictureData::new_from_slice`: `display_resolution`
|
||||||
|
computed as `visible_rect.max` instead of `max - min`. `Sps::visible_rectangle()`
|
||||||
|
returns the crop offset in `min` and the visible *size* in `max` (see its
|
||||||
|
definition: `max.x = width - crop_left - crop_right`); upstream's subtraction
|
||||||
|
double-counts the left/top crop and, worse, panics on u32 underflow for a
|
||||||
|
large-but-parser-valid `frame_crop_left_offset` (e.g. 100 crop units on a 320-wide
|
||||||
|
SPS). Found by pf-bitstream's conformance-window tests; upstream never hits it
|
||||||
|
because real encoders crop right/bottom only. **Reported upstream 2026-08-06:
|
||||||
|
<https://github.com/chromeos/cros-codecs/issues/99>.**
|
||||||
|
|
||||||
|
7. `src/codec/h265/parser.rs` — `parse_slice_header`: reject
|
||||||
|
`num_long_term_sps + num_long_term_pics > 16` before the long-term RPS loop.
|
||||||
|
Upstream bounds the pair only by `MAX_LONG_TERM_REF_PIC_SETS` (32) combined, while
|
||||||
|
every long-term array in `SliceHeader` (`poc_lsb_lt`, `used_by_curr_pic_lt`,
|
||||||
|
`delta_poc_msb_present_flag`, `delta_poc_msb_cycle_lt`, `lt_idx_sps`) is `[_; 16]`
|
||||||
|
— a hostile slice header with 17+ entries panics the parser with an
|
||||||
|
index-out-of-bounds (bounds checks stay on in release). Found by pf-bitstream's
|
||||||
|
H.265 planner review; regression-tested there
|
||||||
|
(`a_hostile_long_term_count_is_a_parse_error_not_a_panic`). **Reported upstream
|
||||||
|
2026-08-06: <https://github.com/chromeos/cros-codecs/issues/100>.**
|
||||||
|
|
||||||
|
Re-sync procedure: fetch the AOSP tree, re-apply this trim, diff `codec/` +
|
||||||
|
`bitstream_utils.rs` (expect near-zero conflicts), update the commit pin above.
|
||||||
@@ -0,0 +1,788 @@
|
|||||||
|
// Copyright 2024 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::io::Seek;
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use crate::codec::h264::parser::Nalu as H264Nalu;
|
||||||
|
use crate::codec::h265::parser::Nalu as H265Nalu;
|
||||||
|
|
||||||
|
/// A bit reader for codec bitstreams. It properly handles emulation-prevention
|
||||||
|
/// bytes and stop bits for H264.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct BitReader<'a> {
|
||||||
|
/// A reference into the next unread byte in the stream.
|
||||||
|
data: Cursor<&'a [u8]>,
|
||||||
|
/// Contents of the current byte. First unread bit starting at position 8 -
|
||||||
|
/// num_remaining_bits_in_curr_bytes.
|
||||||
|
curr_byte: u8,
|
||||||
|
/// Number of bits remaining in `curr_byte`
|
||||||
|
num_remaining_bits_in_curr_byte: usize,
|
||||||
|
/// Used in emulation prevention byte detection.
|
||||||
|
prev_two_bytes: u16,
|
||||||
|
/// Number of emulation prevention bytes (i.e. 0x000003) we found.
|
||||||
|
num_epb: usize,
|
||||||
|
/// Whether or not we need emulation prevention logic.
|
||||||
|
needs_epb: bool,
|
||||||
|
/// How many bits have been read so far.
|
||||||
|
position: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum GetByteError {
|
||||||
|
OutOfBits,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for GetByteError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "reader ran out of bits")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum ReadBitsError {
|
||||||
|
TooManyBitsRequested(usize),
|
||||||
|
GetByte(GetByteError),
|
||||||
|
ConversionFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ReadBitsError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ReadBitsError::TooManyBitsRequested(bits) => {
|
||||||
|
write!(f, "more than 31 ({}) bits were requested", bits)
|
||||||
|
}
|
||||||
|
ReadBitsError::GetByte(_) => write!(f, "failed to advance the current byte"),
|
||||||
|
ReadBitsError::ConversionFailed => {
|
||||||
|
write!(f, "failed to convert read input to target type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetByteError> for ReadBitsError {
|
||||||
|
fn from(err: GetByteError) -> Self {
|
||||||
|
ReadBitsError::GetByte(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BitReader<'a> {
|
||||||
|
pub fn new(data: &'a [u8], needs_epb: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
data: Cursor::new(data),
|
||||||
|
curr_byte: Default::default(),
|
||||||
|
num_remaining_bits_in_curr_byte: Default::default(),
|
||||||
|
prev_two_bytes: 0xffff,
|
||||||
|
num_epb: Default::default(),
|
||||||
|
needs_epb: needs_epb,
|
||||||
|
position: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a single bit from the stream.
|
||||||
|
pub fn read_bit(&mut self) -> Result<bool, String> {
|
||||||
|
let bit = self.read_bits::<u32>(1)?;
|
||||||
|
match bit {
|
||||||
|
1 => Ok(true),
|
||||||
|
0 => Ok(false),
|
||||||
|
_ => panic!("Unexpected value {}", bit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read up to 31 bits from the stream. Note that we don't want to read 32
|
||||||
|
/// bits even though we're returning a u32 because that would break the
|
||||||
|
/// read_bits_signed() function. 31 bits should be overkill for compressed
|
||||||
|
/// header parsing anyway.
|
||||||
|
pub fn read_bits<U: TryFrom<u32>>(&mut self, num_bits: usize) -> Result<U, String> {
|
||||||
|
if num_bits > 31 {
|
||||||
|
return Err(ReadBitsError::TooManyBitsRequested(num_bits).to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bits_left = num_bits;
|
||||||
|
let mut out = 0u32;
|
||||||
|
|
||||||
|
while self.num_remaining_bits_in_curr_byte < bits_left {
|
||||||
|
out |= (self.curr_byte as u32) << (bits_left - self.num_remaining_bits_in_curr_byte);
|
||||||
|
bits_left -= self.num_remaining_bits_in_curr_byte;
|
||||||
|
self.move_to_next_byte().map_err(|err| err.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
out |= (self.curr_byte >> (self.num_remaining_bits_in_curr_byte - bits_left)) as u32;
|
||||||
|
out &= (1 << num_bits) - 1;
|
||||||
|
self.num_remaining_bits_in_curr_byte -= bits_left;
|
||||||
|
self.position += num_bits as u64;
|
||||||
|
|
||||||
|
U::try_from(out).map_err(|_| ReadBitsError::ConversionFailed.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a two's complement signed integer of length |num_bits|.
|
||||||
|
pub fn read_bits_signed<U: TryFrom<i32>>(&mut self, num_bits: usize) -> Result<U, String> {
|
||||||
|
let mut out: i32 = self
|
||||||
|
.read_bits::<u32>(num_bits)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ReadBitsError::ConversionFailed.to_string())?;
|
||||||
|
if out >> (num_bits - 1) != 0 {
|
||||||
|
out |= -1i32 ^ ((1 << num_bits) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
U::try_from(out).map_err(|_| ReadBitsError::ConversionFailed.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads an unsigned integer from the stream and checks if the stream is byte aligned.
|
||||||
|
pub fn read_bits_aligned<U: TryFrom<u32>>(&mut self, num_bits: usize) -> Result<U, String> {
|
||||||
|
if self.num_remaining_bits_in_curr_byte % 8 != 0 {
|
||||||
|
return Err("Attempted unaligned read_le()".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.read_bits(num_bits).map_err(|err| err.to_string())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skip `num_bits` bits from the stream.
|
||||||
|
pub fn skip_bits(&mut self, mut num_bits: usize) -> Result<(), String> {
|
||||||
|
while num_bits > 0 {
|
||||||
|
let n = std::cmp::min(num_bits, 31);
|
||||||
|
self.read_bits::<u32>(n)?;
|
||||||
|
num_bits -= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the amount of bits left in the stream
|
||||||
|
pub fn num_bits_left(&mut self) -> usize {
|
||||||
|
let cur_pos = self.data.position();
|
||||||
|
// This should always be safe to unwrap.
|
||||||
|
let end_pos = self.data.seek(SeekFrom::End(0)).unwrap();
|
||||||
|
let _ = self.data.seek(SeekFrom::Start(cur_pos));
|
||||||
|
((end_pos - cur_pos) as usize) * 8 + self.num_remaining_bits_in_curr_byte
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of emulation-prevention bytes read so far.
|
||||||
|
pub fn num_epb(&self) -> usize {
|
||||||
|
self.num_epb
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the stream still has RBSP data. Implements more_rbsp_data(). See
|
||||||
|
/// the spec for more details.
|
||||||
|
pub fn has_more_rsbp_data(&mut self) -> bool {
|
||||||
|
if self.num_remaining_bits_in_curr_byte == 0 && self.move_to_next_byte().is_err() {
|
||||||
|
// no more data at all in the rbsp
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the next bit is the stop bit, then we should only see unset bits
|
||||||
|
// until the end of the data.
|
||||||
|
if (self.curr_byte & ((1 << (self.num_remaining_bits_in_curr_byte - 1)) - 1)) != 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
let orig_pos = self.data.position();
|
||||||
|
while let Ok(_) = self.data.read_exact(&mut buf) {
|
||||||
|
if buf[0] != 0 {
|
||||||
|
self.data.set_position(orig_pos);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads an Unsigned Exponential golomb coding number from the next bytes in the
|
||||||
|
/// bitstream. This may advance the state of position within the bitstream even if the
|
||||||
|
/// read operation is unsuccessful. See H264 Annex B specification 9.1 for details.
|
||||||
|
pub fn read_ue<U: TryFrom<u32>>(&mut self) -> Result<U, String> {
|
||||||
|
let mut num_bits = 0;
|
||||||
|
|
||||||
|
while self.read_bits::<u32>(1)? == 0 {
|
||||||
|
num_bits += 1;
|
||||||
|
if num_bits > 31 {
|
||||||
|
return Err("invalid stream".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = ((1u32 << num_bits) - 1)
|
||||||
|
.checked_add(self.read_bits::<u32>(num_bits)?)
|
||||||
|
.ok_or::<String>("read number cannot fit in 32 bits".into())?;
|
||||||
|
|
||||||
|
U::try_from(value).map_err(|_| "conversion error".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_ue_bounded<U: TryFrom<u32>>(&mut self, min: u32, max: u32) -> Result<U, String> {
|
||||||
|
let ue = self.read_ue()?;
|
||||||
|
if ue > max || ue < min {
|
||||||
|
Err(format!(
|
||||||
|
"Value out of bounds: expected {} - {}, got {}",
|
||||||
|
min, max, ue
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(U::try_from(ue).map_err(|_| String::from("Conversion error"))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_ue_max<U: TryFrom<u32>>(&mut self, max: u32) -> Result<U, String> {
|
||||||
|
self.read_ue_bounded(0, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a signed exponential golomb coding number. Instead of using two's
|
||||||
|
/// complement, this scheme maps even integers to positive numbers and odd
|
||||||
|
/// integers to negative numbers. The least significant bit indicates the
|
||||||
|
/// sign. See H264 Annex B specification 9.1.1 for details.
|
||||||
|
pub fn read_se<U: TryFrom<i32>>(&mut self) -> Result<U, String> {
|
||||||
|
let ue = self.read_ue::<u32>()? as i32;
|
||||||
|
|
||||||
|
if ue % 2 == 0 {
|
||||||
|
Ok(U::try_from(-(ue / 2)).map_err(|_| String::from("Conversion error"))?)
|
||||||
|
} else {
|
||||||
|
Ok(U::try_from(ue / 2 + 1).map_err(|_| String::from("Conversion error"))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_se_bounded<U: TryFrom<i32>>(&mut self, min: i32, max: i32) -> Result<U, String> {
|
||||||
|
let se = self.read_se()?;
|
||||||
|
if se < min || se > max {
|
||||||
|
Err(format!(
|
||||||
|
"Value out of bounds, expected between {}-{}, got {}",
|
||||||
|
min, max, se
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(U::try_from(se).map_err(|_| String::from("Conversion error"))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read little endian multi-byte integer.
|
||||||
|
pub fn read_le<U: TryFrom<u32>>(&mut self, num_bits: u8) -> Result<U, String> {
|
||||||
|
let mut t = 0;
|
||||||
|
|
||||||
|
for i in 0..num_bits {
|
||||||
|
let byte = self.read_bits_aligned::<u32>(8)?;
|
||||||
|
t += byte << (i * 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(U::try_from(t).map_err(|_| String::from("Conversion error"))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the position of this bitstream in bits.
|
||||||
|
pub fn position(&self) -> u64 {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_byte(&mut self) -> Result<u8, GetByteError> {
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
self.data
|
||||||
|
.read_exact(&mut buf)
|
||||||
|
.map_err(|_| GetByteError::OutOfBits)?;
|
||||||
|
Ok(buf[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_to_next_byte(&mut self) -> Result<(), GetByteError> {
|
||||||
|
let mut byte = self.get_byte()?;
|
||||||
|
|
||||||
|
if self.needs_epb {
|
||||||
|
if self.prev_two_bytes == 0 && byte == 0x03 {
|
||||||
|
// We found an epb
|
||||||
|
self.num_epb += 1;
|
||||||
|
// Read another byte
|
||||||
|
byte = self.get_byte()?;
|
||||||
|
// We need another 3 bytes before another epb can happen.
|
||||||
|
self.prev_two_bytes = 0xffff;
|
||||||
|
}
|
||||||
|
self.prev_two_bytes = (self.prev_two_bytes << 8) | u16::from(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.num_remaining_bits_in_curr_byte = 8;
|
||||||
|
self.curr_byte = byte;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over IVF packets.
|
||||||
|
pub struct IvfIterator<'a> {
|
||||||
|
cursor: Cursor<&'a [u8]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IvfIterator<'a> {
|
||||||
|
pub fn new(data: &'a [u8]) -> Self {
|
||||||
|
let mut cursor = Cursor::new(data);
|
||||||
|
|
||||||
|
// Skip the IVH header entirely.
|
||||||
|
cursor.seek(std::io::SeekFrom::Start(32)).unwrap();
|
||||||
|
|
||||||
|
Self { cursor }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for IvfIterator<'a> {
|
||||||
|
type Item = &'a [u8];
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// Make sure we have a header.
|
||||||
|
let mut len_buf = [0u8; 4];
|
||||||
|
self.cursor.read_exact(&mut len_buf).ok()?;
|
||||||
|
let len = ((len_buf[3] as usize) << 24)
|
||||||
|
| ((len_buf[2] as usize) << 16)
|
||||||
|
| ((len_buf[1] as usize) << 8)
|
||||||
|
| (len_buf[0] as usize);
|
||||||
|
|
||||||
|
// Skip PTS.
|
||||||
|
self.cursor.seek(std::io::SeekFrom::Current(8)).ok()?;
|
||||||
|
|
||||||
|
let start = self.cursor.position() as usize;
|
||||||
|
let _ = self
|
||||||
|
.cursor
|
||||||
|
.seek(std::io::SeekFrom::Current(len as i64))
|
||||||
|
.ok()?;
|
||||||
|
let end = self.cursor.position() as usize;
|
||||||
|
|
||||||
|
Some(&self.cursor.get_ref()[start..end])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper struct for synthesizing IVF file header
|
||||||
|
pub struct IvfFileHeader {
|
||||||
|
pub magic: [u8; 4],
|
||||||
|
pub version: u16,
|
||||||
|
pub header_size: u16,
|
||||||
|
pub codec: [u8; 4],
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
pub framerate: u32,
|
||||||
|
pub timescale: u32,
|
||||||
|
pub frame_count: u32,
|
||||||
|
pub unused: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IvfFileHeader {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
magic: Self::MAGIC,
|
||||||
|
version: 0,
|
||||||
|
header_size: 32,
|
||||||
|
codec: Self::CODEC_VP9,
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
framerate: 1,
|
||||||
|
timescale: 1000,
|
||||||
|
frame_count: 1,
|
||||||
|
unused: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IvfFileHeader {
|
||||||
|
pub const MAGIC: [u8; 4] = *b"DKIF";
|
||||||
|
pub const CODEC_VP8: [u8; 4] = *b"VP80";
|
||||||
|
pub const CODEC_VP9: [u8; 4] = *b"VP90";
|
||||||
|
pub const CODEC_AV1: [u8; 4] = *b"AV01";
|
||||||
|
|
||||||
|
pub fn new(codec: [u8; 4], width: u16, height: u16, framerate: u32, frame_count: u32) -> Self {
|
||||||
|
let default = Self::default();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
codec,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
framerate: framerate * default.timescale,
|
||||||
|
frame_count,
|
||||||
|
..default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IvfFileHeader {
|
||||||
|
/// Writes header into writer
|
||||||
|
pub fn writo_into(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
|
||||||
|
writer.write_all(&self.magic)?;
|
||||||
|
writer.write_all(&self.version.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.header_size.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.codec)?;
|
||||||
|
writer.write_all(&self.width.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.height.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.framerate.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.timescale.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.frame_count.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.unused.to_le_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper struct for synthesizing IVF frame header
|
||||||
|
pub struct IvfFrameHeader {
|
||||||
|
pub frame_size: u32,
|
||||||
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IvfFrameHeader {
|
||||||
|
/// Writes header into writer
|
||||||
|
pub fn writo_into(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
|
||||||
|
writer.write_all(&self.frame_size.to_le_bytes())?;
|
||||||
|
writer.write_all(&self.timestamp.to_le_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator NALUs in a bitstream.
|
||||||
|
pub struct NalIterator<'a, Nalu>(Cursor<&'a [u8]>, PhantomData<Nalu>);
|
||||||
|
|
||||||
|
impl<'a, Nalu> NalIterator<'a, Nalu> {
|
||||||
|
pub fn new(stream: &'a [u8]) -> Self {
|
||||||
|
Self(Cursor::new(stream), PhantomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for NalIterator<'a, H264Nalu<'a>> {
|
||||||
|
type Item = Cow<'a, [u8]>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
H264Nalu::next(&mut self.0).map(|n| n.data).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for NalIterator<'a, H265Nalu<'a>> {
|
||||||
|
type Item = Cow<'a, [u8]>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
H265Nalu::next(&mut self.0).map(|n| n.data).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BitWriterError {
|
||||||
|
InvalidBitCount,
|
||||||
|
Io(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BitWriterError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BitWriterError::InvalidBitCount => write!(f, "invalid bit count"),
|
||||||
|
BitWriterError::Io(x) => write!(f, "{}", x.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for BitWriterError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
BitWriterError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type BitWriterResult<T> = std::result::Result<T, BitWriterError>;
|
||||||
|
|
||||||
|
pub struct BitWriter<W: Write> {
|
||||||
|
out: W,
|
||||||
|
nth_bit: u8,
|
||||||
|
curr_byte: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> BitWriter<W> {
|
||||||
|
pub fn new(writer: W) -> Self {
|
||||||
|
Self {
|
||||||
|
out: writer,
|
||||||
|
curr_byte: 0,
|
||||||
|
nth_bit: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes fixed bit size integer (up to 32 bit)
|
||||||
|
pub fn write_f<T: Into<u32>>(&mut self, bits: usize, value: T) -> BitWriterResult<usize> {
|
||||||
|
let value = value.into();
|
||||||
|
|
||||||
|
if bits > 32 {
|
||||||
|
return Err(BitWriterError::InvalidBitCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut written = 0;
|
||||||
|
for bit in (0..bits).rev() {
|
||||||
|
let bit = (1 << bit) as u32;
|
||||||
|
|
||||||
|
self.write_bit((value & bit) == bit)?;
|
||||||
|
written += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(written)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a single bit that will be outputed to [`std::io::Write`]
|
||||||
|
pub fn write_bit(&mut self, bit: bool) -> BitWriterResult<()> {
|
||||||
|
self.curr_byte |= (bit as u8) << (7u8 - self.nth_bit);
|
||||||
|
self.nth_bit += 1;
|
||||||
|
|
||||||
|
if self.nth_bit == 8 {
|
||||||
|
self.out.write_all(&[self.curr_byte])?;
|
||||||
|
self.nth_bit = 0;
|
||||||
|
self.curr_byte = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Immediately outputs any cached bits to [`std::io::Write`]
|
||||||
|
pub fn flush(&mut self) -> BitWriterResult<()> {
|
||||||
|
if self.nth_bit != 0 {
|
||||||
|
self.out.write_all(&[self.curr_byte])?;
|
||||||
|
self.nth_bit = 0;
|
||||||
|
self.curr_byte = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.out.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if ['Self`] hold data that wasn't written to [`std::io::Write`]
|
||||||
|
pub fn has_data_pending(&self) -> bool {
|
||||||
|
self.nth_bit != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inner(&self) -> &W {
|
||||||
|
&self.out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inner_mut(&mut self) -> &mut W {
|
||||||
|
&mut self.out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Drop for BitWriter<W> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = self.flush() {
|
||||||
|
log::error!("Unable to flush bits {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ivf_file_header() {
|
||||||
|
let mut hdr = IvfFileHeader {
|
||||||
|
version: 0,
|
||||||
|
codec: IvfFileHeader::CODEC_VP9,
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
framerate: 30_000,
|
||||||
|
timescale: 1_000,
|
||||||
|
frame_count: 1,
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
hdr.writo_into(&mut buf).unwrap();
|
||||||
|
|
||||||
|
const EXPECTED: [u8; 32] = [
|
||||||
|
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x00, 0x01,
|
||||||
|
0x00, 0x01, 0x30, 0x75, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(&buf, &EXPECTED);
|
||||||
|
|
||||||
|
hdr.width = 1920;
|
||||||
|
hdr.height = 800;
|
||||||
|
hdr.framerate = 24;
|
||||||
|
hdr.timescale = 1;
|
||||||
|
hdr.frame_count = 100;
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
hdr.writo_into(&mut buf).unwrap();
|
||||||
|
|
||||||
|
const EXPECTED2: [u8; 32] = [
|
||||||
|
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x80, 0x07,
|
||||||
|
0x20, 0x03, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(&buf, &EXPECTED2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ivf_frame_header() {
|
||||||
|
let mut hdr = IvfFrameHeader {
|
||||||
|
frame_size: 199249,
|
||||||
|
timestamp: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
hdr.writo_into(&mut buf).unwrap();
|
||||||
|
|
||||||
|
const EXPECTED: [u8; 12] = [
|
||||||
|
0x51, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(&buf, &EXPECTED);
|
||||||
|
|
||||||
|
hdr.timestamp = 1;
|
||||||
|
hdr.frame_size = 52;
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
hdr.writo_into(&mut buf).unwrap();
|
||||||
|
|
||||||
|
const EXPECTED2: [u8; 12] = [
|
||||||
|
0x34, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(&buf, &EXPECTED2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bitwriter_f1() {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = BitWriter::new(&mut buf);
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, false).unwrap();
|
||||||
|
writer.write_f(1, false).unwrap();
|
||||||
|
writer.write_f(1, false).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(buf, vec![0b10001111u8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bitwriter_f3() {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = BitWriter::new(&mut buf);
|
||||||
|
writer.write_f(3, 0b100u8).unwrap();
|
||||||
|
writer.write_f(3, 0b101u8).unwrap();
|
||||||
|
writer.write_f(3, 0b011u8).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(buf, vec![0b10010101u8, 0b10000000u8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bitwriter_f4() {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = BitWriter::new(&mut buf);
|
||||||
|
writer.write_f(4, 0b1000u8).unwrap();
|
||||||
|
writer.write_f(4, 0b1011u8).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(buf, vec![0b10001011u8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These tests are adapted from the chromium tests at media/video/h264_bit_reader_unitttest.cc
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_stream_without_escape_and_trailing_zero_bytes() {
|
||||||
|
const RBSP: [u8; 6] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xa0];
|
||||||
|
|
||||||
|
let mut reader = BitReader::new(&RBSP, true);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(1).unwrap(), 0);
|
||||||
|
assert_eq!(reader.num_bits_left(), 47);
|
||||||
|
assert!(reader.has_more_rsbp_data());
|
||||||
|
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x02);
|
||||||
|
assert_eq!(reader.num_bits_left(), 39);
|
||||||
|
assert!(reader.has_more_rsbp_data());
|
||||||
|
|
||||||
|
assert_eq!(reader.read_bits::<u32>(31).unwrap(), 0x23456789);
|
||||||
|
assert_eq!(reader.num_bits_left(), 8);
|
||||||
|
assert!(reader.has_more_rsbp_data());
|
||||||
|
|
||||||
|
assert_eq!(reader.read_bits::<u32>(1).unwrap(), 1);
|
||||||
|
assert_eq!(reader.num_bits_left(), 7);
|
||||||
|
assert!(reader.has_more_rsbp_data());
|
||||||
|
|
||||||
|
assert_eq!(reader.read_bits::<u32>(1).unwrap(), 0);
|
||||||
|
assert_eq!(reader.num_bits_left(), 6);
|
||||||
|
assert!(!reader.has_more_rsbp_data());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_byte_stream() {
|
||||||
|
const RBSP: [u8; 1] = [0x18];
|
||||||
|
|
||||||
|
let mut reader = BitReader::new(&RBSP, true);
|
||||||
|
assert_eq!(reader.num_bits_left(), 8);
|
||||||
|
assert!(reader.has_more_rsbp_data());
|
||||||
|
assert_eq!(reader.read_bits::<u32>(4).unwrap(), 1);
|
||||||
|
assert!(!reader.has_more_rsbp_data());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stop_bit_occupy_full_byte() {
|
||||||
|
const RBSP: [u8; 2] = [0xab, 0x80];
|
||||||
|
|
||||||
|
let mut reader = BitReader::new(&RBSP, true);
|
||||||
|
assert_eq!(reader.num_bits_left(), 16);
|
||||||
|
assert!(reader.has_more_rsbp_data());
|
||||||
|
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0xab);
|
||||||
|
assert_eq!(reader.num_bits_left(), 8);
|
||||||
|
|
||||||
|
assert!(!reader.has_more_rsbp_data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that read_ue behaves properly with input at the limits.
|
||||||
|
#[test]
|
||||||
|
fn read_ue() {
|
||||||
|
// Regular value.
|
||||||
|
let mut reader = BitReader::new(&[0b0001_1010], true);
|
||||||
|
assert_eq!(reader.read_ue::<u32>().unwrap(), 12);
|
||||||
|
assert_eq!(reader.data.position(), 1);
|
||||||
|
assert_eq!(reader.num_remaining_bits_in_curr_byte, 1);
|
||||||
|
|
||||||
|
// 0 value.
|
||||||
|
let mut reader = BitReader::new(&[0b1000_0000], true);
|
||||||
|
assert_eq!(reader.read_ue::<u32>().unwrap(), 0);
|
||||||
|
assert_eq!(reader.data.position(), 1);
|
||||||
|
assert_eq!(reader.num_remaining_bits_in_curr_byte, 7);
|
||||||
|
|
||||||
|
// No prefix stop bit.
|
||||||
|
let mut reader = BitReader::new(&[0b0000_0000], true);
|
||||||
|
reader.read_ue::<u32>().unwrap_err();
|
||||||
|
|
||||||
|
// u32 max value: 31 0-bits, 1 bit marker, 31 bits 1-bits.
|
||||||
|
let mut reader = BitReader::new(
|
||||||
|
&[
|
||||||
|
0b0000_0000,
|
||||||
|
0b0000_0000,
|
||||||
|
0b0000_0000,
|
||||||
|
0b0000_0001,
|
||||||
|
0b1111_1111,
|
||||||
|
0b1111_1111,
|
||||||
|
0b1111_1111,
|
||||||
|
0b1111_1110,
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(reader.read_ue::<u32>().unwrap(), 0xffff_fffe);
|
||||||
|
assert_eq!(reader.data.position(), 8);
|
||||||
|
assert_eq!(reader.num_remaining_bits_in_curr_byte, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that emulation prevention is being handled correctly.
|
||||||
|
#[test]
|
||||||
|
fn skip_epb_when_enabled() {
|
||||||
|
let mut reader = BitReader::new(&[0x00, 0x00, 0x03, 0x01], false);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x00);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x00);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x03);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x01);
|
||||||
|
|
||||||
|
let mut reader = BitReader::new(&[0x00, 0x00, 0x03, 0x01], true);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x00);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x00);
|
||||||
|
assert_eq!(reader.read_bits::<u32>(8).unwrap(), 0x01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_signed_bits() {
|
||||||
|
let mut reader = BitReader::new(&[0b1111_0000], false);
|
||||||
|
assert_eq!(reader.read_bits_signed::<i32>(4).unwrap(), -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2023 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
//! Parsers for various kinds of encoded streams.
|
||||||
|
//!
|
||||||
|
//! This module does not provide any actual decoding tools - that's the job of the
|
||||||
|
//! [crate::decoder] module. However the parsers of this module are heavily used in order to
|
||||||
|
//! implement stateless decoding.
|
||||||
|
//!
|
||||||
|
//! There shall be no dependencies from other modules of this crate to this module, so that it
|
||||||
|
//! can be turned into a crate of its own if needed in the future.
|
||||||
|
|
||||||
|
pub mod av1;
|
||||||
|
pub mod h264;
|
||||||
|
pub mod h265;
|
||||||
|
pub mod vp9;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright 2023 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod reader;
|
||||||
|
pub mod synthesizer;
|
||||||
|
pub mod writer;
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// Copyright 2023 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
use crate::codec::av1::parser::NUM_REF_FRAMES;
|
||||||
|
|
||||||
|
const DIV_LUT: [i32; 257] = [
|
||||||
|
16384, 16320, 16257, 16194, 16132, 16070, 16009, 15948, 15888, 15828, 15768, 15709, 15650,
|
||||||
|
15592, 15534, 15477, 15420, 15364, 15308, 15252, 15197, 15142, 15087, 15033, 14980, 14926,
|
||||||
|
14873, 14821, 14769, 14717, 14665, 14614, 14564, 14513, 14463, 14413, 14364, 14315, 14266,
|
||||||
|
14218, 14170, 14122, 14075, 14028, 13981, 13935, 13888, 13843, 13797, 13752, 13707, 13662,
|
||||||
|
13618, 13574, 13530, 13487, 13443, 13400, 13358, 13315, 13273, 13231, 13190, 13148, 13107,
|
||||||
|
13066, 13026, 12985, 12945, 12906, 12866, 12827, 12788, 12749, 12710, 12672, 12633, 12596,
|
||||||
|
12558, 12520, 12483, 12446, 12409, 12373, 12336, 12300, 12264, 12228, 12193, 12157, 12122,
|
||||||
|
12087, 12053, 12018, 11984, 11950, 11916, 11882, 11848, 11815, 11782, 11749, 11716, 11683,
|
||||||
|
11651, 11619, 11586, 11555, 11523, 11491, 11460, 11429, 11398, 11367, 11336, 11305, 11275,
|
||||||
|
11245, 11215, 11185, 11155, 11125, 11096, 11067, 11038, 11009, 10980, 10951, 10923, 10894,
|
||||||
|
10866, 10838, 10810, 10782, 10755, 10727, 10700, 10673, 10645, 10618, 10592, 10565, 10538,
|
||||||
|
10512, 10486, 10460, 10434, 10408, 10382, 10356, 10331, 10305, 10280, 10255, 10230, 10205,
|
||||||
|
10180, 10156, 10131, 10107, 10082, 10058, 10034, 10010, 9986, 9963, 9939, 9916, 9892, 9869,
|
||||||
|
9846, 9823, 9800, 9777, 9754, 9732, 9709, 9687, 9664, 9642, 9620, 9598, 9576, 9554, 9533, 9511,
|
||||||
|
9489, 9468, 9447, 9425, 9404, 9383, 9362, 9341, 9321, 9300, 9279, 9259, 9239, 9218, 9198, 9178,
|
||||||
|
9158, 9138, 9118, 9098, 9079, 9059, 9039, 9020, 9001, 8981, 8962, 8943, 8924, 8905, 8886, 8867,
|
||||||
|
8849, 8830, 8812, 8793, 8775, 8756, 8738, 8720, 8702, 8684, 8666, 8648, 8630, 8613, 8595, 8577,
|
||||||
|
8560, 8542, 8525, 8508, 8490, 8473, 8456, 8439, 8422, 8405, 8389, 8372, 8355, 8339, 8322, 8306,
|
||||||
|
8289, 8273, 8257, 8240, 8224, 8208, 8192,
|
||||||
|
];
|
||||||
|
|
||||||
|
const DIV_LUT_BITS: u32 = 8;
|
||||||
|
const DIV_LUT_PREC_BITS: u32 = 14;
|
||||||
|
|
||||||
|
/// Implements FloorLog2(x), which is defined to be the floor of the base 2
|
||||||
|
/// logarithm of the input x.
|
||||||
|
///
|
||||||
|
/// The input x will always be an integer, and will always be greater than or equal to 1.
|
||||||
|
/// This function extracts the location of the most significant bit in x.
|
||||||
|
pub fn floor_log2(mut x: u32) -> u32 {
|
||||||
|
assert!(x > 0);
|
||||||
|
let mut s = 0;
|
||||||
|
|
||||||
|
while x != 0 {
|
||||||
|
x >>= 1;
|
||||||
|
s += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
s - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements 5.9.3. Get relative distance function
|
||||||
|
pub fn get_relative_dist(enable_order_hint: bool, order_hint_bits: i32, a: i32, b: i32) -> i32 {
|
||||||
|
if !enable_order_hint {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let diff = a - b;
|
||||||
|
let m = 1 << (order_hint_bits - 1);
|
||||||
|
(diff & (m - 1)) - (diff & m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements find_latest_backward from section 7.8.
|
||||||
|
pub fn find_latest_backward(
|
||||||
|
shifted_order_hints: &[i32; NUM_REF_FRAMES],
|
||||||
|
used_frame: &[bool; NUM_REF_FRAMES],
|
||||||
|
cur_frame_hint: i32,
|
||||||
|
latest_order_hint: &mut i32,
|
||||||
|
) -> i32 {
|
||||||
|
let mut _ref = -1;
|
||||||
|
|
||||||
|
for i in 0..NUM_REF_FRAMES {
|
||||||
|
let hint = shifted_order_hints[i];
|
||||||
|
if !used_frame[i] && hint >= cur_frame_hint && (_ref < 0 || hint >= *latest_order_hint) {
|
||||||
|
_ref = i as i32;
|
||||||
|
*latest_order_hint = hint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements find_earliest_backward from section 7.8.
|
||||||
|
pub fn find_earliest_backward(
|
||||||
|
shifted_order_hints: &[i32; NUM_REF_FRAMES],
|
||||||
|
used_frame: &[bool; NUM_REF_FRAMES],
|
||||||
|
cur_frame_hint: i32,
|
||||||
|
earliest_order_hint: &mut i32,
|
||||||
|
) -> i32 {
|
||||||
|
let mut _ref = -1;
|
||||||
|
|
||||||
|
for i in 0..NUM_REF_FRAMES {
|
||||||
|
let hint = shifted_order_hints[i];
|
||||||
|
if !used_frame[i] && hint >= cur_frame_hint && (_ref < 0 || hint < *earliest_order_hint) {
|
||||||
|
_ref = i as i32;
|
||||||
|
*earliest_order_hint = hint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements find_latest_forward from section 7.8.
|
||||||
|
pub fn find_latest_forward(
|
||||||
|
shifted_order_hints: &[i32; NUM_REF_FRAMES],
|
||||||
|
used_frame: &[bool; NUM_REF_FRAMES],
|
||||||
|
cur_frame_hint: i32,
|
||||||
|
latest_order_hint: &mut i32,
|
||||||
|
) -> i32 {
|
||||||
|
let mut _ref = -1;
|
||||||
|
for i in 0..NUM_REF_FRAMES {
|
||||||
|
let hint = shifted_order_hints[i];
|
||||||
|
if !used_frame[i] && hint < cur_frame_hint && (_ref < 0 || hint >= *latest_order_hint) {
|
||||||
|
_ref = i as i32;
|
||||||
|
*latest_order_hint = hint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tile_log2(blk_size: u32, target: u32) -> u32 {
|
||||||
|
let mut k = 0;
|
||||||
|
|
||||||
|
while (blk_size << k) < target {
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
k
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clip3(x: i32, y: i32, z: i32) -> i32 {
|
||||||
|
if z < x {
|
||||||
|
x
|
||||||
|
} else if z > y {
|
||||||
|
y
|
||||||
|
} else {
|
||||||
|
z
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5.9.29
|
||||||
|
pub fn inverse_recenter(r: i32, v: i32) -> i32 {
|
||||||
|
if v > 2 * r {
|
||||||
|
v
|
||||||
|
} else if v & 1 != 0 {
|
||||||
|
r - ((v + 1) >> 1)
|
||||||
|
} else {
|
||||||
|
r + (v >> 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements Round2. See 4.7: mathematical functions.
|
||||||
|
pub fn round2(x: u32, n: u32) -> u32 {
|
||||||
|
(x + 2u32.pow(n - 1)) / 2u32.pow(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements Round2Signed. See 4.7: mathematical functions.
|
||||||
|
pub fn round2signed(x: i32, n: u32) -> Result<i32, String> {
|
||||||
|
if x >= 0 {
|
||||||
|
i32::try_from(round2(x as u32, n)).map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
let x = x as i64;
|
||||||
|
let val = i32::try_from(round2(-x as u32, n)).map_err(|e| e.to_string())?;
|
||||||
|
Ok(-val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements 7.11.3.7. Resolve divisor process
|
||||||
|
pub fn resolve_divisor(d: i32) -> Result<(u32, i32), String> {
|
||||||
|
let abs_d = u32::try_from(d.abs()).unwrap(); // abs cannot return a negative
|
||||||
|
let n = floor_log2(abs_d);
|
||||||
|
let e = abs_d - (1 << n);
|
||||||
|
|
||||||
|
let f = if n > DIV_LUT_BITS {
|
||||||
|
round2(e, n - DIV_LUT_BITS)
|
||||||
|
} else {
|
||||||
|
e << (DIV_LUT_BITS - n)
|
||||||
|
};
|
||||||
|
|
||||||
|
let div_shift = n + DIV_LUT_PREC_BITS;
|
||||||
|
let div_factor = if d < 0 {
|
||||||
|
-DIV_LUT[f as usize]
|
||||||
|
} else {
|
||||||
|
DIV_LUT[f as usize]
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((div_shift, div_factor))
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,251 @@
|
|||||||
|
// Copyright 2023 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
use crate::bitstream_utils::BitReader;
|
||||||
|
|
||||||
|
use crate::codec::av1::helpers;
|
||||||
|
|
||||||
|
use super::parser::AnnexBState;
|
||||||
|
|
||||||
|
pub(crate) struct Reader<'a>(pub BitReader<'a>);
|
||||||
|
|
||||||
|
impl<'a> Reader<'a> {
|
||||||
|
pub fn new(data: &'a [u8]) -> Self {
|
||||||
|
Self(BitReader::new(data, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements uvlc(): Variable length unsigned n-bit number appearing
|
||||||
|
/// directly in the bitstream. See 4.10.3
|
||||||
|
pub fn read_uvlc(&mut self) -> Result<u32, String> {
|
||||||
|
let mut leading_zeroes = 0;
|
||||||
|
loop {
|
||||||
|
let done = self.0.read_bit()?;
|
||||||
|
|
||||||
|
if done {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
leading_zeroes += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if leading_zeroes >= 32 {
|
||||||
|
return Ok(u32::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = self.0.read_bits::<u32>(leading_zeroes)?;
|
||||||
|
Ok(value + (1 << leading_zeroes) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements leb128(): Unsigned integer represented by a variable number
|
||||||
|
/// of little-endian bytes. See 4.10.5
|
||||||
|
pub fn read_leb128(&mut self) -> Result<u32, String> {
|
||||||
|
let mut value = 0u64;
|
||||||
|
|
||||||
|
for i in 0..8 {
|
||||||
|
let byte = u64::from(self.0.read_bits_aligned::<u32>(8)?);
|
||||||
|
value |= (byte & 0x7f) << (i * 7);
|
||||||
|
|
||||||
|
if byte & 0x80 == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(value as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements su(n): Signed integer converted from an n bits unsigned
|
||||||
|
/// integer in the bitstream. (The unsigned integer corresponds to the
|
||||||
|
/// bottom n bits of the signed integer.). See 4.10.6
|
||||||
|
pub fn read_su(&mut self, num_bits: usize) -> Result<i32, String> {
|
||||||
|
let mut value: i32 = self
|
||||||
|
.0
|
||||||
|
.read_bits::<u32>(num_bits)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| String::from("Read more than 31 signed bits!"))?;
|
||||||
|
let sign_mask = 1 << (num_bits - 1);
|
||||||
|
|
||||||
|
if (value & sign_mask) != 0 {
|
||||||
|
value -= 2 * sign_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements ns(n): Unsigned encoded integer with maximum number of values
|
||||||
|
/// n (i.e. output in range 0..n-1). See 4.10.7
|
||||||
|
pub fn read_ns(&mut self, num_bits: usize) -> Result<u32, String> {
|
||||||
|
let w = helpers::floor_log2(num_bits as u32) + 1;
|
||||||
|
let m = (1 << w) - num_bits as u32;
|
||||||
|
let v = self.0.read_bits::<u32>(
|
||||||
|
usize::try_from(w).map_err(|_| String::from("Invalid num_bits"))? - 1,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if v < m.into() {
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extra_bit = self.0.read_bit()?;
|
||||||
|
Ok((v << 1) - u32::from(m) + u32::from(extra_bit))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements 5.9.13: Delta quantizer syntax.
|
||||||
|
pub fn read_delta_q(&mut self) -> Result<i32, String> {
|
||||||
|
let delta_coded = self.0.read_bit()?;
|
||||||
|
|
||||||
|
if delta_coded {
|
||||||
|
self.read_su(7)
|
||||||
|
} else {
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn more_data_in_bitstream(&mut self) -> bool {
|
||||||
|
self.0.num_bits_left() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn consumed(&self, start_pos: u32) -> u32 {
|
||||||
|
(self.0.position() / 8) as u32 - start_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the current OBU in AnnexB format.
|
||||||
|
pub fn current_annexb_obu_length(
|
||||||
|
&mut self,
|
||||||
|
annexb_state: &mut AnnexBState,
|
||||||
|
) -> Result<Option<usize>, String> {
|
||||||
|
if !self.more_data_in_bitstream() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::comparison_chain)]
|
||||||
|
if annexb_state.temporal_unit_consumed == annexb_state.temporal_unit_size {
|
||||||
|
annexb_state.temporal_unit_size = 0;
|
||||||
|
} else if annexb_state.temporal_unit_consumed > annexb_state.temporal_unit_size {
|
||||||
|
return Err(format!(
|
||||||
|
"temporal_unit_size is {} but we consumed {} bytes",
|
||||||
|
annexb_state.temporal_unit_size, annexb_state.temporal_unit_consumed,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if annexb_state.temporal_unit_size == 0 {
|
||||||
|
annexb_state.temporal_unit_size = self.read_leb128()?;
|
||||||
|
if annexb_state.temporal_unit_size == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_pos = self.consumed(0);
|
||||||
|
|
||||||
|
#[allow(clippy::comparison_chain)]
|
||||||
|
if annexb_state.frame_unit_consumed == annexb_state.frame_unit_size {
|
||||||
|
annexb_state.frame_unit_size = 0;
|
||||||
|
} else if annexb_state.frame_unit_consumed > annexb_state.frame_unit_size {
|
||||||
|
return Err(format!(
|
||||||
|
"frame_unit_size is {} but we consumed {} bytes",
|
||||||
|
annexb_state.frame_unit_size, annexb_state.frame_unit_consumed,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if annexb_state.frame_unit_size == 0 {
|
||||||
|
annexb_state.frame_unit_size = self.read_leb128()?;
|
||||||
|
if annexb_state.frame_unit_size == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
annexb_state.temporal_unit_consumed += self.consumed(start_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_pos = self.consumed(0);
|
||||||
|
let obu_length = self.read_leb128()?;
|
||||||
|
let consumed = self.consumed(start_pos);
|
||||||
|
|
||||||
|
annexb_state.temporal_unit_consumed += consumed;
|
||||||
|
annexb_state.frame_unit_consumed += consumed;
|
||||||
|
|
||||||
|
Ok(Some(obu_length.try_into().unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements 5.3.4.
|
||||||
|
pub fn read_trailing_bits(&mut self, mut num_bits: u64) -> Result<(), String> {
|
||||||
|
let trailing_one_bit = self.0.read_bit()?;
|
||||||
|
num_bits -= 1;
|
||||||
|
|
||||||
|
if !trailing_one_bit {
|
||||||
|
return Err("bad padding: trailing_one_bit is not set".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
while num_bits > 0 {
|
||||||
|
let trailing_zero_bit = self.0.read_bit()?;
|
||||||
|
if trailing_zero_bit {
|
||||||
|
return Err("bad padding: trailing_zero_bit is set".into());
|
||||||
|
}
|
||||||
|
num_bits -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_subexp(&mut self, num_syms: i32) -> Result<u32, String> {
|
||||||
|
let mut i = 0;
|
||||||
|
let mut mk = 0;
|
||||||
|
let k = 3;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let b2 = if i != 0 { k + i - 1 } else { k };
|
||||||
|
let a = 1 << b2;
|
||||||
|
if num_syms <= mk + 3 * a {
|
||||||
|
let num_bits = num_syms - mk;
|
||||||
|
let subexp_final_bits = self.read_ns(num_bits as usize)?;
|
||||||
|
return Ok(subexp_final_bits);
|
||||||
|
} else {
|
||||||
|
let subexp_more_bits = self.0.read_bit()?;
|
||||||
|
if subexp_more_bits {
|
||||||
|
i += 1;
|
||||||
|
mk += a;
|
||||||
|
} else {
|
||||||
|
let num_bits = b2 as usize;
|
||||||
|
let subexp_bits = self.0.read_bits::<u32>(num_bits)?;
|
||||||
|
return Ok(subexp_bits + mk as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements 5.9.27.
|
||||||
|
pub fn decode_unsigned_subexp_with_ref(&mut self, mx: i32, r: i32) -> Result<u32, String> {
|
||||||
|
let v = self.decode_subexp(mx)?;
|
||||||
|
if (r << 1) <= mx {
|
||||||
|
Ok(helpers::inverse_recenter(r, v.try_into().unwrap())
|
||||||
|
.try_into()
|
||||||
|
.unwrap())
|
||||||
|
} else {
|
||||||
|
let res = mx - 1 - helpers::inverse_recenter(mx - 1 - r, v.try_into().unwrap());
|
||||||
|
Ok(res.try_into().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements 5.9.26.
|
||||||
|
pub fn decode_signed_subexp_with_ref(
|
||||||
|
&mut self,
|
||||||
|
low: i32,
|
||||||
|
high: i32,
|
||||||
|
r: i32,
|
||||||
|
) -> Result<i32, String> {
|
||||||
|
let x = self.decode_unsigned_subexp_with_ref(high - low, r - low)?;
|
||||||
|
Ok(i32::try_from(x).unwrap() + low)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements 5.3.5 Byte alignment syntax
|
||||||
|
pub fn byte_alignment(&mut self) -> Result<(), String> {
|
||||||
|
while (self.0.position() & 7) != 0 {
|
||||||
|
self.0.read_bit()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Clone for Reader<'a> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Generates the CRCs for all .av1 files in the current directory using ffmpeg.
|
||||||
|
|
||||||
|
for f in `ls *.av1`; do
|
||||||
|
ffmpeg -i $f -pix_fmt nv12 -f framehash -hash crc32 - |grep -v '^#' |awk '{print $6}' >$f.crc
|
||||||
|
ffmpeg -i $f -pix_fmt nv12 -f framehash -hash md5 - |grep -v '^#' |awk '{print $6}' >$f.md5
|
||||||
|
done
|
||||||
BIN
Binary file not shown.
+260
@@ -0,0 +1,260 @@
|
|||||||
|
{
|
||||||
|
"profile": "AV1PROFILE_PROFILE_MAIN",
|
||||||
|
"width": 320,
|
||||||
|
"height": 240,
|
||||||
|
"frame_rate": 25,
|
||||||
|
"num_frames": 250,
|
||||||
|
"num_fragments": 250,
|
||||||
|
"md5_checksums": [
|
||||||
|
"83dab175e49c33a6e3ece5c3758d1bf6",
|
||||||
|
"cedb4e25453dba430cb6ee830c1643f3",
|
||||||
|
"ef14d142df162819eee800f3930a9e95",
|
||||||
|
"11849ccb72cfdabc8f70e33271cd916f",
|
||||||
|
"7c9856ed61566f399a1eda2243e199ca",
|
||||||
|
"9394ed10354e986ecdfa2b753b921d2b",
|
||||||
|
"573005df5ba8f982980c1f427dfb472e",
|
||||||
|
"a96d9a913b1712f37f73b6c47b577cdf",
|
||||||
|
"91fc52b076badd1e6cbddb9bca5c5cc3",
|
||||||
|
"6253dde984bc5282f01005d7ebb55fe9",
|
||||||
|
"ad88146fe374423e7c597a922049c76c",
|
||||||
|
"ca7d1ddea7269e476b43a805bd5dd50b",
|
||||||
|
"e3d1eeec3cbfa363a2222d274890be84",
|
||||||
|
"fd7902a1b7352e04dc3940a010482a67",
|
||||||
|
"cada3ee9e33c11c99f89c3f439e2723e",
|
||||||
|
"1723ee290ae8be930f2904cb5aaf9de4",
|
||||||
|
"bf48d536e70a6a0a6549e221a833ebe4",
|
||||||
|
"f13b2e454420b1e0e5ab0adbbf2ce72f",
|
||||||
|
"31c799b4bb0971b798970e23b906cd25",
|
||||||
|
"3d925424bf645a225caf64757944a6e5",
|
||||||
|
"ac55ba724f54dd068350fd2818064dd5",
|
||||||
|
"a7125b8b0dc1e464e76ee64956897d22",
|
||||||
|
"f2a00db38d83cc778fa9fbdb3f4515e3",
|
||||||
|
"c804823f3401558a0283c4e77888af20",
|
||||||
|
"662d71a06319faef70a0981b664481d2",
|
||||||
|
"ad3a221a5f1f9d1a615733155a194385",
|
||||||
|
"8b071335d6cee4b227782415b1cb8a13",
|
||||||
|
"f3d98bfac8e5083233b88386b539b790",
|
||||||
|
"739c73e71590e64db0629911039962b4",
|
||||||
|
"0944990b0ada4012cc686b06833264ef",
|
||||||
|
"77e306874ec1b0c91668f8df0953831b",
|
||||||
|
"ca376820c5248cccb221ec8cb4b0eb9e",
|
||||||
|
"86a72cec3aaf50e393880ac8d4139921",
|
||||||
|
"b892ef1ff0c169b683b35cbaa9462ee3",
|
||||||
|
"b5feafe6c294d29adb1b05138803be36",
|
||||||
|
"3c3b150801f2dc48d300380c9a932890",
|
||||||
|
"16b06dfb6e426a1b6d88e17de83f5e2c",
|
||||||
|
"b1633b1c661a645bd1b04317b30fdbfa",
|
||||||
|
"56d08d66ad8042ba295c3f86025d44cf",
|
||||||
|
"78f6697fbb3af79dbd614f495c372031",
|
||||||
|
"afb700399aacde4c8c895def06fd8594",
|
||||||
|
"e7ae993d18af5c58047196d8369eee3e",
|
||||||
|
"65d5d77181388229606972ea99f5191a",
|
||||||
|
"bd336774b22d502d8fdca07d881d737e",
|
||||||
|
"5cabd4479d94c040f86892fa41860ca9",
|
||||||
|
"9ab949d5ede2f25889a9612b39f6f158",
|
||||||
|
"bb54bb8d7782c4b2d63f201a8d338e7d",
|
||||||
|
"9bf73fc10b4bb19021fd07586a0401d1",
|
||||||
|
"a37621cda632ac5f58e03272d226e53b",
|
||||||
|
"5c3ca20aa646d72b69f6e56592fd2f0b",
|
||||||
|
"b8ff47ec622ce73e70cfc913a3f8116e",
|
||||||
|
"1d020f74b9d3adf17b3f3da6992fcb52",
|
||||||
|
"0236c4d4a9aa68ba59eeaeb6b010db26",
|
||||||
|
"78a8e16aebbe5fc3d1e6051153031a51",
|
||||||
|
"78a54c1182b8ce007a2c10d17915272d",
|
||||||
|
"c7ac4f87168bbddd01155f54c79bc132",
|
||||||
|
"e074be8ece629e08d6d93029b8d34ce2",
|
||||||
|
"edfba91c624f4cdc558c693b33165542",
|
||||||
|
"f3d005151909922bfed7a905693de7c0",
|
||||||
|
"80118fa45af8b3e486928aae49d85b94",
|
||||||
|
"e7663b97fc9b26348876d5fa64a54d5d",
|
||||||
|
"69200a2a34bd4beccf4d473b4a4976f3",
|
||||||
|
"82f6c63a1d87037c08fba3bf16ef9bf8",
|
||||||
|
"6e3033f25eb56670a3c45c12822f340c",
|
||||||
|
"6085ac2c582fd3048c240d3dc46e3455",
|
||||||
|
"b4bbf1a7ae3f57b1d8f0ed65070e010b",
|
||||||
|
"e7f0aad67af5c7719eb89e457cefe6b4",
|
||||||
|
"6d5fdc1d68e136ce9095696be59b617b",
|
||||||
|
"bf15759321de7b21ec3c1bd2cb98ca69",
|
||||||
|
"fc4d67815a94c2e18e7d8b0b7d866641",
|
||||||
|
"0d96425a800252d500fd25da7dccb94d",
|
||||||
|
"5438c9e83cc90f030ee9b1876ec28bdc",
|
||||||
|
"a88915fd11ad765299cef49c8e65e8ab",
|
||||||
|
"4995a3442fa74849f3c45888037f9b42",
|
||||||
|
"82693761e531127d2a98cdc5615cdb92",
|
||||||
|
"07b9601bebc32dc33fd806f90cee586f",
|
||||||
|
"d68df0d6b1dae0e4428a17717723dbe0",
|
||||||
|
"2851422da5bcceebf57ae37a02a1fb57",
|
||||||
|
"ae9ff5d20ea6ba6105fb5ed3e0109834",
|
||||||
|
"a0f956663545e54acbaecf684d0841ff",
|
||||||
|
"23589649e474e155583e6742fa79098c",
|
||||||
|
"9031fbd94bd7a0508b48f8081018d447",
|
||||||
|
"b72920bce007e8f0584d615529dda39f",
|
||||||
|
"0638d4c62dbf43087b4831f2e39314fe",
|
||||||
|
"7d34a23c9b78a61da7a60c520784e204",
|
||||||
|
"0a3cf1af9095a7cb2787cf79ed257c59",
|
||||||
|
"1c05ab5abe793c14ee53ea80e170d6cc",
|
||||||
|
"02d0b03fe10b1b6675b86390fe317778",
|
||||||
|
"7639df2f62c4f958a90337b19a591530",
|
||||||
|
"6080ee3519bd5dd88fbb4050459c126b",
|
||||||
|
"75ce7282839cda34f7ca6111d6353cdb",
|
||||||
|
"2214093ad2bb512a223808ed78fe3369",
|
||||||
|
"eb9bfad6ef08a5f28b531203db3fcb43",
|
||||||
|
"cf4fd68dd57df907f35cc55fe5930fb8",
|
||||||
|
"0aaf799fc8509acd1168cd667f789c9c",
|
||||||
|
"e36ebe2332eb5a9a0ddd03469493d3cb",
|
||||||
|
"49ec5c818b0fcc02fbe3f5636417a2da",
|
||||||
|
"8430b81f1a90c325d116cec424d3c32c",
|
||||||
|
"c67e1e1f7fec9f9ab688f1d54cf2f4d4",
|
||||||
|
"fd98fabb3db5dc2d841dc50af7395130",
|
||||||
|
"8af0c8f486057cbae86939fa248c8238",
|
||||||
|
"e1888b1f32d2991682261f28f860cb27",
|
||||||
|
"9c0455744df622675a6d5232d790f5b1",
|
||||||
|
"acc73e6665f54bdaa98acead49be5f65",
|
||||||
|
"5c6e6167e6c05411a22c348a2c807fc5",
|
||||||
|
"89d09cb95dc2e212efed266b4b687b35",
|
||||||
|
"9d35702693b699ac0ccb4bffc559e2e8",
|
||||||
|
"afbeb04e4e00a1b0a4407b4a34815a63",
|
||||||
|
"6f3515d4331d83abfd1dbdfb2de78189",
|
||||||
|
"be6cb0da9149ad5d68269d0ed657cd38",
|
||||||
|
"9e9f498bc71df3097caa266089a77fba",
|
||||||
|
"40d9198beea17a254ee6b74544317a7a",
|
||||||
|
"e2f04358338ae115635e621cf229df56",
|
||||||
|
"5da22d2d7c5c304fc6198f809206de1d",
|
||||||
|
"4951c1ad5ea945a098c6a4d7fad163fa",
|
||||||
|
"5a25300d1ef28b60f9e137b47b2e4364",
|
||||||
|
"5954dd49f459ea4c0984dba30076db89",
|
||||||
|
"820a838b57e43271a55f2f17c3e0676f",
|
||||||
|
"b486477fe004ed8ee213afe8ecdb35c6",
|
||||||
|
"0715a96ada76fe2d713cdfc2b9abf11d",
|
||||||
|
"c30544fc39df1c7876043115ceed355f",
|
||||||
|
"1f7ce18debf0e339db20c68c1f0bf8b8",
|
||||||
|
"e10229f1f8a95d2e59a086e5d6ac4faa",
|
||||||
|
"8ad6c73b38d32fd1712a1fd67750d364",
|
||||||
|
"30fdf5b06ae3fd2c0df9e631ea1e1048",
|
||||||
|
"0026b1e7db5e25651ecaaf19685b2dc2",
|
||||||
|
"0b53f3f2ca17d00138b4678580d63ab6",
|
||||||
|
"cb08de2030a42826ef573082188d3614",
|
||||||
|
"e14242c776b3a32da936d1f6484e29ba",
|
||||||
|
"41a43d3ee9021bcda09efa1b218cc65a",
|
||||||
|
"9b884d243e8b330edb8f569e416960f1",
|
||||||
|
"5737c4f611cdc9871df094ba51a6a6d5",
|
||||||
|
"9d47768c1a15c0b20d28545ad5e6ca53",
|
||||||
|
"8c981af4e6c3432a330b6d8e330bc03a",
|
||||||
|
"195910783821f51cb3dad54f651c1a75",
|
||||||
|
"659833dc03dc5f929eb140e188f2b1f6",
|
||||||
|
"f2f30eaa72c5a093d67cebfda15fac73",
|
||||||
|
"3c521e3025340933cf826027a994114b",
|
||||||
|
"469b72a8db2ac77c70a792c8035b0288",
|
||||||
|
"e2cf1d7930d44341c5c363b0ceaa6c84",
|
||||||
|
"469d4283cca04e78ac2cb9407531db67",
|
||||||
|
"02416a6c77174796ecbd61adab5177b9",
|
||||||
|
"f89077b3dfb54119157e04dff24b79bd",
|
||||||
|
"2d1a8fc14b649e343288b5972abe1fdf",
|
||||||
|
"3d059186dab49cd8d13584dc365862db",
|
||||||
|
"765ef42f457dee16234df6992ec74cd2",
|
||||||
|
"957589c357d82631477177e6250c713c",
|
||||||
|
"2473af8395d08e9396216c082f854d6f",
|
||||||
|
"6a117d16f8e8ffe87aebec05844d6a0a",
|
||||||
|
"8e467cfcbd66ed80fca648248af56b3f",
|
||||||
|
"71e39c4304e09688750aa85e9d040f4a",
|
||||||
|
"3071ee85d87ef8029619d4777c799c2b",
|
||||||
|
"cc86f09e3f228001e806fd3afe5e1271",
|
||||||
|
"3828673cf67828240005f7bf9fe37412",
|
||||||
|
"12700b04cc22fba7688ae1709e48b886",
|
||||||
|
"a1045aa13b0007d23f22ad8e83c4bdb0",
|
||||||
|
"a12e6c2d2805336c407417b7503670fe",
|
||||||
|
"11dd1907c6785eccbedeba6a5708c516",
|
||||||
|
"3fb8be1276b83db1ccd8206cc69ec736",
|
||||||
|
"980ce03ae4e6f2eceb314f37290e0dd2",
|
||||||
|
"c39395d9b6108f1bd4ab1f004c44a5f0",
|
||||||
|
"a7244c89efd3b00611c92ebaf98793e8",
|
||||||
|
"fbede5a2957c023216e79cc31ab32946",
|
||||||
|
"bb40544a3adfe992e07a8eb3e5bf6966",
|
||||||
|
"0f71f57100699e95fff51dabde86cb42",
|
||||||
|
"6ba085efefacf385728423ca99bd4629",
|
||||||
|
"4df2d956234cb2129ebefdd90f28185a",
|
||||||
|
"d0a0d4263ef4f32d59b004752a5b94af",
|
||||||
|
"a9deed8bf550f4ee370730e1d993d4dc",
|
||||||
|
"eca65ba94930bf78854ce991c05e3d9c",
|
||||||
|
"4eef9fa5bca7ee74e97a0ac6e12e5f53",
|
||||||
|
"84e97d688ba1922f9ed71d72cea67259",
|
||||||
|
"407dc974664f93be14c53a2b291cc422",
|
||||||
|
"31eb9e589470cfbe4e72e1c441d56e3f",
|
||||||
|
"ea4f696ad2ae150a1f0e622874f67fca",
|
||||||
|
"7552fea18e053b4771c0490015488291",
|
||||||
|
"6863cd478581244fff72f6a0021a9fb8",
|
||||||
|
"9118deacdd49db27c317eeec868cd870",
|
||||||
|
"50290c87c5ee1c558bd304fc0f4a15ea",
|
||||||
|
"5aef6d78538151648450fd391c9deaec",
|
||||||
|
"a7e3feb23e01e0b4556446e01b2d81eb",
|
||||||
|
"6918645d2b0bd511247c6ed372bf213e",
|
||||||
|
"2f9661df4d114ab9f3a98a255a8177f2",
|
||||||
|
"baaea6f9f05a7a2bc10721f1afe6f57b",
|
||||||
|
"366ab99c236a8e84e658bad69d82a1ba",
|
||||||
|
"f950a6508c051ef32e4c04be56fcc719",
|
||||||
|
"9e586c73c3b9daebc608600efbee33b0",
|
||||||
|
"e9fb7690077e8cbac15d5564130a8d7c",
|
||||||
|
"ef3c8d2b5376753c26a795bf403cdcc1",
|
||||||
|
"be46eec37b67dfc8077705ce89588a7c",
|
||||||
|
"dba1b08cfeddbcf4e8d79621ee343bd4",
|
||||||
|
"a4a612bcb6c33c7799433c998c5f12d1",
|
||||||
|
"5f45fa214bc860825510d034f1ff26de",
|
||||||
|
"1766dd6fe6080c408a4baca135609f1c",
|
||||||
|
"34dc16d50d7afeb93a874563f1f5e4fd",
|
||||||
|
"0a5314f217972a9271c4792921f61153",
|
||||||
|
"7c228f1f5ccc6f7307526531f79a307d",
|
||||||
|
"188544479f5a44b6a90b6be62e33611a",
|
||||||
|
"9cb9be5574c6a2c06862a3ed605c68b5",
|
||||||
|
"845b033cc82472d8e647f2a3bb3eb653",
|
||||||
|
"2c6b66c68107fd13395a1ed5f0639355",
|
||||||
|
"0436424e24e562c52ab7e7063b04c129",
|
||||||
|
"21248608746bb252faa347028f5343d7",
|
||||||
|
"81607476ae05bbf7386dea9d8eac352b",
|
||||||
|
"fadc83171e49e779ebe5d81769049cef",
|
||||||
|
"e768c96238a8cf14caf8d03420981265",
|
||||||
|
"c57aa4d8e2f7c9101868246de4de1c13",
|
||||||
|
"b405a01f8eb1d4ffade0e49f593e5a8f",
|
||||||
|
"76f02bab5b932bf4bcf4ce06bdf0e42c",
|
||||||
|
"51b09ce7117e15cf654ad76e13263f0c",
|
||||||
|
"45f650b845e31b87b522574cc7afde80",
|
||||||
|
"858605e70124ef7130742b713b079ad9",
|
||||||
|
"5d955329a007742ecab7153d2e69b262",
|
||||||
|
"c4139450cb6ffcac62b764d62083bc31",
|
||||||
|
"8b13de43cffa8d12dd17b8e749a375b2",
|
||||||
|
"b000fbdb48be876302bc336aeb6deba1",
|
||||||
|
"88e2b7abbc33f70231889113582f8f8c",
|
||||||
|
"d9f31d4cfeee31da719b3567d5d11a19",
|
||||||
|
"7c2a6cc9be9ce8cb96b5a1616909941b",
|
||||||
|
"954c2be78496f0235ab137a98a5ae11a",
|
||||||
|
"9efd13fe15e8900f67a6c76103c9ce78",
|
||||||
|
"b04aedbee4993ff955ebba1dcb8a04e1",
|
||||||
|
"83b045a2a43445f97d82c1faaf33b332",
|
||||||
|
"1cf416bf7bf4a2124932d4b1bb0aca94",
|
||||||
|
"ac17d329b587b065512269fa3def8279",
|
||||||
|
"fcf2b7ab8b7d53741c737f75c3587a6f",
|
||||||
|
"0aaaaca0aa165a6786e2161043aa1e72",
|
||||||
|
"8d19a0848fad7b32a15522cc0b24b4d1",
|
||||||
|
"be0a1be3f90ff47b80be31be021c28f1",
|
||||||
|
"c9e423c59186d93100c56ad8f14c0fb4",
|
||||||
|
"052dbb80693750c716acce06f3ad7266",
|
||||||
|
"6a2c8002df49d3832b7e1abd70af542f",
|
||||||
|
"92bec3aa91edfd2cc5dee7d1f66ac189",
|
||||||
|
"579d29110e7a7a71152aedb9a462c79f",
|
||||||
|
"b0bb435df9dd211dd5da3c547c8887b8",
|
||||||
|
"fb7a3cf05afd06668287f5dcc9c20a55",
|
||||||
|
"bbd27b9b555f361478f9516e839077a5",
|
||||||
|
"a74dc95fe982709b25434bcc53a91973",
|
||||||
|
"27f82418568440caea8df7bcdf56eae7",
|
||||||
|
"d5ff54f75c8fa6d60428816945ee5e89",
|
||||||
|
"4b6677c4cc9866f477ecda5618411c8d",
|
||||||
|
"d56046f25113a82b090bf9a91291587c",
|
||||||
|
"cb56aa0f4343c7da821c04e55e8af21a",
|
||||||
|
"c633ea8a4c20ea4d70c8c241d0293e9b",
|
||||||
|
"dd2b157ce8a010a61908170ecb3e31b0",
|
||||||
|
"3c3509c92b03a702ac1f4a28b5abd3d5",
|
||||||
|
"d1400e54b8b61241cabad5291d6e3a43",
|
||||||
|
"c29470cd6afb0aee4ae4a8d0622a468d",
|
||||||
|
"e35764f7bd48746d37478b2d6f3f2755",
|
||||||
|
"3dcf1fd38fbcc1d45f98574b17ac5710"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
+250
@@ -0,0 +1,250 @@
|
|||||||
|
6aea6152
|
||||||
|
cb4a90b3
|
||||||
|
fd83e35f
|
||||||
|
074bd081
|
||||||
|
216fcc04
|
||||||
|
c73ca1e4
|
||||||
|
fbfb2a30
|
||||||
|
cd587935
|
||||||
|
e8bc2912
|
||||||
|
051517b7
|
||||||
|
f3e9831d
|
||||||
|
812e7bba
|
||||||
|
3e2054e6
|
||||||
|
7446385e
|
||||||
|
8b75d043
|
||||||
|
f930d9a7
|
||||||
|
7bf6b591
|
||||||
|
253c5389
|
||||||
|
11a25f1f
|
||||||
|
c5101d08
|
||||||
|
ee1c0aae
|
||||||
|
ce055a9f
|
||||||
|
0ed4a046
|
||||||
|
aa0a72c3
|
||||||
|
87f7a598
|
||||||
|
aa3a422e
|
||||||
|
0ee0e533
|
||||||
|
5ce3d683
|
||||||
|
cc7e88e5
|
||||||
|
3e1c7774
|
||||||
|
bd6708d3
|
||||||
|
0ba6a0ee
|
||||||
|
ac00d2da
|
||||||
|
f1222e65
|
||||||
|
3c6aadc8
|
||||||
|
a28e7327
|
||||||
|
fd2d0d8c
|
||||||
|
e0a791bf
|
||||||
|
90a33d7e
|
||||||
|
63ae28ce
|
||||||
|
26230e31
|
||||||
|
215e9021
|
||||||
|
6ca1295b
|
||||||
|
01604116
|
||||||
|
15f8d3c9
|
||||||
|
6f28a1d4
|
||||||
|
8d54633d
|
||||||
|
08cccd41
|
||||||
|
0b082923
|
||||||
|
0aaae5a1
|
||||||
|
98310e98
|
||||||
|
33f12578
|
||||||
|
9fd072e0
|
||||||
|
36a1220e
|
||||||
|
a278c894
|
||||||
|
828dcf3f
|
||||||
|
3573505c
|
||||||
|
861874b7
|
||||||
|
dd78b1fd
|
||||||
|
310ca9a0
|
||||||
|
74d670f3
|
||||||
|
93c48c50
|
||||||
|
814991b1
|
||||||
|
fca836a7
|
||||||
|
9299e121
|
||||||
|
a8d3b267
|
||||||
|
661a1c05
|
||||||
|
50038462
|
||||||
|
bc866fe1
|
||||||
|
46e2d826
|
||||||
|
7035f78c
|
||||||
|
00c2ed17
|
||||||
|
6b64a0c6
|
||||||
|
315f4761
|
||||||
|
df4c3ef7
|
||||||
|
7ad4996a
|
||||||
|
3185972f
|
||||||
|
19af5f17
|
||||||
|
01c4e3a3
|
||||||
|
3728bd7e
|
||||||
|
16adc384
|
||||||
|
5798878c
|
||||||
|
d0ac34ee
|
||||||
|
56f76f8a
|
||||||
|
2b6f3f47
|
||||||
|
4a5ec1a9
|
||||||
|
75f0027c
|
||||||
|
7a1dd1f6
|
||||||
|
50c0f14f
|
||||||
|
978d00e7
|
||||||
|
a1c430b8
|
||||||
|
7c67432a
|
||||||
|
4615b4cb
|
||||||
|
c53c7308
|
||||||
|
876d9f98
|
||||||
|
b0015dd1
|
||||||
|
73288a1e
|
||||||
|
d843f40a
|
||||||
|
4127f309
|
||||||
|
3adbe013
|
||||||
|
9e84104c
|
||||||
|
9f98680d
|
||||||
|
380a36e2
|
||||||
|
2202f414
|
||||||
|
ca931e02
|
||||||
|
182b20e1
|
||||||
|
815a6be2
|
||||||
|
59cc5d53
|
||||||
|
0a05d420
|
||||||
|
d53b5f2d
|
||||||
|
98c613aa
|
||||||
|
e4de4fc1
|
||||||
|
2b8f6d29
|
||||||
|
48ce9586
|
||||||
|
d6e822dd
|
||||||
|
c8674577
|
||||||
|
75dfa0e9
|
||||||
|
7ad6ed39
|
||||||
|
f7725767
|
||||||
|
a44e0f11
|
||||||
|
bcd743cd
|
||||||
|
1ccf1835
|
||||||
|
8ceb6153
|
||||||
|
0a43af09
|
||||||
|
79bab263
|
||||||
|
855dd65a
|
||||||
|
a37d34fe
|
||||||
|
be063b4d
|
||||||
|
aec5df0c
|
||||||
|
7b12399b
|
||||||
|
c5ad7294
|
||||||
|
2f609050
|
||||||
|
8bb2dcba
|
||||||
|
c19aa763
|
||||||
|
700eb99b
|
||||||
|
11bf5174
|
||||||
|
e74d5eda
|
||||||
|
f54e54ef
|
||||||
|
0173e328
|
||||||
|
162372d6
|
||||||
|
3c8f18a3
|
||||||
|
ce8579c7
|
||||||
|
91bdbf86
|
||||||
|
745edb27
|
||||||
|
df783664
|
||||||
|
e7462cfd
|
||||||
|
30d8d761
|
||||||
|
f15474e6
|
||||||
|
4c5bb874
|
||||||
|
27c36dd2
|
||||||
|
c40981c0
|
||||||
|
1cc4b7c0
|
||||||
|
ebd96a40
|
||||||
|
a5f4d31e
|
||||||
|
cd1a957a
|
||||||
|
0b0d8e1d
|
||||||
|
c5d30b99
|
||||||
|
d7d6b75e
|
||||||
|
0ea512b8
|
||||||
|
dd12fced
|
||||||
|
29eab68d
|
||||||
|
e4bb47d1
|
||||||
|
e3060770
|
||||||
|
0483e6ca
|
||||||
|
296b084f
|
||||||
|
d11abff6
|
||||||
|
c7589394
|
||||||
|
b187d62e
|
||||||
|
2e217b3c
|
||||||
|
b516603b
|
||||||
|
287c8b71
|
||||||
|
2f24a994
|
||||||
|
7ea617d0
|
||||||
|
7ba0024e
|
||||||
|
7067ae3e
|
||||||
|
95650cbd
|
||||||
|
7b2e7deb
|
||||||
|
d1b6da6d
|
||||||
|
76f22a5a
|
||||||
|
9ce6fac7
|
||||||
|
1e5cf6c7
|
||||||
|
bbe20b47
|
||||||
|
2f2cc450
|
||||||
|
3f0297f3
|
||||||
|
a2084503
|
||||||
|
06487daa
|
||||||
|
5d06ae48
|
||||||
|
02cbba1b
|
||||||
|
b386b4f4
|
||||||
|
47623370
|
||||||
|
1acb6e9d
|
||||||
|
5b55fd00
|
||||||
|
70d43ae3
|
||||||
|
8a0703c0
|
||||||
|
4d62a79b
|
||||||
|
e358a27d
|
||||||
|
b89fcec0
|
||||||
|
59508160
|
||||||
|
f2c9f57d
|
||||||
|
2b57ec09
|
||||||
|
9427c163
|
||||||
|
91169e76
|
||||||
|
eda7cda1
|
||||||
|
289c7bc0
|
||||||
|
821dc9d5
|
||||||
|
b003d6ce
|
||||||
|
3d8dbc53
|
||||||
|
d6c3324a
|
||||||
|
76a7b9c9
|
||||||
|
a4d4b21b
|
||||||
|
51ce1f2c
|
||||||
|
49cc8861
|
||||||
|
b4fc2309
|
||||||
|
c435862e
|
||||||
|
3bceba0f
|
||||||
|
7ee0c219
|
||||||
|
cb04facd
|
||||||
|
b2fad23b
|
||||||
|
2b8cf4f1
|
||||||
|
fcdcbad2
|
||||||
|
f3c038c6
|
||||||
|
71e639e9
|
||||||
|
282325bf
|
||||||
|
52c64dc8
|
||||||
|
9ebe8705
|
||||||
|
f8679dc3
|
||||||
|
906116a2
|
||||||
|
9f740960
|
||||||
|
3cad2af8
|
||||||
|
c7765bb4
|
||||||
|
c8f38650
|
||||||
|
1e5d7d36
|
||||||
|
3374dc4c
|
||||||
|
e83476e5
|
||||||
|
6a9c0d00
|
||||||
|
08b8cb58
|
||||||
|
bd74ea9e
|
||||||
|
399bce4c
|
||||||
|
1577335d
|
||||||
|
cdbbea48
|
||||||
|
d1373c35
|
||||||
|
cc62212a
|
||||||
|
8a5e59e2
|
||||||
|
75f5f297
|
||||||
|
8c5f3776
|
||||||
|
fce09105
|
||||||
|
92b2d17e
|
||||||
|
416e8530
|
||||||
|
968d1450
|
||||||
|
d20688a8
|
||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
ce5386b1beaa89f73a668d3720717fb4
|
||||||
|
8f5d21bf3d78d9c242609bdd8df6cfd6
|
||||||
|
7d75a95c82171c8cf4b09d694d4cbef9
|
||||||
|
20d4bebdc51702ee7f40e187747696c7
|
||||||
|
9c55dc575db2a189c5576537afca2c54
|
||||||
|
68a15a8cdbc0aeaf10dff65db4d1a810
|
||||||
|
dfd03584b10852eb16901d52578d8f8e
|
||||||
|
a9a0cffed57bba344c9366ce91408e26
|
||||||
|
9fdf7ac0fc07f2391da56462faf3e2e0
|
||||||
|
b83d2da8f82e1a06d26bbb514b471f4b
|
||||||
|
17c92a58fb68d0b3b02cb5b9105dbb37
|
||||||
|
1e5bd3c05fb67f2aefc01611ed43d117
|
||||||
|
eb287667a027f3e5325bfb27652021a6
|
||||||
|
67e388816af98b99e4b9134516205c52
|
||||||
|
35cba13cd8a6f23d6dfd95803af0886b
|
||||||
|
45966e4a1b83dce6a2b0c259fea10cae
|
||||||
|
416a95236e46a7bb0f11cf1147631979
|
||||||
|
f23ab59d3c4efbb8eb2d80d67a2e5136
|
||||||
|
ab7bce374a3468c03483418ccffb52de
|
||||||
|
bdaaf22c6c4e58d393d7c051d4d0ae8a
|
||||||
|
6f7de2de3e8d79c726f0f2c2fe52ee58
|
||||||
|
d2fc7067797bbffe3765a238ff803b85
|
||||||
|
769dbc781b43759287da0b2888e8338b
|
||||||
|
2abd608af57978290e24c4cd62e4904f
|
||||||
|
4fb54d7b0a71a4d3ca95b56d7b3d9a81
|
||||||
|
262ba91321a0edac67c01e6ddcc2e4f7
|
||||||
|
4c9c2d967fb853ecdc6042f223a7d138
|
||||||
|
f21ccd6a4eb5f561b0a0eff9cb1956ec
|
||||||
|
3c206ed277864249e7eaade75e71c815
|
||||||
|
7167fed00621472662b19a0215dd00c8
|
||||||
|
76e6f142e710ff5d7f6797d3fc1b8dfd
|
||||||
|
4d814bb1fec0a2bca9d92140d5734ae0
|
||||||
|
65b576bf469b182c8954591fd15dd21c
|
||||||
|
abb7e8f11617aa0c9bf6870e537b008d
|
||||||
|
02b86ea6b2f8699097d6899ab1f073c4
|
||||||
|
610786c84b9fc118679afb09404c15ae
|
||||||
|
7feb8fae65767f848ebbefa730f15d68
|
||||||
|
12677768a4266571e85b750b51ef9fc8
|
||||||
|
5b6541b178f6a885d0439b381e501967
|
||||||
|
352cc90ecc1f3e26c19970f106c475fa
|
||||||
|
9c5215a9fce00f0ec30615649171f8e4
|
||||||
|
27e593a4b30c0b64939c62e5e0488e5a
|
||||||
|
d8a5fd54958527ecefc642f6730e4164
|
||||||
|
754dfeb4337108e534ba70bab429777a
|
||||||
|
821ad11c37ffda3ad5438b57a9523e9f
|
||||||
|
add12a889a5e08a5358e2e0e086fe522
|
||||||
|
fa100278f49cbb7f9c9d7dc5e12fc0e1
|
||||||
|
ef46a151df454084d0f0bc644adeb47d
|
||||||
|
c02f8023a4be8a3249bcf6e1b223dffa
|
||||||
|
41a11da85806e85fa86a9de41e1cd93f
|
||||||
|
e792d89b5df357d1b03b1812a762832c
|
||||||
|
90f72aead8124bc6ce51495b4b99da75
|
||||||
|
af25c00b1990b778b50f529ae81efa30
|
||||||
|
900f66f0e222a8d253153711a23a5ee9
|
||||||
|
0850cad30d59e1b5e5e2943e6b916d0a
|
||||||
|
03ca961e9876ee96b14711c44a9f2a69
|
||||||
|
fa6bf9c30dd8618fdab499e9dd217582
|
||||||
|
4e09e08e1f99bf210d4757281ab4c836
|
||||||
|
dbf2bc5355a6846a028a7b3140fdef34
|
||||||
|
b38e1973cabab58f8137cf66fc7fa3aa
|
||||||
|
0ee4998cda155ca2823f5c9336a62cc2
|
||||||
|
4f9918b4782a59a0361619389bd22d54
|
||||||
|
c5c82a59505a9888b95e012a068b3254
|
||||||
|
5eabc71dca6f57407ba306db416c5031
|
||||||
|
41b8b1155bf6144aeb822acbff051e1a
|
||||||
|
12e7d40ccd4298400cf3b5577370bff8
|
||||||
|
2f22b7979183c9c3b26838c7e84a49a8
|
||||||
|
68af2e17a487001d9374f6b38ed950af
|
||||||
|
3a55bc482844ecc366e10544c38cc890
|
||||||
|
6a08d19bbecb554837c9922b9818455a
|
||||||
|
b2f95f48c1214a5dfb8f4528afcbf901
|
||||||
|
c3c4fc391a1a040c71cb65a8c654a5b1
|
||||||
|
86786c55d138bb9a4a4165522cd15253
|
||||||
|
b329ece66a9dab6bebab9e3687629e7c
|
||||||
|
7d6d818e7fe9a9b335c72ee6aedf9980
|
||||||
|
317956c14cb119eda584de03dbf089cc
|
||||||
|
d095660e8dd16899be9e916e0ff7946c
|
||||||
|
12571880bd56bed1bbbd034094fabbaa
|
||||||
|
7f702375e8c43036e36ddc07b543afb2
|
||||||
|
e254d4c9854b28db694fb5477144551e
|
||||||
|
24a4b250bc0461ad4894334838fbb645
|
||||||
|
16b3dca0bfcad463498a62bf8a3f06a8
|
||||||
|
77dbe27a373d87a6fc1012caa166a0a5
|
||||||
|
1b304ea04baf90e1bf11d6296f64f933
|
||||||
|
42c94efb8ab8ceb6ff98497ef0981c66
|
||||||
|
1074398b6c210e833b4135c7d2caeb91
|
||||||
|
3bce8712a7ba7b037a2cc85e4f72aea4
|
||||||
|
42c1bc84350e82cbead374b41732d8d5
|
||||||
|
76f8fc8e26d1b64e8afb8f263ec3f795
|
||||||
|
ef9f8b042eb888f5408ccb500c9a157f
|
||||||
|
d46505a331887bf49e157f8de4c17a98
|
||||||
|
0be7df49394337bb277b63a6b9147520
|
||||||
|
deb977415048b0b7fa1fd02878c1b10e
|
||||||
|
a0395e32b4cc279f8545b791cee5d394
|
||||||
|
c7dcd8741840799886a1f3b5208c0b36
|
||||||
|
02b1297f58b9e699f232b9ed7e8bbcd0
|
||||||
|
48852b209e405d7aaab50c5810448579
|
||||||
|
d8a5598b2e7183d72cd3a81e54f301ce
|
||||||
|
dfe2e4af7518f0d1f04fd825069d575b
|
||||||
|
e1a70679c95173e6a1504b7c52dd0d3c
|
||||||
|
16ae515dfb1268052bfe330a1cbbbffa
|
||||||
|
f17c090f2140cfd3060558ceefe4d1bd
|
||||||
|
58b1ba1fbbd76670c9218a846a8f6baf
|
||||||
|
f543d8380c2d095f9b62d4ffaaee7467
|
||||||
|
1b16d3e1d688a5a905ff22680ad803cb
|
||||||
|
12e16d58422d5a5899379f11c6ca5b9a
|
||||||
|
f1986c3e4a4545356656f76c27db0ae7
|
||||||
|
324a3d45f631aed6c758888254ae07ad
|
||||||
|
49532c06798f685e2a41dc683b081f79
|
||||||
|
95934c5a6e2cc8de08c8b8342936cf38
|
||||||
|
67d89790392f18908c24cc36bbe50362
|
||||||
|
c4f963cd490f9241f4959cca6a6d35ce
|
||||||
|
b21db693ba10898ec3205cb0cb343a02
|
||||||
|
72e63227e74f2473a44da365fa9bc02f
|
||||||
|
220d247b6c0f9512256dfcdf95582148
|
||||||
|
86c290279ffa61ca615f1f8f8877e807
|
||||||
|
6651d56c548fd4c5e8b2c86354c28ca3
|
||||||
|
d863321ec53cd582d34a8ee5e88a5576
|
||||||
|
f262bda775cf67026d8d226620c9d4aa
|
||||||
|
15816d939647890021ac31aa1c3b2f75
|
||||||
|
229b295a4bc44d8522580dd4383812d0
|
||||||
|
833afe87641e34e60f2d127f44a7cbeb
|
||||||
|
62cc76ec3a853878ebfff04ebbc8addc
|
||||||
|
40fca93e2d6a976de641cdbfb56257b5
|
||||||
|
65d654e9145527f8fcd24608d64f68f1
|
||||||
|
1b2b0c68b6d9305f46642109b92a234b
|
||||||
|
85aa859b29f2caeb60343cf53718e451
|
||||||
|
3aa590c6c8a4dee128f1e7fcb2b82a2a
|
||||||
|
699cb26687934746b722e2dfbc1d2c82
|
||||||
|
9cc9b7f51cbdbaf15bc8eec9c1a1cf7c
|
||||||
|
b1be900fe6a6eb21a563754420c5f38e
|
||||||
|
47976686aff65b42661ae0fb2685f462
|
||||||
|
6b3f96a1be97c4c2a371d5935672096a
|
||||||
|
ac9b60c754c95ccbaf56f68544712dd6
|
||||||
|
7dab91b68788d2ed61cd40d3073091a8
|
||||||
|
35b367c591e71f11ddcd9dcc75be79e7
|
||||||
|
d3321d793265630e81e6d52b20e58f44
|
||||||
|
9d97047182efd2ba0594a569f6f2ce04
|
||||||
|
14c772a342a28bd6785bb722e67fc767
|
||||||
|
288802e107c373c9d28eeb3f8726214e
|
||||||
|
1c9d8ff04c346a59ea4f3a349dc4b891
|
||||||
|
2c857a65df4619d06f424ddcccbd2221
|
||||||
|
10a0b3e4e7825d21fc22bcff34a3b26c
|
||||||
|
78c8a5ee6e6880a3af222bfc27de62a1
|
||||||
|
9e16c4346a69ff9127bb6c9287d98859
|
||||||
|
1f1d95fa30c9c3a58f41d9657aa0e6ec
|
||||||
|
5e8c6b3b4690da7f4175abe288d0c509
|
||||||
|
0bf315da2ec1e135963cc4c9cf73ea1a
|
||||||
|
2992fdf295438ec9f8dc7979b6d4e5ff
|
||||||
|
a3d43afab0119aa3b6e38a81d244c388
|
||||||
|
889d049dc64457637ad089e18e718a23
|
||||||
|
46016c611b9323fe8f4206bd393a6741
|
||||||
|
f555b79be75ba2a1a741f467192b693e
|
||||||
|
242d4b4c323c7c1e9648de52ec8d7c2c
|
||||||
|
383d2ee4d28919f193f28448ba28ddc6
|
||||||
|
2c7cd221bd1025547fc654ca6717553d
|
||||||
|
e16de72e05b548758832102ffb39d242
|
||||||
|
5cecf3fc2823225f0f1748a1fedadf35
|
||||||
|
4e709cb904317a43ffd94739281d32bc
|
||||||
|
b6dfd4657668ed2c1ebaff9fa92702ab
|
||||||
|
3c618cd95706cc25a0565fc0ed00e352
|
||||||
|
dca8dbc91708a94525f16c064454a2ce
|
||||||
|
fe3fbff45e30dcc3fa4f57ae78114f7f
|
||||||
|
0f7bc1eab5cf9faeaa127c0243675253
|
||||||
|
19b6fca6e3f0d039074753265436d2c5
|
||||||
|
fce950fc2ec2526c0b29b6869ff58dee
|
||||||
|
4c9c0b19668dc20a9fac84e13c590deb
|
||||||
|
8fe06eb38ee870ffd7cb9f065476ac78
|
||||||
|
515ce9eaa9a6970f809475e416325123
|
||||||
|
6d58868e827dd9dca698c5b24380fea7
|
||||||
|
072951a0e1d06aa829d716a86ae498fe
|
||||||
|
07f23add152c44f754eac930093e6b59
|
||||||
|
e98798eb29c162ddfff7ec72aa7b65dc
|
||||||
|
62f4e1ca0294a3944224fb4a4280ed56
|
||||||
|
28fc910d3b7a926f63e7bfaf18878cc1
|
||||||
|
40065fe5c0e32a16622988bc1485a038
|
||||||
|
b2623ec75309e28c6c0a4b179e4acd6d
|
||||||
|
3ea6b991014bde2185be7a6c92745c28
|
||||||
|
5fde25c7fe79a9c0e2b86eb1878de02c
|
||||||
|
5bf42dc3006f4c601d01ee4820e44d38
|
||||||
|
44500b37f18b2eb499894bc1ff1efdcc
|
||||||
|
f5bfe3352c4281083083e51ef1fe0a72
|
||||||
|
abf9c6ba639f68d9b8f8c0d9d97a19d3
|
||||||
|
75f6dc9ccbc6a1381785bf9440a95744
|
||||||
|
4db0fd27cac20f59718fe00c5dc13202
|
||||||
|
9e0d611017698f2d1716f8d9d81a8bf7
|
||||||
|
13c7015f10b37b3dbe1618cde5edc2ee
|
||||||
|
70e2a0182471290a46961a0d62611053
|
||||||
|
47ba942fc3396104b3731ea40eb97339
|
||||||
|
bf10238458969233c0062b09a68f22a7
|
||||||
|
115b7375a9aa156b8203dd88acdc5672
|
||||||
|
1b39aa22702c993aa1e7606e9afad9c5
|
||||||
|
c85e67c08e02538f151c6276662ba69b
|
||||||
|
89dcf41c5adb944bcd46ca3f67666a75
|
||||||
|
90fd81f9b572ede873313f4faa58da4b
|
||||||
|
00badd56e7d5b2dddb10ef7de05be515
|
||||||
|
a13c3404ac033c9fce8a6435f2a4a416
|
||||||
|
af83e8f7af9785c96ff61c354417aab4
|
||||||
|
5176b27945cc7686520b1425144dc4d3
|
||||||
|
e5f248be2c0e83ad0a6bfd2eff759f6e
|
||||||
|
cd693cce87898dafa5c68dbaeeb151fa
|
||||||
|
2a47245fc3da1229e3dc288f216c4e20
|
||||||
|
c6f8e88fbf5566000f8f86b0d4bf9020
|
||||||
|
3e88c5f7b571a292c998cac33b41215b
|
||||||
|
2a2e8271ecba2cec040ef343317e2ae9
|
||||||
|
8e693282602bde3452061affde3ce514
|
||||||
|
992acab07cf4d6aa5b95b4c4cc33cf0c
|
||||||
|
6db1ad3494be9c3c9758b00d956bd2cb
|
||||||
|
824d130a16d7bc2c6d6b26c31c2a0e05
|
||||||
|
d286bd8f5cdfd65b695815ff68bb191f
|
||||||
|
4c5bbeecda37bf4fb6dd79f266aac3d6
|
||||||
|
c4f64e991e13dfc3913246f730c31e87
|
||||||
|
459d8fd53975479aa977c61dc3ef1b9e
|
||||||
|
ece59558539b52684433b54bf2c7e214
|
||||||
|
4362122d7ed40dd5ad0886cdf901539e
|
||||||
|
66ffe4fb31a5e4fa9462a3aa57224476
|
||||||
|
86586a04e75ebde21903f407e6037e50
|
||||||
|
93b2e3af91cb24ffafa6418c76cc1ce7
|
||||||
|
41ca4598aac764fa944d8f578b2ab5ab
|
||||||
|
6ef7ce05836d63520925817c220a5274
|
||||||
|
c4e902503f3d5602ba2454907d7809f7
|
||||||
|
8f3cb447682bebd3a230740e11d35dff
|
||||||
|
4340ec4c3e62118b2749264393fc78ee
|
||||||
|
8178fbeab83126f2111ab74c23d241c9
|
||||||
|
db298bcb2a999b7ac2037991ef078d32
|
||||||
|
f586895d6dca606e2d62dfdc92cc2021
|
||||||
|
b5aaf4b91dd7b6cda3a2635c603c4500
|
||||||
|
0f024b59afa957b59f5ebb9b63ff1b1a
|
||||||
|
41acc378b08b5d9b7da2f644b79aebdf
|
||||||
|
cd173062829e6e86eb094b8d40d16579
|
||||||
|
ef55ddf0fa94dcb4f275033bb55e8466
|
||||||
|
48a3efa1f9d8291d99ce434fc5474fb6
|
||||||
|
731a649c764fa964d6e2ce640b34c25a
|
||||||
|
5f9ac7b1dca56c8dab9ce1f92341094b
|
||||||
|
e9b0ee7355299aaf55cce1e240050125
|
||||||
|
8a285fc7c728cdfa1f4b651c64eab7e5
|
||||||
|
05823b86e463ad514a4cd7eacd9c01bb
|
||||||
|
0ce34318119eb0e8921b5e798789bea0
|
||||||
|
e46f4c793b42136b1eefbece7b867052
|
||||||
|
1331c7fcae627f290299b978299a9b48
|
||||||
|
285c5783029a7b23374e91cce19df0de
|
||||||
|
670fba4fad181308c15778f38e619646
|
||||||
|
46a3350a39c21f45bafed75d339ed101
|
||||||
|
b1d6f3230e58d2647d1aa4a5446061ed
|
||||||
|
f6eb5936ec9bb3212b14716da8329839
|
||||||
|
1ecb00343900a550ad33ba10777df23d
|
||||||
|
4b9f70244adda8e0a42f4da7341b8d84
|
||||||
|
6caafeb26793a011ebef352ef1d1aa34
|
||||||
|
723ab0797281e7ee36109b53140a324d
|
||||||
|
7a5e9be8c7c8307aa22c8b48708424a2
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// Copyright 2024 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::bitstream_utils::BitWriter;
|
||||||
|
use crate::bitstream_utils::BitWriterError;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ObuWriterError {
|
||||||
|
BitWriterError(BitWriterError),
|
||||||
|
UnalignedLeb128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ObuWriterError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ObuWriterError::BitWriterError(x) => write!(f, "{}", x.to_string()),
|
||||||
|
ObuWriterError::UnalignedLeb128 => {
|
||||||
|
write!(f, "attempted to write leb128 on unaligned position")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BitWriterError> for ObuWriterError {
|
||||||
|
fn from(err: BitWriterError) -> Self {
|
||||||
|
ObuWriterError::BitWriterError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ObuWriterResult<T> = std::result::Result<T, ObuWriterError>;
|
||||||
|
|
||||||
|
pub struct ObuWriter<W: Write>(BitWriter<W>);
|
||||||
|
|
||||||
|
impl<W: Write> ObuWriter<W> {
|
||||||
|
pub fn new(writer: W) -> Self {
|
||||||
|
Self(BitWriter::new(writer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes fixed bit size integer. Corresponds to `f(n)` in AV1 spec defined in 4.10.2.
|
||||||
|
pub fn write_f<T: Into<u32>>(&mut self, bits: usize, value: T) -> ObuWriterResult<usize> {
|
||||||
|
self.0
|
||||||
|
.write_f(bits, value)
|
||||||
|
.map_err(ObuWriterError::BitWriterError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes variable length unsigned n-bit number. Corresponds to `uvlc()` in AV1 spec
|
||||||
|
/// defined in 4.10.3.
|
||||||
|
pub fn write_uvlc<T: Into<u32>>(&mut self, value: T) -> ObuWriterResult<usize> {
|
||||||
|
let value: u32 = value.into();
|
||||||
|
if value == u32::MAX {
|
||||||
|
return self.write_f(32, 0u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = value + 1;
|
||||||
|
let leading_zeros = (32 - value.leading_zeros()) as usize;
|
||||||
|
|
||||||
|
Ok(self.write_f(leading_zeros - 1, 0u32)? + self.write_f(leading_zeros, value)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes unsigned little-endian n-byte integer. Corresponds to `le(n)` in AV1 spec
|
||||||
|
/// defined in 4.10.4.
|
||||||
|
pub fn write_le<T: Into<u32>>(&mut self, n: usize, value: T) -> ObuWriterResult<usize> {
|
||||||
|
let value: u32 = value.into();
|
||||||
|
let mut value = value.to_le();
|
||||||
|
|
||||||
|
for _ in 0..n {
|
||||||
|
self.write_f(4, value & 0xff)?;
|
||||||
|
value >>= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes unsigned integer represented by a variable number of little-endian bytes.
|
||||||
|
/// Corresponds to `leb128()` in AV1 spec defined in 4.10.4.
|
||||||
|
///
|
||||||
|
/// Note: Despite the name, the AV1 4.10.4 limits the value to [`u32::MAX`] = (1 << 32) - 1.
|
||||||
|
pub fn write_leb128<T: Into<u32>>(
|
||||||
|
&mut self,
|
||||||
|
value: T,
|
||||||
|
min_bytes: usize,
|
||||||
|
) -> ObuWriterResult<usize> {
|
||||||
|
if !self.aligned() {
|
||||||
|
return Err(ObuWriterError::UnalignedLeb128);
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: u32 = value.into();
|
||||||
|
let mut value: u32 = value.to_le();
|
||||||
|
let mut bytes = 0;
|
||||||
|
|
||||||
|
for _ in 0..8 {
|
||||||
|
bytes += 1;
|
||||||
|
|
||||||
|
if value >= 0x7f || bytes < min_bytes {
|
||||||
|
self.write_f(8, 0x80 | (value & 0x7f))?;
|
||||||
|
value >>= 7;
|
||||||
|
} else {
|
||||||
|
self.write_f(8, value & 0x7f)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(value < 0x7f);
|
||||||
|
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_su<T: Into<i32>>(&mut self, bits: usize, value: T) -> ObuWriterResult<usize> {
|
||||||
|
let mut value: i32 = value.into();
|
||||||
|
if value < 0 {
|
||||||
|
value += 1 << bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(value >= 0);
|
||||||
|
self.write_f(bits, value.unsigned_abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aligned(&self) -> bool {
|
||||||
|
!self.0.has_data_pending()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::codec::av1::reader::Reader;
|
||||||
|
|
||||||
|
const TEST_VECTOR: &[u32] = &[
|
||||||
|
// some random test values
|
||||||
|
u32::MAX,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
7312,
|
||||||
|
8832,
|
||||||
|
10123,
|
||||||
|
47457,
|
||||||
|
21390213,
|
||||||
|
u32::MIN,
|
||||||
|
u32::MAX - 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uvlc() {
|
||||||
|
for &value in TEST_VECTOR {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
|
||||||
|
ObuWriter::new(&mut buf).write_uvlc(value).unwrap();
|
||||||
|
|
||||||
|
if value == u32::MAX {
|
||||||
|
// force stop uvlc
|
||||||
|
buf.push(0x80);
|
||||||
|
}
|
||||||
|
|
||||||
|
let read = Reader::new(&buf).read_uvlc().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(read, value, "failed testing {}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_leb128() {
|
||||||
|
for &value in TEST_VECTOR {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
|
||||||
|
ObuWriter::new(&mut buf).write_leb128(value, 0).unwrap();
|
||||||
|
let read = Reader::new(&buf).read_leb128().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(read, value, "failed testing {}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_su() {
|
||||||
|
let vector = TEST_VECTOR
|
||||||
|
.iter()
|
||||||
|
.map(|e| *e as i32)
|
||||||
|
.chain(TEST_VECTOR.iter().map(|e| -(*e as i32)));
|
||||||
|
|
||||||
|
for value in vector {
|
||||||
|
let bits = 32 - value.abs().leading_zeros() as usize + 1; // For sign
|
||||||
|
if bits >= 32 {
|
||||||
|
// Skip too big nubmers
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
|
||||||
|
ObuWriter::new(&mut buf).write_su(bits, value).unwrap();
|
||||||
|
|
||||||
|
let read = Reader::new(&buf).read_su(bits as usize).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(read, value, "failed testing {}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright 2023 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
pub mod dpb;
|
||||||
|
pub mod nalu;
|
||||||
|
pub mod nalu_writer;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod picture;
|
||||||
|
pub mod synthesizer;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
|||||||
|
// Copyright 2023 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::io::Seek;
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
|
||||||
|
#[allow(clippy::len_without_is_empty)]
|
||||||
|
pub trait Header: Sized {
|
||||||
|
/// Parse the NALU header, returning it.
|
||||||
|
fn parse<T: AsRef<[u8]>>(cursor: &mut Cursor<T>) -> Result<Self, String>;
|
||||||
|
/// Whether this header type indicates EOS.
|
||||||
|
fn is_end(&self) -> bool;
|
||||||
|
/// The length of the header.
|
||||||
|
fn len(&self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Nalu<'a, U> {
|
||||||
|
pub header: U,
|
||||||
|
/// The mapping that backs this NALU. Possibly shared with the other NALUs
|
||||||
|
/// in the Access Unit.
|
||||||
|
pub data: Cow<'a, [u8]>,
|
||||||
|
|
||||||
|
pub size: usize,
|
||||||
|
pub offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, U> Nalu<'a, U>
|
||||||
|
where
|
||||||
|
U: Debug + Header,
|
||||||
|
{
|
||||||
|
/// Find the next Annex B encoded NAL unit.
|
||||||
|
pub fn next(cursor: &mut Cursor<&'a [u8]>) -> Result<Nalu<'a, U>, String> {
|
||||||
|
let bitstream = cursor.clone().into_inner();
|
||||||
|
let pos = usize::try_from(cursor.position()).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
// Find the start code for this NALU
|
||||||
|
let current_nalu_offset = match Nalu::<'a, U>::find_start_code(cursor, pos) {
|
||||||
|
Some(offset) => offset,
|
||||||
|
None => return Err("No NAL found".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut start_code_offset = pos + current_nalu_offset;
|
||||||
|
|
||||||
|
// If the preceding byte is 00, then we actually have a four byte SC,
|
||||||
|
// i.e. 00 00 00 01 Where the first 00 is the "zero_byte()"
|
||||||
|
if start_code_offset > 0 && cursor.get_ref()[start_code_offset - 1] == 00 {
|
||||||
|
start_code_offset -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The NALU offset is its offset + 3 bytes to skip the start code.
|
||||||
|
let nalu_offset = pos + current_nalu_offset + 3;
|
||||||
|
|
||||||
|
// Set the bitstream position to the start of the current NALU
|
||||||
|
cursor.set_position(u64::try_from(nalu_offset).map_err(|err| err.to_string())?);
|
||||||
|
|
||||||
|
let hdr = U::parse(cursor)?;
|
||||||
|
|
||||||
|
// Find the start of the subsequent NALU.
|
||||||
|
let mut next_nalu_offset = match Nalu::<'a, U>::find_start_code(cursor, nalu_offset) {
|
||||||
|
Some(offset) => offset,
|
||||||
|
None => {
|
||||||
|
let cur_pos = cursor.position();
|
||||||
|
let end_pos = cursor
|
||||||
|
.seek(SeekFrom::End(0))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let _ = cursor
|
||||||
|
.seek(SeekFrom::Start(cur_pos))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
(end_pos - cur_pos) as usize
|
||||||
|
} // Whatever data is left must be part of the current NALU
|
||||||
|
};
|
||||||
|
|
||||||
|
while next_nalu_offset > 0 && cursor.get_ref()[nalu_offset + next_nalu_offset - 1] == 00 {
|
||||||
|
// Discard trailing_zero_8bits
|
||||||
|
next_nalu_offset -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nal_size = if hdr.is_end() {
|
||||||
|
// the NALU is comprised of only the header
|
||||||
|
hdr.len()
|
||||||
|
} else {
|
||||||
|
next_nalu_offset
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Nalu {
|
||||||
|
header: hdr,
|
||||||
|
data: Cow::from(&bitstream[start_code_offset..nalu_offset + nal_size]),
|
||||||
|
size: nal_size,
|
||||||
|
offset: nalu_offset - start_code_offset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, U> Nalu<'a, U>
|
||||||
|
where
|
||||||
|
U: Debug,
|
||||||
|
{
|
||||||
|
fn find_start_code(data: &mut Cursor<&'a [u8]>, offset: usize) -> Option<usize> {
|
||||||
|
// discard all zeroes until the start code pattern is found
|
||||||
|
data.get_ref()[offset..]
|
||||||
|
.windows(3)
|
||||||
|
.position(|window| window == [0x00, 0x00, 0x01])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_owned(self) -> Nalu<'static, U> {
|
||||||
|
Nalu {
|
||||||
|
header: self.header,
|
||||||
|
size: self.size,
|
||||||
|
offset: self.offset,
|
||||||
|
data: Cow::Owned(self.data.into_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, U> AsRef<[u8]> for Nalu<'a, U> {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.data[self.offset..self.offset + self.size]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
// Copyright 2024 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::bitstream_utils::BitWriter;
|
||||||
|
use crate::bitstream_utils::BitWriterError;
|
||||||
|
|
||||||
|
/// Internal wrapper over [`std::io::Write`] for possible emulation prevention
|
||||||
|
struct EmulationPrevention<W: Write> {
|
||||||
|
out: W,
|
||||||
|
prev_bytes: [Option<u8>; 2],
|
||||||
|
|
||||||
|
/// Emulation prevention enabled.
|
||||||
|
ep_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> EmulationPrevention<W> {
|
||||||
|
fn new(writer: W, ep_enabled: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
out: writer,
|
||||||
|
prev_bytes: [None; 2],
|
||||||
|
ep_enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_byte(&mut self, curr_byte: u8) -> std::io::Result<()> {
|
||||||
|
if self.prev_bytes[1] == Some(0x00) && self.prev_bytes[0] == Some(0x00) && curr_byte <= 0x03
|
||||||
|
{
|
||||||
|
self.out.write_all(&[0x00, 0x00, 0x03, curr_byte])?;
|
||||||
|
self.prev_bytes = [None; 2];
|
||||||
|
} else {
|
||||||
|
if let Some(byte) = self.prev_bytes[1] {
|
||||||
|
self.out.write_all(&[byte])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prev_bytes[1] = self.prev_bytes[0];
|
||||||
|
self.prev_bytes[0] = Some(curr_byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a H.264 NALU header.
|
||||||
|
fn write_header(&mut self, idc: u8, type_: u8) -> NaluWriterResult<()> {
|
||||||
|
self.out.write_all(&[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
(idc & 0b11) << 5 | (type_ & 0b11111),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_data_pending(&self) -> bool {
|
||||||
|
self.prev_bytes[0].is_some() || self.prev_bytes[1].is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Write for EmulationPrevention<W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
if !self.ep_enabled {
|
||||||
|
self.out.write_all(buf)?;
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
for byte in buf {
|
||||||
|
self.write_byte(*byte)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
if let Some(byte) = self.prev_bytes[1].take() {
|
||||||
|
self.out.write_all(&[byte])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(byte) = self.prev_bytes[0].take() {
|
||||||
|
self.out.write_all(&[byte])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.out.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Drop for EmulationPrevention<W> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = self.flush() {
|
||||||
|
log::error!("Unable to flush pending bytes {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum NaluWriterError {
|
||||||
|
Overflow,
|
||||||
|
Io(std::io::Error),
|
||||||
|
BitWriterError(BitWriterError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for NaluWriterError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
NaluWriterError::Overflow => write!(f, "value increment caused value overflow"),
|
||||||
|
NaluWriterError::Io(x) => write!(f, "{}", x.to_string()),
|
||||||
|
NaluWriterError::BitWriterError(x) => write!(f, "{}", x.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for NaluWriterError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
NaluWriterError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BitWriterError> for NaluWriterError {
|
||||||
|
fn from(err: BitWriterError) -> Self {
|
||||||
|
NaluWriterError::BitWriterError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NaluWriterResult<T> = std::result::Result<T, NaluWriterError>;
|
||||||
|
|
||||||
|
/// A writer for H.264 bitstream. It is capable of outputing bitstream with
|
||||||
|
/// emulation-prevention.
|
||||||
|
pub struct NaluWriter<W: Write>(BitWriter<EmulationPrevention<W>>);
|
||||||
|
|
||||||
|
impl<W: Write> NaluWriter<W> {
|
||||||
|
pub fn new(writer: W, ep_enabled: bool) -> Self {
|
||||||
|
Self(BitWriter::new(EmulationPrevention::new(writer, ep_enabled)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes fixed bit size integer (up to 32 bit) output with emulation
|
||||||
|
/// prevention if enabled. Corresponds to `f(n)` in H.264 spec.
|
||||||
|
pub fn write_f<T: Into<u32>>(&mut self, bits: usize, value: T) -> NaluWriterResult<usize> {
|
||||||
|
self.0
|
||||||
|
.write_f(bits, value)
|
||||||
|
.map_err(NaluWriterError::BitWriterError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An alias to [`Self::write_f`] Corresponds to `n(n)` in H.264 spec.
|
||||||
|
pub fn write_u<T: Into<u32>>(&mut self, bits: usize, value: T) -> NaluWriterResult<usize> {
|
||||||
|
self.write_f(bits, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a number in exponential golumb format.
|
||||||
|
pub fn write_exp_golumb(&mut self, value: u32) -> NaluWriterResult<()> {
|
||||||
|
let value = value.checked_add(1).ok_or(NaluWriterError::Overflow)?;
|
||||||
|
let bits = 32 - value.leading_zeros() as usize;
|
||||||
|
let zeros = bits - 1;
|
||||||
|
|
||||||
|
self.write_f(zeros, 0u32)?;
|
||||||
|
self.write_f(bits, value)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a unsigned integer in exponential golumb format.
|
||||||
|
/// Coresponds to `ue(v)` in H.264 spec.
|
||||||
|
pub fn write_ue<T: Into<u32>>(&mut self, value: T) -> NaluWriterResult<()> {
|
||||||
|
let value = value.into();
|
||||||
|
|
||||||
|
self.write_exp_golumb(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a signed integer in exponential golumb format.
|
||||||
|
/// Coresponds to `se(v)` in H.264 spec.
|
||||||
|
pub fn write_se<T: Into<i32>>(&mut self, value: T) -> NaluWriterResult<()> {
|
||||||
|
let value: i32 = value.into();
|
||||||
|
let abs_value: u32 = value.unsigned_abs();
|
||||||
|
|
||||||
|
if value <= 0 {
|
||||||
|
self.write_ue(2 * abs_value)
|
||||||
|
} else {
|
||||||
|
self.write_ue(2 * abs_value - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if ['Self`] hold data that wasn't written to [`std::io::Write`]
|
||||||
|
pub fn has_data_pending(&self) -> bool {
|
||||||
|
self.0.has_data_pending() || self.0.inner().has_data_pending()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a H.264 NALU header.
|
||||||
|
pub fn write_header(&mut self, idc: u8, _type: u8) -> NaluWriterResult<()> {
|
||||||
|
self.0.flush()?;
|
||||||
|
self.0.inner_mut().write_header(idc, _type)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if next bits will be aligned to 8
|
||||||
|
pub fn aligned(&self) -> bool {
|
||||||
|
!self.0.has_data_pending()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::bitstream_utils::BitReader;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_bits() {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = NaluWriter::new(&mut buf, false);
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, false).unwrap();
|
||||||
|
writer.write_f(1, false).unwrap();
|
||||||
|
writer.write_f(1, false).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
writer.write_f(1, true).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(buf, vec![0b10001111u8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_first_few_ue() {
|
||||||
|
fn single_ue(value: u32) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = NaluWriter::new(&mut buf, false);
|
||||||
|
writer.write_ue(value).unwrap();
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(single_ue(0), vec![0b10000000u8]);
|
||||||
|
assert_eq!(single_ue(1), vec![0b01000000u8]);
|
||||||
|
assert_eq!(single_ue(2), vec![0b01100000u8]);
|
||||||
|
assert_eq!(single_ue(3), vec![0b00100000u8]);
|
||||||
|
assert_eq!(single_ue(4), vec![0b00101000u8]);
|
||||||
|
assert_eq!(single_ue(5), vec![0b00110000u8]);
|
||||||
|
assert_eq!(single_ue(6), vec![0b00111000u8]);
|
||||||
|
assert_eq!(single_ue(7), vec![0b00010000u8]);
|
||||||
|
assert_eq!(single_ue(8), vec![0b00010010u8]);
|
||||||
|
assert_eq!(single_ue(9), vec![0b00010100u8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writer_reader() {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = NaluWriter::new(&mut buf, false);
|
||||||
|
writer.write_ue(10u32).unwrap();
|
||||||
|
writer.write_se(-42).unwrap();
|
||||||
|
writer.write_se(3).unwrap();
|
||||||
|
writer.write_ue(5u32).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reader = BitReader::new(&buf, true);
|
||||||
|
|
||||||
|
assert_eq!(reader.read_ue::<u32>().unwrap(), 10);
|
||||||
|
assert_eq!(reader.read_se::<i32>().unwrap(), -42);
|
||||||
|
assert_eq!(reader.read_se::<i32>().unwrap(), 3);
|
||||||
|
assert_eq!(reader.read_ue::<u32>().unwrap(), 5);
|
||||||
|
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = NaluWriter::new(&mut buf, false);
|
||||||
|
writer.write_se(30).unwrap();
|
||||||
|
writer.write_ue(100u32).unwrap();
|
||||||
|
writer.write_se(-402).unwrap();
|
||||||
|
writer.write_ue(50u32).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reader = BitReader::new(&buf, true);
|
||||||
|
|
||||||
|
assert_eq!(reader.read_se::<i32>().unwrap(), 30);
|
||||||
|
assert_eq!(reader.read_ue::<u32>().unwrap(), 100);
|
||||||
|
assert_eq!(reader.read_se::<i32>().unwrap(), -402);
|
||||||
|
assert_eq!(reader.read_ue::<u32>().unwrap(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writer_emulation_prevention() {
|
||||||
|
fn test(input: &[u8], bitstream: &[u8]) {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
{
|
||||||
|
let mut writer = NaluWriter::new(&mut buf, true);
|
||||||
|
for byte in input {
|
||||||
|
writer.write_f(8, *byte).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(buf, bitstream);
|
||||||
|
{
|
||||||
|
let mut reader = BitReader::new(&buf, true);
|
||||||
|
for byte in input {
|
||||||
|
assert_eq!(*byte, reader.read_bits::<u8>(8).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test(&[0x00, 0x00, 0x00], &[0x00, 0x00, 0x03, 0x00]);
|
||||||
|
test(&[0x00, 0x00, 0x01], &[0x00, 0x00, 0x03, 0x01]);
|
||||||
|
test(&[0x00, 0x00, 0x02], &[0x00, 0x00, 0x03, 0x02]);
|
||||||
|
test(&[0x00, 0x00, 0x03], &[0x00, 0x00, 0x03, 0x03]);
|
||||||
|
|
||||||
|
test(&[0x00, 0x00, 0x00, 0x00], &[0x00, 0x00, 0x03, 0x00, 0x00]);
|
||||||
|
test(&[0x00, 0x00, 0x00, 0x01], &[0x00, 0x00, 0x03, 0x00, 0x01]);
|
||||||
|
test(&[0x00, 0x00, 0x00, 0x02], &[0x00, 0x00, 0x03, 0x00, 0x02]);
|
||||||
|
test(&[0x00, 0x00, 0x00, 0x03], &[0x00, 0x00, 0x03, 0x00, 0x03]);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,443 @@
|
|||||||
|
// Copyright 2022 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::rc::Weak;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
use crate::codec::h264::parser::MaxLongTermFrameIdx;
|
||||||
|
use crate::codec::h264::parser::RefPicMarking;
|
||||||
|
use crate::codec::h264::parser::Slice;
|
||||||
|
use crate::codec::h264::parser::SliceType;
|
||||||
|
use crate::codec::h264::parser::Sps;
|
||||||
|
use crate::Resolution;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum Field {
|
||||||
|
#[default]
|
||||||
|
Frame,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Field {
|
||||||
|
/// Returns the field of opposite parity.
|
||||||
|
pub fn opposite(&self) -> Self {
|
||||||
|
match *self {
|
||||||
|
Field::Frame => Field::Frame,
|
||||||
|
Field::Top => Field::Bottom,
|
||||||
|
Field::Bottom => Field::Top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum Reference {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
ShortTerm,
|
||||||
|
LongTerm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum IsIdr {
|
||||||
|
#[default]
|
||||||
|
No,
|
||||||
|
Yes {
|
||||||
|
idr_pic_id: u16,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rank of a field, i.e. whether it is the first or second one to be parsed from the stream.
|
||||||
|
/// This is unrelated to the `Field` type, as the first field can be either `Top` or `Bottom`.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub enum FieldRank {
|
||||||
|
/// Frame has a single field.
|
||||||
|
#[default]
|
||||||
|
Single,
|
||||||
|
/// Frame is interlaced, and this is the first field (with a reference to the second one).
|
||||||
|
First(Weak<RefCell<PictureData>>),
|
||||||
|
/// Frame is interlaced, and this is the second field (with a reference to the first one).
|
||||||
|
Second(Rc<RefCell<PictureData>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PictureData {
|
||||||
|
pub pic_order_cnt_type: u8,
|
||||||
|
pub top_field_order_cnt: i32,
|
||||||
|
pub bottom_field_order_cnt: i32,
|
||||||
|
pub pic_order_cnt: i32,
|
||||||
|
pub pic_order_cnt_msb: i32,
|
||||||
|
pub pic_order_cnt_lsb: i32,
|
||||||
|
pub delta_pic_order_cnt_bottom: i32,
|
||||||
|
pub delta_pic_order_cnt0: i32,
|
||||||
|
pub delta_pic_order_cnt1: i32,
|
||||||
|
|
||||||
|
pub pic_num: i32,
|
||||||
|
pub long_term_pic_num: u32,
|
||||||
|
pub frame_num: u32,
|
||||||
|
pub frame_num_offset: u32,
|
||||||
|
pub frame_num_wrap: i32,
|
||||||
|
pub long_term_frame_idx: u32,
|
||||||
|
|
||||||
|
pub coded_resolution: Resolution,
|
||||||
|
pub display_resolution: Resolution,
|
||||||
|
|
||||||
|
pub type_: SliceType,
|
||||||
|
pub nal_ref_idc: u8,
|
||||||
|
pub is_idr: IsIdr,
|
||||||
|
reference: Reference,
|
||||||
|
pub ref_pic_list_modification_flag_l0: i32,
|
||||||
|
pub abs_diff_pic_num_minus1: i32,
|
||||||
|
|
||||||
|
// Does memory management op 5 needs to be executed after this
|
||||||
|
// picture has finished decoding?
|
||||||
|
pub has_mmco_5: bool,
|
||||||
|
|
||||||
|
// Created by the decoding process for gaps in frame_num.
|
||||||
|
// Not for decode or output.
|
||||||
|
pub nonexisting: bool,
|
||||||
|
|
||||||
|
pub field: Field,
|
||||||
|
|
||||||
|
// Values from slice_hdr to be used during reference marking and
|
||||||
|
// memory management after finishing this picture.
|
||||||
|
pub ref_pic_marking: RefPicMarking,
|
||||||
|
|
||||||
|
field_rank: FieldRank,
|
||||||
|
|
||||||
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `PictureData` within a `Rc<RefCell>` which field rank is guaranteed to be correct.
|
||||||
|
///
|
||||||
|
/// The field rank of `PictureData` is only final after both fields have been constructed - namely,
|
||||||
|
/// the first field can only point to the second one after the latter is available as a Rc. Methods
|
||||||
|
/// [`PictureData::into_rc`] and [`PictureData::split_frame`] take care of this, and is this only
|
||||||
|
/// producer of this type, ensuring all instances are correct.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct RcPictureData {
|
||||||
|
pic: Rc<RefCell<PictureData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for RcPictureData {
|
||||||
|
type Target = Rc<RefCell<PictureData>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.pic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PictureData {
|
||||||
|
pub fn new_non_existing(frame_num: u32, timestamp: u64) -> Self {
|
||||||
|
PictureData {
|
||||||
|
frame_num,
|
||||||
|
nonexisting: true,
|
||||||
|
nal_ref_idc: 1,
|
||||||
|
field: Field::Frame,
|
||||||
|
pic_num: frame_num as i32,
|
||||||
|
reference: Reference::ShortTerm,
|
||||||
|
timestamp,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new picture from a `slice`, `sps`, and `timestamp`.
|
||||||
|
///
|
||||||
|
/// `first_field` is set if this picture is the second field of a frame.
|
||||||
|
pub fn new_from_slice(
|
||||||
|
slice: &Slice,
|
||||||
|
sps: &Sps,
|
||||||
|
timestamp: u64,
|
||||||
|
first_field: Option<&RcPictureData>,
|
||||||
|
) -> Self {
|
||||||
|
let hdr = &slice.header;
|
||||||
|
let nalu_hdr = &slice.nalu.header;
|
||||||
|
|
||||||
|
let is_idr = if nalu_hdr.idr_pic_flag {
|
||||||
|
IsIdr::Yes {
|
||||||
|
idr_pic_id: hdr.idr_pic_id,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IsIdr::No
|
||||||
|
};
|
||||||
|
|
||||||
|
let field = if hdr.field_pic_flag {
|
||||||
|
if hdr.bottom_field_flag {
|
||||||
|
Field::Bottom
|
||||||
|
} else {
|
||||||
|
Field::Top
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Field::Frame
|
||||||
|
};
|
||||||
|
|
||||||
|
let reference = if nalu_hdr.ref_idc != 0 {
|
||||||
|
Reference::ShortTerm
|
||||||
|
} else {
|
||||||
|
Reference::None
|
||||||
|
};
|
||||||
|
|
||||||
|
let pic_num = if !hdr.field_pic_flag {
|
||||||
|
hdr.frame_num
|
||||||
|
} else {
|
||||||
|
2 * hdr.frame_num + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let (
|
||||||
|
pic_order_cnt_lsb,
|
||||||
|
delta_pic_order_cnt_bottom,
|
||||||
|
delta_pic_order_cnt0,
|
||||||
|
delta_pic_order_cnt1,
|
||||||
|
) = match sps.pic_order_cnt_type {
|
||||||
|
0 => (
|
||||||
|
hdr.pic_order_cnt_lsb,
|
||||||
|
hdr.delta_pic_order_cnt_bottom,
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
1 => (
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
hdr.delta_pic_order_cnt[0],
|
||||||
|
hdr.delta_pic_order_cnt[1],
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let coded_resolution = Resolution::from((sps.width(), sps.height()));
|
||||||
|
|
||||||
|
let visible_rect = sps.visible_rectangle();
|
||||||
|
|
||||||
|
// punktfunk deviation (PROVENANCE.md #6): `Sps::visible_rectangle()` returns
|
||||||
|
// the crop offset in `min` and the visible SIZE in `max` (not a corner);
|
||||||
|
// upstream's `max - min` double-counts the left/top crop and panics on a u32
|
||||||
|
// underflow for large-but-parser-valid left/top offsets.
|
||||||
|
let display_resolution = Resolution {
|
||||||
|
width: visible_rect.max.x,
|
||||||
|
height: visible_rect.max.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pic = PictureData {
|
||||||
|
pic_order_cnt_type: sps.pic_order_cnt_type,
|
||||||
|
pic_order_cnt_lsb: i32::from(pic_order_cnt_lsb),
|
||||||
|
delta_pic_order_cnt_bottom,
|
||||||
|
delta_pic_order_cnt0,
|
||||||
|
delta_pic_order_cnt1,
|
||||||
|
pic_num: i32::from(pic_num),
|
||||||
|
frame_num: u32::from(hdr.frame_num),
|
||||||
|
nal_ref_idc: nalu_hdr.ref_idc,
|
||||||
|
is_idr,
|
||||||
|
reference,
|
||||||
|
field,
|
||||||
|
ref_pic_marking: hdr.dec_ref_pic_marking.clone(),
|
||||||
|
coded_resolution,
|
||||||
|
display_resolution,
|
||||||
|
timestamp,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(first_field) = first_field {
|
||||||
|
pic.set_first_field_to(first_field);
|
||||||
|
}
|
||||||
|
|
||||||
|
pic
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the current picture is a reference, either ShortTerm or LongTerm.
|
||||||
|
pub fn is_ref(&self) -> bool {
|
||||||
|
!matches!(self.reference, Reference::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this picture is a second field.
|
||||||
|
pub fn is_second_field(&self) -> bool {
|
||||||
|
matches!(self.field_rank, FieldRank::Second(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the field rank of this picture, including a reference to its other field.
|
||||||
|
pub fn field_rank(&self) -> &FieldRank {
|
||||||
|
&self.field_rank
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the picture's Reference
|
||||||
|
pub fn reference(&self) -> &Reference {
|
||||||
|
&self.reference
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the picture as a reference picture.
|
||||||
|
pub fn set_reference(&mut self, reference: Reference, apply_to_other_field: bool) {
|
||||||
|
log::debug!("Set reference of {:#?} to {:?}", self, reference);
|
||||||
|
self.reference = reference;
|
||||||
|
|
||||||
|
if apply_to_other_field {
|
||||||
|
if let Some(other_field) = self.other_field() {
|
||||||
|
log::debug!(
|
||||||
|
"other_field: Set reference of {:#?} to {:?}",
|
||||||
|
&other_field.borrow(),
|
||||||
|
reference
|
||||||
|
);
|
||||||
|
other_field.borrow_mut().reference = reference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the picture's other field, if there is any
|
||||||
|
/// and its reference is still valid.
|
||||||
|
pub fn other_field(&self) -> Option<Rc<RefCell<PictureData>>> {
|
||||||
|
match &self.field_rank {
|
||||||
|
FieldRank::Single => None,
|
||||||
|
FieldRank::First(other_field) => other_field.upgrade(),
|
||||||
|
FieldRank::Second(other_field) => Some(other_field.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this picture's second field.
|
||||||
|
fn set_second_field_to(&mut self, other_field: &Rc<RefCell<Self>>) {
|
||||||
|
self.field_rank = FieldRank::First(Rc::downgrade(other_field));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the current picture is the second field of a complementary ref pair.
|
||||||
|
pub fn is_second_field_of_complementary_ref_pair(&self) -> bool {
|
||||||
|
self.is_ref()
|
||||||
|
&& matches!(self.field_rank(), FieldRank::Second(first_field) if first_field.borrow().is_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this picture's first field.
|
||||||
|
fn set_first_field_to(&mut self, other_field: &Rc<RefCell<Self>>) {
|
||||||
|
self.field_rank = FieldRank::Second(other_field.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pic_num_f(&self, max_pic_num: i32) -> i32 {
|
||||||
|
if !matches!(self.reference(), Reference::LongTerm) {
|
||||||
|
self.pic_num
|
||||||
|
} else {
|
||||||
|
max_pic_num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn long_term_pic_num_f(&self, max_long_term_frame_idx: MaxLongTermFrameIdx) -> u32 {
|
||||||
|
if matches!(self.reference(), Reference::LongTerm) {
|
||||||
|
self.long_term_pic_num
|
||||||
|
} else {
|
||||||
|
2 * max_long_term_frame_idx.to_value_plus1()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume this picture and return a Rc'd version.
|
||||||
|
///
|
||||||
|
/// If the picture was a second field, adjust the field of the first field to point to this
|
||||||
|
/// one.
|
||||||
|
pub fn into_rc(self) -> RcPictureData {
|
||||||
|
let self_rc = Rc::new(RefCell::new(self));
|
||||||
|
|
||||||
|
if let FieldRank::Second(first_field) = self_rc.borrow().field_rank() {
|
||||||
|
first_field.borrow_mut().set_second_field_to(&self_rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
RcPictureData { pic: self_rc }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split a frame into two complementary fields that reference one another.
|
||||||
|
pub fn split_frame(mut self) -> (RcPictureData, RcPictureData) {
|
||||||
|
assert!(matches!(self.field, Field::Frame));
|
||||||
|
assert!(matches!(self.field_rank, FieldRank::Single));
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Splitting picture (frame_num, POC) ({:?}, {:?})",
|
||||||
|
self.frame_num, self.pic_order_cnt
|
||||||
|
);
|
||||||
|
|
||||||
|
let second_pic_order_cnt = if self.top_field_order_cnt < self.bottom_field_order_cnt {
|
||||||
|
self.field = Field::Top;
|
||||||
|
self.pic_order_cnt = self.top_field_order_cnt;
|
||||||
|
|
||||||
|
self.bottom_field_order_cnt
|
||||||
|
} else {
|
||||||
|
self.field = Field::Bottom;
|
||||||
|
self.pic_order_cnt = self.bottom_field_order_cnt;
|
||||||
|
|
||||||
|
self.top_field_order_cnt
|
||||||
|
};
|
||||||
|
|
||||||
|
let second_field = PictureData {
|
||||||
|
top_field_order_cnt: self.top_field_order_cnt,
|
||||||
|
bottom_field_order_cnt: self.bottom_field_order_cnt,
|
||||||
|
frame_num: self.frame_num,
|
||||||
|
reference: self.reference,
|
||||||
|
nonexisting: self.nonexisting,
|
||||||
|
pic_order_cnt: second_pic_order_cnt,
|
||||||
|
field: self.field.opposite(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Split into picture (frame_num, POC) ({:?}, {:?}), field: {:?}",
|
||||||
|
self.frame_num, self.pic_order_cnt, self.field
|
||||||
|
);
|
||||||
|
debug!(
|
||||||
|
"Split into picture (frame_num, POC) ({:?}, {:?}), field {:?}",
|
||||||
|
second_field.frame_num, second_field.pic_order_cnt, second_field.field
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_field = Rc::new(RefCell::new(self));
|
||||||
|
let second_field = Rc::new(RefCell::new(second_field));
|
||||||
|
|
||||||
|
first_field.borrow_mut().set_second_field_to(&second_field);
|
||||||
|
second_field.borrow_mut().set_first_field_to(&first_field);
|
||||||
|
|
||||||
|
(
|
||||||
|
RcPictureData { pic: first_field },
|
||||||
|
RcPictureData { pic: second_field },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PictureData {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("PictureData")
|
||||||
|
.field("pic_order_cnt_type", &self.pic_order_cnt_type)
|
||||||
|
.field("top_field_order_cnt", &self.top_field_order_cnt)
|
||||||
|
.field("bottom_field_order_cnt", &self.bottom_field_order_cnt)
|
||||||
|
.field("pic_order_cnt", &self.pic_order_cnt)
|
||||||
|
.field("pic_order_cnt_msb", &self.pic_order_cnt_msb)
|
||||||
|
.field("pic_order_cnt_lsb", &self.pic_order_cnt_lsb)
|
||||||
|
.field(
|
||||||
|
"delta_pic_order_cnt_bottom",
|
||||||
|
&self.delta_pic_order_cnt_bottom,
|
||||||
|
)
|
||||||
|
.field("delta_pic_order_cnt0", &self.delta_pic_order_cnt0)
|
||||||
|
.field("delta_pic_order_cnt1", &self.delta_pic_order_cnt1)
|
||||||
|
.field("pic_num", &self.pic_num)
|
||||||
|
.field("long_term_pic_num", &self.long_term_pic_num)
|
||||||
|
.field("frame_num", &self.frame_num)
|
||||||
|
.field("frame_num_offset", &self.frame_num_offset)
|
||||||
|
.field("frame_num_wrap", &self.frame_num_wrap)
|
||||||
|
.field("long_term_frame_idx", &self.long_term_frame_idx)
|
||||||
|
.field("coded_resolution", &self.coded_resolution)
|
||||||
|
.field("display_resolution", &self.display_resolution)
|
||||||
|
.field("type_", &self.type_)
|
||||||
|
.field("nal_ref_idc", &self.nal_ref_idc)
|
||||||
|
.field("is_idr", &self.is_idr)
|
||||||
|
.field("reference", &self.reference)
|
||||||
|
.field(
|
||||||
|
"ref_pic_list_modification_flag_l0",
|
||||||
|
&self.ref_pic_list_modification_flag_l0,
|
||||||
|
)
|
||||||
|
.field("abs_diff_pic_num_minus1", &self.abs_diff_pic_num_minus1)
|
||||||
|
.field("has_mmco_5", &self.has_mmco_5)
|
||||||
|
.field("nonexisting", &self.nonexisting)
|
||||||
|
.field("field", &self.field)
|
||||||
|
.field("ref_pic_marking", &self.ref_pic_marking)
|
||||||
|
.field("field_rank", &self.field_rank)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
// Copyright 2024 The ChromiumOS Authors
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::codec::h264::nalu_writer::NaluWriter;
|
||||||
|
use crate::codec::h264::nalu_writer::NaluWriterError;
|
||||||
|
use crate::codec::h264::parser::HrdParams;
|
||||||
|
use crate::codec::h264::parser::NaluType;
|
||||||
|
use crate::codec::h264::parser::Pps;
|
||||||
|
use crate::codec::h264::parser::Sps;
|
||||||
|
use crate::codec::h264::parser::DEFAULT_4X4_INTER;
|
||||||
|
use crate::codec::h264::parser::DEFAULT_4X4_INTRA;
|
||||||
|
use crate::codec::h264::parser::DEFAULT_8X8_INTER;
|
||||||
|
use crate::codec::h264::parser::DEFAULT_8X8_INTRA;
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
pub trait NaluStruct {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl private::NaluStruct for Sps {}
|
||||||
|
|
||||||
|
impl private::NaluStruct for Pps {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SynthesizerError {
|
||||||
|
Unsupported,
|
||||||
|
NaluWriter(NaluWriterError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SynthesizerError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
SynthesizerError::Unsupported => write!(f, "tried to synthesize unsupported settings"),
|
||||||
|
SynthesizerError::NaluWriter(x) => write!(f, "{}", x.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NaluWriterError> for SynthesizerError {
|
||||||
|
fn from(err: NaluWriterError) -> Self {
|
||||||
|
SynthesizerError::NaluWriter(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SynthesizerResult<T> = Result<T, SynthesizerError>;
|
||||||
|
|
||||||
|
/// A helper to output typed NALUs to [`std::io::Write`] using [`NaluWriter`].
|
||||||
|
pub struct Synthesizer<'n, N: private::NaluStruct, W: Write> {
|
||||||
|
writer: NaluWriter<W>,
|
||||||
|
nalu: &'n N,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extended Sample Aspect Ratio - H.264 Table E-1
|
||||||
|
const EXTENDED_SAR: u8 = 255;
|
||||||
|
|
||||||
|
impl<N: private::NaluStruct, W: Write> Synthesizer<'_, N, W> {
|
||||||
|
fn u<T: Into<u32>>(&mut self, bits: usize, value: T) -> SynthesizerResult<()> {
|
||||||
|
self.writer.write_u(bits, value)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn f<T: Into<u32>>(&mut self, bits: usize, value: T) -> SynthesizerResult<()> {
|
||||||
|
self.writer.write_f(bits, value)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ue<T: Into<u32>>(&mut self, value: T) -> SynthesizerResult<()> {
|
||||||
|
self.writer.write_ue(value)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn se<T: Into<i32>>(&mut self, value: T) -> SynthesizerResult<()> {
|
||||||
|
self.writer.write_se(value)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scaling_list(&mut self, list: &[u8], default: &[u8]) -> SynthesizerResult<()> {
|
||||||
|
// H.264 7.3.2.1.1.1
|
||||||
|
if list == default {
|
||||||
|
self.se(-8)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The number of list values we want to encode.
|
||||||
|
let mut run = list.len();
|
||||||
|
|
||||||
|
// Check how many values at the end of the matrix are the same,
|
||||||
|
// so we can save on encoding those.
|
||||||
|
for j in (1..list.len()).rev() {
|
||||||
|
if list[j - 1] != list[j] {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
run -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode deltas.
|
||||||
|
let mut last_scale = 8;
|
||||||
|
for scale in &list[0..run] {
|
||||||
|
let delta_scale = *scale as i32 - last_scale;
|
||||||
|
self.se(delta_scale)?;
|
||||||
|
last_scale = *scale as i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't encode all values, encode -|last_scale| to set decoder's
|
||||||
|
// |next_scale| (H.264 7.3.2.1.1.1) to zero, i.e. decoder should repeat
|
||||||
|
// last values in matrix.
|
||||||
|
if run < list.len() {
|
||||||
|
self.se(-last_scale)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_scaling_list(i: usize) -> &'static [u8] {
|
||||||
|
// H.264 Table 7-2
|
||||||
|
match i {
|
||||||
|
0 => &DEFAULT_4X4_INTRA[..],
|
||||||
|
1 => &DEFAULT_4X4_INTRA[..],
|
||||||
|
2 => &DEFAULT_4X4_INTRA[..],
|
||||||
|
3 => &DEFAULT_4X4_INTER[..],
|
||||||
|
4 => &DEFAULT_4X4_INTER[..],
|
||||||
|
5 => &DEFAULT_4X4_INTER[..],
|
||||||
|
6 => &DEFAULT_8X8_INTRA[..],
|
||||||
|
7 => &DEFAULT_8X8_INTER[..],
|
||||||
|
8 => &DEFAULT_8X8_INTRA[..],
|
||||||
|
9 => &DEFAULT_8X8_INTER[..],
|
||||||
|
10 => &DEFAULT_8X8_INTRA[..],
|
||||||
|
11 => &DEFAULT_8X8_INTER[..],
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rbsp_trailing_bits(&mut self) -> SynthesizerResult<()> {
|
||||||
|
self.f(1, 1u32)?;
|
||||||
|
|
||||||
|
while !self.writer.aligned() {
|
||||||
|
self.f(1, 0u32)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'n, W: Write> Synthesizer<'n, Sps, W> {
|
||||||
|
pub fn synthesize(
|
||||||
|
ref_idc: u8,
|
||||||
|
sps: &'n Sps,
|
||||||
|
writer: W,
|
||||||
|
ep_enabled: bool,
|
||||||
|
) -> SynthesizerResult<()> {
|
||||||
|
let mut s = Self {
|
||||||
|
writer: NaluWriter::<W>::new(writer, ep_enabled),
|
||||||
|
nalu: sps,
|
||||||
|
};
|
||||||
|
|
||||||
|
s.writer.write_header(ref_idc, NaluType::Sps as u8)?;
|
||||||
|
s.seq_parameter_set_data()?;
|
||||||
|
s.rbsp_trailing_bits()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hrd_parameters(&mut self, hrd_params: &HrdParams) -> SynthesizerResult<()> {
|
||||||
|
self.ue(hrd_params.cpb_cnt_minus1)?;
|
||||||
|
self.u(4, hrd_params.bit_rate_scale)?;
|
||||||
|
self.u(4, hrd_params.cpb_size_scale)?;
|
||||||
|
|
||||||
|
for i in 0..=(hrd_params.cpb_cnt_minus1 as usize) {
|
||||||
|
self.ue(hrd_params.bit_rate_value_minus1[i])?;
|
||||||
|
self.ue(hrd_params.cpb_size_value_minus1[i])?;
|
||||||
|
self.u(1, hrd_params.cbr_flag[i])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(5, hrd_params.initial_cpb_removal_delay_length_minus1)?;
|
||||||
|
self.u(5, hrd_params.cpb_removal_delay_length_minus1)?;
|
||||||
|
self.u(5, hrd_params.dpb_output_delay_length_minus1)?;
|
||||||
|
self.u(5, hrd_params.time_offset_length)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vui_parameters(&mut self) -> SynthesizerResult<()> {
|
||||||
|
// H.264 E.1.1
|
||||||
|
let vui_params = &self.nalu.vui_parameters;
|
||||||
|
|
||||||
|
self.u(1, vui_params.aspect_ratio_info_present_flag)?;
|
||||||
|
if vui_params.aspect_ratio_info_present_flag {
|
||||||
|
self.u(8, vui_params.aspect_ratio_idc)?;
|
||||||
|
if vui_params.aspect_ratio_idc == EXTENDED_SAR {
|
||||||
|
self.u(16, vui_params.sar_width)?;
|
||||||
|
self.u(16, vui_params.sar_height)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, vui_params.overscan_info_present_flag)?;
|
||||||
|
if vui_params.overscan_info_present_flag {
|
||||||
|
self.u(1, vui_params.overscan_appropriate_flag)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, vui_params.video_signal_type_present_flag)?;
|
||||||
|
if vui_params.video_signal_type_present_flag {
|
||||||
|
self.u(3, vui_params.video_format)?;
|
||||||
|
self.u(1, vui_params.video_full_range_flag)?;
|
||||||
|
|
||||||
|
self.u(1, vui_params.colour_description_present_flag)?;
|
||||||
|
if vui_params.colour_description_present_flag {
|
||||||
|
self.u(8, vui_params.colour_primaries)?;
|
||||||
|
self.u(8, vui_params.transfer_characteristics)?;
|
||||||
|
self.u(8, vui_params.matrix_coefficients)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, vui_params.chroma_loc_info_present_flag)?;
|
||||||
|
if vui_params.chroma_loc_info_present_flag {
|
||||||
|
self.ue(vui_params.chroma_sample_loc_type_top_field)?;
|
||||||
|
self.ue(self.nalu.vui_parameters.chroma_sample_loc_type_bottom_field)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, vui_params.timing_info_present_flag)?;
|
||||||
|
if vui_params.timing_info_present_flag {
|
||||||
|
self.u(32, vui_params.num_units_in_tick)?;
|
||||||
|
self.u(32, vui_params.time_scale)?;
|
||||||
|
self.u(1, vui_params.fixed_frame_rate_flag)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, vui_params.nal_hrd_parameters_present_flag)?;
|
||||||
|
if vui_params.nal_hrd_parameters_present_flag {
|
||||||
|
self.hrd_parameters(&vui_params.nal_hrd_parameters)?;
|
||||||
|
}
|
||||||
|
self.u(1, vui_params.vcl_hrd_parameters_present_flag)?;
|
||||||
|
if vui_params.vcl_hrd_parameters_present_flag {
|
||||||
|
self.hrd_parameters(&vui_params.vcl_hrd_parameters)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if vui_params.nal_hrd_parameters_present_flag || vui_params.vcl_hrd_parameters_present_flag
|
||||||
|
{
|
||||||
|
self.u(1, vui_params.low_delay_hrd_flag)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, vui_params.pic_struct_present_flag)?;
|
||||||
|
|
||||||
|
self.u(1, vui_params.bitstream_restriction_flag)?;
|
||||||
|
if vui_params.bitstream_restriction_flag {
|
||||||
|
self.u(1, vui_params.motion_vectors_over_pic_boundaries_flag)?;
|
||||||
|
self.ue(vui_params.max_bytes_per_pic_denom)?;
|
||||||
|
self.ue(vui_params.max_bits_per_mb_denom)?;
|
||||||
|
self.ue(vui_params.log2_max_mv_length_horizontal)?;
|
||||||
|
self.ue(vui_params.log2_max_mv_length_vertical)?;
|
||||||
|
self.ue(vui_params.max_num_reorder_frames)?;
|
||||||
|
self.ue(vui_params.max_dec_frame_buffering)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seq_parameter_set_data(&mut self) -> SynthesizerResult<()> {
|
||||||
|
// H.264 7.3.2.1.1
|
||||||
|
self.u(8, self.nalu.profile_idc)?;
|
||||||
|
self.u(1, self.nalu.constraint_set0_flag)?;
|
||||||
|
self.u(1, self.nalu.constraint_set1_flag)?;
|
||||||
|
self.u(1, self.nalu.constraint_set2_flag)?;
|
||||||
|
self.u(1, self.nalu.constraint_set3_flag)?;
|
||||||
|
self.u(1, self.nalu.constraint_set4_flag)?;
|
||||||
|
self.u(1, self.nalu.constraint_set5_flag)?;
|
||||||
|
self.u(2, /* reserved_zero_2bits */ 0u32)?;
|
||||||
|
self.u(8, self.nalu.level_idc as u32)?;
|
||||||
|
self.ue(self.nalu.seq_parameter_set_id)?;
|
||||||
|
|
||||||
|
if self.nalu.profile_idc == 100
|
||||||
|
|| self.nalu.profile_idc == 110
|
||||||
|
|| self.nalu.profile_idc == 122
|
||||||
|
|| self.nalu.profile_idc == 244
|
||||||
|
|| self.nalu.profile_idc == 44
|
||||||
|
|| self.nalu.profile_idc == 83
|
||||||
|
|| self.nalu.profile_idc == 86
|
||||||
|
|| self.nalu.profile_idc == 118
|
||||||
|
|| self.nalu.profile_idc == 128
|
||||||
|
|| self.nalu.profile_idc == 138
|
||||||
|
|| self.nalu.profile_idc == 139
|
||||||
|
|| self.nalu.profile_idc == 134
|
||||||
|
|| self.nalu.profile_idc == 135
|
||||||
|
{
|
||||||
|
self.ue(self.nalu.chroma_format_idc)?;
|
||||||
|
|
||||||
|
if self.nalu.chroma_format_idc == 3 {
|
||||||
|
self.u(1, self.nalu.separate_colour_plane_flag)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ue(self.nalu.bit_depth_luma_minus8)?;
|
||||||
|
self.ue(self.nalu.bit_depth_chroma_minus8)?;
|
||||||
|
self.u(1, self.nalu.qpprime_y_zero_transform_bypass_flag)?;
|
||||||
|
self.u(1, self.nalu.seq_scaling_matrix_present_flag)?;
|
||||||
|
|
||||||
|
if self.nalu.seq_scaling_matrix_present_flag {
|
||||||
|
let scaling_list_count = if self.nalu.chroma_format_idc != 3 {
|
||||||
|
8
|
||||||
|
} else {
|
||||||
|
12
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..scaling_list_count {
|
||||||
|
// Assume if scaling lists are zeroed that they are not present.
|
||||||
|
if i < 6 {
|
||||||
|
if self.nalu.scaling_lists_4x4[i] == [0; 16] {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ false)?;
|
||||||
|
} else {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ true)?;
|
||||||
|
self.scaling_list(
|
||||||
|
&self.nalu.scaling_lists_4x4[i],
|
||||||
|
Self::default_scaling_list(i),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else if self.nalu.scaling_lists_8x8[i - 6] == [0; 64] {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ false)?;
|
||||||
|
} else {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ true)?;
|
||||||
|
self.scaling_list(
|
||||||
|
&self.nalu.scaling_lists_8x8[i - 6],
|
||||||
|
Self::default_scaling_list(i),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ue(self.nalu.log2_max_frame_num_minus4)?;
|
||||||
|
self.ue(self.nalu.pic_order_cnt_type)?;
|
||||||
|
|
||||||
|
if self.nalu.pic_order_cnt_type == 0 {
|
||||||
|
self.ue(self.nalu.log2_max_pic_order_cnt_lsb_minus4)?;
|
||||||
|
} else if self.nalu.pic_order_cnt_type == 1 {
|
||||||
|
self.u(1, self.nalu.delta_pic_order_always_zero_flag)?;
|
||||||
|
self.se(self.nalu.offset_for_non_ref_pic)?;
|
||||||
|
self.se(self.nalu.offset_for_top_to_bottom_field)?;
|
||||||
|
self.ue(self.nalu.num_ref_frames_in_pic_order_cnt_cycle)?;
|
||||||
|
|
||||||
|
for offset_for_ref_frame in &self.nalu.offset_for_ref_frame {
|
||||||
|
self.se(*offset_for_ref_frame)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ue(self.nalu.max_num_ref_frames)?;
|
||||||
|
self.u(1, self.nalu.gaps_in_frame_num_value_allowed_flag)?;
|
||||||
|
self.ue(self.nalu.pic_width_in_mbs_minus1)?;
|
||||||
|
self.ue(self.nalu.pic_height_in_map_units_minus1)?;
|
||||||
|
self.u(1, self.nalu.frame_mbs_only_flag)?;
|
||||||
|
if !self.nalu.frame_mbs_only_flag {
|
||||||
|
self.u(1, self.nalu.mb_adaptive_frame_field_flag)?;
|
||||||
|
}
|
||||||
|
self.u(1, self.nalu.direct_8x8_inference_flag)?;
|
||||||
|
|
||||||
|
self.u(1, self.nalu.frame_cropping_flag)?;
|
||||||
|
if self.nalu.frame_cropping_flag {
|
||||||
|
self.ue(self.nalu.frame_crop_left_offset)?;
|
||||||
|
self.ue(self.nalu.frame_crop_right_offset)?;
|
||||||
|
self.ue(self.nalu.frame_crop_top_offset)?;
|
||||||
|
self.ue(self.nalu.frame_crop_bottom_offset)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, self.nalu.vui_parameters_present_flag)?;
|
||||||
|
if self.nalu.vui_parameters_present_flag {
|
||||||
|
self.vui_parameters()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'n, W: Write> Synthesizer<'n, Pps, W> {
|
||||||
|
pub fn synthesize(
|
||||||
|
ref_idc: u8,
|
||||||
|
pps: &'n Pps,
|
||||||
|
writer: W,
|
||||||
|
ep_enabled: bool,
|
||||||
|
) -> SynthesizerResult<()> {
|
||||||
|
let mut s = Self {
|
||||||
|
writer: NaluWriter::<W>::new(writer, ep_enabled),
|
||||||
|
nalu: pps,
|
||||||
|
};
|
||||||
|
|
||||||
|
s.writer.write_header(ref_idc, NaluType::Pps as u8)?;
|
||||||
|
s.pic_parameter_set_rbsp()?;
|
||||||
|
s.rbsp_trailing_bits()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pic_parameter_set_rbsp(&mut self) -> SynthesizerResult<()> {
|
||||||
|
self.ue(self.nalu.pic_parameter_set_id)?;
|
||||||
|
self.ue(self.nalu.seq_parameter_set_id)?;
|
||||||
|
self.u(1, self.nalu.entropy_coding_mode_flag)?;
|
||||||
|
self.u(1, self.nalu.bottom_field_pic_order_in_frame_present_flag)?;
|
||||||
|
|
||||||
|
self.ue(self.nalu.num_slice_groups_minus1)?;
|
||||||
|
if self.nalu.num_slice_groups_minus1 > 0 {
|
||||||
|
return Err(SynthesizerError::Unsupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ue(self.nalu.num_ref_idx_l0_default_active_minus1)?;
|
||||||
|
self.ue(self.nalu.num_ref_idx_l1_default_active_minus1)?;
|
||||||
|
self.u(1, self.nalu.weighted_pred_flag)?;
|
||||||
|
self.u(2, self.nalu.weighted_bipred_idc)?;
|
||||||
|
self.se(self.nalu.pic_init_qp_minus26)?;
|
||||||
|
self.se(self.nalu.pic_init_qs_minus26)?;
|
||||||
|
self.se(self.nalu.chroma_qp_index_offset)?;
|
||||||
|
self.u(1, self.nalu.deblocking_filter_control_present_flag)?;
|
||||||
|
self.u(1, self.nalu.constrained_intra_pred_flag)?;
|
||||||
|
self.u(1, self.nalu.redundant_pic_cnt_present_flag)?;
|
||||||
|
|
||||||
|
if !(self.nalu.transform_8x8_mode_flag
|
||||||
|
|| self.nalu.pic_scaling_matrix_present_flag
|
||||||
|
|| self.nalu.second_chroma_qp_index_offset != 0)
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u(1, self.nalu.transform_8x8_mode_flag)?;
|
||||||
|
self.u(1, self.nalu.pic_scaling_matrix_present_flag)?;
|
||||||
|
|
||||||
|
if self.nalu.pic_scaling_matrix_present_flag {
|
||||||
|
let mut scaling_list_count = 6;
|
||||||
|
if self.nalu.transform_8x8_mode_flag {
|
||||||
|
if self.nalu.sps.chroma_format_idc != 3 {
|
||||||
|
scaling_list_count += 2;
|
||||||
|
} else {
|
||||||
|
scaling_list_count += 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..scaling_list_count {
|
||||||
|
// Assume if scaling lists are zeroed that they are not present.
|
||||||
|
if i < 6 {
|
||||||
|
if self.nalu.scaling_lists_4x4[i] == [0; 16] {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ false)?;
|
||||||
|
} else {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ true)?;
|
||||||
|
self.scaling_list(
|
||||||
|
&self.nalu.scaling_lists_4x4[i],
|
||||||
|
Self::default_scaling_list(i),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else if self.nalu.scaling_lists_8x8[i - 6] == [0; 64] {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ false)?;
|
||||||
|
} else {
|
||||||
|
self.u(1, /* seq_scaling_list_present_flag */ true)?;
|
||||||
|
self.scaling_list(
|
||||||
|
&self.nalu.scaling_lists_8x8[i - 6],
|
||||||
|
Self::default_scaling_list(i),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.se(self.nalu.second_chroma_qp_index_offset)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::codec::h264::parser::Nalu;
|
||||||
|
use crate::codec::h264::parser::NaluType;
|
||||||
|
use crate::codec::h264::parser::Parser;
|
||||||
|
use crate::codec::h264::parser::Profile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthesize_sps() {
|
||||||
|
let raw_sps_buf = [0x00, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x0a, 0xfb, 0x88];
|
||||||
|
let mut raw_sps = Cursor::new(&raw_sps_buf[..]);
|
||||||
|
|
||||||
|
let nalu = Nalu::next(&mut raw_sps).unwrap();
|
||||||
|
assert_eq!(nalu.header.type_, NaluType::Sps);
|
||||||
|
|
||||||
|
let mut parser = Parser::default();
|
||||||
|
let sps = parser.parse_sps(&nalu).unwrap();
|
||||||
|
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
Synthesizer::<'_, Sps, _>::synthesize(0, sps, &mut buf, false).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(buf, raw_sps_buf);
|
||||||
|
|
||||||
|
let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true");
|
||||||
|
if write_to_file {
|
||||||
|
let mut out = std::fs::File::create("sps.h264").unwrap();
|
||||||
|
out.write_all(&buf).unwrap();
|
||||||
|
out.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(&buf[..]);
|
||||||
|
let nalu = Nalu::next(&mut cursor).unwrap();
|
||||||
|
|
||||||
|
let mut parser = Parser::default();
|
||||||
|
|
||||||
|
let sps2 = parser.parse_sps(&nalu).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(sps, sps2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthesize_sps_scaling_lists() {
|
||||||
|
let sps = Sps {
|
||||||
|
profile_idc: Profile::High as u8,
|
||||||
|
seq_scaling_matrix_present_flag: true,
|
||||||
|
scaling_lists_4x4: [[
|
||||||
|
11, 20, 10, 20, 10, 22, 10, 20, 10, 20, 13, 20, 10, 20, 10, 24,
|
||||||
|
]; 6],
|
||||||
|
scaling_lists_8x8: [
|
||||||
|
[
|
||||||
|
33, 20, 10, 21, 33, 20, 12, 20, 33, 23, 10, 20, 33, 20, 10, 20, 33, 24, 10, 20,
|
||||||
|
33, 20, 15, 20, 33, 20, 10, 26, 33, 20, 17, 20, 33, 28, 10, 20, 33, 20, 10, 20,
|
||||||
|
33, 29, 10, 20, 33, 20, 11, 20, 33, 20, 10, 20, 33, 20, 10, 20, 33, 20, 10, 20,
|
||||||
|
33, 20, 10, 20,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
10, 77, 11, 20, 10, 77, 12, 20, 10, 77, 13, 20, 10, 77, 14, 20, 10, 77, 15, 20,
|
||||||
|
10, 77, 16, 20, 10, 77, 17, 20, 10, 77, 18, 20, 10, 77, 19, 20, 10, 77, 10, 20,
|
||||||
|
10, 77, 10, 21, 10, 77, 10, 22, 10, 77, 10, 23, 10, 77, 10, 24, 10, 77, 10, 26,
|
||||||
|
10, 77, 10, 28,
|
||||||
|
],
|
||||||
|
[0; 64],
|
||||||
|
[0; 64],
|
||||||
|
[0; 64],
|
||||||
|
[0; 64],
|
||||||
|
],
|
||||||
|
frame_mbs_only_flag: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
Synthesizer::<'_, Sps, _>::synthesize(0, &sps, &mut buf, false).unwrap();
|
||||||
|
|
||||||
|
let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true");
|
||||||
|
if write_to_file {
|
||||||
|
let mut out = std::fs::File::create("sps.h264").unwrap();
|
||||||
|
out.write_all(&buf).unwrap();
|
||||||
|
out.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(&buf[..]);
|
||||||
|
let nalu = Nalu::next(&mut cursor).unwrap();
|
||||||
|
|
||||||
|
let mut parser = Parser::default();
|
||||||
|
|
||||||
|
let sps2 = parser.parse_sps(&nalu).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(sps.scaling_lists_4x4, sps2.scaling_lists_4x4);
|
||||||
|
assert_eq!(sps.scaling_lists_8x8, sps2.scaling_lists_8x8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthesize_pps() {
|
||||||
|
let raw_sps_pps = [
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x07, 0x4d, 0x40, 0x0d, 0xa9, 0x18, 0x28, 0x3e, 0x60, 0x0d,
|
||||||
|
0x41, 0x80, 0x41, 0xad, 0xb0, 0xad, 0x7b, 0xdf, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08,
|
||||||
|
0xde, 0x09, 0x88,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
let mut out = Cursor::new(&mut buf);
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(&raw_sps_pps[..]);
|
||||||
|
let mut parser: Parser = Default::default();
|
||||||
|
|
||||||
|
while let Ok(nalu) = Nalu::next(&mut cursor) {
|
||||||
|
match nalu.header.type_ {
|
||||||
|
NaluType::Sps => {
|
||||||
|
let sps = parser.parse_sps(&nalu).unwrap();
|
||||||
|
Synthesizer::<'_, Sps, _>::synthesize(0, sps, &mut out, false).unwrap();
|
||||||
|
}
|
||||||
|
NaluType::Pps => {
|
||||||
|
let pps = parser.parse_pps(&nalu).unwrap();
|
||||||
|
Synthesizer::<'_, Pps, _>::synthesize(0, pps, &mut out, false).unwrap();
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let write_to_file = std::option_env!("CROS_CODECS_TEST_WRITE_TO_FILE") == Some("true");
|
||||||
|
if write_to_file {
|
||||||
|
let mut out = std::fs::File::create("sps_pps.h264").unwrap();
|
||||||
|
out.write_all(&buf).unwrap();
|
||||||
|
out.flush().unwrap();
|
||||||
|
|
||||||
|
let mut out = std::fs::File::create("sps_pps_ref.h264").unwrap();
|
||||||
|
out.write_all(&raw_sps_pps).unwrap();
|
||||||
|
out.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(buf, raw_sps_pps);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
43656b2f
|
||||||
|
c9dd1361
|
||||||
|
62d34555
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
45ba0c1a27d0ff82fd7a969631adffe6
|
||||||
|
cee875ded4998c9810a14f9497a11f72
|
||||||
|
d1e0b9347134ba7a07cbcf249066f022
|
||||||
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
|||||||
|
ee936370
|
||||||
|
0e5e577c
|
||||||
|
bfe430af
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
fe6701d54768bc37c76e630435e8fe02
|
||||||
|
3298d47365ac1f9dc0942ece9e104246
|
||||||
|
7cb9209603f53ba995f7eee20b2bbf4b
|
||||||
BIN
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
|||||||
|
9fc67012
|
||||||
|
7f0b441e
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
115a8e6899b71c04e32a0254ba62b30a
|
||||||
|
1fa3f5a930e08943e6d6bfbd5d43004e
|
||||||
Binary file not shown.
+1
@@ -0,0 +1 @@
|
|||||||
|
7dd66ef1
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
d2304abbf0349ec63324741bf723960d
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# H.264 Test Data
|
||||||
|
|
||||||
|
This document lists the test data used by the H.264 decoder.
|
||||||
|
|
||||||
|
Unless otherwise noted, the CRCs were computed using GStreamer's VA-API decoder in
|
||||||
|
`gst-plugins-bad`.
|
||||||
|
|
||||||
|
## 16x16-I.h264
|
||||||
|
|
||||||
|
A 16x16 progressive byte-stream encoded I-frame to make it easier to spot errors on the libva trace.
|
||||||
|
Encoded with the following GStreamer pipeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
gst-launch-1.0 videotestsrc num-buffers=1 ! video/x-raw,format=I420,width=16,height=16 ! \
|
||||||
|
x264enc ! video/x-h264,profile=constrained-baseline,stream-format=byte-stream ! \
|
||||||
|
filesink location="/tmp/16x16-I.h264"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16x16-I-P.h264
|
||||||
|
|
||||||
|
A 16x16 progressive byte-stream encoded I-frame and P-frame to make it easier to spot errors on the
|
||||||
|
libva trace. Encoded with the following GStreamer pipeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
gst-launch-1.0 videotestsrc num-buffers=2 ! video/x-raw,format=I420,width=16,height=16 ! \
|
||||||
|
x264enc b-adapt=false ! video/x-h264,profile=constrained-baseline,stream-format=byte-stream ! \
|
||||||
|
filesink location="/tmp/16x16-I-P.h264"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16x16-I-P-B-P.h264
|
||||||
|
|
||||||
|
A 16x16 progressive byte-stream encoded I-P-B-P sequence to make it easier to it easier to spot
|
||||||
|
errors on the libva trace. Encoded with the following GStreamer pipeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
gst-launch-1.0 videotestsrc num-buffers=3 ! video/x-raw,format=I420,width=16,height=16 ! \
|
||||||
|
x264enc b-adapt=false bframes=1 ! video/x-h264,profile=constrained-baseline,stream-format=byte-stream ! \
|
||||||
|
filesink location="/tmp/16x16-I-B-P.h264"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16x16-I-P-B-P-high.h264
|
||||||
|
|
||||||
|
A 16x16 progressive byte-stream encoded I-P-B-P sequence to make it easier to it easier to spot
|
||||||
|
errors on the libva trace. Also tests whether the decoder supports the high profile. Encoded with
|
||||||
|
the following GStreamer pipeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
gst-launch-1.0 videotestsrc num-buffers=3 ! video/x-raw,format=I420,width=16,height=16 ! \
|
||||||
|
x264enc b-adapt=false bframes=1 ! video/x-h264,profile=high,stream-format=byte-stream ! \
|
||||||
|
filesink location="/tmp/16x16-I-B-P-high.h264"
|
||||||
|
```
|
||||||
|
|
||||||
|
## test-25fps.h264
|
||||||
|
|
||||||
|
Same as Chromium's `test-25fps.h264`. The slice data in `test-25fps-h264-slice-data-*.bin` was
|
||||||
|
manually extracted from GStreamer using GDB.
|
||||||
|
|
||||||
|
## test-25fps-interlaced.h264
|
||||||
|
|
||||||
|
Adapted from Chromium's `test-25fps.h264`. Same file as above, but encoded as interlaced instead
|
||||||
|
using the following ffmpeg command:
|
||||||
|
|
||||||
|
```
|
||||||
|
ffmpeg -i \
|
||||||
|
src/third_party/blink/web_tests/media/content/test-25fps.mp4 \
|
||||||
|
-flags +ilme+ildct -vbsf h264_mp4toannexb -an test-25fps.h264
|
||||||
|
```
|
||||||
|
|
||||||
|
This test makes sure that the interlaced logic in the decoder actually works, specially that "frame
|
||||||
|
splitting" works, as the fields here were encoded as frames.
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Generates the CRCs for all .h264 files in the current directory using ffmpeg.
|
||||||
|
|
||||||
|
for f in `ls *.h264`; do
|
||||||
|
ffmpeg -i $f -pix_fmt nv12 -f framehash -hash crc32 - |grep -v '^#' |awk '{print $6}' >$f.crc
|
||||||
|
ffmpeg -i $f -pix_fmt nv12 -f framehash -hash md5 - |grep -v '^#' |awk '{print $6}' >$f.md5
|
||||||
|
done
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user