File ab-av1-0.10.1.obscpio of Package ab-av1
07070100000000000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000001600000000ab-av1-0.10.1/.github07070100000001000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000002000000000ab-av1-0.10.1/.github/workflows07070100000002000081A40000000000000000000000016828905A00000364000000000000000000000000000000000000002700000000ab-av1-0.10.1/.github/workflows/ci.ymlname: Rust
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
env:
RUST_BACKTRACE: 1
steps:
- run: rustup update stable
- uses: actions/checkout@v4
- run: cargo test --locked
# check print-completions don't fail
- run: cargo run --locked -- print-completions bash
- run: cargo run --locked -- print-completions fish
- run: cargo run --locked -- print-completions zsh
# Disabled while we no longer need cfg(windows) code
# test-windows:
# runs-on: windows-latest
# env:
# RUST_BACKTRACE: 1
# steps:
# - run: rustup update stable
# - uses: actions/checkout@v4
# - run: cargo check
rustfmt:
runs-on: ubuntu-latest
steps:
- run: rustup update stable
- uses: actions/checkout@v4
- run: cargo fmt -- --check
07070100000003000081A40000000000000000000000016828905A00000499000000000000000000000000000000000000002C00000000ab-av1-0.10.1/.github/workflows/release.ymlname: Release
on:
push:
tags:
- '*'
jobs:
linux-bin:
name: Build Linux musl binary
runs-on: ubuntu-latest
steps:
- run: rustup update stable
- run: rustup target add x86_64-unknown-linux-musl
- uses: actions/checkout@v4
- run: cargo build --release --locked --target=x86_64-unknown-linux-musl
- run: tar c ab-av1 | zstd -T0 -19 > ab-av1-${{ github.ref_name }}-x86_64-unknown-linux-musl.tar.zst
working-directory: target/x86_64-unknown-linux-musl/release/
- uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: target/x86_64-unknown-linux-musl/release/ab-av1-${{ github.ref_name }}-x86_64-unknown-linux-musl.tar.zst
tag: ${{ github.ref }}
overwrite: true
win-bin:
name: Build Windows binary
runs-on: windows-latest
steps:
- run: rustup update stable
- uses: actions/checkout@v4
- run: cargo build --release --locked
- uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: target/release/ab-av1.exe
tag: ${{ github.ref }}
overwrite: true
07070100000004000081A40000000000000000000000016828905A00000024000000000000000000000000000000000000001900000000ab-av1-0.10.1/.gitignore/target
/aur
*.log
.DS_Store
*.json
07070100000005000081A40000000000000000000000016828905A00003EEA000000000000000000000000000000000000001B00000000ab-av1-0.10.1/CHANGELOG.md# v0.10.1
* Support setting `--enc-input hwaccel=none --enc-input hwaccel_output_format=none` to omit defaults
for *_vaapi, *_vulkan vcodecs introduced in v0.9.4.
# v0.10.0
* `--pix-format` no longer generally defaults to "yuv420p", instead if not specified no -pix_fmt
will be passed to ffmpeg allowing use of upstream defaults.
However, libsvtav1, libaom-av1 & librav1e will continue to default to "yuv420p10le".
* Allow specifying ffmpeg decoder using `--enc-input c:v=CODEC`.
# v0.9.4
* Encoder *_vaapi: Default args `--enc-input hwaccel=vaapi --enc-input hwaccel_output_format=vaapi`.
* Encoder *_vulkan: Map `--crf` to ffmpeg `-qp`.
* Encoder *_vulkan: Default args `--enc-input hwaccel=vulkan --enc-input hwaccel_output_format=vulkan`.
* Encoder libvvenc: Map `--crf` to ffmpeg `-qp`.
# v0.9.3
* Support setting per-stream audio codec, e.g. `--enc c:a:1=libopus`.
* Support `--pix-format yuv422p10le`.
* Write video stream metadata "AB_AV1_FFMPEG_ARGS" to encoded output, include a subset of relevant
ffmpeg args used. E.g. `AB_AV1_FFMPEG_ARGS: -c:v libsvtav1 -crf 25 -preset 8`.
(Not supported by mp4 files).
# v0.9.2
* Log crf results, instead of printing, if stderr is not a terminal.
* Wait for all child processes (ffmpeg etc) to finish before temp file cleanup and exit.
# v0.9.1
* Fix xpsnr inf score parsing.
* Fix xpsnr reference vfilter usage.
* Add `--xpsnr-fps`: Frame rate override used to analyse both reference & distorted videos. Default 60.
# v0.9.0
* Add XPSNR support as a VMAF alternative.
- Add sample-encode `--xpsnr` arg which toggles use of XPSNR instead of VMAF.
- Add crf-search, auto-encode `--min-xpsnr` arg _(alternative to `--min-vmaf`)_.
- Add `xpsnr` command for measuring XPSNR score.
* Support negative `--preset` args.
* Add `--vmaf-fps`: Frame rate override used to analyse both reference & distorted videos. Default 25.
* Omit data streams when outputting to matroska (.mkv or .webm).
* Omit audio, subtitle & data streams in VMAF calls to work around possible ffmpeg memory leaks.
* mpeg2video: map `--crf` to ffmpeg `-q` and set default crf range to 2-30.
# v0.8.0
* crf-search: Tweak 2nd iteration logic that slices the crf range at the 25% or 75% crf point.
- Widen to 20%/80% to account for searches of the "middle" two subranges being more optimal.
- Disable when using custom min/max crf ranges under half the default.
* Add sample-encode info to crf-search & auto-encode. Show sample progress and encoding/vmaf fps.
* Improve sample-encode progress format consistency.
* Add crf-search `-v` flag to print per-sample results.
* Add auto-encode `-v` flag to print per-crf results, `-vv` to also print per-sample results.
# v0.7.19
* Fix stdin handling sometimes breaking bash shells.
# v0.7.18
* Use default .265, .264 image output extensions for libx265, libx264.
Fixes crf-search for images with these codecs.
* Improve `--vfilter` docs, clarify VMAF usage.
# v0.7.17
* Improve failing ffmpeg stderr printing:
- Don't allow many '\r'-ending updates to cause all other stored info to be truncated.
- Increase max heap storage of output ~4k->32k to allow more complete output in some cases.
* Fix caching unaffected by `--reference-vfilter` usage.
* Improve `--vfilter` docs. Describe VMAF usage & mention `--reference-vfilter`.
* Improve `--vmaf-scale` docs.
* VMAF: Remove `-r 24` ffmpeg input.
* VMAF: Add new default options "shortest=true", "ts_sync_mode=nearest" and use vfilter "settb=AVTB".
# v0.7.16
* Fix VMAF score parse failure of certain successful ffmpeg outputs.
# v0.7.15
* Show full ffmpeg command after errors.
* For *_vaapi encoders map `--crf` to ffmpeg `-q` (instead of `-qp`).
* Set av1_vaapi default `--max-crf` to 255.
* Fix sample-encode printing output to non-terminals.
* Omit "Encode with: ..." stderr hint for non-terminals.
* Support logging enabled when stderr is not a terminal or by setting env var `RUST_LOG`. E.g:
- `RUST_LOG=ab_av1=info` "info" level logs various progress results like sample encode info
- `RUST_LOG=ab_av1=debug` "debug" level logs include ffmpeg calls
* Don't panic on non-zero status exit.
* When unable to parse a vmaf score fail faster and include ffmpeg output.
* Add `--reference-vfilter` arg to _sample-encode_, _crf-search_, _auto-encode_ to allow
overriding `--vfilter` for VMAF.
* Add `--sample-duration` arg to configure the duration of each sample. Default 20s.
# v0.7.14
* Fix bash completions of some filenames.
# v0.7.13
* Use a single ffmpeg process to calculate VMAF replacing multi process piping.
* Exclude subtitle tracks from samples.
* Add `--keep` option for _crf-search_ & _auto-encode_.
# v0.7.12
* Improve eta stability.
# v0.7.11
* Fix sample-encode caching to consider vmaf args.
# v0.7.10
* Fix validation preventing use of svt args starting with "-i", "-b".
# v0.7.9
* Fix validation preventing use of ffmpeg --enc args starting with "-i", e.g. "-init_hw_device".
# v0.7.8
* Fix ETA calculation overflow panic scenario.
# v0.7.7
* Add `--video-only` option for _encode_ & _auto-encode_.
# v0.7.6
* Fix nested temp directories not being cleaned properly.
* Temp directories will now start with "." and be created in the working dir instead of the input parent
(unless setting --temp-dir).
# v0.7.5
* Add `-e librav1e` support. Map `--crf` to ffmpeg `-qp` (default max 255), `--preset` to `-speed` (0-10).
* Disallow `--enc svtav1-params=` usage. libsvtav1 params should instead be set with `--svt`.
# v0.7.4
* Add `--encoder` support for qsv family of ffmpeg encoders: av1_qsv, hevc_qsv, vp9_qsv, h264_qsv and mpeg2_qsv.
* Enable lookahead mode by default for encoders: av1_qsv, hevc_qsv, h264_qsv.
# v0.7.3
* Include all other non-main video streams by copying instead of encoding them with the same
settings as the main video stream.
* Always copy audio unless `--acodec` or `--downmix-to-stereo` are specified. Previously would
re-encode to opus when changing container.
# v0.7.2
* Print failing ffmpeg stderr output.
* Preserve all input file streams (e.g. audio, subs, attachments) into output.
* Support concurrent running processes out of the box by segregating temp-dirs & fixing cache access.
* Improve vmaf accuracy in some cases by forcing 24fps & synchronizing the presentation timestamp.
* Automatically workaround ffmpeg _"Can't write packet with unknown timestamp"_ sample generation failures
(typically encountered with old avi files) by using \`-fflags +genpts\`.
# v0.7.1
* Fix _crf-search_ incorrectly picking a rate that exceeds the `--max-encoded-percent`.
* Improve _auto-encode_ crf float display rounding.
# v0.7.0
* Use ffmpeg for svt-av1 encodes instead of invoking to SvtAv1EncApp directly. This unifies the handling of
other encoders & allows svt-av1 encoding to benefit from more built-in ffmpeg behaviours like aspect preservation.<br/>
**An ffmpeg build with libsvtav1 enabled is now required**. SvtAv1EncApp is no longer required.
* Improve image detection.
* Add `--encoder` support for nvenc family of ffmpeg encoders: av1_nvenc, hevc_nvenc, and h264_nvenc.
# v0.6.1
* Add _sample-encode_, _crf-search_, _auto-encode_ arg `--min-samples`.
* Revert libvpx-vp9 `--crf-increment` default to **1**.
# v0.6.0
* Support decimal crf values in _sample-encode_, _encode_ subcommands (note svt-av1 only supports integer crf).
* Add _crf-search_, _auto-encode_ arg `--crf-increment`. Previously this would always be 1.
Defaults to **1**. -e libx264, libx265 & libvpx-vp9 default to **0.1**.
* Add _crf-search_, _auto-encode_ arg `--thorough` which more exhaustively searches to find
a crf value close to the specified min-vmaf.
* Cache _sample-encode_ results in $CACHE_DIR/ab-av1 directory. This allows repeated same crf sample encoding
to be avoided when running _sample-encode_, _crf-search_ & _auto-encode_. E.g. repeating a _crf-search_ with
a different min-vmaf.<br/>
Caching is enabled by default. Can be disabled with `--cache false` or setting env var `AB_AV1_CACHE=false`.
* Use mkv containers for all lossless samples. Previously mp4 samples were used for mp4 inputs, however in all test cases
mkv 20s samples were better quality. This change improves accuracy for all mp4 input files.
* Default `--max-crf` to **46** for libx264 & libx265 encoders.
* Encode webm outputs with the "cues" seek index at the front to optimise stream usage (as done with mkv).
# v0.5.2
* Fix ffprobe duration conversion error scenarios panicking.
* Tweak encoded size prediction logic to consider both input file size & encoded sample duration.
# v0.5.1
* Change encoded size prediction logic to estimate video stream size (or image size) only.
This should be much more consistent than the previous method.
Change _crf-search_, _sample-encode_ result text to clarify this.
* Improve video size prediction logic to account for samples that do not turn out as 20s.
* Fix full-pass sample encode progress bar.
* Use label "Full pass" instead of "Sample 1/1" when doing a full pass _sample-encode_.
* Add VMAF auto model, n_threads & scaling documentation.
# v0.5.0
* Default to .mkv output format for all inputs (except .mp4 which will continue to output .mp4 by default).
This also applies to ffmpeg encoder sample output format. The previous behaviour used the input extension
which may not have supported av1 (e.g. .m2ts).
* For _auto-encode_ use the output extension also for ffmpeg encoder sample outputs if applicable.
* When creating lossless samples for encode analysis use .mkv (or .mp4) extension for better ffmpeg compatibility.
* Encode mkv outputs with the "cues" seek index at the front to optimise stream usage.
* Optimise pixel format choice for VMAF comparisons. Can significantly improve VMAF fps.
_E.g. if both videos are yuv420p use that instead of yuv444p10le_.
* When sampling use full input video when sample time would be >= 85% of the total (down from 100%).
* Eliminate repeated redundant ffprobe calls.
* Windows: Support VMAF pixel format conversion for both distorted and reference.
Gives more consistently accurate results and brings Windows in line with Linux functionality.
* Windows: ab-av1.exe binaries will now be automatically built and attached to releases.
# v0.4.4
* Add _crf-search_, _auto-encode_, _encode_ & _vmaf_ command support for encoding images into avif.
This works in the same way as videos, example:
```
ab-av1 auto-encode -i pic.jpg
```
The default encoder svt-av1 has some dimension limitations which may cause this to fail. `-e libaom-av1` also works and supports more dimensions.
* Convert to yuv444p10le pixel format when calculating VMAF for accuracy and compatibility.
* Update to clap v4 which changes help/about output & reduces binary size.
* Print _crf-search_ attempts even when stderr is not a tty.
# v0.4.3
* Fix terminal breaking sometimes after exiting early.
# v0.4.2
* Update _indicatif_ dependency to `0.17`.
# v0.4.1
* For `-e libvpx-vp9` map `--preset` number to ffmpeg `-cpu-used` (0-5).
* When overriding with a ffmpeg encoder avoid setting `b:a`, `movflags` or `ac` if explicitly set via `--enc`.
* Add error output when using `--enc-input` with the default svt-av1 encoder.
* Add errors for `--enc`/`--enc-input` args that are already provided by existing args or inferred.
# v0.4.0
* Add `--encoder`/`-e` encoder override.
Any [encoder ffmpeg supports](https://ffmpeg.org/ffmpeg-all.html#toc-Video-Encoders)
and that may be controlled using `-crf` may be used.
* Add `--enc $FFMPEG_ARG` for providing arbitrary output options to the ffmpeg encoder invocation.
These only work when overriding the encoder with `-e`.
<br/>_E.g. Set x265 params: `-e libx265 --enc x265-params=lossless=1`._
* Add `--enc-input $FFMPEG_ARG` for providing ffmpeg input file options, similar to `--enc`.
* `--preset` now supports also word presets like `slow`, `veryfast` for ffmpeg encoders like libx264.
* `--preset` is **no longer required**. Default svt-av1 `--preset` is now **8**.
* Support setting keyint for `-e` encoders in a similar way as is done for av1.
* Add default vp9 & libaom-av1 `-b:v 0` setting so constant quality crf based encoding works consistently.
* For `-e libaom-av1` map `--preset` number to ffmpeg `-cpu-used` (0-8).
* For *_vaapi encoders map `--crf` to ffmpeg `-qp` as crf is not supported.
* Shell escape file name in "Encoding ..." output.
# v0.3.4
* Shell escape file names when hinting commands.
# v0.3.3
* Show more info when auto-encode fails to find a suitable crf.
# v0.3.2
* Improve sample generation speed & frame duration accuracy.
# v0.3.1
* Fix some cases where ffmpeg progress & VMAF score output parsing failed.
* Fix some edge cases where crf-search would succeed exceeding the specified `--max-encoded-percent`.
# v0.3.0
* Select vmaf model `model=version=vmaf_4k_v0.6.1` for videos larger than 2560x1440 if no other model is specified.
This will raise VMAF scores for 4k videos that previously were getting harsher treatment from the 1k model.
* Add `--vmaf-scale` option which sets the video resolution scale to use in VMAF analysis.
`auto` (default) auto scales based on model & resolution, `none` no scaling or custom `WxH`
format, e.g. `1920x1080`.
- `auto` upscale 1728x972 & smaller to 1080p, preserving aspect, when using the default 1k VMAF model.
This will reduce VMAF scores that previously were getting more generous treatment from the 1k model.
- `auto` upscale 3456x1944 & smaller to 4k, preserving aspect, when using the 4k VMAF model.
* Add `--downmix-to-stereo` option, if enabled & the input streams use > 3 channels (dts 5.1 etc),
downmix input audio streams to stereo.
* After encoding print per-stream sizes in addition to the file size & percent.
* Add predicted video stream percent reduction to _auto-encode_ search progress bar after a successful search.
* Support non-video/audio/subtitle streams from input to output, e.g. attachments.
* When defaulting the output file don't use input extension if it is _avi, y4m, ivf_, use mp4 instead.
* Fix clearing _crf-search_ progress bar output on error.
* Strip debug symbols in release builds by default which reduces binary size _(requires rustc 1.59)_.
# v0.2.0
* Add svt-av1 option `--keyint FRAME-OR-DURATION` argument supporting frame integer or duration string.
_E.g. `--keyint=300` or `--keyint=10s`_.
Default keyint to `10s` when input duration is over 3m.
* Add svt-av1 option `--scd true|false` argument enabling scene change detection.
Default scd on when using default keyint & input duration is over 3m.
* Add `--svt ARG` for additional args, _e.g. `--svt mbr=2000 --svt film-grain=30`_.
* Add `--vfilter ARG` argument to apply a ffmpeg video filter (crop, scale etc) to the input before av1 encoding.
<br/>_E.g. `--vfilter "scale=1280:-1,fps=24"`_.
* Add `--pix-format ARG` argument supporting `yuv420p10le` (default) & `yuv420p`.
* Add vmaf configuration `--vmaf ARG`, _e.g. `--vmaf n_threads=8 --vmaf n_subsample=4`_.
* Rename _vmaf_ command argument `--reference` (was `--original`).
* Add _vmaf_ command `--reference-vfilter` argument, similar to `--vfilter`.
* Default vmaf n_threads to the number of logical CPUs.
* Add `--temp-dir` argument to specify storage of sample data.
May also be set with env var `AB_AV1_TEMP_DIR`.
* Add `--sample-every DURATION` argument, default "12m".
* Remove 3 sample default, this is now calculated using `--sample-every` 12m default.
* Create samples concurrently while encoding to reduce io lags waiting to encode.
* _crf-search_ re-use samples for crf analysis.
* Linux: _vmaf_ use fifo to convert both reference & distorted to yuv which fixes vmaf accuracy in some cases.
* Support multiple audio & subtitle streams.
* Use 128k bitrate as a default for libopus audio.
* Remove `--aq`.
* Fail fast if ffmpeg cut samples are empty (< 1K).
* Handle input durations lower than the sample duration by using the whole input as a single sample.
# v0.1.1
* Add command to generate bash,fish & zsh completions `ab-av1 print-completions [SHELL]`.
# v0.1.0
* Initial release.
07070100000006000081A40000000000000000000000016828905A00008085000000000000000000000000000000000000001900000000ab-av1-0.10.1/Cargo.lock# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ab-av1"
version = "0.10.1"
dependencies = [
"anyhow",
"async-stream",
"blake3",
"clap",
"clap-verbosity-flag",
"clap_complete",
"console",
"dirs",
"env_logger",
"fastrand",
"ffprobe",
"futures-util",
"humantime",
"indicatif",
"infer",
"log",
"pin-project-lite",
"serde",
"serde_json",
"shell-escape",
"sled",
"time",
"tokio",
"tokio-process-stream",
"tokio-stream",
]
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "blake3"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap-verbosity-flag"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84"
dependencies = [
"clap",
"log",
]
[[package]]
name = "clap_builder"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"terminal_size",
]
[[package]]
name = "clap_complete"
version = "4.5.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"windows-sys 0.59.0",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "errno"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "ffprobe"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ffef835e1f9ac151db5bb2adbb95c9dfe1f315f987f011dd89cd655b4e9a52c"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "humantime"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
[[package]]
name = "indicatif"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
"portable-atomic",
"unicode-width",
"web-time",
]
[[package]]
name = "infer"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.1",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi",
"windows-sys 0.52.0",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "shell-escape"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "sled"
version = "0.34.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
dependencies = [
"crc32fast",
"crossbeam-epoch",
"crossbeam-utils",
"fs2",
"fxhash",
"libc",
"log",
"parking_lot",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "terminal_size"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tokio"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-process-stream"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f09c7fc9546d3b9586bc95c58ac2bdb48b07c538a26e317b558e6de2fac98b8"
dependencies = [
"anyhow",
"bytes",
"futures",
"pin-project-lite",
"tokio",
"tokio-stream",
"tokio-util",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
07070100000007000081A40000000000000000000000016828905A000004E8000000000000000000000000000000000000001900000000ab-av1-0.10.1/Cargo.toml[package]
name = "ab-av1"
version = "0.10.1"
authors = ["Alex Butler <alexheretic@gmail.com>"]
edition = "2024"
description = "AV1 encoding with fast VMAF sampling"
repository = "https://github.com/alexheretic/ab-av1"
keywords = ["av1", "vmaf"]
license = "MIT"
readme = "README.md"
[dependencies]
anyhow = "1.0.53"
async-stream = "0.3.5"
blake3 = "1.3.3"
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
clap-verbosity-flag = "3.0.2"
clap_complete = "4.4.10"
console = "0.15.4"
dirs = "6"
env_logger = { version = "0.11.3", default-features = false, features = [
"auto-color",
"humantime",
] }
fastrand = "2"
ffprobe = "0.4"
futures-util = "0.3.19"
humantime = "2.1"
indicatif = "0.17"
infer = { version = "0.19", default-features = false }
log = "0.4.21"
pin-project-lite = "0.2.16"
serde = { version = "1.0.185", features = ["derive"] }
serde_json = "1.0.105"
shell-escape = "0.1.5"
sled = "0.34.7"
time = { version = "0.3", features = ["parsing", "macros"] }
tokio = { version = "1.15", features = [
"rt",
"macros",
"process",
"fs",
"signal",
] }
tokio-process-stream = "0.4"
tokio-stream = "0.1"
[profile.release]
lto = true
opt-level = "s"
strip = true
[lints.rust]
unused_crate_dependencies = "deny"
07070100000008000081A40000000000000000000000016828905A00000465000000000000000000000000000000000000001900000000ab-av1-0.10.1/DockerfileFROM rust:latest as builder
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN rustup default stable
RUN cargo build --release
FROM debian:bookworm-slim as runtime
RUN apt-get update && apt-get install -y \
wget \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN dpkgArch="$(dpkg --print-architecture)" \
&& case "${dpkgArch##*-}" in \
amd64) wget https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.0-latest-linux64-gpl-7.0.tar.xz -O /tmp/ffmpeg.tar.xz && \
tar -xvf /tmp/ffmpeg.tar.xz && cd ffmpeg-n7.0-latest-linux64-gpl-7.0/bin && mv ffmpeg ffprobe /usr/local/bin ;; \
arm64) wget https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.0-latest-linuxarm64-gpl-7.0.tar.xz -O /tmp/ffmpeg.tar.xz && \
tar -xvf /tmp/ffmpeg.tar.xz && cd ffmpeg-n7.0-latest-linuxarm64-gpl-7.0/bin && mv ffmpeg ffprobe /usr/local/bin ;; \
*) echo "Unsupported architecture: ${dpkgArch}"; exit 1 ;; \
esac
COPY --from=builder /build/target/release/ab-av1 /app/ab-av1
WORKDIR /videos
ENTRYPOINT ["/app/ab-av1"]
07070100000009000081A40000000000000000000000016828905A0000042C000000000000000000000000000000000000001600000000ab-av1-0.10.1/LICENSECopyright (c) 2022 Alex Butler
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
0707010000000A000081A40000000000000000000000016828905A00000C9F000000000000000000000000000000000000001800000000ab-av1-0.10.1/README.md# ab-av1
AV1 video encoding tool with fast VMAF sampling & automatic encoder crf calculation.
Uses _ffmpeg_, _svt-av1_ & _vmaf_.

Also supports other ffmpeg compatible encoders like libx265 & libx264.
### Command: auto-encode
Automatically determine the best crf to deliver the `--min-vmaf` and use it to encode a video or image.
Two phases:
* [crf-search](#command-crf-search) to determine the best --crf value
* ffmpeg to encode using the settings
```
ab-av1 auto-encode [OPTIONS] -i <INPUT> --preset <PRESET> --min-vmaf <MIN_VMAF>
```
### Command: crf-search
Interpolated binary search using [sample-encode](#command-sample-encode) to find the best
crf value delivering `--min-vmaf` & `--max-encoded-percent`.
Outputs:
* Best crf value
* Mean sample VMAF score
* Predicted full encode size
* Predicted full encode time
```
ab-av1 crf-search [OPTIONS] -i <INPUT> --preset <PRESET> --min-vmaf <MIN_VMAF>
```
#### Notable options
* `--min-xpsnr <MIN_XPSNR>` may be used as an alternative to VMAF.
### Command: sample-encode
Encode short video samples of an input using provided **crf** & **preset**.
This is much quicker than full encode/vmaf run.
Outputs:
* Mean sample VMAF score
* Predicted full encode size
* Predicted full encode time
```
ab-av1 sample-encode [OPTIONS] -i <INPUT> --crf <CRF> --preset <PRESET>
```
#### Notable options
* `--xpsnr` specifies calculation of XPSNR score instead of VMAF.
### Command: encode
Invoke ffmpeg to encode a video or image.
```
ab-av1 encode [OPTIONS] -i <INPUT> --crf <CRF> --preset <PRESET>
```
### Command: vmaf
Full VMAF score calculation, distorted file vs reference file.
Works with videos and images.
* Auto sets model version (4k or 1k) according to resolution.
* Auto sets _n_threads_ to system threads.
* Auto upscales lower resolution videos to the model.
```
ab-av1 vmaf --reference <REFERENCE> --distorted <DISTORTED>
```
### Command: xpsnr
Full XPSNR score calculation, distorted file vs reference file.
Works with videos and images.
```
ab-av1 xpsnr --reference <REFERENCE> --distorted <DISTORTED>
```
## Install
### Arch Linux
Available in the [AUR](https://aur.archlinux.org/packages/ab-av1).
### Linux
Pre-built statically linked x86_64-unknown-linux-musl binary included in the [latest release](https://github.com/alexheretic/ab-av1/releases/latest).
### Windows
Pre-built **ab-av1.exe** included in the [latest release](https://github.com/alexheretic/ab-av1/releases/latest).
### Using cargo
Latest release
```sh
cargo install ab-av1
```
Latest code direct from git
```sh
cargo install --git https://github.com/alexheretic/ab-av1
```
### Requirements
**ffmpeg** newer than git-2022-02-24 with libsvtav1, libvmaf, libopus enabled.
`ffmpeg` should be in `$PATH`.
## Debug
Enable debug logs by setting env var `RUST_LOG=ab_av1=debug`. This includes all ffmpeg calls.
```
$ RUST_LOG=ab_av1=debug ab-av1 auto-encode -i vid.mkv
```
## Minimum supported rust compiler
Maintained with [latest stable rust](https://gist.github.com/alexheretic/d1e98d8433b602e57f5d0a9637927e0c).
0707010000000B000081ED0000000000000000000000016828905A000000E9000000000000000000000000000000000000001500000000ab-av1-0.10.1/deploy#!/usr/bin/env bash
set -eu
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$dir"
cargo +nightly fmt -- --check
cargo build --release
cp "${CARGO_TARGET_DIR:-./target}/release/ab-av1" ~/bin/ab-av1
ls -lh ~/bin/ab-av1
0707010000000C000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000001200000000ab-av1-0.10.1/src0707010000000D000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000001A00000000ab-av1-0.10.1/src/command0707010000000E000081A40000000000000000000000016828905A000002D5000000000000000000000000000000000000001D00000000ab-av1-0.10.1/src/command.rspub mod args;
pub mod auto_encode;
pub mod crf_search;
pub mod encode;
pub mod print_completions;
pub mod sample_encode;
pub mod vmaf;
pub mod xpsnr;
pub use auto_encode::auto_encode;
pub use crf_search::crf_search;
pub use encode::encode;
pub use print_completions::print_completions;
pub use sample_encode::sample_encode;
pub use vmaf::vmaf;
pub use xpsnr::xpsnr;
const PROGRESS_CHARS: &str = "##-";
/// Helper trait for durations under 584942 years or so.
trait SmallDuration {
/// Returns the total number of whole microseconds.
fn as_micros_u64(&self) -> u64;
}
impl SmallDuration for std::time::Duration {
fn as_micros_u64(&self) -> u64 {
self.as_micros().try_into().unwrap_or(u64::MAX)
}
}
0707010000000F000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000001F00000000ab-av1-0.10.1/src/command/args07070100000010000081A40000000000000000000000016828905A0000116A000000000000000000000000000000000000002200000000ab-av1-0.10.1/src/command/args.rs//! Shared argument logic.
mod encode;
mod vmaf;
pub use encode::*;
pub use vmaf::*;
use crate::{command::encode::default_output_ext, ffprobe::Ffprobe};
use clap::{Parser, ValueHint};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
/// Encoding args that apply when encoding to an output.
#[derive(Parser, Clone)]
pub struct EncodeToOutput {
/// Output file, by default the same as input with `.av1` before the extension.
///
/// E.g. if unspecified: -i vid.mkv --> vid.av1.mkv
#[arg(short, long, value_hint = ValueHint::FilePath)]
pub output: Option<PathBuf>,
/// Set the output ffmpeg audio codec.
/// By default 'copy' is used. Otherwise, if re-encoding is necessary, 'libopus' is default.
///
/// See https://ffmpeg.org/ffmpeg.html#Audio-Options.
#[arg(long = "acodec")]
pub audio_codec: Option<String>,
/// Downmix input audio streams to stereo if input streams use greater than
/// 3 channels.
///
/// No effect if the input audio has 3 or fewer channels.
#[arg(long)]
pub downmix_to_stereo: bool,
/// Only process the main video stream, drop all other streams.
///
/// The output will be a single video stream.
#[arg(long)]
pub video_only: bool,
}
/// Sampling arguments.
#[derive(Parser, Clone)]
pub struct Sample {
/// Number of samples to use across the input video. Overrides --sample-every.
/// More samples take longer but may provide a more accurate result.
#[arg(long)]
pub samples: Option<u64>,
/// Calculate number of samples by dividing the input duration by this value.
/// So "12m" would mean with an input 25-36 minutes long, 3 samples would be used.
/// More samples take longer but may provide a more accurate result.
///
/// Setting --samples overrides this value.
#[arg(long, default_value = "12m", value_parser = humantime::parse_duration)]
pub sample_every: Duration,
/// Minimum number of samples. So at least this many samples will be used.
#[arg(long)]
pub min_samples: Option<u64>,
/// Duration of each sample.
#[arg(long, default_value = "20s", value_parser = humantime::parse_duration)]
pub sample_duration: Duration,
/// Keep temporary files after exiting.
#[arg(long)]
pub keep: bool,
/// Directory to store temporary sample data in.
/// Defaults to using the input's directory.
#[arg(long, env = "AB_AV1_TEMP_DIR", value_hint = ValueHint::DirPath)]
pub temp_dir: Option<PathBuf>,
/// Extension preference for encoded samples (ffmpeg encoder only).
#[arg(skip)]
pub extension: Option<Arc<str>>,
}
impl Sample {
/// Calculate the desired sample count using `samples` or `sample_every` & `min_samples`.
pub fn sample_count(&self, input_duration: Duration) -> u64 {
match self.samples {
Some(s) => s,
None => {
(input_duration.as_secs_f64() / self.sample_every.as_secs_f64().max(1.0)).ceil()
as _
}
}
.max(self.min_samples.unwrap_or(1))
.max(1)
}
pub fn set_extension_from_input(&mut self, input: &Path, encoder: &Encoder, probe: &Ffprobe) {
self.extension = Some(default_output_ext(input, encoder, probe.is_image).into());
}
pub fn set_extension_from_output(&mut self, output: &Path) {
self.extension = output.extension().and_then(|e| e.to_str().map(Into::into));
}
}
/// Args for when VMAF/XPSNR are used to score ref vs distorted.
#[derive(Debug, Parser, Clone, Hash)]
pub struct ScoreArgs {
/// Ffmpeg video filter applied to the VMAF/XPSNR reference before analysis.
/// E.g. --reference-vfilter "scale=1280:-1,fps=24".
///
/// Overrides --vfilter which would otherwise be used.
#[arg(long)]
pub reference_vfilter: Option<Arc<str>>,
}
/// Common xpsnr options.
#[derive(Debug, Parser, Clone, Copy)]
pub struct Xpsnr {
/// Frame rate override used to analyse both reference & distorted videos.
/// Maps to ffmpeg `-r` input arg.
///
/// Setting to 0 disables use.
#[arg(long, default_value_t = 60.0)]
pub xpsnr_fps: f32,
}
impl Xpsnr {
pub fn fps(&self) -> Option<f32> {
Some(self.xpsnr_fps).filter(|r| *r > 0.0)
}
}
impl std::hash::Hash for Xpsnr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.xpsnr_fps.to_ne_bytes().hash(state);
}
}
07070100000011000081A40000000000000000000000016828905A00005869000000000000000000000000000000000000002900000000ab-av1-0.10.1/src/command/args/encode.rsuse crate::{
ffmpeg::FfmpegEncodeArgs,
ffprobe::{Ffprobe, ProbeError},
float::TerseF32,
};
use anyhow::ensure;
use clap::{Parser, ValueHint};
use std::{
collections::HashMap,
fmt::{self, Write},
path::PathBuf,
sync::Arc,
time::Duration,
};
/// Common svt-av1/ffmpeg input encoding arguments.
#[derive(Parser, Clone)]
pub struct Encode {
/// Encoder override. See https://ffmpeg.org/ffmpeg-all.html#toc-Video-Encoders.
///
/// [possible values: libsvtav1, libx264, libx265, libvpx-vp9, ...]
#[arg(value_enum, short, long, default_value = "libsvtav1")]
pub encoder: Encoder,
/// Input video file.
#[arg(short, long, value_hint = ValueHint::FilePath)]
pub input: PathBuf,
/// Ffmpeg video filter applied to the input before encoding.
/// E.g. --vfilter "scale=1280:-1,fps=24".
///
/// See https://ffmpeg.org/ffmpeg-filters.html#Video-Filters
///
/// For VMAF calculations this is also applied to the reference video meaning VMAF
/// scores represent the quality of input stream *after* applying filters compared
/// to the encoded result.
/// This allows filters like cropping to work with VMAF, as it would be the
/// cropped stream that is VMAF compared to a cropped-then-encoded stream. Such filters
/// would not otherwise generally be comparable.
///
/// A consequence is the VMAF score will not reflect any quality lost
/// by the vfilter itself, only the encode.
/// To override the VMAF vfilter set --reference-vfilter.
#[arg(long)]
pub vfilter: Option<String>,
/// Pixel format. libsvtav1, libaom-av1 & librav1e default to yuv420p10le.
#[arg(value_enum, long)]
pub pix_format: Option<PixelFormat>,
/// Encoder preset (0-13).
/// Higher presets means faster encodes, but with a quality tradeoff.
///
/// For some ffmpeg encoders a word may be used, e.g. "fast".
/// libaom-av1 preset is mapped to equivalent -cpu-used argument.
///
/// [svt-av1 default: 8]
#[arg(long, allow_hyphen_values = true)]
pub preset: Option<Arc<str>>,
/// Interval between keyframes. Can be specified as a number of frames, or a duration.
/// E.g. "300" or "10s". Defaults to 10s if the input duration is over 3m.
///
/// Longer intervals can give better compression but make seeking more coarse.
/// Durations will be converted to frames using the input fps.
///
/// Works on svt-av1 & most ffmpeg encoders set with --encoder.
#[arg(long)]
pub keyint: Option<KeyInterval>,
/// Svt-av1 scene change detection, inserts keyframes at scene changes.
/// Defaults on if using default keyint & the input duration is over 3m. Otherwise off.
#[arg(long)]
pub scd: Option<bool>,
/// Additional svt-av1 arg(s). E.g. --svt mbr=2000 --svt film-grain=8
///
/// See https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/svt-av1_encoder_user_guide.md#options
#[arg(long = "svt", value_parser = parse_svt_arg)]
pub svt_args: Vec<Arc<str>>,
/// Additional ffmpeg encoder arg(s). E.g. `--enc x265-params=lossless=1`
/// These are added as ffmpeg output file options.
///
/// The first '=' symbol will be used to infer that this is an option with a value.
/// Passed to ffmpeg like "x265-params=lossless=1" -> ['-x265-params', 'lossless=1']
#[arg(long = "enc", allow_hyphen_values = true, value_parser = parse_enc_arg)]
pub enc_args: Vec<String>,
/// Additional ffmpeg input encoder arg(s). E.g. `--enc-input r=1`
/// These are added as ffmpeg input file options.
///
/// See --enc docs.
///
/// *_vaapi (e.g. h264_vaapi) encoder default:
/// `--enc-input hwaccel=vaapi --enc-input hwaccel_output_format=vaapi`.
///
/// *_vulkan encoder default: `--enc-input hwaccel=vulkan --enc-input hwaccel_output_format=vulkan`.
///
/// Disable defaults by setting them to "none"
/// e.g. `-enc-input hwaccel=none --enc-input hwaccel_output_format=none`
#[arg(long = "enc-input", allow_hyphen_values = true, value_parser = parse_enc_arg)]
pub enc_input_args: Vec<String>,
}
fn parse_svt_arg(arg: &str) -> anyhow::Result<Arc<str>> {
let arg = arg.trim_start_matches('-').to_owned();
for deny in ["crf", "preset", "keyint", "scd", "input-depth"] {
ensure!(!arg.starts_with(deny), "'{deny}' cannot be used here");
}
Ok(arg.into())
}
fn parse_enc_arg(arg: &str) -> anyhow::Result<String> {
let mut arg = arg.to_owned();
if !arg.starts_with('-') {
arg.insert(0, '-');
}
ensure!(
!arg.starts_with("-svtav1-params"),
"'svtav1-params' cannot be set here, use `--svt`"
);
Ok(arg)
}
impl Encode {
pub fn to_encoder_args(
&self,
crf: f32,
probe: &Ffprobe,
) -> anyhow::Result<FfmpegEncodeArgs<'_>> {
self.to_ffmpeg_args(crf, probe)
}
pub fn encode_hint(&self, crf: f32) -> String {
let Self {
encoder,
input,
vfilter,
preset,
pix_format,
keyint,
scd,
svt_args,
enc_args,
enc_input_args,
} = self;
let input = shell_escape::escape(input.display().to_string().into());
let mut hint = "ab-av1 encode".to_owned();
let vcodec = encoder.as_str();
if vcodec != "libsvtav1" {
write!(hint, " -e {vcodec}").unwrap();
}
write!(hint, " -i {input} --crf {}", TerseF32(crf)).unwrap();
if let Some(preset) = preset {
write!(hint, " --preset {preset}").unwrap();
}
if let Some(keyint) = keyint {
write!(hint, " --keyint {keyint}").unwrap();
}
if let Some(scd) = scd {
write!(hint, " --scd {scd}").unwrap();
}
if let Some(pix_fmt) = pix_format {
write!(hint, " --pix-format {pix_fmt}").unwrap();
}
if let Some(filter) = vfilter {
write!(hint, " --vfilter {filter:?}").unwrap();
}
for arg in svt_args {
write!(hint, " --svt {arg}").unwrap();
}
for arg in enc_input_args {
let arg = arg.trim_start_matches('-');
write!(hint, " --enc-input {arg}").unwrap();
}
for arg in enc_args {
let arg = arg.trim_start_matches('-');
write!(hint, " --enc {arg}").unwrap();
}
hint
}
fn to_ffmpeg_args(&self, crf: f32, probe: &Ffprobe) -> anyhow::Result<FfmpegEncodeArgs<'_>> {
let vcodec = &self.encoder.0;
let svtav1 = vcodec.as_ref() == "libsvtav1";
ensure!(
svtav1 || self.svt_args.is_empty(),
"--svt may only be used with svt-av1"
);
let preset = match &self.preset {
Some(n) => Some(n.clone()),
None if svtav1 => Some("8".into()),
None => None,
};
let keyint = self.keyint(probe)?;
let mut svtav1_params = vec![];
if svtav1 {
let scd = match (self.scd, self.keyint, keyint) {
(Some(true), ..) | (_, None, Some(_)) => 1,
_ => 0,
};
svtav1_params.push(format!("scd={scd}"));
// add all --svt args
svtav1_params.extend(self.svt_args.iter().map(|a| a.to_string()));
}
let mut args: Vec<Arc<String>> = self
.enc_args
.iter()
.flat_map(|arg| {
if let Some((opt, val)) = arg.split_once('=') {
if opt == "svtav1-params" {
svtav1_params.push(arg.clone());
vec![].into_iter()
} else {
vec![opt.to_owned().into(), val.to_owned().into()].into_iter()
}
} else {
vec![arg.clone().into()].into_iter()
}
})
.collect();
if !svtav1_params.is_empty() {
args.push("-svtav1-params".to_owned().into());
args.push(svtav1_params.join(":").into());
}
// Set keyint/-g for all vcodecs
if let Some(keyint) = keyint {
if !args.iter().any(|a| &**a == "-g") {
args.push("-g".to_owned().into());
args.push(keyint.to_string().into());
}
}
for (name, val) in self.encoder.default_ffmpeg_args() {
if !args.iter().any(|arg| &**arg == name) {
args.push(name.to_string().into());
args.push(val.to_string().into());
}
}
let pix_fmt = self.pix_format.or_else(|| match &**vcodec {
"libsvtav1" | "libaom-av1" | "librav1e" => Some(PixelFormat::Yuv420p10le),
_ => None,
});
let mut input_args: Vec<Arc<String>> = self
.enc_input_args
.iter()
.flat_map(|arg| {
if let Some((opt, val)) = arg.split_once('=') {
vec![opt.to_owned().into(), val.to_owned().into()].into_iter()
} else {
vec![arg.clone().into()].into_iter()
}
})
.collect();
for (name, val) in self.encoder.default_ffmpeg_input_args() {
if !input_args.iter().any(|arg| &**arg == name) {
input_args.push(name.to_string().into());
input_args.push(val.to_string().into());
}
}
// support setting possibly default args as "none" to omit them
for (name, _) in self.encoder.default_ffmpeg_input_args() {
if let Some(idx) = input_args
.windows(2)
.position(|w| *w[0] == *name && *w[1] == "none")
{
input_args.splice(idx..idx + 2, []);
}
}
// ban usage of the bits we already set via other args & logic
let input_reserved = HashMap::from([
("-i", ""),
("-y", ""),
("-n", ""),
("-pix_fmt", " use --pix-format"),
("-crf", ""),
("-preset", " use --preset"),
("-vf", " use --vfilter"),
("-filter:v", " use --vfilter"),
]);
for arg in &input_args {
if let Some(hint) = input_reserved.get(arg.as_str()) {
anyhow::bail!("Encoder argument `{arg}` not allowed{hint}");
}
}
let output_reserved = {
let mut r = input_reserved;
r.extend([
("-c:a", " use --acodec"),
("-codec:a", " use --acodec"),
("-acodec", " use --acodec"),
("-c:v", " use --encoder"),
("-c:v:0", " use --encoder"),
("-codec:v", " use --encoder"),
("-codec:v:0", " use --encoder"),
("-vcodec", " use --encoder"),
]);
r
};
for arg in &args {
if let Some(hint) = output_reserved.get(arg.as_str()) {
anyhow::bail!("Encoder argument `{arg}` not allowed{hint}");
}
}
Ok(FfmpegEncodeArgs {
input: &self.input,
vcodec: Arc::clone(vcodec),
pix_fmt,
vfilter: self.vfilter.as_deref(),
crf,
preset,
output_args: args,
input_args,
video_only: false,
})
}
fn keyint(&self, probe: &Ffprobe) -> anyhow::Result<Option<i32>> {
const KEYINT_DEFAULT_INPUT_MIN: Duration = Duration::from_secs(60 * 3);
const KEYINT_DEFAULT: Duration = Duration::from_secs(10);
let filter_fps = self.vfilter.as_deref().and_then(try_parse_fps_vfilter);
Ok(
match (self.keyint, &probe.duration, &probe.fps, filter_fps) {
// use the filter-fps if used, otherwise the input fps
(Some(ki), .., Some(fps)) => Some(ki.keyint_number(Ok(fps))?),
(Some(ki), _, fps, None) => Some(ki.keyint_number(fps.clone())?),
(None, Ok(duration), _, Some(fps)) if *duration >= KEYINT_DEFAULT_INPUT_MIN => {
Some(KeyInterval::Duration(KEYINT_DEFAULT).keyint_number(Ok(fps))?)
}
(None, Ok(duration), Ok(fps), None) if *duration >= KEYINT_DEFAULT_INPUT_MIN => {
Some(KeyInterval::Duration(KEYINT_DEFAULT).keyint_number(Ok(*fps))?)
}
_ => None,
},
)
}
}
/// Video codec for encoding.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Encoder(Arc<str>);
impl Encoder {
/// vcodec name that would work if you used it as the -e argument.
pub fn as_str(&self) -> &str {
&self.0
}
/// Returns default crf-increment.
///
/// Generally 0.1 if codec supports decimal crf.
pub fn default_crf_increment(&self) -> f32 {
match self.as_str() {
"libx264" | "libx265" => 0.1,
_ => 1.0,
}
}
pub fn default_min_crf(&self) -> f32 {
match self.as_str() {
"mpeg2video" => 2.0,
_ => 10.0,
}
}
pub fn default_max_crf(&self) -> f32 {
match self.as_str() {
"librav1e" | "av1_vaapi" => 255.0,
"libx264" | "libx265" => 46.0,
"mpeg2video" => 30.0,
// Works well for svt-av1
_ => 55.0,
}
}
pub fn default_image_ext(&self) -> &'static str {
match self.as_str() {
// ffmpeg doesn't currently have good heif support,
// these raw formats allow crf-search to work
"libx264" => "264",
"libx265" => "265",
// otherwise assume av1
_ => "avif",
}
}
/// Additional encoder specific ffmpeg arg defaults.
fn default_ffmpeg_args(&self) -> &[(&'static str, &'static str)] {
match self.as_str() {
// add `-b:v 0` for aom & vp9 to use "constant quality" mode
"libaom-av1" | "libvpx-vp9" => &[("-b:v", "0")],
// enable lookahead mode for qsv encoders
"av1_qsv" | "hevc_qsv" | "h264_qsv" => &[
("-look_ahead", "1"),
("-extbrc", "1"),
("-look_ahead_depth", "40"),
],
_ => &[],
}
}
/// Additional encoder specific ffmpeg input arg defaults.
fn default_ffmpeg_input_args(&self) -> &[(&'static str, &'static str)] {
match self.as_str() {
e if e.ends_with("_vaapi") => {
&[("-hwaccel", "vaapi"), ("-hwaccel_output_format", "vaapi")]
}
e if e.ends_with("_vulkan") => {
&[("-hwaccel", "vulkan"), ("-hwaccel_output_format", "vulkan")]
}
_ => <_>::default(),
}
}
}
impl std::str::FromStr for Encoder {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
Ok(match s {
// Support "svt-av1" alias for back compat
"svt-av1" => Self("libsvtav1".into()),
vcodec => Self(vcodec.into()),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyInterval {
Frames(i32),
Duration(Duration),
}
impl KeyInterval {
pub fn keyint_number(&self, fps: Result<f64, ProbeError>) -> Result<i32, ProbeError> {
Ok(match self {
Self::Frames(keyint) => *keyint,
Self::Duration(duration) => (duration.as_secs_f64() * fps?).round() as i32,
})
}
}
impl fmt::Display for KeyInterval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Frames(frames) => write!(f, "{frames}"),
Self::Duration(d) => write!(f, "{}", humantime::format_duration(*d)),
}
}
}
/// Parse as integer frames or a duration.
impl std::str::FromStr for KeyInterval {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let frame_err = match s.parse::<i32>() {
Ok(f) => return Ok(Self::Frames(f)),
Err(err) => err,
};
match humantime::parse_duration(s) {
Ok(d) => Ok(Self::Duration(d)),
Err(e) => Err(anyhow::anyhow!("frames: {frame_err}, duration: {e}")),
}
}
}
/// Ordered by ascending quality.
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[clap(rename_all = "lower")]
pub enum PixelFormat {
Yuv420p,
Yuv420p10le,
Yuv422p10le,
Yuv444p10le,
}
impl PixelFormat {
/// Returns the max quality pixel format, or None if both are None.
pub fn opt_max(a: Option<Self>, b: Option<Self>) -> Option<Self> {
match (a, b) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, b) => a.or(b),
}
}
}
#[test]
fn pixel_format_order() {
use PixelFormat::*;
assert!(Yuv420p < Yuv420p10le);
assert!(Yuv420p10le < Yuv422p10le);
assert!(Yuv422p10le < Yuv444p10le);
}
impl PixelFormat {
pub fn as_str(self) -> &'static str {
match self {
Self::Yuv420p10le => "yuv420p10le",
Self::Yuv422p10le => "yuv422p10le",
Self::Yuv444p10le => "yuv444p10le",
Self::Yuv420p => "yuv420p",
}
}
}
impl fmt::Display for PixelFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for PixelFormat {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"yuv420p10le" => Ok(Self::Yuv420p10le),
"yuv422p10le" => Ok(Self::Yuv422p10le),
"yuv444p10le" => Ok(Self::Yuv444p10le),
"yuv420p" => Ok(Self::Yuv420p),
_ => Err(()),
}
}
}
fn try_parse_fps_vfilter(vfilter: &str) -> Option<f64> {
let fps_filter = vfilter
.split(',')
.find_map(|vf| vf.trim().strip_prefix("fps="))?
.trim();
match fps_filter {
"ntsc" => Some(30000.0 / 1001.0),
"pal" => Some(25.0),
"film" => Some(24.0),
"ntsc_film" => Some(24000.0 / 1001.0),
_ => crate::ffprobe::parse_frame_rate(fps_filter),
}
}
#[test]
fn test_try_parse_fps_vfilter() {
let fps = try_parse_fps_vfilter("scale=1280:-1, fps=24, transpose=1").unwrap();
assert!((fps - 24.0).abs() < f64::EPSILON, "{fps:?}");
let fps = try_parse_fps_vfilter("scale=1280:-1, fps=ntsc, transpose=1").unwrap();
assert!((fps - 30000.0 / 1001.0).abs() < f64::EPSILON, "{fps:?}");
}
#[test]
fn frame_interval_from_str() {
use std::str::FromStr;
let from_300 = KeyInterval::from_str("300").unwrap();
assert_eq!(from_300, KeyInterval::Frames(300));
}
#[test]
fn duration_interval_from_str() {
use std::{str::FromStr, time::Duration};
let from_10s = KeyInterval::from_str("10s").unwrap();
assert_eq!(from_10s, KeyInterval::Duration(Duration::from_secs(10)));
}
/// Should use keyint & scd defaults for >3m inputs.
#[test]
fn svtav1_to_ffmpeg_args_default_over_3m() {
let enc = Encode {
encoder: Encoder("libsvtav1".into()),
input: "vid.mp4".into(),
vfilter: Some("scale=320:-1,fps=film".into()),
preset: None,
pix_format: None,
keyint: None,
scd: None,
svt_args: vec!["film-grain=30".into()],
enc_args: <_>::default(),
enc_input_args: <_>::default(),
};
let probe = Ffprobe {
duration: Ok(Duration::from_secs(300)),
has_audio: true,
max_audio_channels: None,
fps: Ok(30.0),
resolution: Some((1280, 720)),
is_image: false,
pix_fmt: None,
};
let FfmpegEncodeArgs {
input,
vcodec,
vfilter,
pix_fmt,
crf,
preset,
output_args,
input_args,
video_only,
} = enc.to_ffmpeg_args(32.0, &probe).expect("to_ffmpeg_args");
assert_eq!(&*vcodec, "libsvtav1");
assert_eq!(input, enc.input);
assert_eq!(vfilter, Some("scale=320:-1,fps=film"));
assert_eq!(crf, 32.0);
assert_eq!(preset, Some("8".into()));
assert_eq!(pix_fmt, Some(PixelFormat::Yuv420p10le));
assert!(!video_only);
assert!(
output_args
.windows(2)
.any(|w| w[0].as_str() == "-g" && w[1].as_str() == "240"),
"expected -g in {output_args:?}"
);
let svtargs_idx = output_args
.iter()
.position(|a| a.as_str() == "-svtav1-params")
.expect("missing -svtav1-params");
let svtargs = output_args
.get(svtargs_idx + 1)
.expect("missing -svtav1-params value")
.as_str();
assert_eq!(svtargs, "scd=1:film-grain=30");
assert!(input_args.is_empty());
}
#[test]
fn svtav1_to_ffmpeg_args_default_under_3m() {
let enc = Encode {
encoder: Encoder("libsvtav1".into()),
input: "vid.mp4".into(),
vfilter: None,
preset: Some("7".into()),
pix_format: Some(PixelFormat::Yuv420p),
keyint: None,
scd: None,
svt_args: vec![],
enc_args: <_>::default(),
enc_input_args: <_>::default(),
};
let probe = Ffprobe {
duration: Ok(Duration::from_secs(179)),
has_audio: true,
max_audio_channels: None,
fps: Ok(24.0),
resolution: Some((1280, 720)),
is_image: false,
pix_fmt: None,
};
let FfmpegEncodeArgs {
input,
vcodec,
vfilter,
pix_fmt,
crf,
preset,
output_args,
input_args,
video_only,
} = enc.to_ffmpeg_args(32.0, &probe).expect("to_ffmpeg_args");
assert_eq!(&*vcodec, "libsvtav1");
assert_eq!(input, enc.input);
assert_eq!(vfilter, None);
assert_eq!(crf, 32.0);
assert_eq!(preset, Some("7".into()));
assert_eq!(pix_fmt, Some(PixelFormat::Yuv420p));
assert!(!video_only);
assert!(
!output_args.iter().any(|a| a.as_str() == "-g"),
"unexpected -g in {output_args:?}"
);
let svtargs_idx = output_args
.iter()
.position(|a| a.as_str() == "-svtav1-params")
.expect("missing -svtav1-params");
let svtargs = output_args
.get(svtargs_idx + 1)
.expect("missing -svtav1-params value")
.as_str();
assert_eq!(svtargs, "scd=0");
assert!(input_args.is_empty());
}
07070100000012000081A40000000000000000000000016828905A00003460000000000000000000000000000000000000002700000000ab-av1-0.10.1/src/command/args/vmaf.rsuse crate::command::args::PixelFormat;
use anyhow::Context;
use clap::Parser;
use std::{borrow::Cow, fmt::Display, sync::Arc, thread};
const DEFAULT_VMAF_FPS: f32 = 25.0;
/// Common vmaf options.
#[derive(Debug, Parser, Clone)]
pub struct Vmaf {
/// Additional vmaf arg(s). E.g. --vmaf n_threads=8 --vmaf n_subsample=4
///
/// By default `n_threads` is set to available system threads.
///
/// Also see https://ffmpeg.org/ffmpeg-filters.html#libvmaf.
#[arg(long = "vmaf", value_parser = parse_vmaf_arg)]
pub vmaf_args: Vec<Arc<str>>,
/// Video resolution scale to use in VMAF analysis. If set, video streams will be bicubic
/// scaled to this during VMAF analysis. `auto` (default) automatically sets
/// based on the model and input video resolution. `none` disables any scaling.
/// `WxH` format may be used to specify custom scaling, e.g. `1920x1080`.
///
/// auto behaviour:
/// * 1k model (default for resolutions <= 2560x1440) if width and height
/// are less than 1728 & 972 respectively upscale to 1080p. Otherwise no scaling.
/// * 4k model (default for resolutions > 2560x1440) if width and height
/// are less than 3456 & 1944 respectively upscale to 4k. Otherwise no scaling.
///
/// The auto behaviour is based on the distorted video dimensions, equivalent
/// to post input/reference vfilter dimensions.
///
/// Scaling happens after any input/reference vfilters.
#[arg(long, default_value_t, value_parser = parse_vmaf_scale)]
pub vmaf_scale: VmafScale,
/// Frame rate override used to analyse both reference & distorted videos.
/// Maps to ffmpeg `-r` input arg.
///
/// Setting to 0 disables use.
#[arg(long, default_value_t = DEFAULT_VMAF_FPS)]
pub vmaf_fps: f32,
}
impl Default for Vmaf {
fn default() -> Self {
Self {
vmaf_args: <_>::default(),
vmaf_scale: <_>::default(),
vmaf_fps: DEFAULT_VMAF_FPS,
}
}
}
impl std::hash::Hash for Vmaf {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.vmaf_args.hash(state);
self.vmaf_scale.hash(state);
self.vmaf_fps.to_ne_bytes().hash(state);
}
}
fn parse_vmaf_arg(arg: &str) -> anyhow::Result<Arc<str>> {
Ok(arg.to_owned().into())
}
impl Vmaf {
pub fn fps(&self) -> Option<f32> {
Some(self.vmaf_fps).filter(|r| *r > 0.0)
}
/// Returns ffmpeg `filter_complex`/`lavfi` value for calculating vmaf.
pub fn ffmpeg_lavfi(
&self,
distorted_res: Option<(u32, u32)>,
pix_fmt: Option<PixelFormat>,
ref_vfilter: Option<&str>,
) -> String {
let mut args = self.vmaf_args.clone();
if !args.iter().any(|a| a.contains("n_threads")) {
// default n_threads to all cores
args.push(
format!(
"n_threads={}",
thread::available_parallelism().map_or(1, |p| p.get())
)
.into(),
);
}
let mut lavfi = args.join(":");
lavfi.insert_str(0, "libvmaf=shortest=true:ts_sync_mode=nearest:");
let mut model = VmafModel::from_args(&args);
if let (None, Some((w, h))) = (model, distorted_res) {
if w > 2560 && h > 1440 {
// for >2k resolutions use 4k model
lavfi.push_str(":model=version=vmaf_4k_v0.6.1");
model = Some(VmafModel::Vmaf4K);
}
}
let ref_vf: Cow<_> = match ref_vfilter {
None => "".into(),
Some(vf) if vf.ends_with(',') => vf.into(),
Some(vf) => format!("{vf},").into(),
};
let format = pix_fmt.map(|v| format!("format={v},")).unwrap_or_default();
let scale = self
.vf_scale(model.unwrap_or_default(), distorted_res)
.map(|(w, h)| format!("scale={w}:{h}:flags=bicubic,"))
.unwrap_or_default();
// prefix:
// * Add reference-vfilter if any
// * convert both streams to common pixel format
// * scale to vmaf width if necessary
// * sync presentation timestamp
let prefix = format!(
"[0:v]{format}{scale}setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]{format}{ref_vf}{scale}setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]"
);
lavfi.insert_str(0, &prefix);
lavfi
}
fn vf_scale(&self, model: VmafModel, distorted_res: Option<(u32, u32)>) -> Option<(i32, i32)> {
match (self.vmaf_scale, distorted_res) {
(VmafScale::Auto, Some((w, h))) => match model {
// upscale small resolutions to 1k for use with the 1k model
VmafModel::Vmaf1K if w < 1728 && h < 972 => {
Some(minimally_scale((w, h), (1920, 1080)))
}
// upscale small resolutions to 4k for use with the 4k model
VmafModel::Vmaf4K if w < 3456 && h < 1944 => {
Some(minimally_scale((w, h), (3840, 2160)))
}
_ => None,
},
(VmafScale::Custom { width, height }, Some((w, h))) => {
Some(minimally_scale((w, h), (width, height)))
}
(VmafScale::Custom { width, height }, None) => Some((width as _, height as _)),
_ => None,
}
}
}
/// Return the smallest ffmpeg vf `(w, h)` scale values so that at least one of the
/// `target_w` or `target_h` bounds are met.
fn minimally_scale((from_w, from_h): (u32, u32), (target_w, target_h): (u32, u32)) -> (i32, i32) {
let w_factor = from_w as f64 / target_w as f64;
let h_factor = from_h as f64 / target_h as f64;
if h_factor > w_factor {
(-1, target_h as _) // scale vertically
} else {
(target_w as _, -1) // scale horizontally
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VmafScale {
None,
#[default]
Auto,
Custom {
width: u32,
height: u32,
},
}
fn parse_vmaf_scale(vs: &str) -> anyhow::Result<VmafScale> {
const ERR: &str = "vmaf-scale must be 'none', 'auto' or WxH format e.g. '1920x1080'";
match vs {
"none" => Ok(VmafScale::None),
"auto" => Ok(VmafScale::Auto),
_ => {
let (w, h) = vs.split_once('x').context(ERR)?;
let (width, height) = (w.parse().context(ERR)?, h.parse().context(ERR)?);
Ok(VmafScale::Custom { width, height })
}
}
}
impl Display for VmafScale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => "none".fmt(f),
Self::Auto => "auto".fmt(f),
Self::Custom { width, height } => write!(f, "{width}x{height}"),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
enum VmafModel {
/// Default 1080p model.
#[default]
Vmaf1K,
/// 4k model.
Vmaf4K,
/// Some other user specified model.
Custom,
}
impl VmafModel {
fn from_args(args: &[Arc<str>]) -> Option<Self> {
let mut using_custom_model: Vec<_> = args.iter().filter(|v| v.contains("model")).collect();
match using_custom_model.len() {
0 => None,
1 => Some(match using_custom_model.remove(0) {
v if v.ends_with("version=vmaf_v0.6.1") => Self::Vmaf1K,
v if v.ends_with("version=vmaf_4k_v0.6.1") => Self::Vmaf4K,
_ => Self::Custom,
}),
_ => Some(Self::Custom),
}
}
}
#[test]
fn vmaf_lavfi() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
..<_>::default()
};
assert_eq!(
vmaf.ffmpeg_lavfi(
None,
Some(PixelFormat::Yuv420p),
Some("scale=1280:-1,fps=24")
),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,scale=1280:-1,fps=24,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads=5:n_subsample=4"
);
}
#[test]
fn vmaf_lavfi_default() {
let vmaf = Vmaf::default();
let expected = format!(
"[0:v]setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads={}",
thread::available_parallelism().map_or(1, |p| p.get())
);
assert_eq!(vmaf.ffmpeg_lavfi(None, None, None), expected);
}
#[test]
fn vmaf_lavfi_default_pix_fmt() {
let vmaf = Vmaf::default();
let expected = format!(
"[0:v]format=yuv420p10le,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p10le,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads={}",
thread::available_parallelism().map_or(1, |p| p.get())
);
assert_eq!(
vmaf.ffmpeg_lavfi(None, Some(PixelFormat::Yuv420p10le), None),
expected
);
}
#[test]
fn vmaf_lavfi_include_n_threads() {
let vmaf = Vmaf {
vmaf_args: vec!["log_path=output.xml".into()],
..<_>::default()
};
let expected = format!(
"[0:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:log_path=output.xml:n_threads={}",
thread::available_parallelism().map_or(1, |p| p.get())
);
assert_eq!(
vmaf.ffmpeg_lavfi(None, Some(PixelFormat::Yuv420p), None),
expected
);
}
/// Low resolution videos should be upscaled to 1080p
#[test]
fn vmaf_lavfi_small_width() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
..<_>::default()
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1280, 720)), Some(PixelFormat::Yuv420p), None),
"[0:v]format=yuv420p,scale=1920:-1:flags=bicubic,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,scale=1920:-1:flags=bicubic,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads=5:n_subsample=4"
);
}
/// 4k videos should use 4k model
#[test]
fn vmaf_lavfi_4k() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
..<_>::default()
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((3840, 2160)), Some(PixelFormat::Yuv420p), None),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads=5:n_subsample=4:model=version=vmaf_4k_v0.6.1"
);
}
/// >2k videos should be upscaled to 4k & use 4k model
#[test]
fn vmaf_lavfi_3k_upscale_to_4k() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into()],
..<_>::default()
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((3008, 1692)), Some(PixelFormat::Yuv420p), None),
"[0:v]format=yuv420p,scale=3840:-1:flags=bicubic,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,scale=3840:-1:flags=bicubic,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads=5:model=version=vmaf_4k_v0.6.1"
);
}
/// If user has overridden the model, don't default a vmaf width
#[test]
fn vmaf_lavfi_small_width_custom_model() {
let vmaf = Vmaf {
vmaf_args: vec![
"model=version=foo".into(),
"n_threads=5".into(),
"n_subsample=4".into(),
],
..<_>::default()
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1280, 720)), Some(PixelFormat::Yuv420p), None),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:model=version=foo:n_threads=5:n_subsample=4"
);
}
#[test]
fn vmaf_lavfi_custom_model_and_width() {
let vmaf = Vmaf {
vmaf_args: vec![
"model=version=foo".into(),
"n_threads=5".into(),
"n_subsample=4".into(),
],
// if specified just do it
vmaf_scale: VmafScale::Custom {
width: 123,
height: 720,
},
..<_>::default()
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1280, 720)), Some(PixelFormat::Yuv420p), None),
"[0:v]format=yuv420p,scale=123:-1:flags=bicubic,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,scale=123:-1:flags=bicubic,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:model=version=foo:n_threads=5:n_subsample=4"
);
}
#[test]
fn vmaf_lavfi_1080p() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
..<_>::default()
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1920, 1080)), Some(PixelFormat::Yuv420p), None),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[ref];\
[dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads=5:n_subsample=4"
);
}
07070100000013000081A40000000000000000000000016828905A00001909000000000000000000000000000000000000002900000000ab-av1-0.10.1/src/command/auto_encode.rsuse crate::{
command::{
PROGRESS_CHARS, args, crf_search,
encode::{self, default_output_name},
sample_encode::{self, Work},
},
console_ext::style,
ffprobe,
float::TerseF32,
temporary,
};
use anyhow::Context;
use clap::Parser;
use console::style;
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use std::{pin::pin, sync::Arc, time::Duration};
const BAR_LEN: u64 = 1024 * 1024 * 1024;
/// Automatically determine the best crf to deliver the min-vmaf and use it to encode a video or image.
///
/// Two phases:
/// * crf-search to determine the best --crf value
/// * ffmpeg & SvtAv1EncApp to encode using the settings
///
/// Use -v to print per-crf results.
/// Use -vv to print per-sample results.
#[derive(Parser)]
#[clap(verbatim_doc_comment)]
#[group(skip)]
pub struct Args {
#[clap(flatten)]
pub search: crf_search::Args,
#[clap(flatten)]
pub encode: args::EncodeToOutput,
}
pub async fn auto_encode(Args { mut search, encode }: Args) -> anyhow::Result<()> {
const SPINNER_RUNNING: &str = "{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})";
const SPINNER_FINISHED: &str =
"{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg})";
let defaulting_output = encode.output.is_none();
let input_probe = Arc::new(ffprobe::probe(&search.args.input));
let output = encode.output.unwrap_or_else(|| {
default_output_name(
&search.args.input,
&search.args.encoder,
input_probe.is_image,
)
});
search.sample.set_extension_from_output(&output);
let bar = ProgressBar::new(BAR_LEN).with_style(
ProgressStyle::default_bar()
.template(SPINNER_RUNNING)?
.progress_chars(PROGRESS_CHARS),
);
bar.enable_steady_tick(Duration::from_millis(100));
if defaulting_output {
let out = shell_escape::escape(output.display().to_string().into());
bar.println(style!("Encoding {out}").dim().to_string());
}
let min_score = search.min_score();
let max_encoded_percent = search.max_encoded_percent;
let enc_args = search.args.clone();
let thorough = search.thorough;
let verbose = search.verbose;
let mut crf_search = pin!(crf_search::run(search, input_probe.clone()));
let mut best = None;
while let Some(update) = crf_search.next().await {
match update {
Err(err) => {
if let crf_search::Error::NoGoodCrf { last } = &err {
// show last sample attempt in progress bar
bar.set_style(
ProgressStyle::default_bar()
.template(SPINNER_FINISHED)?
.progress_chars(PROGRESS_CHARS),
);
let mut vmaf = style(last.enc.score);
if last.enc.score < min_score {
vmaf = vmaf.red();
}
let mut percent = style!("{:.0}%", last.enc.encode_percent);
if last.enc.encode_percent > max_encoded_percent as _ {
percent = percent.red();
}
let score_kind = last.enc.score_kind;
bar.finish_with_message(format!("{score_kind} {vmaf:.2}, size {percent}"));
}
bar.finish();
return Err(err.into());
}
Ok(crf_search::Update::Status {
crf_run,
crf,
sample:
sample_encode::Status {
work,
fps,
progress,
sample,
samples,
full_pass,
},
}) => {
bar.set_position(crf_search::guess_progress(crf_run, progress, thorough) as _);
let crf = TerseF32(crf);
match full_pass {
true => bar.set_prefix(format!("crf {crf} full pass")),
false => bar.set_prefix(format!("crf {crf} {sample}/{samples}")),
}
let label = work.fps_label();
match work {
Work::Encode if fps <= 0.0 => bar.set_message("encoding, "),
_ if fps <= 0.0 => bar.set_message(format!("{label}, ")),
_ => bar.set_message(format!("{label} {fps} fps, ")),
}
}
Ok(crf_search::Update::SampleResult {
crf,
sample,
result,
}) => {
if verbose
.log_level()
.is_some_and(|lvl| lvl > log::Level::Warn)
{
result.print_attempt(&bar, sample, Some(crf))
}
}
Ok(crf_search::Update::RunResult(result)) => {
if verbose
.log_level()
.is_some_and(|lvl| lvl > log::Level::Error)
{
result.print_attempt(&bar, min_score, max_encoded_percent)
}
}
Ok(crf_search::Update::Done(result)) => best = Some(result),
}
}
let best = best.context("no crf-search best?")?;
bar.set_style(
ProgressStyle::default_bar()
.template(SPINNER_FINISHED)?
.progress_chars(PROGRESS_CHARS),
);
bar.finish_with_message(format!(
"{} {:.2}, size {}",
best.enc.score_kind,
style(best.enc.score).green(),
style(format!("{:.0}%", best.enc.encode_percent)).green(),
));
temporary::clean_all().await;
let bar = ProgressBar::new(12).with_style(
ProgressStyle::default_bar()
.template(SPINNER_RUNNING)?
.progress_chars(PROGRESS_CHARS),
);
bar.set_prefix("Encoding");
bar.enable_steady_tick(Duration::from_millis(100));
encode::run(
encode::Args {
args: enc_args,
crf: best.crf(),
encode: args::EncodeToOutput {
output: Some(output),
..encode
},
},
input_probe,
&bar,
)
.await
}
07070100000014000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000002500000000ab-av1-0.10.1/src/command/crf_search07070100000015000081A40000000000000000000000016828905A000044F1000000000000000000000000000000000000002800000000ab-av1-0.10.1/src/command/crf_search.rsmod err;
pub use err::Error;
use crate::{
command::{
PROGRESS_CHARS, args,
sample_encode::{self, Work},
},
console_ext::style,
ffprobe::{self, Ffprobe},
float::TerseF32,
};
use anyhow::Context;
use clap::{ArgAction, Parser};
use console::style;
use futures_util::{Stream, StreamExt};
use indicatif::{HumanBytes, HumanDuration, ProgressBar, ProgressStyle};
use log::info;
use std::{io::IsTerminal, pin::pin, sync::Arc, time::Duration};
const BAR_LEN: u64 = 1024 * 1024 * 1024;
const DEFAULT_MIN_VMAF: f32 = 95.0;
/// Interpolated binary search using sample-encode to find the best crf
/// value delivering min-vmaf & max-encoded-percent.
///
/// Outputs:
/// * Best crf value
/// * Mean sample VMAF score
/// * Predicted full encode size
/// * Predicted full encode time
///
/// Use -v to print per-sample results.
#[derive(Parser)]
#[clap(verbatim_doc_comment)]
#[group(skip)]
pub struct Args {
#[clap(flatten)]
pub args: args::Encode,
/// Desired min VMAF score to deliver.
///
/// [default: 95]
#[arg(long, group = "min_score")]
pub min_vmaf: Option<f32>,
/// Desired min XPSNR score to deliver.
///
/// Enables use of XPSNR for score analysis instead of VMAF.
#[arg(long, group = "min_score")]
pub min_xpsnr: Option<f32>,
/// Maximum desired encoded size percentage of the input size.
#[arg(long, default_value_t = 80.0)]
pub max_encoded_percent: f32,
/// Minimum (highest quality) crf value to try.
///
/// [default: 10, 2 for mpeg2video]
#[arg(long)]
pub min_crf: Option<f32>,
/// Maximum (lowest quality) crf value to try.
///
/// [default: 55, 46 for x264,x265, 255 for rav1e,av1_vaapi, 30 for mpeg2video]
#[arg(long)]
pub max_crf: Option<f32>,
/// Keep searching until a crf is found no more than min_vmaf+0.05 or all
/// possibilities have been attempted.
///
/// By default the "higher vmaf tolerance" increases with each attempt (0.1, 0.2, 0.4 etc...).
#[arg(long)]
pub thorough: bool,
/// Constant rate factor search increment precision.
///
/// [default: 1.0, 0.1 for x264,x265,vp9]
#[arg(long)]
pub crf_increment: Option<f32>,
/// Enable sample-encode caching.
#[arg(
long,
default_value_t = true,
env = "AB_AV1_CACHE",
action(ArgAction::Set)
)]
pub cache: bool,
#[clap(flatten)]
pub sample: args::Sample,
#[clap(flatten)]
pub vmaf: args::Vmaf,
#[clap(flatten)]
pub score: args::ScoreArgs,
#[clap(flatten)]
pub xpsnr: args::Xpsnr,
#[command(flatten)]
pub verbose: clap_verbosity_flag::Verbosity,
}
impl Args {
pub fn min_score(&self) -> f32 {
self.min_vmaf.or(self.min_xpsnr).unwrap_or(DEFAULT_MIN_VMAF)
}
}
pub async fn crf_search(mut args: Args) -> anyhow::Result<()> {
let bar = ProgressBar::new(BAR_LEN).with_style(
ProgressStyle::default_bar()
.template("{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})")?
.progress_chars(PROGRESS_CHARS)
);
bar.enable_steady_tick(Duration::from_millis(100));
let probe = ffprobe::probe(&args.args.input);
let input_is_image = probe.is_image;
args.sample
.set_extension_from_input(&args.args.input, &args.args.encoder, &probe);
let min_score = args.min_score();
let max_encoded_percent = args.max_encoded_percent;
let thorough = args.thorough;
let enc_args = args.args.clone();
let verbose = args.verbose;
let mut run = pin!(run(args, probe.into()));
while let Some(update) = run.next().await {
let update = update.inspect_err(|e| {
if let Error::NoGoodCrf { last } = e {
last.print_attempt(&bar, min_score, max_encoded_percent);
}
})?;
match update {
Update::Status {
crf_run,
crf,
sample:
sample_encode::Status {
work,
fps,
progress,
sample,
samples,
full_pass,
},
} => {
bar.set_position(guess_progress(crf_run, progress, thorough) as _);
let crf = TerseF32(crf);
match full_pass {
true => bar.set_prefix(format!("crf {crf} full pass")),
false => bar.set_prefix(format!("crf {crf} {sample}/{samples}")),
}
let label = work.fps_label();
match work {
Work::Encode if fps <= 0.0 => bar.set_message("encoding, "),
_ if fps <= 0.0 => bar.set_message(format!("{label}, ")),
_ => bar.set_message(format!("{label} {fps} fps, ")),
}
}
Update::SampleResult {
crf,
sample,
result,
} => {
if verbose
.log_level()
.is_some_and(|lvl| lvl > log::Level::Error)
{
result.print_attempt(&bar, sample, Some(crf))
}
}
Update::RunResult(result) => result.print_attempt(&bar, min_score, max_encoded_percent),
Update::Done(best) => {
info!("crf {} successful", best.crf());
bar.finish_with_message("");
if std::io::stderr().is_terminal() {
eprintln!(
"\n{} {}\n",
style("Encode with:").dim(),
style(enc_args.encode_hint(best.crf())).dim().italic(),
);
}
StdoutFormat::Human.print_result(&best, input_is_image);
return Ok(());
}
}
}
unreachable!()
}
pub fn run(
Args {
args,
min_vmaf,
min_xpsnr,
max_encoded_percent,
min_crf,
max_crf,
crf_increment,
thorough,
sample,
cache,
vmaf,
score,
xpsnr,
verbose: _,
}: Args,
input_probe: Arc<Ffprobe>,
) -> impl Stream<Item = Result<Update, Error>> {
async_stream::try_stream! {
let default_max_crf = args.encoder.default_max_crf();
let max_crf = max_crf.unwrap_or(default_max_crf);
let default_min_crf = args.encoder.default_min_crf();
let min_crf = min_crf.unwrap_or(default_min_crf);
Error::ensure_other(min_crf < max_crf, "Invalid --min-crf & --max-crf")?;
// by default use vmaf 95, otherwise use whatever is specified
let min_score = min_vmaf.or(min_xpsnr).unwrap_or(DEFAULT_MIN_VMAF);
// Whether to make the 2nd iteration on the ~20%/~80% crf point instead of the min/max to
// improve interpolation by narrowing the crf range a 20% (or 30%) subrange.
//
// 20/80% is preferred to 25/75% to account for searches in the "middle" benefitting from
// having both bounds computed after the 2nd iteration, whereas the two edges must compute
// the min/max crf on the 3rd iter.
//
// If a custom crf range is being used under half the default, this 2nd cut is not needed.
let cut_on_iter2 = (max_crf - min_crf) > (default_max_crf - default_min_crf) * 0.5;
let crf_increment = crf_increment
.unwrap_or_else(|| args.encoder.default_crf_increment())
.max(0.001);
let min_q = q_from_crf(min_crf, crf_increment);
let max_q = q_from_crf(max_crf, crf_increment);
let mut q: u64 = (min_q + max_q) / 2;
let mut args = sample_encode::Args {
args: args.clone(),
crf: 0.0,
sample: sample.clone(),
cache,
stdout_format: sample_encode::StdoutFormat::Json,
vmaf: vmaf.clone(),
score: score.clone(),
xpsnr: min_xpsnr.is_some(),
xpsnr_opts: xpsnr,
};
let mut crf_attempts = Vec::new();
for run in 1.. {
// how much we're prepared to go higher than the min-vmaf
let higher_tolerance = match thorough {
true => 0.05,
// increment 1.0 => +0.1, +0.2, +0.4, +0.8 ..
// increment 0.1 => +0.1, +0.1, +0.1, +0.16 ..
_ => (crf_increment * 2_f32.powi(run as i32 - 1) * 0.1).max(0.1),
};
args.crf = q.to_crf(crf_increment);
let mut sample_enc = pin!(sample_encode::run(args.clone(), input_probe.clone()));
let mut sample_enc_output = None;
while let Some(update) = sample_enc.next().await {
match update? {
sample_encode::Update::Status(status) => {
yield Update::Status { crf_run: run, crf: args.crf, sample: status };
}
sample_encode::Update::SampleResult { sample, result } => {
yield Update::SampleResult { crf: args.crf, sample, result };
}
sample_encode::Update::Done(output) => sample_enc_output = Some(output),
}
}
let sample = Sample {
crf_increment,
q,
enc: sample_enc_output.context("no sample output?")?,
};
crf_attempts.push(sample.clone());
let sample_small_enough = sample.enc.encode_percent <= max_encoded_percent as _;
if sample.enc.score > min_score {
// good
if sample_small_enough && sample.enc.score < min_score + higher_tolerance {
yield Update::Done(sample);
return;
}
let u_bound = crf_attempts
.iter()
.filter(|s| s.q > sample.q)
.min_by_key(|s| s.q);
match u_bound {
Some(upper) if upper.q == sample.q + 1 => {
Error::ensure_or_no_good_crf(sample_small_enough, &sample)?;
yield Update::Done(sample);
return;
}
Some(upper) => {
q = vmaf_lerp_q(min_score, upper, &sample);
}
None if sample.q == max_q => {
Error::ensure_or_no_good_crf(sample_small_enough, &sample)?;
yield Update::Done(sample);
return;
}
None if cut_on_iter2 && run == 1 && sample.q + 1 < max_q => {
q = (sample.q as f32 * 0.4 + max_q as f32 * 0.6).round() as _;
}
None => q = max_q,
};
} else {
// not good enough
if !sample_small_enough || sample.q == min_q {
Err(Error::NoGoodCrf { last: sample.clone() })?;
}
let l_bound = crf_attempts
.iter()
.filter(|s| s.q < sample.q)
.max_by_key(|s| s.q);
match l_bound {
Some(lower) if lower.q + 1 == sample.q => {
Error::ensure_or_no_good_crf(lower.enc.encode_percent <= max_encoded_percent as _, &sample)?;
yield Update::RunResult(sample.clone());
yield Update::Done(lower.clone());
return;
}
Some(lower) => {
q = vmaf_lerp_q(min_score, &sample, lower);
}
None if cut_on_iter2 && run == 1 && sample.q > min_q + 1 => {
q = (sample.q as f32 * 0.4 + min_q as f32 * 0.6).round() as _;
}
None => q = min_q,
};
}
yield Update::RunResult(sample.clone());
}
unreachable!();
}
}
#[derive(Debug, Clone)]
pub struct Sample {
pub enc: sample_encode::Output,
pub crf_increment: f32,
pub q: u64,
}
impl Sample {
pub fn crf(&self) -> f32 {
self.q.to_crf(self.crf_increment)
}
pub fn print_attempt(&self, bar: &ProgressBar, min_score: f32, max_encoded_percent: f32) {
if bar.is_hidden() {
info!(
"crf {} {} {:.2} ({:.0}%){}",
TerseF32(self.crf()),
self.enc.score_kind,
self.enc.score,
self.enc.encode_percent,
if self.enc.from_cache { " (cache)" } else { "" }
);
return;
}
let crf_label = style("- crf").dim();
let mut crf = style(TerseF32(self.crf()));
let vmaf_label = style(self.enc.score_kind).dim();
let mut vmaf = style(self.enc.score);
let mut percent = style!("{:.0}%", self.enc.encode_percent);
let open = style("(").dim();
let close = style(")").dim();
let cache_msg = match self.enc.from_cache {
true => style(" (cache)").dim(),
false => style(""),
};
if self.enc.score < min_score {
crf = crf.red().bright();
vmaf = vmaf.red().bright();
}
if self.enc.encode_percent > max_encoded_percent as _ {
crf = crf.red().bright();
percent = percent.red().bright();
}
bar.println(format!(
"{crf_label} {crf} {vmaf_label} {vmaf:.2} {open}{percent}{close}{cache_msg}"
));
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum StdoutFormat {
Human,
}
impl StdoutFormat {
fn print_result(self, sample: &Sample, image: bool) {
match self {
Self::Human => {
let crf = style(TerseF32(sample.crf())).bold().green();
let enc = &sample.enc;
let score = style(enc.score).bold().green();
let score_kind = enc.score_kind;
let size = style(HumanBytes(enc.predicted_encode_size)).bold().green();
let percent = style!("{}%", enc.encode_percent.round()).bold().green();
let time = style(HumanDuration(enc.predicted_encode_time)).bold();
let enc_description = match image {
true => "image",
false => "video stream",
};
println!(
"crf {crf} {score_kind} {score:.2} predicted {enc_description} size {size} ({percent}) taking {time}"
);
}
}
}
}
/// Produce a q value between given samples using vmaf score linear interpolation
/// so the output q value should produce the `min_vmaf`.
///
/// Note: `worse_q` will be a numerically higher q value (worse quality),
/// `better_q` a numerically lower q value (better quality).
///
/// # Issues
/// Crf values do not linearly map to VMAF changes (or anything?) so this is a flawed method,
/// though it seems to work better than a binary search.
/// Perhaps a better approximation of a general crf->vmaf model could be found.
/// This would be helpful particularly for small crf-increments.
fn vmaf_lerp_q(min_vmaf: f32, worse_q: &Sample, better_q: &Sample) -> u64 {
assert!(
worse_q.enc.score <= min_vmaf
&& worse_q.enc.score < better_q.enc.score
&& worse_q.q > better_q.q,
"invalid vmaf_lerp_crf usage: ({min_vmaf}, {worse_q:?}, {better_q:?})"
);
let vmaf_diff = better_q.enc.score - worse_q.enc.score;
let vmaf_factor = (min_vmaf - worse_q.enc.score) / vmaf_diff;
let q_diff = worse_q.q - better_q.q;
let lerp = (worse_q.q as f32 - q_diff as f32 * vmaf_factor).round() as u64;
lerp.clamp(better_q.q + 1, worse_q.q - 1)
}
/// sample_progress: [0, 1]
pub fn guess_progress(run: usize, sample_progress: f32, thorough: bool) -> f64 {
let total_runs_guess = match () {
// Guess 6 iterations for a "thorough" search
_ if thorough && run < 7 => 6.0,
// Guess 4 iterations initially
_ if run < 5 => 4.0,
// Otherwise guess next will work
_ => run as f64,
};
((run - 1) as f64 + sample_progress as f64) * BAR_LEN as f64 / total_runs_guess
}
/// Calculate "q" as a quality value integer multiple of crf.
///
/// * crf=33.5, inc=0.1 -> q=335
/// * crf=27, inc=1 -> q=27
#[inline]
fn q_from_crf(crf: f32, crf_increment: f32) -> u64 {
(f64::from(crf) / f64::from(crf_increment)).round() as _
}
trait QualityValue {
fn to_crf(self, crf_increment: f32) -> f32;
}
impl QualityValue for u64 {
#[inline]
fn to_crf(self, crf_increment: f32) -> f32 {
((self as f64) * f64::from(crf_increment)) as _
}
}
#[test]
fn q_crf_conversions() {
assert_eq!(q_from_crf(33.5, 0.1), 335);
assert_eq!(q_from_crf(27.0, 1.0), 27);
}
#[derive(Debug)]
pub enum Update {
Status {
/// run number starting from `1`.
crf_run: usize,
/// crf of this run
crf: f32,
sample: sample_encode::Status,
},
SampleResult {
crf: f32,
/// Sample number `1,....,n`
sample: u64,
result: sample_encode::EncodeResult,
},
/// Run result (excludes successful final runs)
RunResult(Sample),
Done(Sample),
}
07070100000016000081A40000000000000000000000016828905A0000045D000000000000000000000000000000000000002C00000000ab-av1-0.10.1/src/command/crf_search/err.rsuse crate::command::crf_search::Sample;
use std::fmt;
#[derive(Debug)]
pub enum Error {
NoGoodCrf { last: Sample },
Other(anyhow::Error),
}
impl Error {
pub fn ensure_other(condition: bool, reason: &'static str) -> Result<(), Self> {
if !condition {
return Err(Self::Other(anyhow::anyhow!(reason)));
}
Ok(())
}
pub fn ensure_or_no_good_crf(condition: bool, last: &Sample) -> Result<(), Self> {
if !condition {
return Err(Self::NoGoodCrf { last: last.clone() });
}
Ok(())
}
}
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Self {
Self::Other(err)
}
}
impl From<tokio::task::JoinError> for Error {
fn from(err: tokio::task::JoinError) -> Self {
Self::Other(err.into())
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoGoodCrf { .. } => "Failed to find a suitable crf".fmt(f),
Self::Other(err) => err.fmt(f),
}
}
}
impl std::error::Error for Error {}
07070100000017000081A40000000000000000000000016828905A0000155E000000000000000000000000000000000000002400000000ab-av1-0.10.1/src/command/encode.rsuse crate::{
command::{
PROGRESS_CHARS, SmallDuration,
args::{self, Encoder},
},
console_ext::style,
ffmpeg,
ffprobe::{self, Ffprobe},
log::ProgressLogger,
process::FfmpegOut,
temporary::{self, TempKind},
};
use clap::Parser;
use console::style;
use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
use log::info;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant},
};
use tokio::fs;
use tokio_stream::StreamExt;
/// Invoke ffmpeg to encode a video or image.
#[derive(Parser)]
#[group(skip)]
pub struct Args {
#[clap(flatten)]
pub args: args::Encode,
/// Encoder constant rate factor (1-63). Lower means better quality.
#[arg(long)]
pub crf: f32,
#[clap(flatten)]
pub encode: args::EncodeToOutput,
}
pub async fn encode(args: Args) -> anyhow::Result<()> {
let bar = ProgressBar::new(1).with_style(
ProgressStyle::default_bar()
.template("{spinner:.cyan.bold} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg}eta {eta})")?
.progress_chars(PROGRESS_CHARS)
);
bar.enable_steady_tick(Duration::from_millis(100));
let probe = ffprobe::probe(&args.args.input);
run(args, probe.into(), &bar).await
}
pub async fn run(
Args {
args,
crf,
encode:
args::EncodeToOutput {
output,
audio_codec,
downmix_to_stereo,
video_only,
},
}: Args,
probe: Arc<Ffprobe>,
bar: &ProgressBar,
) -> anyhow::Result<()> {
let defaulting_output = output.is_none();
// let probe = ffprobe::probe(&args.input);
let output =
output.unwrap_or_else(|| default_output_name(&args.input, &args.encoder, probe.is_image));
// output is temporary until encoding has completed successfully
temporary::add(&output, TempKind::NotKeepable);
if defaulting_output {
let out = shell_escape::escape(output.display().to_string().into());
bar.println(style!("Encoding {out}").dim().to_string());
}
bar.set_message("encoding, ");
let mut enc_args = args.to_encoder_args(crf, &probe)?;
enc_args.video_only = video_only;
let has_audio = probe.has_audio;
if let Ok(d) = &probe.duration {
bar.set_length(d.as_micros_u64().max(1));
}
// only downmix if achannels > 3
let stereo_downmix = downmix_to_stereo && probe.max_audio_channels.is_some_and(|c| c > 3);
let audio_codec = audio_codec.as_deref();
if stereo_downmix && audio_codec == Some("copy") {
anyhow::bail!("--stereo-downmix cannot be used with --acodec copy");
}
info!(
"encoding {}",
output.file_name().and_then(|n| n.to_str()).unwrap_or("")
);
let mut enc = ffmpeg::encode(enc_args, &output, has_audio, audio_codec, stereo_downmix)?;
let mut logger = ProgressLogger::new(module_path!(), Instant::now());
let mut stream_sizes = None;
while let Some(progress) = enc.next().await {
match progress? {
FfmpegOut::Progress { fps, time, .. } => {
if fps > 0.0 {
bar.set_message(format!("{fps} fps, "));
}
if let Ok(d) = &probe.duration {
bar.set_position(time.as_micros_u64());
logger.update(*d, time, fps);
}
}
FfmpegOut::StreamSizes {
video,
audio,
subtitle,
other,
} => stream_sizes = Some((video, audio, subtitle, other)),
}
}
enc.wait().await?; // ensure process has exited
bar.finish();
// successful encode, so don't delete it!
temporary::unadd(&output);
// print output info
let output_size = fs::metadata(&output).await?.len();
let output_percent = 100.0 * output_size as f64 / fs::metadata(&args.input).await?.len() as f64;
let output_size = style(HumanBytes(output_size)).dim().bold();
let output_percent = style!("{}%", output_percent.round()).dim().bold();
eprint!(
"{} {output_size} {}{output_percent}",
style("Encoded").dim(),
style("(").dim(),
);
if let Some((video, audio, subtitle, other)) = stream_sizes {
if audio > 0 || subtitle > 0 || other > 0 {
for (label, size) in [
("video:", video),
("audio:", audio),
("subs:", subtitle),
("other:", other),
] {
if size > 0 {
let size = style(HumanBytes(size)).dim();
eprint!("{} {}{size}", style(",").dim(), style(label).dim(),);
}
}
}
}
eprintln!("{}", style(")").dim());
Ok(())
}
/// * vid.mp4 -> "mp4"
/// * vid.??? -> "mkv"
/// * image.??? -> "avif"
pub fn default_output_ext(input: &Path, encoder: &Encoder, is_image: bool) -> &'static str {
if is_image {
return encoder.default_image_ext();
}
match input.extension().and_then(|e| e.to_str()) {
Some("mp4") => "mp4",
_ => "mkv",
}
}
/// E.g. vid.mkv -> "vid.av1.mkv"
pub fn default_output_name(input: &Path, encoder: &Encoder, is_image: bool) -> PathBuf {
let pre = ffmpeg::pre_extension_name(encoder.as_str());
let ext = default_output_ext(input, encoder, is_image);
input.with_extension(format!("{pre}.{ext}"))
}
07070100000018000081A40000000000000000000000016828905A000001A9000000000000000000000000000000000000002F00000000ab-av1-0.10.1/src/command/print_completions.rsuse clap::{CommandFactory, Parser};
use clap_complete::Shell;
/// Print shell completions.
#[derive(Parser)]
#[group(skip)]
pub struct Args {
/// Shell.
#[arg(value_enum, default_value_t = Shell::Bash)]
shell: Shell,
}
pub fn print_completions(Args { shell }: Args) {
clap_complete::generate(
shell,
&mut crate::Command::command(),
"ab-av1",
&mut std::io::stdout(),
);
}
07070100000019000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000002800000000ab-av1-0.10.1/src/command/sample_encode0707010000001A000081A40000000000000000000000016828905A00007069000000000000000000000000000000000000002B00000000ab-av1-0.10.1/src/command/sample_encode.rsmod cache;
use crate::{
command::{
PROGRESS_CHARS, SmallDuration,
args::{self, PixelFormat},
sample_encode::cache::ScoringInfo,
},
console_ext::style,
ffmpeg::{self, FfmpegEncodeArgs},
ffprobe::{self, Ffprobe},
log::ProgressLogger,
process::FfmpegOut,
sample, temporary,
vmaf::{self, VmafOut},
xpsnr::{self, XpsnrOut},
};
use anyhow::{Context, ensure};
use clap::{ArgAction, Parser};
use console::style;
use futures_util::Stream;
use indicatif::{HumanBytes, HumanDuration, ProgressBar, ProgressStyle};
use log::info;
use std::{
fmt::Display,
io::{self, IsTerminal},
path::{Path, PathBuf},
pin::pin,
sync::Arc,
time::{Duration, Instant},
};
use tokio::fs;
use tokio_stream::StreamExt;
/// Encode & analyse input samples to predict how a full encode would go.
/// This is much quicker than a full encode/vmaf run.
///
/// Outputs:
/// * Mean sample score
/// * Predicted full encode size
/// * Predicted full encode time
#[derive(Parser, Clone)]
#[clap(verbatim_doc_comment)]
#[group(skip)]
pub struct Args {
#[clap(flatten)]
pub args: args::Encode,
/// Encoder constant rate factor (1-63). Lower means better quality.
#[arg(long)]
pub crf: f32,
#[clap(flatten)]
pub sample: args::Sample,
/// Enable sample-encode caching.
#[arg(
long,
default_value_t = true,
env = "AB_AV1_CACHE",
action(ArgAction::Set)
)]
pub cache: bool,
/// Stdout message format `human` or `json`.
#[arg(long, value_enum, default_value_t = StdoutFormat::Human)]
pub stdout_format: StdoutFormat,
#[clap(flatten)]
pub vmaf: args::Vmaf,
#[clap(flatten)]
pub score: args::ScoreArgs,
#[clap(flatten)]
pub xpsnr_opts: args::Xpsnr,
/// Calculate a XPSNR score instead of VMAF.
#[arg(long)]
pub xpsnr: bool,
}
pub async fn sample_encode(mut args: Args) -> anyhow::Result<()> {
const BAR_LEN: u64 = 1024 * 1024 * 1024;
const BAR_LEN_F: f32 = BAR_LEN as _;
let bar = ProgressBar::new(BAR_LEN).with_style(
ProgressStyle::default_bar()
.template("{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})")?
.progress_chars(PROGRESS_CHARS)
);
bar.enable_steady_tick(Duration::from_millis(100));
let probe = ffprobe::probe(&args.args.input);
args.sample
.set_extension_from_input(&args.args.input, &args.args.encoder, &probe);
let enc_args = args.args.clone();
let crf = args.crf;
let stdout_fmt = args.stdout_format;
let input_is_image = probe.is_image;
let mut run = pin!(run(args, probe.into()));
while let Some(update) = run.next().await {
match update? {
Update::Status(Status {
work,
fps,
progress,
sample,
samples,
full_pass,
}) => {
match full_pass {
true => bar.set_prefix("Full pass"),
false => bar.set_prefix(format!("Sample {sample}/{samples}")),
}
let label = work.fps_label();
match work {
Work::Encode if fps <= 0.0 => bar.set_message("encoding, "),
_ if fps <= 0.0 => bar.set_message(format!("{label}, ")),
_ => bar.set_message(format!("{label} {fps} fps, ")),
}
bar.set_position((progress * BAR_LEN_F).round() as _);
}
Update::SampleResult { sample, result } => result.print_attempt(&bar, sample, None),
Update::Done(output) => {
bar.finish();
if io::stderr().is_terminal() {
eprintln!(
"\n{} {}\n",
style("Encode with:").dim(),
style(enc_args.encode_hint(crf)).dim().italic(),
);
}
stdout_fmt.print_result(&output, input_is_image);
}
}
}
Ok(())
}
pub fn run(
Args {
args,
crf,
sample: sample_args,
cache,
stdout_format: _,
vmaf,
score,
xpsnr,
xpsnr_opts,
}: Args,
input_probe: Arc<Ffprobe>,
) -> impl Stream<Item = anyhow::Result<Update>> {
async_stream::try_stream! {
let input = Arc::new(args.input.clone());
let input_pix_fmt = input_probe.pixel_format();
let input_is_image = input_probe.is_image;
let input_len = fs::metadata(&*input).await?.len();
let enc_args = args.to_encoder_args(crf, &input_probe)?;
let duration = input_probe.duration.clone()?;
let input_fps = input_probe.fps.clone()?;
let samples = sample_args.sample_count(duration).max(1);
let keep = sample_args.keep;
let temp_dir = sample_args.temp_dir;
let scoring = match xpsnr {
true => ScoringInfo::Xpsnr(&xpsnr_opts, &score),
_ => ScoringInfo::Vmaf(&vmaf, &score),
};
let (samples, sample_duration, full_pass) = {
if input_is_image {
(1, duration.max(Duration::from_secs(1)), true)
} else if sample_args.sample_duration.is_zero()
|| sample_args.sample_duration * samples as _ >= duration.mul_f64(0.85)
{
// if the sample time is most of the full input time just encode the whole thing
(1, duration, true)
} else {
let sample_duration = if input_fps > 0.0 {
// if sample-length is lower than a single frame use the frame time
let one_frame_duration = Duration::from_secs_f64(1.0 / input_fps);
sample_args.sample_duration.max(one_frame_duration)
} else {
sample_args.sample_duration
};
(samples, sample_duration, false)
}
};
let sample_duration_us = sample_duration.as_micros_u64();
// Start creating copy samples async, this is IO bound & not cpu intensive
let (tx, mut sample_tasks) = tokio::sync::mpsc::unbounded_channel();
let sample_temp = temp_dir.clone();
let sample_in = input.clone();
tokio::task::spawn_local(async move {
if full_pass {
// Use the entire video as a single sample
let _ = tx.send((0, Ok((sample_in.clone(), input_len))));
} else {
for sample_idx in 0..samples {
let sample = sample(
sample_in.clone(),
sample_idx,
samples,
sample_duration,
duration,
input_fps,
sample_temp.clone(),
)
.await;
if tx.send((sample_idx, sample)).is_err() {
break;
}
}
}
});
let mut results = Vec::new();
loop {
let (sample_idx, sample) = match sample_tasks.recv().await {
Some(s) => s,
None => break,
};
let sample_n = sample_idx + 1;
let (sample, sample_size) = sample?;
info!("encoding sample {sample_n}/{samples} crf {crf}");
yield Update::Status(Status {
work: Work::Encode,
fps: 0.0,
progress: sample_idx as f32 / samples as f32,
full_pass,
sample: sample_n,
samples,
});
// encode sample
let result = match cache::cached_encode(
cache,
&sample,
duration,
input.extension(),
input_len,
full_pass,
&enc_args,
scoring,
)
.await
{
(Some(result), _) => {
if samples > 1 {
result.log_attempt(sample_n, samples, crf);
}
result
}
(None, key) => {
let b = Instant::now();
let mut logger = ProgressLogger::new(module_path!(), b);
let (encoded_sample, mut output) = ffmpeg::encode_sample(
FfmpegEncodeArgs {
input: &sample,
..enc_args.clone()
},
temp_dir.clone(),
sample_args.extension.as_deref().unwrap_or("mkv"),
)?;
while let Some(enc_progress) = output.next().await {
if let FfmpegOut::Progress { time, fps, .. } = enc_progress? {
yield Update::Status(Status {
work: Work::Encode,
fps,
progress: (time.as_micros_u64() + sample_idx * sample_duration_us * 2) as f32
/ (sample_duration_us * samples * 2) as f32,
full_pass,
sample: sample_n,
samples,
});
logger.update(sample_duration, time, fps);
}
}
output.wait().await?; // ensure process has exited
let encode_time = b.elapsed();
let encoded_size = fs::metadata(&encoded_sample).await?.len();
let encoded_probe = ffprobe::probe(&encoded_sample);
let result = match scoring {
ScoringInfo::Vmaf(..) => {
yield Update::Status(Status {
work: Work::Score(ScoreKind::Vmaf),
fps: 0.0,
progress: (sample_idx as f32 + 0.5) / samples as f32,
full_pass,
sample: sample_n,
samples,
});
let vmaf = vmaf::run(
&sample,
&encoded_sample,
&vmaf.ffmpeg_lavfi(
encoded_probe.resolution,
PixelFormat::opt_max(enc_args.pix_fmt, input_pix_fmt),
score.reference_vfilter.as_deref().or(args.vfilter.as_deref()),
),
vmaf.fps(),
)?;
let mut vmaf = pin!(vmaf);
let mut logger = ProgressLogger::new("ab_av1::vmaf", Instant::now());
let mut vmaf_score = None;
while let Some(vmaf) = vmaf.next().await {
match vmaf {
VmafOut::Done(score) => {
vmaf_score = Some(score);
break;
}
VmafOut::Progress(FfmpegOut::Progress { time, fps, .. }) => {
yield Update::Status(Status {
work: Work::Score(ScoreKind::Vmaf),
fps,
progress: (sample_duration_us +
time.as_micros_u64() +
sample_idx * sample_duration_us * 2) as f32
/ (sample_duration_us * samples * 2) as f32,
full_pass,
sample: sample_n,
samples,
});
logger.update(sample_duration, time, fps);
}
VmafOut::Progress(_) => {}
VmafOut::Err(e) => Err(e)?,
}
}
EncodeResult {
score: vmaf_score.context("no vmaf score")?,
score_kind: ScoreKind::Vmaf,
sample_size,
encoded_size,
encode_time,
sample_duration: encoded_probe
.duration
.ok()
.filter(|d| !d.is_zero())
.unwrap_or(sample_duration),
from_cache: false,
}
}
ScoringInfo::Xpsnr(..) => {
yield Update::Status(Status {
work: Work::Score(ScoreKind::Xpsnr),
fps: 0.0,
progress: (sample_idx as f32 + 0.5) / samples as f32,
full_pass,
sample: sample_n,
samples,
});
let lavfi = super::xpsnr::lavfi(
score.reference_vfilter.as_deref().or(args.vfilter.as_deref())
);
let xpsnr_out = xpsnr::run(&sample, &encoded_sample, &lavfi, xpsnr_opts.fps())?;
let mut xpsnr_out = pin!(xpsnr_out);
let mut logger = ProgressLogger::new("ab_av1::xpsnr", Instant::now());
let mut score = None;
while let Some(next) = xpsnr_out.next().await {
match next {
XpsnrOut::Done(s) => {
score = Some(s);
break;
}
XpsnrOut::Progress(FfmpegOut::Progress { time, fps, .. }) => {
yield Update::Status(Status {
work: Work::Score(ScoreKind::Xpsnr),
fps,
progress: (sample_duration_us +
time.as_micros_u64() +
sample_idx * sample_duration_us * 2) as f32
/ (sample_duration_us * samples * 2) as f32,
full_pass,
sample: sample_n,
samples,
});
logger.update(sample_duration, time, fps);
}
XpsnrOut::Progress(_) => {}
XpsnrOut::Err(e) => Err(e)?,
}
}
EncodeResult {
score: score.context("no xpsnr score")?,
score_kind: ScoreKind::Xpsnr,
sample_size,
encoded_size,
encode_time,
sample_duration: encoded_probe
.duration
.ok()
.filter(|d| !d.is_zero())
.unwrap_or(sample_duration),
from_cache: false,
}
}
};
if samples > 1 {
result.log_attempt(sample_n, samples, crf);
}
if let Some(k) = key {
cache::cache_result(k, &result).await?;
}
// Early clean. Note: Avoid cleaning copy samples
temporary::clean(true).await;
if !keep {
let _ = tokio::fs::remove_file(encoded_sample).await;
}
result
}
};
results.push(result.clone());
yield Update::SampleResult { sample: sample_n, result };
}
let score_kind = results.score_kind();
let output = Output {
score: results.mean_score(),
score_kind,
// Using file size * encode_percent can over-estimate. However, if it ends up less
// than the duration estimation it may turn out to be more accurate.
predicted_encode_size: results
.estimate_encode_size_by_duration(duration, full_pass)
.min(estimate_encode_size_by_file_percent(&results, &input, full_pass).await?),
encode_percent: results.encoded_percent_size(),
predicted_encode_time: results.estimate_encode_time(duration, full_pass),
from_cache: results.iter().all(|r| r.from_cache),
};
info!(
"crf {crf} {score_kind} {:.2} predicted video stream size {} ({:.0}%) taking {}{}",
output.score,
HumanBytes(output.predicted_encode_size),
output.encode_percent,
HumanDuration(output.predicted_encode_time),
if output.from_cache { " (cache)" } else { "" }
);
yield Update::Done(output);
}
}
/// Copy a sample from the input to the temp_dir (or input dir).
async fn sample(
input: Arc<PathBuf>,
sample_idx: u64,
samples: u64,
sample_duration: Duration,
duration: Duration,
fps: f64,
temp_dir: Option<PathBuf>,
) -> anyhow::Result<(Arc<PathBuf>, u64)> {
let sample_n = sample_idx + 1;
let sample_start = (duration.saturating_sub(sample_duration * samples as _)
/ (samples as u32 + 1))
* sample_n as _
+ sample_duration * sample_idx as _;
let sample_frames = ((sample_duration.as_secs_f64() * fps).round() as u32).max(1);
let floor_to_sec = sample_duration >= Duration::from_secs(2);
let sample = sample::copy(&input, sample_start, floor_to_sec, sample_frames, temp_dir).await?;
let sample_size = fs::metadata(&sample).await?.len();
ensure!(
// ffmpeg copy may fail successfully and give us a small/empty output
sample_size > 1024,
"ffmpeg copy failed: encoded sample too small"
);
Ok((sample.into(), sample_size))
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EncodeResult {
pub sample_size: u64,
pub encoded_size: u64,
pub score: f32,
pub score_kind: ScoreKind,
pub encode_time: Duration,
/// Duration of the sample.
///
/// This should be close to `SAMPLE_SIZE` but may deviate due to how samples are cut.
pub sample_duration: Duration,
/// Result read from cache.
pub from_cache: bool,
}
impl EncodeResult {
pub fn print_attempt(&self, bar: &ProgressBar, sample_n: u64, crf: Option<f32>) {
let Self {
sample_size,
encoded_size,
score,
score_kind,
from_cache,
..
} = self;
bar.println(
style!(
"- {}Sample {sample_n} ({:.0}%) {score_kind} {score:.2}{}",
crf.map(|crf| format!("crf {crf}: ")).unwrap_or_default(),
100.0 * *encoded_size as f32 / *sample_size as f32,
if *from_cache { " (cache)" } else { "" },
)
.dim()
.to_string(),
);
}
pub fn log_attempt(&self, sample_n: u64, samples: u64, crf: f32) {
let Self {
sample_size,
encoded_size,
score,
score_kind,
from_cache,
..
} = self;
info!(
"sample {sample_n}/{samples} crf {crf} {score_kind} {score:.2} ({:.0}%){}",
100.0 * *encoded_size as f32 / *sample_size as f32,
if *from_cache { " (cache)" } else { "" }
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ScoreKind {
Vmaf,
Xpsnr,
}
impl ScoreKind {
/// Display label for fps in progress bar.
pub fn fps_label(&self) -> &'static str {
match self {
Self::Vmaf => "vmaf",
Self::Xpsnr => "xpsnr",
}
}
/// General display name.
pub fn display_str(&self) -> &'static str {
match self {
Self::Vmaf => "VMAF",
Self::Xpsnr => "XPSNR",
}
}
}
impl Display for ScoreKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.display_str())
}
}
trait EncodeResults {
fn encoded_percent_size(&self) -> f64;
fn score_kind(&self) -> ScoreKind;
fn mean_score(&self) -> f32;
/// Return estimated encoded **video stream** size by multiplying sample size by duration.
fn estimate_encode_size_by_duration(
&self,
input_duration: Duration,
single_full_pass: bool,
) -> u64;
fn estimate_encode_time(&self, input_duration: Duration, single_full_pass: bool) -> Duration;
}
impl EncodeResults for Vec<EncodeResult> {
fn encoded_percent_size(&self) -> f64 {
if self.is_empty() {
return 100.0;
}
let encoded = self.iter().map(|r| r.encoded_size).sum::<u64>() as f64;
let sample = self.iter().map(|r| r.sample_size).sum::<u64>() as f64;
encoded * 100.0 / sample
}
fn score_kind(&self) -> ScoreKind {
self.first()
.map(|r| r.score_kind)
.unwrap_or(ScoreKind::Vmaf)
}
fn mean_score(&self) -> f32 {
if self.is_empty() {
return 0.0;
}
self.iter().map(|r| r.score).sum::<f32>() / self.len() as f32
}
fn estimate_encode_size_by_duration(
&self,
input_duration: Duration,
single_full_pass: bool,
) -> u64 {
if self.is_empty() {
return 0;
}
if single_full_pass {
return self[0].encoded_size;
}
let sample_duration: Duration = self.iter().map(|s| s.sample_duration).sum();
let sample_factor = input_duration.as_secs_f64() / sample_duration.as_secs_f64();
let sample_encode_size: f64 = self.iter().map(|r| r.encoded_size as f64).sum();
(sample_encode_size * sample_factor).round() as _
}
fn estimate_encode_time(&self, input_duration: Duration, single_full_pass: bool) -> Duration {
if self.is_empty() {
return Duration::ZERO;
}
if single_full_pass {
return self[0].encode_time;
}
let sample_duration: Duration = self.iter().map(|s| s.sample_duration).sum();
let sample_factor = input_duration.as_secs_f64() / sample_duration.as_secs_f64();
let sample_encode_time: Duration = self.iter().map(|r| r.encode_time).sum();
let estimate = sample_encode_time.mul_f64(sample_factor);
if estimate < Duration::from_secs(1) {
estimate
} else {
Duration::from_secs(estimate.as_secs())
}
}
}
/// Return estimated encoded **video stream** size by applying the sample percentage
/// change to the input file size.
///
/// This can over-estimate the larger the non-video proportion of the input.
async fn estimate_encode_size_by_file_percent(
results: &Vec<EncodeResult>,
input: &Path,
single_full_pass: bool,
) -> anyhow::Result<u64> {
if results.is_empty() {
return Ok(0);
}
if single_full_pass {
return Ok(results[0].encoded_size);
}
let encode_proportion = results.encoded_percent_size() / 100.0;
Ok((fs::metadata(input).await?.len() as f64 * encode_proportion).round() as _)
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum StdoutFormat {
Human,
Json,
}
impl StdoutFormat {
fn print_result(
self,
Output {
score,
score_kind,
predicted_encode_size,
encode_percent,
predicted_encode_time,
from_cache: _,
}: &Output,
image: bool,
) {
match self {
Self::Human => {
let score = match (*score, score_kind) {
(v, ScoreKind::Vmaf) if v >= 95.0 => style(v).bold().green(),
(v, ScoreKind::Vmaf) if v < 80.0 => style(v).bold().red(),
(v, _) => style(v).bold(),
};
let percent = encode_percent.round();
let size = match *predicted_encode_size {
v if percent < 80.0 => style(HumanBytes(v)).bold().green(),
v if percent >= 100.0 => style(HumanBytes(v)).bold().red(),
v => style(HumanBytes(v)).bold(),
};
let percent = match percent {
v if v < 80.0 => style!("{}%", v).bold().green(),
v if v >= 100.0 => style!("{}%", v).bold().red(),
v => style!("{}%", v).bold(),
};
let time = style(HumanDuration(*predicted_encode_time)).bold();
let enc_description = match image {
true => "image",
false => "video stream",
};
println!(
"{score_kind} {score:.2} predicted {enc_description} size {size} ({percent}) taking {time}"
);
}
Self::Json => {
let mut json = serde_json::json!({
"predicted_encode_size": predicted_encode_size,
"predicted_encode_percent": encode_percent,
"predicted_encode_seconds": predicted_encode_time.as_secs(),
});
match score_kind {
ScoreKind::Vmaf => json["vmaf"] = (*score).into(),
ScoreKind::Xpsnr => json["xpsnr"] = (*score).into(),
}
println!("{json}");
}
}
}
}
/// Sample encode result.
#[derive(Debug, Clone)]
pub struct Output {
/// Sample mean score.
pub score: f32,
pub score_kind: ScoreKind,
/// Estimated full encoded **video stream** size.
///
/// Encoded sample size multiplied by duration.
pub predicted_encode_size: u64,
/// Sample mean encoded percentage.
pub encode_percent: f64,
/// Estimated full encode time.
///
/// Sample encode time multiplied by duration.
pub predicted_encode_time: Duration,
/// All sample results were read from the cache.
pub from_cache: bool,
}
/// Kinds of sample-encode work.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Work {
#[default]
Encode,
Score(ScoreKind),
}
impl Work {
/// Display label for fps in progress bar.
pub fn fps_label(&self) -> &'static str {
match self {
Self::Encode => "enc",
Self::Score(kind) => kind.fps_label(),
}
}
}
#[derive(Debug)]
pub struct Status {
/// Kind of work being performed
pub work: Work,
/// fps, `0.0` may be interpreted as "unknown"
pub fps: f32,
/// sample progress `[0, 1]`
pub progress: f32,
/// Sample number `1,....,n`
pub sample: u64,
/// Total samples
pub samples: u64,
/// Encoding the entire input video
pub full_pass: bool,
}
#[derive(Debug)]
pub enum Update {
Status(Status),
SampleResult {
/// Sample number `1,....,n`
sample: u64,
result: EncodeResult,
},
Done(Output),
}
0707010000001B000081A40000000000000000000000016828905A00000DF2000000000000000000000000000000000000003100000000ab-av1-0.10.1/src/command/sample_encode/cache.rs//! _sample-encode_ file system caching logic.
use crate::{
command::args::{ScoreArgs, Vmaf, Xpsnr},
ffmpeg::FfmpegEncodeArgs,
};
use anyhow::Context;
use std::{
ffi::OsStr,
hash::Hash,
path::Path,
time::{Duration, Instant},
};
/// Return a previous stored encode result for the same sample & args.
#[allow(clippy::too_many_arguments)]
pub async fn cached_encode(
cache: bool,
sample: &Path,
input_duration: Duration,
input_extension: Option<&OsStr>,
input_size: u64,
full_pass: bool,
enc_args: &FfmpegEncodeArgs<'_>,
scoring: ScoringInfo<'_>,
) -> (Option<super::EncodeResult>, Option<Key>) {
if !cache {
return (None, None);
}
let hash = hash_encode(
// hashing the sample file name (which includes input name, frames & start)
// + input duration, extension & size should be reasonably unique for an input.
// and is much faster than hashing the entire file.
(
sample.file_name(),
input_duration,
input_extension,
input_size,
full_pass,
),
enc_args,
scoring,
);
let key = Key(hash);
let cached = tokio::task::spawn_blocking::<_, anyhow::Result<_>>(move || {
let db = open_db()?;
Ok(match db.get(key.0.to_hex().as_bytes())? {
Some(data) => Some(serde_json::from_slice::<super::EncodeResult>(&data)?),
None => None,
})
})
.await
.context("db.get task failed")
.and_then(|r| r);
match cached {
Ok(Some(mut result)) => {
result.from_cache = true;
(Some(result), Some(key))
}
Ok(None) => (None, Some(key)),
Err(err) => {
eprintln!("cache error: {err}");
(None, None)
}
}
}
#[derive(Debug, Hash, Clone, Copy)]
pub enum ScoringInfo<'a> {
Vmaf(&'a Vmaf, &'a ScoreArgs),
Xpsnr(&'a Xpsnr, &'a ScoreArgs),
}
pub async fn cache_result(key: Key, result: &super::EncodeResult) -> anyhow::Result<()> {
let data = serde_json::to_vec(result)?;
let insert = tokio::task::spawn_blocking(move || {
let db = open_db()?;
db.insert(key.0.to_hex().as_bytes(), data)?;
db.flush()
})
.await
.context("db.insert task failed")
.and_then(|r| Ok(r?));
if let Err(err) = insert {
eprintln!("cache error: {err}")
}
Ok(())
}
fn open_db() -> sled::Result<sled::Db> {
const LOCK_MAX_WAIT: Duration = Duration::from_secs(2);
let mut path = dirs::cache_dir().expect("no cache dir found");
path.push("ab-av1");
path.push("sample-encode-cache");
let a = Instant::now();
let mut db = sled::open(&path);
while db.is_err() && a.elapsed() < LOCK_MAX_WAIT {
std::thread::yield_now();
db = sled::open(&path);
}
db
}
#[derive(Debug, Clone, Copy)]
pub struct Key(blake3::Hash);
fn hash_encode(
input_info: impl Hash,
enc_args: &FfmpegEncodeArgs<'_>,
scoring_info: impl Hash,
) -> blake3::Hash {
let mut hasher = blake3::Hasher::new();
let mut std_hasher = BlakeStdHasher(&mut hasher);
input_info.hash(&mut std_hasher);
enc_args.sample_encode_hash(&mut std_hasher);
scoring_info.hash(&mut std_hasher);
hasher.finalize()
}
struct BlakeStdHasher<'a>(&'a mut blake3::Hasher);
impl std::hash::Hasher for BlakeStdHasher<'_> {
fn finish(&self) -> u64 {
unimplemented!()
}
#[inline]
fn write(&mut self, bytes: &[u8]) {
self.0.update(bytes);
}
}
0707010000001C000081A40000000000000000000000016828905A00000BEA000000000000000000000000000000000000002200000000ab-av1-0.10.1/src/command/vmaf.rsuse crate::{
command::{
PROGRESS_CHARS,
args::{self, PixelFormat},
},
ffprobe,
log::ProgressLogger,
process::FfmpegOut,
vmaf::{self, VmafOut},
};
use anyhow::Context;
use clap::Parser;
use indicatif::{ProgressBar, ProgressStyle};
use std::{
path::PathBuf,
pin::pin,
time::{Duration, Instant},
};
use tokio_stream::StreamExt;
/// Full VMAF score calculation, distorted file vs reference file.
/// Works with videos and images.
///
/// * Auto sets model version (4k or 1k) according to resolution.
/// * Auto sets `n_threads` to system threads.
/// * Auto upscales lower resolution videos to the model.
#[derive(Parser)]
#[clap(verbatim_doc_comment)]
#[group(skip)]
pub struct Args {
/// Reference video file.
#[arg(long)]
pub reference: PathBuf,
/// Re-encoded/distorted video file.
#[arg(long)]
pub distorted: PathBuf,
#[clap(flatten)]
pub vmaf: args::Vmaf,
#[clap(flatten)]
pub score: args::ScoreArgs,
}
pub async fn vmaf(
Args {
reference,
distorted,
vmaf,
score,
}: Args,
) -> anyhow::Result<()> {
let bar = ProgressBar::new(1).with_style(
ProgressStyle::default_bar()
.template("{spinner:.cyan.bold} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg}eta {eta})")?
.progress_chars(PROGRESS_CHARS)
);
bar.enable_steady_tick(Duration::from_millis(100));
bar.set_message("vmaf running, ");
let dprobe = ffprobe::probe(&distorted);
let rprobe = ffprobe::probe(&reference);
let nframes = dprobe.nframes().or_else(|_| rprobe.nframes());
let duration = dprobe.duration.as_ref().or(rprobe.duration.as_ref());
if let Ok(nframes) = nframes {
bar.set_length(nframes);
}
let mut vmaf = pin!(vmaf::run(
&reference,
&distorted,
&vmaf.ffmpeg_lavfi(
dprobe.resolution,
PixelFormat::opt_max(dprobe.pixel_format(), rprobe.pixel_format()),
score.reference_vfilter.as_deref(),
),
vmaf.fps(),
)?);
let mut logger = ProgressLogger::new(module_path!(), Instant::now());
let mut vmaf_score = None;
while let Some(vmaf) = vmaf.next().await {
match vmaf {
VmafOut::Done(score) => {
vmaf_score = Some(score);
break;
}
VmafOut::Progress(FfmpegOut::Progress {
frame, fps, time, ..
}) => {
if fps > 0.0 {
bar.set_message(format!("vmaf {fps} fps, "));
}
if nframes.is_ok() {
bar.set_position(frame);
}
if let Ok(total) = duration {
logger.update(*total, time, fps);
}
}
VmafOut::Progress(FfmpegOut::StreamSizes { .. }) => {}
VmafOut::Err(e) => return Err(e),
}
}
bar.finish();
println!("{}", vmaf_score.context("no vmaf score")?);
Ok(())
}
0707010000001D000081A40000000000000000000000016828905A00000CC3000000000000000000000000000000000000002300000000ab-av1-0.10.1/src/command/xpsnr.rsuse crate::{
command::{PROGRESS_CHARS, args},
ffprobe,
log::ProgressLogger,
process::FfmpegOut,
xpsnr::{self, XpsnrOut},
};
use anyhow::Context;
use clap::Parser;
use indicatif::{ProgressBar, ProgressStyle};
use std::{
borrow::Cow,
path::PathBuf,
pin::pin,
sync::LazyLock,
time::{Duration, Instant},
};
use tokio_stream::StreamExt;
/// Full XPSNR score calculation, distorted file vs reference file.
/// Works with videos and images.
#[derive(Parser)]
#[clap(verbatim_doc_comment)]
#[group(skip)]
pub struct Args {
/// Reference video file.
#[arg(long)]
pub reference: PathBuf,
/// Re-encoded/distorted video file.
#[arg(long)]
pub distorted: PathBuf,
#[clap(flatten)]
pub score: args::ScoreArgs,
#[clap(flatten)]
pub xpsnr: args::Xpsnr,
}
pub async fn xpsnr(
Args {
reference,
distorted,
score,
xpsnr,
}: Args,
) -> anyhow::Result<()> {
let bar = ProgressBar::new(1).with_style(
ProgressStyle::default_bar()
.template("{spinner:.cyan.bold} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg}eta {eta})")?
.progress_chars(PROGRESS_CHARS)
);
bar.enable_steady_tick(Duration::from_millis(100));
bar.set_message("xpsnr running, ");
let dprobe = ffprobe::probe(&distorted);
let rprobe = LazyLock::new(|| ffprobe::probe(&reference));
let nframes = dprobe.nframes().or_else(|_| rprobe.nframes());
let duration = dprobe
.duration
.as_ref()
.or_else(|_| rprobe.duration.as_ref());
if let Ok(nframes) = nframes {
bar.set_length(nframes);
}
let mut xpsnr_out = pin!(xpsnr::run(
&reference,
&distorted,
&lavfi(score.reference_vfilter.as_deref()),
xpsnr.fps(),
)?);
let mut logger = ProgressLogger::new(module_path!(), Instant::now());
let mut score = None;
while let Some(next) = xpsnr_out.next().await {
match next {
XpsnrOut::Done(s) => {
score = Some(s);
break;
}
XpsnrOut::Progress(FfmpegOut::Progress {
frame, fps, time, ..
}) => {
if fps > 0.0 {
bar.set_message(format!("xpsnr {fps} fps, "));
}
if nframes.is_ok() {
bar.set_position(frame);
}
if let Ok(total) = duration {
logger.update(*total, time, fps);
}
}
XpsnrOut::Progress(FfmpegOut::StreamSizes { .. }) => {}
XpsnrOut::Err(e) => return Err(e),
}
}
bar.finish();
println!("{}", score.context("no xpsnr score")?);
Ok(())
}
pub fn lavfi(ref_vfilter: Option<&str>) -> Cow<'static, str> {
match ref_vfilter {
None => "xpsnr=stats_file=-".into(),
Some(vf) => format!("[0:v]{vf}[ref];[ref][1:v]xpsnr=stats_file=-").into(),
}
}
#[test]
fn test_lavfi_default() {
assert_eq!(lavfi(None), "xpsnr=stats_file=-");
}
#[test]
fn test_lavfi_ref_vfilter() {
assert_eq!(
lavfi(Some("scale=1280:-1")),
"[0:v]scale=1280:-1[ref];\
[ref][1:v]xpsnr=stats_file=-"
);
}
0707010000001E000081A40000000000000000000000016828905A00000070000000000000000000000000000000000000002100000000ab-av1-0.10.1/src/console_ext.rsmacro_rules! style {
($($x:tt)*) => {
console::style(format!($($x)*))
}
}
pub(crate) use style;
0707010000001F000081A40000000000000000000000016828905A00001C87000000000000000000000000000000000000001C00000000ab-av1-0.10.1/src/ffmpeg.rs//! ffmpeg encoding logic
use crate::{
command::args::PixelFormat,
float::TerseF32,
process::{CommandExt, FfmpegOut, FfmpegOutStream},
temporary::{self, TempKind},
};
use anyhow::Context;
use log::debug;
use std::{
collections::HashSet,
fmt::Write,
hash::{Hash, Hasher},
path::{Path, PathBuf},
process::Stdio,
sync::{Arc, LazyLock},
};
use tokio::process::Command;
/// Exposed ffmpeg encoding args.
#[derive(Debug, Clone)]
pub struct FfmpegEncodeArgs<'a> {
pub input: &'a Path,
pub vcodec: Arc<str>,
pub vfilter: Option<&'a str>,
pub pix_fmt: Option<PixelFormat>,
pub crf: f32,
pub preset: Option<Arc<str>>,
pub output_args: Vec<Arc<String>>,
pub input_args: Vec<Arc<String>>,
pub video_only: bool,
}
impl FfmpegEncodeArgs<'_> {
pub fn sample_encode_hash(&self, state: &mut impl Hasher) {
static SVT_AV1_V: LazyLock<Vec<u8>> = LazyLock::new(|| {
std::process::Command::new("SvtAv1EncApp")
.arg("--version")
.output()
.map(|o| o.stdout)
.unwrap_or_default()
});
// hashing svt-av1 version means new encoder releases will avoid old cache data
if &*self.vcodec == "libsvtav1" {
SVT_AV1_V.hash(state);
}
// input not relevant to sample encoding
self.vcodec.hash(state);
self.vfilter.hash(state);
self.pix_fmt.hash(state);
self.crf.to_bits().hash(state);
self.preset.hash(state);
self.output_args.hash(state);
self.input_args.hash(state);
}
}
/// Encode a sample.
pub fn encode_sample(
FfmpegEncodeArgs {
input,
vcodec,
vfilter,
pix_fmt,
crf,
preset,
output_args,
input_args,
video_only: _,
}: FfmpegEncodeArgs,
temp_dir: Option<PathBuf>,
dest_ext: &str,
) -> anyhow::Result<(PathBuf, FfmpegOutStream)> {
let pre = pre_extension_name(&vcodec);
let crf_str = format!("{}", TerseF32(crf)).replace('.', "_");
let dest_file_name = match &preset {
Some(p) => input.with_extension(format!("{pre}.crf{crf_str}.{p}.{dest_ext}")),
None => input.with_extension(format!("{pre}.crf{crf_str}.{dest_ext}")),
};
let dest_file_name = dest_file_name.file_name().unwrap();
let mut dest = temporary::process_dir(temp_dir);
dest.push(dest_file_name);
temporary::add(&dest, TempKind::Keepable);
let mut cmd = Command::new("ffmpeg");
cmd.kill_on_drop(true)
.arg("-y")
.args(input_args.iter().map(|a| &**a))
.arg2("-i", input)
.arg2("-c:v", &*vcodec)
.args(output_args.iter().map(|a| &**a))
.arg2(vcodec.crf_arg(), crf)
.arg2_opt("-pix_fmt", pix_fmt.map(|v| v.as_str()))
.arg2_opt(vcodec.preset_arg(), preset)
.arg2_opt("-vf", vfilter)
.arg("-an")
.arg(&dest)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
let cmd_str = cmd.to_cmd_str();
debug!("cmd `{cmd_str}`");
let enc = cmd.spawn().context("ffmpeg encode_sample")?;
let stream = FfmpegOut::stream(enc, "ffmpeg encode_sample", cmd_str);
Ok((dest, stream))
}
/// Encode to output.
pub fn encode(
FfmpegEncodeArgs {
input,
vcodec,
vfilter,
pix_fmt,
crf,
preset,
output_args,
input_args,
video_only,
}: FfmpegEncodeArgs,
output: &Path,
has_audio: bool,
audio_codec: Option<&str>,
downmix_to_stereo: bool,
) -> anyhow::Result<FfmpegOutStream> {
let oargs: HashSet<_> = output_args.iter().map(|a| a.as_str()).collect();
let output_ext = output.extension().and_then(|e| e.to_str());
let add_faststart = output_ext == Some("mp4") && !oargs.contains("-movflags");
let matroska = matches!(output_ext, Some("mkv") | Some("webm"));
let add_cues_to_front = matroska && !oargs.contains("-cues_to_front");
let audio_codec = audio_codec.unwrap_or(if downmix_to_stereo && has_audio {
"libopus"
} else {
"copy"
});
let set_ba_128k = audio_codec == "libopus" && !oargs.contains("-b:a");
let downmix_to_stereo = downmix_to_stereo && !oargs.contains("-ac");
let map = match video_only {
true => "0:v:0",
false => "0",
};
// This doesn't seem to work on .mp4 files
let mut metadata = format!(
"AB_AV1_FFMPEG_ARGS=-c:v {vcodec} {} {crf}",
vcodec.crf_arg()
);
if let Some(preset) = &preset {
write!(&mut metadata, " {} {preset}", vcodec.preset_arg()).unwrap();
}
let mut cmd = Command::new("ffmpeg");
cmd.kill_on_drop(true)
.args(input_args.iter().map(|a| &**a))
.arg("-y")
.arg2("-i", input)
.arg2("-map", map)
.arg2("-c:v", "copy")
.arg2("-c:v:0", &*vcodec)
.arg2("-metadata", metadata)
.arg2("-c:a", audio_codec)
.arg2("-c:s", "copy")
.args(output_args.iter().map(|a| &**a))
.arg2(vcodec.crf_arg(), crf)
.arg2_opt("-pix_fmt", pix_fmt.map(|v| v.as_str()))
.arg2_opt(vcodec.preset_arg(), preset)
.arg2_opt("-vf", vfilter)
.arg_if(matroska, "-dn") // "Only audio, video, and subtitles are supported for Matroska"
.arg2_if(downmix_to_stereo, "-ac", 2)
.arg2_if(set_ba_128k, "-b:a", "128k")
.arg2_if(add_faststart, "-movflags", "+faststart")
.arg2_if(add_cues_to_front, "-cues_to_front", "y")
.arg(output)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
let cmd_str = cmd.to_cmd_str();
debug!("cmd `{cmd_str}`");
let enc = cmd.spawn().context("ffmpeg encode")?;
Ok(FfmpegOut::stream(enc, "ffmpeg encode", cmd_str))
}
pub fn pre_extension_name(vcodec: &str) -> &str {
match vcodec.strip_prefix("lib").filter(|s| !s.is_empty()) {
Some("svtav1") => "av1",
Some("vpx-vp9") => "vp9",
Some(suffix) => suffix,
_ => vcodec,
}
}
trait VCodecSpecific {
/// Arg to use preset values with, normally `-preset`.
fn preset_arg(&self) -> &str;
/// Arg to use crf values with, normally `-crf`.
fn crf_arg(&self) -> &str;
}
impl VCodecSpecific for Arc<str> {
fn preset_arg(&self) -> &str {
match &**self {
"libaom-av1" | "libvpx-vp9" => "-cpu-used",
"librav1e" => "-speed",
_ => "-preset",
}
}
fn crf_arg(&self) -> &str {
// use crf-like args to support encoders that don't have crf
match &**self {
// https://ffmpeg.org//ffmpeg-codecs.html#librav1e
// https://github.com/fraunhoferhhi/vvenc/wiki/FFmpeg-Integration#fix-qp-mode-constant-quality-mode
"librav1e" | "libvvenc" => "-qp",
"mpeg2video" => "-q",
// https://ffmpeg.org//ffmpeg-codecs.html#VAAPI-encoders
e if e.ends_with("_vaapi") => "-q",
e if e.ends_with("_vulkan") => "-qp",
e if e.ends_with("_nvenc") => "-cq",
// https://ffmpeg.org//ffmpeg-codecs.html#QSV-Encoders
e if e.ends_with("_qsv") => "-global_quality",
_ => "-crf",
}
}
}
07070100000020000081A40000000000000000000000016828905A000012E8000000000000000000000000000000000000001D00000000ab-av1-0.10.1/src/ffprobe.rs//! ffprobe logic
use crate::command::args::PixelFormat;
use anyhow::{Context, anyhow};
use std::{fmt, fs::File, io::Read, path::Path, time::Duration};
pub struct Ffprobe {
/// Duration of video.
pub duration: Result<Duration, ProbeError>,
/// The video has audio stream(s).
pub has_audio: bool,
/// Audio number of channels (if multiple channel the highest).
pub max_audio_channels: Option<i64>,
/// Video frame rate.
pub fps: Result<f64, ProbeError>,
pub resolution: Option<(u32, u32)>,
pub is_image: bool,
pub pix_fmt: Option<String>,
}
impl Ffprobe {
pub fn pixel_format(&self) -> Option<PixelFormat> {
let pf = self.pix_fmt.as_deref()?;
PixelFormat::try_from(pf).ok()
}
pub fn nframes(&self) -> Result<u64, ProbeError> {
match (&self.fps, &self.duration) {
(Ok(fps), Ok(duration)) => {
let frames = (fps * duration.as_secs_f64()).round();
if frames.is_normal() && frames.is_sign_positive() {
Ok(frames as _)
} else {
Err(ProbeError(format!("Invalid nframes {frames}")))
}
}
(Err(e), _) | (_, Err(e)) => Err(e.clone()),
}
}
}
/// Try to ffprobe the given input.
pub fn probe(input: &Path) -> Ffprobe {
let is_image = is_image(input).unwrap_or(false);
let probe = match ffprobe::ffprobe(input) {
Ok(p) => p,
Err(err) => {
return Ffprobe {
duration: Err(ProbeError(format!("ffprobe: {err}"))),
fps: Err(ProbeError(format!("ffprobe: {err}"))),
has_audio: true,
max_audio_channels: None,
resolution: None,
is_image: false,
pix_fmt: None,
};
}
};
let fps = read_fps(&probe);
let duration = read_duration(&probe);
let has_audio = probe
.streams
.iter()
.any(|s| s.codec_type.as_deref() == Some("audio"));
let max_audio_channels = probe
.streams
.iter()
.filter(|s| s.codec_type.as_deref() == Some("audio"))
.filter_map(|a| a.channels)
.max();
let resolution = probe
.streams
.iter()
.filter(|s| s.codec_type.as_deref() == Some("video"))
.find_map(|s| {
let w = s.width.and_then(|w| u32::try_from(w).ok())?;
let h = s.height.and_then(|w| u32::try_from(w).ok())?;
Some((w, h))
});
let pix_fmt = probe
.streams
.into_iter()
.filter(|s| s.codec_type.as_deref() == Some("video"))
.find_map(|s| s.pix_fmt);
Ffprobe {
duration: duration.map_err(ProbeError::from),
fps: fps.map_err(ProbeError::from),
has_audio,
max_audio_channels,
resolution,
is_image,
pix_fmt,
}
}
fn is_image(path: &Path) -> anyhow::Result<bool> {
let file = File::open(path)?;
let mut file_header = Vec::with_capacity(8192);
file.take(8192).read_to_end(&mut file_header)?;
Ok(infer::is_image(&file_header))
}
fn read_duration(probe: &ffprobe::FfProbe) -> anyhow::Result<Duration> {
match probe.format.duration.as_deref() {
Some(duration_s) => {
let duration_f = duration_s
.parse::<f64>()
.with_context(|| format!("invalid ffprobe video duration: {duration_s:?}"))?;
Duration::try_from_secs_f64(duration_f)
.map_err(|e| anyhow!("{e}: ffprobe video duration: {duration_s:?}"))
}
None => Ok(Duration::ZERO),
}
}
fn read_fps(probe: &ffprobe::FfProbe) -> anyhow::Result<f64> {
let vstream = probe
.streams
.iter()
.find(|s| s.codec_type.as_deref() == Some("video"))
.context("no video stream found")?;
parse_frame_rate(&vstream.avg_frame_rate)
.or_else(|| parse_frame_rate(&vstream.r_frame_rate))
.context("invalid ffprobe video frame rate")
}
/// parse "x/y" or float strings.
pub fn parse_frame_rate(rate: &str) -> Option<f64> {
if let Some((x, y)) = rate.split_once('/') {
let x: f64 = x.parse().ok()?;
let y: f64 = y.parse().ok()?;
if x <= 0.0 || y <= 0.0 {
return None;
}
Some(x / y)
} else {
rate.parse()
.ok()
.filter(|f: &f64| f.is_finite() && *f > 0.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProbeError(String);
impl fmt::Display for ProbeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<anyhow::Error> for ProbeError {
fn from(err: anyhow::Error) -> Self {
Self(format!("{err}"))
}
}
impl std::error::Error for ProbeError {}
07070100000021000081A40000000000000000000000016828905A00000287000000000000000000000000000000000000001B00000000ab-av1-0.10.1/src/float.rs/// f32 wrapper that displays minimal decimal places.
#[derive(Debug, Clone, Copy)]
pub struct TerseF32(pub f32);
impl std::fmt::Display for TerseF32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if pseudo_int(self.0.into()) {
write!(f, "{:.0}", self.0)
} else if pseudo_int(f64::from(self.0) * 10.0) {
write!(f, "{:.1}", self.0)
} else if pseudo_int(f64::from(self.0) * 100.0) {
write!(f, "{:.2}", self.0)
} else {
self.0.fmt(f)
}
}
}
#[inline]
fn pseudo_int(f: f64) -> bool {
!(0.0002..=0.9998).contains(&f.fract())
}
07070100000022000081A40000000000000000000000016828905A0000066A000000000000000000000000000000000000001900000000ab-av1-0.10.1/src/log.rsuse indicatif::HumanDuration;
use log::{Level, info, log_enabled};
use std::time::{Duration, Instant};
/// Struct that info logs progress messages on a stream action like encoding.
#[derive(Debug)]
pub struct ProgressLogger {
target: &'static str,
start: Instant,
log_count: u32,
}
impl ProgressLogger {
pub fn new(target: &'static str, start: Instant) -> Self {
Self {
target,
start,
log_count: 0,
}
}
/// Update and potentially log progress on a stream action.
/// * `total` total duration of the stream
/// * `complete` the duration that has been completed at this time
/// * `fps` frames per second
pub fn update(&mut self, total: Duration, completed: Duration, fps: f32) {
if log_enabled!(Level::Info) && completed > Duration::ZERO {
let done = completed.as_secs_f64() / total.as_secs_f64();
let elapsed = self.start.elapsed();
let before_count = self.log_count;
while elapsed > self.next_log() {
self.log_count += 1;
}
if before_count == self.log_count {
return;
}
let eta = Duration::from_secs_f64(elapsed.as_secs_f64() / done).saturating_sub(elapsed);
info!(
target: self.target,
"{:.0}%, {fps} fps, eta {}",
done * 100.0,
HumanDuration(eta)
);
}
}
/// First log after >=16s, then >=32s etc
fn next_log(&self) -> Duration {
Duration::from_secs(2_u64.pow(self.log_count + 4))
}
}
07070100000023000081A40000000000000000000000016828905A00000A40000000000000000000000000000000000000001A00000000ab-av1-0.10.1/src/main.rsmod command;
mod console_ext;
mod ffmpeg;
mod ffprobe;
mod float;
mod log;
mod process;
mod sample;
mod temporary;
mod vmaf;
mod xpsnr;
use ::log::LevelFilter;
use anyhow::anyhow;
use clap::Parser;
use futures_util::FutureExt;
use std::io::IsTerminal;
use tokio::signal;
#[derive(Parser)]
#[command(version, about)]
enum Command {
SampleEncode(command::sample_encode::Args),
Vmaf(command::vmaf::Args),
Xpsnr(command::xpsnr::Args),
Encode(command::encode::Args),
CrfSearch(command::crf_search::Args),
AutoEncode(command::auto_encode::Args),
PrintCompletions(command::print_completions::Args),
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
env_logger::builder()
.filter_module(
"ab_av1",
match std::io::stderr().is_terminal() {
true => LevelFilter::Off,
false => LevelFilter::Info,
},
)
.parse_default_env()
.init();
let action = Command::parse();
let keep = action.keep_temp_files();
let local = tokio::task::LocalSet::new();
let command = local.run_until(match action {
Command::SampleEncode(args) => command::sample_encode(args).boxed_local(),
Command::Vmaf(args) => command::vmaf(args).boxed_local(),
Command::Xpsnr(args) => command::xpsnr(args).boxed_local(),
Command::Encode(args) => command::encode(args).boxed_local(),
Command::CrfSearch(args) => command::crf_search(args).boxed_local(),
Command::AutoEncode(args) => command::auto_encode(args).boxed_local(),
Command::PrintCompletions(args) => return command::print_completions(args),
});
let out = tokio::select! {
r = command => r,
_ = signal::ctrl_c() => Err(anyhow!("ctrl_c")),
};
drop(local);
crate::process::child::wait().await;
// Final cleanup. Samples are already deleted (if wished by the user) during `command::sample_encode::run`.
temporary::clean(keep).await;
if let Err(err) = out {
eprintln!("Error: {err}");
std::process::exit(1);
}
}
impl Command {
/// This decides what commands will keep temp files.
///
/// # Important
///
/// Add commands using the sample sub-args here referencing the `keep` flag,
/// or the temp files will be removed anyways.
fn keep_temp_files(&self) -> bool {
match self {
Self::SampleEncode(args) => args.sample.keep,
Self::CrfSearch(args) => args.sample.keep,
Self::AutoEncode(args) => args.search.sample.keep,
_ => false,
}
}
}
07070100000024000041ED0000000000000000000000026828905A00000000000000000000000000000000000000000000001A00000000ab-av1-0.10.1/src/process07070100000025000081A40000000000000000000000016828905A00003067000000000000000000000000000000000000001D00000000ab-av1-0.10.1/src/process.rspub mod child;
use anyhow::{anyhow, ensure};
use std::{
borrow::Cow,
ffi::OsStr,
fmt::Display,
io,
pin::Pin,
process::{ExitStatus, Output},
sync::Arc,
task::{Context, Poll, ready},
time::Duration,
};
use time::macros::format_description;
use tokio::process::Child;
use tokio_process_stream::{Item, ProcessChunkStream};
use tokio_stream::Stream;
pub fn ensure_success(name: &'static str, out: &Output) -> anyhow::Result<()> {
ensure!(
out.status.success(),
"{name} exit code {}\n---stderr---\n{}\n------------",
out.status
.code()
.map(|c| c.to_string())
.unwrap_or_else(|| "None".into()),
String::from_utf8_lossy(&out.stderr).trim(),
);
Ok(())
}
/// Convert exit code result into simple result.
pub fn exit_ok(name: &'static str, done: io::Result<ExitStatus>) -> anyhow::Result<()> {
let code = done?;
ensure!(
code.success(),
"{name} exit code {}",
code.code()
.map(|c| c.to_string())
.unwrap_or_else(|| "None".into())
);
Ok(())
}
/// Convert exit code result into simple result adding stderr to error messages.
pub fn exit_ok_stderr(
name: &'static str,
done: io::Result<ExitStatus>,
cmd_str: &str,
stderr: &Chunks,
) -> anyhow::Result<()> {
exit_ok(name, done).map_err(|e| cmd_err(e, cmd_str, stderr))
}
pub fn cmd_err(err: impl Display, cmd_str: &str, stderr: &Chunks) -> anyhow::Error {
anyhow!(
"{err}\n----cmd-----\n{cmd_str}\n---stderr---\n{}\n------------",
String::from_utf8_lossy(&stderr.out).trim()
)
}
#[derive(Debug, PartialEq)]
pub enum FfmpegOut {
Progress {
frame: u64,
fps: f32,
time: Duration,
},
StreamSizes {
video: u64,
audio: u64,
subtitle: u64,
other: u64,
},
}
impl FfmpegOut {
pub fn try_parse(line: &str) -> Option<Self> {
if line.starts_with("frame=") {
let frame: u64 = parse_label_substr("frame=", line)?.parse().ok()?;
let fps: f32 = parse_label_substr("fps=", line)?.parse().ok()?;
let (h, m, s, ns) = time::Time::parse(
parse_label_substr("time=", line)?,
&format_description!("[hour]:[minute]:[second].[subsecond]"),
)
.ok()?
.as_hms_nano();
return Some(Self::Progress {
frame,
fps,
time: Duration::new(h as u64 * 60 * 60 + m as u64 * 60 + s as u64, ns),
});
}
if line.starts_with("video:") && line.contains("muxing overhead") {
let video = parse_label_size("video:", line)?;
let audio = parse_label_size("audio:", line)?;
let subtitle = parse_label_size("subtitle:", line)?;
let other = parse_label_size("other streams:", line)?;
return Some(Self::StreamSizes {
video,
audio,
subtitle,
other,
});
}
None
}
pub fn stream(child: Child, name: &'static str, cmd_str: String) -> FfmpegOutStream {
FfmpegOutStream {
chunk_stream: ProcessChunkStream::from(child),
chunks: <_>::default(),
name,
cmd_str,
}
}
}
/// Parse a ffmpeg `label= value ` type substring.
fn parse_label_substr<'a>(label: &str, line: &'a str) -> Option<&'a str> {
let line = &line[line.find(label)? + label.len()..];
let val_start = line.char_indices().find(|(_, c)| !c.is_whitespace())?.0;
let val_end = val_start
+ line[val_start..]
.char_indices()
.find(|(_, c)| c.is_whitespace())
.map(|(idx, _)| idx)
.unwrap_or_else(|| line[val_start..].len());
Some(&line[val_start..val_end])
}
fn parse_label_size(label: &str, line: &str) -> Option<u64> {
let size = parse_label_substr(label, line)?;
let kbs: u64 = size.strip_suffix("kB")?.parse().ok()?;
Some(kbs * 1024)
}
/// Output chunk storage.
///
/// Stores up to ~32k chunk data on the heap.
#[derive(Default)]
pub struct Chunks {
out: Vec<u8>,
/// Truncate to this index before the next Self::push
trunc_next_push: Option<usize>,
}
impl Chunks {
/// Append a chunk.
///
/// If the chunk **ends** in a '\r' carriage returns this will trigger
/// appropriate overwriting on the next call to `push`.
///
/// Removes oldest lines if storage exceeds maximum.
pub fn push(&mut self, chunk: &[u8]) {
const MAX_LEN: usize = 32_000;
if let Some(idx) = self.trunc_next_push.take() {
self.out.truncate(idx);
}
self.out.extend(chunk);
// if too long remove lines until small
while self.out.len() > MAX_LEN {
self.rm_oldest_line();
}
// Setup `trunc_next_push` driven by '\r'
// Typically progress updates, e.g. ffmpeg:
// ```text
// frame= 495 fps= 25 q=40.0 size= 768KiB time=00:00:16.47 bitrate= 381.8kbits/s speed=0.844x \r
// ```
if chunk.ends_with(b"\r") {
self.trunc_next_push = Some(self.after_last_line_feed());
}
}
/// Returns index after the latest '\n' or 0 if there are none.
fn after_last_line_feed(&self) -> usize {
self.out
.iter()
.rposition(|b| *b == b'\n')
.map(|n| n + 1)
.unwrap_or(0)
}
fn rm_oldest_line(&mut self) {
let mut next_eol = self
.out
.iter()
.position(|b| *b == b'\n')
.unwrap_or(self.out.len() - 1);
if self.out.get(next_eol + 1) == Some(&b'\r') {
next_eol += 1;
}
self.out.splice(..next_eol + 1, []);
}
pub fn rfind_line(&self, predicate: impl Fn(&str) -> bool) -> Option<&str> {
self.rfind_line_map(|line| predicate(line).then_some(line))
}
pub fn rfind_line_map<'a, T>(&'a self, f: impl Fn(&'a str) -> Option<T>) -> Option<T> {
let lines = self
.out
.rsplit(|b| *b == b'\n')
.flat_map(|l| l.rsplit(|b| *b == b'\r'));
for line in lines {
if let Ok(line) = std::str::from_utf8(line) {
if let Some(out) = f(line) {
return Some(out);
}
}
}
None
}
/// Returns last non-empty line, if any.
pub fn last_line(&self) -> &str {
self.rfind_line(|l| !l.is_empty()).unwrap_or_default()
}
}
pin_project_lite::pin_project! {
#[must_use = "streams do nothing unless polled"]
pub struct FfmpegOutStream {
#[pin]
chunk_stream: ProcessChunkStream,
name: &'static str,
cmd_str: String,
chunks: Chunks,
}
}
impl FfmpegOutStream {
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
match self.chunk_stream.child_mut() {
Some(c) => c.wait().await,
None => Ok(<_>::default()),
}
}
}
impl Stream for FfmpegOutStream {
type Item = anyhow::Result<FfmpegOut>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
match ready!(self.as_mut().project().chunk_stream.poll_next(cx)) {
Some(item) => match item {
Item::Stderr(chunk) => {
self.chunks.push(&chunk);
if let Some(out) = FfmpegOut::try_parse(self.chunks.last_line()) {
return Poll::Ready(Some(Ok(out)));
}
}
Item::Stdout(_) => {}
Item::Done(code) => {
if let Err(err) =
exit_ok_stderr(self.name, code, &self.cmd_str, &self.chunks)
{
return Poll::Ready(Some(Err(err)));
}
}
},
None => return Poll::Ready(None),
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(0, self.chunk_stream.size_hint().1)
}
}
#[test]
fn parse_ffmpeg_progress_chunk() {
let out = "frame= 288 fps= 94 q=-0.0 size=N/A time=01:23:12.34 bitrate=N/A speed=3.94x \r";
assert_eq!(
FfmpegOut::try_parse(out),
Some(FfmpegOut::Progress {
frame: 288,
fps: 94.0,
time: Duration::new(60 * 60 + 23 * 60 + 12, 340_000_000),
})
);
}
#[test]
fn parse_ffmpeg_progress_line() {
let out = "frame= 161 fps= 73 q=-0.0 size= 978076kB time=00:00:06.71 bitrate=1193201.6kbits/s dup=13 drop=0 speed=3.03x ";
assert_eq!(
FfmpegOut::try_parse(out),
Some(FfmpegOut::Progress {
frame: 161,
fps: 73.0,
time: Duration::new(6, 710_000_000),
})
);
}
#[test]
fn parse_ffmpeg_progress_na_time() {
let out = "frame= 288 fps= 94 q=-0.0 size=N/A time=N/A bitrate=N/A speed=3.94x ";
assert_eq!(FfmpegOut::try_parse(out), None);
}
#[test]
fn parse_ffmpeg_stream_sizes() {
let out = "video:2897022kB audio:537162kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.289700%\n";
assert_eq!(
FfmpegOut::try_parse(out),
Some(FfmpegOut::StreamSizes {
video: 2897022 * 1024,
audio: 537162 * 1024,
subtitle: 0,
other: 0,
})
);
}
pub trait CommandExt {
/// Adds two arguments.
fn arg2(&mut self, a: impl ArgString, b: impl ArgString) -> &mut Self;
/// Adds two arguments, the 2nd an option. `None` mean noop.
fn arg2_opt(&mut self, a: impl ArgString, b: Option<impl ArgString>) -> &mut Self;
/// Adds two arguments if `condition` otherwise noop.
fn arg2_if(&mut self, condition: bool, a: impl ArgString, b: impl ArgString) -> &mut Self;
/// Adds an argument if `condition` otherwise noop.
fn arg_if(&mut self, condition: bool, a: impl ArgString) -> &mut Self;
/// Convert to readable shell-like string.
fn to_cmd_str(&self) -> String;
}
impl CommandExt for tokio::process::Command {
fn arg2(&mut self, a: impl ArgString, b: impl ArgString) -> &mut Self {
self.arg(a.arg_string()).arg(b.arg_string())
}
fn arg2_opt(&mut self, a: impl ArgString, b: Option<impl ArgString>) -> &mut Self {
match b {
Some(b) => self.arg2(a, b),
None => self,
}
}
fn arg2_if(&mut self, c: bool, a: impl ArgString, b: impl ArgString) -> &mut Self {
match c {
true => self.arg2(a, b),
false => self,
}
}
fn arg_if(&mut self, condition: bool, a: impl ArgString) -> &mut Self {
match condition {
true => self.arg(a.arg_string()),
false => self,
}
}
fn to_cmd_str(&self) -> String {
let cmd = self.as_std();
cmd.get_args().map(|a| a.to_string_lossy()).fold(
cmd.get_program().to_string_lossy().to_string(),
|mut all, next| {
all.push(' ');
all += &next;
all
},
)
}
}
pub trait ArgString {
fn arg_string(&self) -> Cow<'_, OsStr>;
}
macro_rules! impl_arg_string_as_ref {
($t:ty) => {
impl ArgString for $t {
fn arg_string(&self) -> Cow<'_, OsStr> {
Cow::Borrowed(self.as_ref())
}
}
};
}
impl_arg_string_as_ref!(String);
impl_arg_string_as_ref!(&'_ String);
impl_arg_string_as_ref!(&'_ str);
impl_arg_string_as_ref!(&'_ &'_ str);
impl_arg_string_as_ref!(&'_ std::path::Path);
impl_arg_string_as_ref!(&'_ std::path::PathBuf);
macro_rules! impl_arg_string_display {
($t:ty) => {
impl ArgString for $t {
fn arg_string(&self) -> Cow<'_, OsStr> {
Cow::Owned(self.to_string().into())
}
}
};
}
impl_arg_string_display!(u8);
impl_arg_string_display!(u16);
impl_arg_string_display!(u32);
impl_arg_string_display!(i32);
impl_arg_string_display!(f32);
impl ArgString for Arc<str> {
fn arg_string(&self) -> Cow<'_, OsStr> {
Cow::Borrowed((**self).as_ref())
}
}
07070100000026000081A40000000000000000000000016828905A00000AAA000000000000000000000000000000000000002300000000ab-av1-0.10.1/src/process/child.rsuse log::info;
use std::{
io::IsTerminal,
mem,
ops::{Deref, DerefMut},
pin::pin,
sync::{LazyLock, Mutex},
time::Duration,
};
use tokio::{
signal,
time::{Instant, timeout_at},
};
use tokio_process_stream::ProcessChunkStream;
static RUNNING: LazyLock<Mutex<Vec<ProcessChunkStream>>> = LazyLock::new(<_>::default);
/// Add a child process so it may be waited on before exiting.
pub fn add(mut child: ProcessChunkStream) {
let mut running = RUNNING.lock().unwrap();
// remove any that have exited already
running.retain_mut(|c| c.child_mut().is_some_and(|c| c.try_wait().is_err()));
if child.child_mut().is_some_and(|c| c.try_wait().is_err()) {
running.push(child);
}
}
/// Wait for all child processes, that were added with [`add`], to exit.
pub async fn wait() {
// if waiting takes >500ms log what's happening
let mut log_deadline = Some(Instant::now() + Duration::from_millis(500));
let procs = mem::take(&mut *RUNNING.lock().unwrap());
let mut ctrl_c = pin!(signal::ctrl_c());
for mut proc in procs {
if let Some(child) = proc.child_mut() {
if let Some(deadline) = log_deadline {
if timeout_at(deadline, child.wait()).await.is_err() {
log_waiting();
log_deadline = None;
}
}
tokio::select! {
_ = &mut ctrl_c => {
log_abort_wait();
return;
}
_ = child.wait() => {}
}
}
}
}
fn log_waiting() {
match std::io::stderr().is_terminal() {
true => eprintln!("Waiting for child processes to exit..."),
_ => info!("Waiting for child processes to exit"),
}
}
fn log_abort_wait() {
match std::io::stderr().is_terminal() {
true => eprintln!("Aborting wait for child processes"),
_ => info!("Aborting wait for child processes"),
}
}
/// Wrapper that [`add`]s the inner on drop.
#[derive(Debug)]
pub struct AddOnDropChunkStream(Option<ProcessChunkStream>);
impl From<ProcessChunkStream> for AddOnDropChunkStream {
fn from(v: ProcessChunkStream) -> Self {
Self(Some(v))
}
}
impl Drop for AddOnDropChunkStream {
fn drop(&mut self) {
if let Some(child) = self.0.take() {
add(child);
}
}
}
impl Deref for AddOnDropChunkStream {
type Target = ProcessChunkStream;
fn deref(&self) -> &Self::Target {
self.0.as_ref().unwrap() // only none after drop
}
}
impl DerefMut for AddOnDropChunkStream {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().unwrap() // only none after drop
}
}
07070100000027000081A40000000000000000000000016828905A000008F2000000000000000000000000000000000000001C00000000ab-av1-0.10.1/src/sample.rs//! ffmpeg logic
use crate::{
process::{CommandExt, ensure_success},
temporary::{self, TempKind},
};
use anyhow::Context;
use std::{
path::{Path, PathBuf},
process::Stdio,
time::Duration,
};
use tokio::process::Command;
/// Create a sample from `sample_start` + `frames`.
///
/// Fast as this uses `-c:v copy`.
pub async fn copy(
input: &Path,
sample_start: Duration,
floor_to_sec: bool,
frames: u32,
temp_dir: Option<PathBuf>,
) -> anyhow::Result<PathBuf> {
let mut sample_start_s = sample_start.as_secs_f32();
if floor_to_sec {
sample_start_s = sample_start_s.floor();
}
let mut dest = temporary::process_dir(temp_dir);
// Always using mkv for the samples works better than, e.g. using mp4 for mp4s
// see https://github.com/alexheretic/ab-av1/issues/82#issuecomment-1337306325
dest.push(
input
.with_extension(format!("sample{sample_start_s}+{frames}f.mkv"))
.file_name()
.unwrap(),
);
if dest.exists() {
return Ok(dest);
}
temporary::add(&dest, TempKind::Keepable);
// Note: `-ss` before `-i` & `-frames:v` instead of `-t`
// See https://github.com/alexheretic/ab-av1/issues/36#issuecomment-1146634936
let mut out = Command::new("ffmpeg")
.arg("-y")
.arg2("-ss", sample_start_s)
.arg2("-i", input)
.arg2("-frames:v", frames)
.arg2("-c:v", "copy")
.arg("-an")
.arg("-sn")
.arg(&dest)
.stdin(Stdio::null())
.output()
.await
.context("ffmpeg copy")?;
if !out.status.success()
&& String::from_utf8_lossy(&out.stderr)
.contains("Can't write packet with unknown timestamp")
{
out = Command::new("ffmpeg")
.arg("-y")
// try +genpts workaround
.arg2("-fflags", "+genpts")
.arg2("-ss", sample_start_s)
.arg2("-i", input)
.arg2("-frames:v", frames)
.arg2("-c:v", "copy")
.arg("-an")
.arg("-sn")
.arg(&dest)
.stdin(Stdio::null())
.output()
.await
.context("ffmpeg copy")?;
}
ensure_success("ffmpeg copy", &out)?;
Ok(dest)
}
07070100000028000081A40000000000000000000000016828905A00000AE3000000000000000000000000000000000000001F00000000ab-av1-0.10.1/src/temporary.rs//! temp file logic
use std::{
collections::HashMap,
env, iter,
path::{Path, PathBuf},
sync::{LazyLock, Mutex},
};
static TEMPS: LazyLock<Mutex<HashMap<PathBuf, TempKind>>> = LazyLock::new(<_>::default);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TempKind {
/// Should always be deleted at the end of the program.
NotKeepable,
/// Usually deleted but may be kept, e.g. with --keep.
Keepable,
}
/// Add a file as temporary so it can be deleted later.
pub fn add(file: impl Into<PathBuf>, kind: TempKind) {
TEMPS.lock().unwrap().insert(file.into(), kind);
}
/// Remove a previously added file so that it won't be deleted later,
/// if it hasn't already.
pub fn unadd(file: &Path) -> bool {
TEMPS.lock().unwrap().remove(file).is_some()
}
/// Delete all added temporary files.
/// If `keep_keepables` true don't delete [`TempKind::Keepable`] temporary files.
pub async fn clean(keep_keepables: bool) {
match keep_keepables {
true => clean_non_keepables().await,
false => clean_all().await,
}
}
/// Delete all added temporary files.
pub async fn clean_all() {
let mut files: Vec<_> = std::mem::take(&mut *TEMPS.lock().unwrap())
.into_keys()
.collect();
files.sort_by_key(|f| f.is_dir()); // rm dir at the end
for file in files {
match file.is_dir() {
true => _ = tokio::fs::remove_dir(file).await,
false => _ = tokio::fs::remove_file(file).await,
}
}
}
async fn clean_non_keepables() {
let mut matching: Vec<_> = TEMPS
.lock()
.unwrap()
.iter()
.filter(|(_, k)| **k == TempKind::NotKeepable)
.map(|(f, _)| f.clone())
.collect();
matching.sort_by_key(|f| f.is_dir()); // rm dir at the end
for file in matching {
match file.is_dir() {
true => _ = tokio::fs::remove_dir(&file).await,
false => _ = tokio::fs::remove_file(&file).await,
}
TEMPS.lock().unwrap().remove(&file);
}
}
/// Return a temporary directory that is distinct per process/run.
///
/// Configured --temp-dir is used as a parent or, if not set, the current working dir.
pub fn process_dir(conf_parent: Option<PathBuf>) -> PathBuf {
static SUBDIR: LazyLock<String> = LazyLock::new(|| {
let mut subdir = String::from(".ab-av1-");
subdir.extend(iter::repeat_with(fastrand::alphanumeric).take(12));
subdir
});
let mut temp_dir =
conf_parent.unwrap_or_else(|| env::current_dir().expect("current working directory"));
temp_dir.push(&*SUBDIR);
if !temp_dir.exists() {
add(&temp_dir, TempKind::Keepable);
std::fs::create_dir_all(&temp_dir).expect("failed to create temp-dir");
}
temp_dir
}
07070100000029000081A40000000000000000000000016828905A00002461000000000000000000000000000000000000001A00000000ab-av1-0.10.1/src/vmaf.rs//! vmaf logic
use crate::process::{Chunks, CommandExt, FfmpegOut, cmd_err, exit_ok_stderr};
use anyhow::Context;
use log::{debug, info};
use std::{path::Path, process::Stdio};
use tokio::process::Command;
use tokio_process_stream::{Item, ProcessChunkStream};
use tokio_stream::{Stream, StreamExt};
/// Calculate VMAF score using ffmpeg.
pub fn run(
reference: &Path,
distorted: &Path,
filter_complex: &str,
fps: Option<f32>,
) -> anyhow::Result<impl Stream<Item = VmafOut> + use<>> {
info!(
"vmaf {} vs reference {}",
distorted.file_name().and_then(|n| n.to_str()).unwrap_or(""),
reference.file_name().and_then(|n| n.to_str()).unwrap_or(""),
);
let mut cmd = Command::new("ffmpeg");
cmd.kill_on_drop(true)
.arg2_opt("-r", fps)
.arg2("-i", distorted)
.arg2_opt("-r", fps)
.arg2("-i", reference)
.arg2("-filter_complex", filter_complex)
// Workaround unused streams causing ffmpeg memory leaks
// See https://github.com/alexheretic/ab-av1/issues/189
.arg("-an")
.arg("-sn")
.arg("-dn")
.arg2("-f", "null")
.arg("-")
.stdin(Stdio::null());
let cmd_str = cmd.to_cmd_str();
debug!("cmd `{cmd_str}`");
let mut vmaf = crate::process::child::AddOnDropChunkStream::from(
ProcessChunkStream::try_from(cmd).context("ffmpeg vmaf")?,
);
Ok(async_stream::stream! {
let mut chunks = Chunks::default();
let mut parsed_done = false;
while let Some(next) = vmaf.next().await {
match next {
Item::Stderr(chunk) => {
if let Some(out) = VmafOut::try_from_chunk(&chunk, &mut chunks) {
if matches!(out, VmafOut::Done(_)) {
parsed_done = true;
}
yield out;
}
}
Item::Stdout(_) => {}
Item::Done(code) => {
if let Err(err) = exit_ok_stderr("ffmpeg vmaf", code, &cmd_str, &chunks) {
yield VmafOut::Err(err);
}
}
}
}
if !parsed_done {
yield VmafOut::Err(cmd_err(
"could not parse ffmpeg vmaf score",
&cmd_str,
&chunks,
));
}
})
}
#[derive(Debug)]
pub enum VmafOut {
Progress(FfmpegOut),
Done(f32),
Err(anyhow::Error),
}
impl VmafOut {
fn try_from_chunk(chunk: &[u8], chunks: &mut Chunks) -> Option<Self> {
const SCORE_PREFIX: &str = "VMAF score: ";
chunks.push(chunk);
if let Some(line) = chunks.rfind_line(|l| l.contains(SCORE_PREFIX)) {
let idx = line.find(SCORE_PREFIX).unwrap();
return Some(Self::Done(
line[idx + SCORE_PREFIX.len()..].trim().parse().ok()?,
));
}
if let Some(progress) = FfmpegOut::try_parse(chunks.last_line()) {
return Some(Self::Progress(progress));
}
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_vmaf_score_207() {
const FFMPEG_OUT: &str = r#"ffmpeg version n7.0.1 Copyright (c) 2000-2024 the FFmpeg developers
built with gcc 14.1.1 (GCC) 20240522
configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-amf --enable-avisynth --enable-cuda-llvm --enable-lto --enable-fontconfig --enable-frei0r --enable-gmp --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libdav1d --enable-libdrm --enable-libdvdnav --enable-libdvdread --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libharfbuzz --enable-libiec61883 --enable-libjack --enable-libjxl --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libplacebo --enable-libpulse --enable-librav1e --enable-librsvg --enable-librubberband --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpl --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-libzimg --enable-mbedtls --enable-nvdec --enable-nvenc --enable-opencl --enable-opengl --enable-shared --enable-vapoursynth --enable-version3 --enable-vulkan
libavutil 59. 8.100 / 59. 8.100
libavcodec 61. 3.100 / 61. 3.100
libavformat 61. 1.100 / 61. 1.100
libavdevice 61. 1.100 / 61. 1.100
libavfilter 10. 1.100 / 10. 1.100
libswscale 8. 1.100 / 8. 1.100
libswresample 5. 1.100 / 5. 1.100
libpostproc 58. 1.100 / 58. 1.100
libavutil 59. 8.100 / 59. 8.100
libavcodec 61. 3.100 / 61. 3.100
libavformat 61. 1.100 / 61. 1.100
libavdevice 61. 1.100 / 61. 1.100
libavfilter 10. 1.100 / 10. 1.100
libswscale 8. 1.100 / 8. 1.100
libswresample 5. 1.100 / 5. 1.100
libpostproc 58. 1.100 / 58. 1.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'C:\Users\Administrator\Personal_scripts\Python\PythonScripts\PythonScripts\src\.ab-av1-RM46M2PZOVjb\A11 崩三 黑曼巴之影_1.sample2+600f.av1.crf37.5.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomav01iso2mp41
title : Project 1
date : 2019-07-11
encoder : Lavf61.1.100
Duration: 00:00:20.00, start: 0.000000, bitrate: 1562 kb/s
Stream #0:0[0x1](und): Video: av1 (libdav1d) (Main) (av01 / 0x31307661), yuv420p10le(tv, progressive), 1000x696, 1560 kb/s, SAR 1:1 DAR 125:87, 30 fps, 30 tbr, 15360 tbn (default)
Metadata:
handler_name : VideoHandler
vendor_id : [0][0][0][0]
encoder : Lavc61.3.100 libsvtav1
Input #1, matroska,webm, from 'C:\Users\Administrator\Personal_scripts\Python\PythonScripts\PythonScripts\src\.ab-av1-RM46M2PZOVjb\A11 崩三 黑曼巴之影_1.sample2+600f.mkv':
Metadata:
title : Project 1
DATE : 2019-07-11
MAJOR_BRAND : isom
MINOR_VERSION : 512
COMPATIBLE_BRANDS: isomiso2mp41
ENCODER : Lavf61.1.100
Duration: 00:00:20.00, start: 0.000000, bitrate: 6114 kb/s
Stream #1:0: Video: mpeg4 (Simple Profile), yuv420p, 1000x696 [SAR 1:1 DAR 125:87], 30 fps, 30 tbr, 1k tbn (default)
Metadata:
HANDLER_NAME : VideoHandler
VENDOR_ID : [0][0][0][0]
DURATION : 00:00:20.000000000
Stream mapping:
Stream #0:0 (libdav1d) -> format:default
Stream #1:0 (mpeg4) -> format:default
libvmaf:default -> Stream #0:0 (wrapped_avframe)
Press [q] to stop, [?] for help
Output #0, null, to 'pipe:':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomav01iso2mp41
title : Project 1
date : 2019-07-11
encoder : Lavf61.1.100
Stream #0:0: Video: wrapped_avframe, yuv420p10le(tv, progressive), 1552x1080 [SAR 5625:5626 DAR 125:87], q=2-31, 200 kb/s, 24 tbn
Metadata:
encoder : Lavc61.3.100 wrapped_avframe
frame= 48 fps=0.0 q=-0.0 size=N/A time=00:00:01.95 bitrate=N/A speed=3.79x
frame= 101 fps= 97 q=-0.0 size=N/A time=00:00:04.16 bitrate=N/A speed= 4x
frame= 156 fps=100 q=-0.0 size=N/A time=00:00:06.45 bitrate=N/A speed=4.14x
frame= 209 fps=101 q=-0.0 size=N/A time=00:00:08.66 bitrate=N/A speed= 4.2x
frame= 264 fps=102 q=-0.0 size=N/A time=00:00:10.95 bitrate=N/A speed=4.23x
frame= 319 fps=103 q=-0.0 size=N/A time=00:00:13.25 bitrate=N/A speed=4.26x
frame= 373 fps=103 q=-0.0 size=N/A time=00:00:15.50 bitrate=N/A speed=4.27x
frame= 429 fps=103 q=-0.0 size=N/A time=00:00:17.83 bitrate=N/A speed= 4.3x
frame= 482 fps=103 q=-0.0 size=N/A time=00:00:20.04 bitrate=N/A speed=4.29x
frame= 536 fps=104 q=-0.0 size=N/A time=00:00:22.29 bitrate=N/A speed=4.31x
frame= 589 fps=103 q=-0.0 size=N/A time=00:00:24.50 bitrate=N/A speed= 4.3x
[Parsed_libvmaf_6 @ 000002b296bac480] VMAF score: 94.826380
[out#0/null @ 000002b2916f8b80] video:258KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: unknown
frame= 600 fps=102 q=-0.0 Lsize=N/A time=00:00:24.95 bitrate=N/A speed=4.24x"#;
const CHUNK_SIZE: usize = 64;
let ffmpeg = FFMPEG_OUT.as_bytes();
let mut chunks = Chunks::default();
let mut start_idx = 0;
let mut vmaf_score = None;
while start_idx < ffmpeg.len() {
let chunk = &ffmpeg[start_idx..(start_idx + CHUNK_SIZE).min(FFMPEG_OUT.len())];
println!("* {}", String::from_utf8_lossy(chunk).trim());
if let Some(vmaf) = VmafOut::try_from_chunk(chunk, &mut chunks) {
println!("{vmaf:?}");
if let VmafOut::Done(score) = vmaf {
vmaf_score = Some(score);
}
}
start_idx += CHUNK_SIZE;
}
assert_eq!(vmaf_score, Some(94.82638), "failed to parse vmaf score");
}
}
0707010000002A000081A40000000000000000000000016828905A000023E3000000000000000000000000000000000000001B00000000ab-av1-0.10.1/src/xpsnr.rs//! xpsnr logic
use crate::process::{Chunks, CommandExt, FfmpegOut, cmd_err, exit_ok_stderr};
use anyhow::Context;
use log::{debug, info};
use std::{path::Path, process::Stdio};
use tokio::process::Command;
use tokio_process_stream::{Item, ProcessChunkStream};
use tokio_stream::{Stream, StreamExt};
/// Calculate XPSNR score using ffmpeg.
pub fn run(
reference: &Path,
distorted: &Path,
filter_complex: &str,
fps: Option<f32>,
) -> anyhow::Result<impl Stream<Item = XpsnrOut> + use<>> {
info!(
"xpsnr {} vs reference {}",
distorted.file_name().and_then(|n| n.to_str()).unwrap_or(""),
reference.file_name().and_then(|n| n.to_str()).unwrap_or(""),
);
let mut cmd = Command::new("ffmpeg");
cmd.kill_on_drop(true)
.arg2_opt("-r", fps)
.arg2("-i", reference)
.arg2_opt("-r", fps)
.arg2("-i", distorted)
.arg2("-filter_complex", filter_complex)
.arg2("-f", "null")
.arg("-")
.stdin(Stdio::null());
let cmd_str = cmd.to_cmd_str();
debug!("cmd `{cmd_str}`");
let mut xpsnr = crate::process::child::AddOnDropChunkStream::from(
ProcessChunkStream::try_from(cmd).context("ffmpeg xpsnr")?,
);
Ok(async_stream::stream! {
let mut chunks = Chunks::default();
let mut parsed_done = false;
while let Some(next) = xpsnr.next().await {
match next {
Item::Stderr(chunk) => {
if let Some(out) = XpsnrOut::try_from_chunk(&chunk, &mut chunks) {
if matches!(out, XpsnrOut::Done(_)) {
parsed_done = true;
}
yield out;
}
}
Item::Stdout(_) => {}
Item::Done(code) => {
if let Err(err) = exit_ok_stderr("ffmpeg xpsnr", code, &cmd_str, &chunks) {
yield XpsnrOut::Err(err);
}
}
}
}
if !parsed_done {
yield XpsnrOut::Err(cmd_err(
"could not parse ffmpeg xpsnr score",
&cmd_str,
&chunks,
));
}
})
}
#[derive(Debug)]
pub enum XpsnrOut {
Progress(FfmpegOut),
Done(f32),
Err(anyhow::Error),
}
impl XpsnrOut {
fn try_from_chunk(chunk: &[u8], chunks: &mut Chunks) -> Option<Self> {
chunks.push(chunk);
if let Some(score) = chunks.rfind_line_map(score_from_line) {
return Some(Self::Done(score));
}
if let Some(progress) = FfmpegOut::try_parse(chunks.last_line()) {
return Some(Self::Progress(progress));
}
None
}
}
// E.g. "[Parsed_xpsnr_0 @ 0x711494004cc0] XPSNR y: 33.6547 u: 41.8741 v: 42.2571 (minimum: 33.6547)"
fn score_from_line(line: &str) -> Option<f32> {
const MIN_PREFIX: &str = "minimum: ";
if !line.contains("XPSNR") {
return None;
}
let yidx = line.find(MIN_PREFIX)?;
let tail = &line[yidx + MIN_PREFIX.len()..];
if tail.starts_with("inf") {
return Some(f32::INFINITY);
}
let end_idx = tail
.char_indices()
.take_while(|(_, c)| *c == '.' || c.is_numeric())
.last()?
.0;
tail[..=end_idx].parse().ok()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_rgb_line() {
let score = score_from_line(
"XPSNR average, 1 frames r: 40.6130 g: 41.0275 b: 40.6961 (minimum: 40.6130)",
);
assert_eq!(score, Some(40.6130));
}
#[test]
fn parse_xpsnr_score() {
// Note: some lines omitted for brevity
const FFMPEG_OUT: &str = r#"Input #0, matroska,webm, from 'tmp.mkv':
Metadata:
COMPATIBLE_BRANDS: isomiso2avc1mp41
MAJOR_BRAND : isom
MINOR_VERSION : 512
ENCODER : Lavf61.7.100
Duration: 00:00:53.77, start: -0.007000, bitrate: 2698 kb/s
Stream #0:0(eng): Video: av1 (libdav1d) (Main), yuv420p10le(tv, progressive), 3840x2160, 25 fps, 25 tbr, 1k tbn (default)
Metadata:
HANDLER_NAME : ?Mainconcept Video Media Handler
VENDOR_ID : [0][0][0][0]
ENCODER : Lavc61.19.100 libsvtav1
DURATION : 00:00:53.760000000
Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
Metadata:
title : Opus 96Kbps
HANDLER_NAME : #Mainconcept MP4 Sound Media Handler
VENDOR_ID : [0][0][0][0]
ENCODER : Lavc61.19.100 libopus
DURATION : 00:00:53.768000000
Input #1, mov,mp4,m4a,3gp,3g2,mj2, from 'pixabay-lemon-82602.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf58.20.100
Duration: 00:00:53.76, start: 0.000000, bitrate: 14109 kb/s
Stream #1:0[0x1](eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 3840x2160, 14101 kb/s, 25 fps, 25 tbr, 12800 tbn (default)
Metadata:
handler_name : ?Mainconcept Video Media Handler
vendor_id : [0][0][0][0]
Stream #1:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 2 kb/s (default)
Metadata:
handler_name : #Mainconcept MP4 Sound Media Handler
vendor_id : [0][0][0][0]
Stream mapping:
Stream #0:0 (libdav1d) -> xpsnr
Stream #1:0 (h264) -> xpsnr
xpsnr:default -> Stream #0:0 (wrapped_avframe)
Stream #0:1 -> #0:1 (opus (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
[Parsed_xpsnr_0 @ 0x78341c004d00] not matching timebases found between first input: 1/1000 and second input 1/12800, results may be incorrect!
Output #0, null, to 'pipe:':
Metadata:
COMPATIBLE_BRANDS: isomiso2avc1mp41
MAJOR_BRAND : isom
MINOR_VERSION : 512
encoder : Lavf61.7.100
Stream #0:0: Video: wrapped_avframe, yuv420p10le(tv, progressive), 3840x2160 [SAR 1:1 DAR 16:9], q=2-31, 200 kb/s, 25 fps, 25 tbn
Metadata:
encoder : Lavc61.19.100 wrapped_avframe
Stream #0:1(eng): Audio: pcm_s16le, 48000 Hz, stereo, s16, 1536 kb/s (default)
Metadata:
title : Opus 96Kbps
HANDLER_NAME : #Mainconcept MP4 Sound Media Handler
VENDOR_ID : [0][0][0][0]
DURATION : 00:00:53.768000000
encoder : Lavc61.19.100 pcm_s16le
frame= 9 fps=0.0 q=-0.0 size=N/A time=00:00:00.32 bitrate=N/A speed=0.64x
frame= 28 fps= 28 q=-0.0 size=N/A time=00:00:01.08 bitrate=N/A speed=1.08x
frame= 46 fps= 31 q=-0.0 size=N/A time=00:00:01.80 bitrate=N/A speed= 1.2x
frame= 65 fps= 32 q=-0.0 size=N/A time=00:00:02.56 bitrate=N/A speed=1.28x
n: 1 XPSNR y: 54.5266 XPSNR u: 56.3886 XPSNR v: 58.7794
n: 2 XPSNR y: 40.6035 XPSNR u: 39.3487 XPSNR v: 42.3634
n: 3 XPSNR y: 40.9764 XPSNR u: 38.8791 XPSNR v: 41.8961
n: 64 XPSNR y: 41.0726 XPSNR u: 39.7731 XPSNR v: 42.5210
n: 65 XPSNR y: 41.3476 XPSNR u: 39.6055 XPSNR v: 42.4262
n: 66 XPSNR y: 41.1029 XPSNR u: 39.8779 XPSNR v: 42.6400
frame= 84 fps= 34 q=-0.0 size=N/A time=00:00:03.32 bitrate=N/A speed=1.33x
frame= 102 fps= 34 q=-0.0 size=N/A time=00:00:04.04 bitrate=N/A speed=1.35x
frame= 120 fps= 34 q=-0.0 size=N/A time=00:00:04.76 bitrate=N/A speed=1.36x
n: 67 XPSNR y: 40.9642 XPSNR u: 39.5204 XPSNR v: 42.1316
n: 68 XPSNR y: 40.2677 XPSNR u: 38.9371 XPSNR v: 41.9560
n: 69 XPSNR y: 40.6431 XPSNR u: 38.8864 XPSNR v: 41.6902
n: 1319 XPSNR y: 41.4316 XPSNR u: 40.5146 XPSNR v: 42.1970
n: 1320 XPSNR y: 41.4623 XPSNR u: 40.5527 XPSNR v: 42.3358
n: 1321 XPSNR y: 42.5312 XPSNR u: 41.2487 XPSNR v: 42.8495
frame= 1328 fps= 37 q=-0.0 size=N/A time=00:00:53.08 bitrate=N/A speed=1.47x
[Parsed_xpsnr_0 @ 0x78341c004d00] XPSNR y: 40.7139 u: 39.1440 v: 41.7907 (minimum: 39.1440)
[out#0/null @ 0x64006e11b1c0] video:578KiB audio:10080KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: unknown
frame= 1344 fps= 37 q=-0.0 Lsize=N/A time=00:00:53.72 bitrate=N/A speed=1.48x
n: 1342 XPSNR y: 40.6841 XPSNR u: 39.0209 XPSNR v: 40.9250
n: 1343 XPSNR y: 41.0269 XPSNR u: 39.2465 XPSNR v: 41.1238
n: 1344 XPSNR y: 39.8468 XPSNR u: 38.4587 XPSNR v: 40.5844
XPSNR average, 1344 frames y: 40.7139
"#;
const CHUNK_SIZE: usize = 64;
let ffmpeg = FFMPEG_OUT.as_bytes();
let mut chunks = Chunks::default();
let mut start_idx = 0;
let mut xpsnr_score = None;
while start_idx < ffmpeg.len() {
let chunk = &ffmpeg[start_idx..(start_idx + CHUNK_SIZE).min(FFMPEG_OUT.len())];
// println!("* {}", String::from_utf8_lossy(chunk).trim());
if let Some(xpsnr) = XpsnrOut::try_from_chunk(chunk, &mut chunks) {
println!("{xpsnr:?}");
if let XpsnrOut::Done(score) = xpsnr {
xpsnr_score = Some(score);
}
}
start_idx += CHUNK_SIZE;
}
assert_eq!(xpsnr_score, Some(39.1440), "failed to parse xpsnr score");
}
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!452 blocks